webpack
,打包所有的资源
不知道不觉,webpack
已经偷偷更新到4.34
版本了,本人决定,这是今年最后一篇写webpack
的文章,除非它更新到版本5,本人今年剩下的时间都会放在Golang
和二进制数据操作以及后端的生态上
在看本文前,假设你对webpack
有一定了解,如果不了解,可以看看我之前的手写React
和Vue
脚手架的文章
- 手写优化版React脚手架
- 手写Vue的脚手架
- 前端性能优化不完全手册
- 跨平台webpack配置
- 都是百星
star
的优质文章
在此对webpack
的性能优化进行几点声明:
- 在部分极度复杂的环境下,需要双
package.json
文件,即实行三次打包 - 在代码分割时,低于
18K
的文件没必要单独打包成一个chunk
,http
请求次数过多反而影响性能 -
prerender
和PWA
互斥,这个问题暂时没有解决 -
babel
缓存编译缓存的是索引,即hash
值,非常吃内存,每次开发完记得清理内存 -
babel-polyfill
按需加载在某些非常复杂的场景下比较适合 -
prefetch,preload
对首屏优化提升是明显 - 代码分割不管什么技术栈,一定要做,不然就是垃圾项目
- 多线程编译对构建速度提升也很明显
- 代码分割配合
PWA
+预渲染+preload
是首屏优化的巅峰,但是pwa
无法缓存预渲染的html
文件
本文的webpack
主要针对React
技术栈,实现功能如下:
- 开发模式热更新
- 识别
JSX
文件 - 识别
class
组件 - 代码混淆压缩,防止反编译代码,加密代码
- 配置
alias
别名,简化import
的长字段 - 同构直出,
SSR
的热调试(基于Node
做中间件) - 实现
javaScript
的tree shaking
摇树优化 删除掉无用代码 - 实现
CSS
的tree shaking
- 识别
async / await
和 箭头函数 -
react-hot-loader
记录react
页面留存状态state
-
PWA
功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用 -
preload
预加载资源prefetch
按需请求资源 -
CSS
模块化,不怕命名冲突 - 小图片的
base64
处理 - 文件后缀省掉
jsx js json
等 - 实现React懒加载,按需加载 , 代码分割 并且支持服务端渲染
- 支持
less sass stylus
等预处理 -
code spliting
优化首屏加载时间 不让一个文件体积过大 - 加入
dns-prefetch
和preload
预请求必要的资源,加快首屏渲染(京东策略) - 加入
prerender
,极大加快首屏渲染速度 - 提取公共代码,打包成一个
chunk
- 每个
chunk
有对应的chunkhash
,每个文件有对应的contenthash
,方便浏览器区别缓存 - 图片压缩
-
CSS
压缩 - 增加
CSS
前缀 兼容各种浏览器 - 对于各种不同文件打包输出指定文件夹下
- 缓存
babel
的编译结果,加快编译速度 - 每个入口文件,对应一个
chunk
,打包出来后对应一个文件 也是code spliting
- 删除
HTML
文件的注释等无用内容 - 每次编译删除旧的打包代码
- 将
CSS
文件单独抽取出来 - 让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
- 等等....
本质上,webpack
是一个现代 JavaScript
应用程序的静态模块打包器(module bundler
)。当 webpack
处理应用程序时,它会递归地构建一个依赖关系图(dependency graph
),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle
webpack
打包原理
- 识别入口文件
- 通过逐层识别模块依赖。(
Commonjs、amd
或者es6的import,webpack
都会对其进行分析。来获取代码的依赖) -
webpack
做的就是分析代码。转换代码,编译代码,输出代码 - 最终形成打包后的代码
- 这些都是
webpack
的一些基础知识,对于理解webpack
的工作机制很有帮助。
舒适的开发体验,有助于提高我们的开发效率,优化开发体验也至关重要
- 组件热刷新、CSS热刷新
- 自从webpack推出热刷新后,前端开发者在开环境下体验大幅提高。
- 没有热刷新能力,我们修改一个组件后
- 加入热刷新后
主要看一下React
技术栈,如何在构建中接入热刷新
- 无论什么技术栈,都需要在
dev
模式下加上webpack.HotModuleReplacementPlugin
插件
devServer: {
contentBase: '../build',
open: true,
port: 5000,
hot: true
},
注:也可以使用react-hot-loader来实现,具体参考官方文档
在开发模式下也要代码分割,加快打开页面速度
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all',
minSize: 10000, // 提高缓存利用率,这需要在http2/spdy
maxSize: 0,//没有限制
minChunks: 3,// 共享最少的chunk数,使用次数超过这个值才会被提取
maxAsyncRequests: 5,//最多的异步chunk数
maxInitialRequests: 5,// 最多的同步chunks数
automaticNameDelimiter: '~',// 多页面共用chunk命名分隔符
name: true,
cacheGroups: {// 声明的公共chunk
vendor: {
// 过滤需要打入的模块
test: module => {
if (module.resource) {
const include = [/[\\/]node_modules[\\/]/].every(reg => {
return reg.test(module.resource);
});
const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => {
return reg.test(module.resource);
});
return include && !exclude;
}
return false;
},
name: 'vendor',
priority: 50,// 确定模块打入的优先级
reuseExistingChunk: true,// 使用复用已经存在的模块
},
react: {
test({ resource }) {
return /[\\/]node_modules[\\/](react|redux)/.test(resource);
},
name: 'react',
priority: 20,
reuseExistingChunk: true,
},
antd: {
test: /[\\/]node_modules[\\/]antd/,
name: 'antd',
priority: 15,
reuseExistingChunk: true,
},
},
}
}
简要解释上面这段配置
- 将node_modules共用部分打入
vendor.js bundle
中; - 将react全家桶打入
react.js bundle
中; - 如果项目依赖了
antd
,那么将antd
打入单独的bundle
中;(其实不用这样,可以看我下面的babel
配置,性能更高) - 最后剩下的业务模块超过3次引用的公共模块,将自动提取公共块
注意 上面的配置只是为了给大家看,其实这样配置代码分割,性能更高
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all',
}
}
react-hot-loader
记录react
页面留存状态state
yarn add react-hot-loader
// 在入口文件里这样写
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";-------------------1、首先引入AppContainre
import { BrowserRouter } from "react-router-dom";
import Router from "./router";
/*初始化*/
renderWithHotReload(Router);-------------------2、初始化
/*热更新*/
if (module.hot) {-------------------3、热更新操作
module.hot.accept("./router/index.js", () => {
const Router = require("./router/index.js").default;
renderWithHotReload(Router);
});
}
function renderWithHotReload(Router) {-------------------4、定义渲染函数
ReactDOM.render(
<AppContainer>
<BrowserRouter>
<Router />
</BrowserRouter>
</AppContainer>,
document.getElementById("app")
);
}
然后你再刷新试试
React
的按需加载,附带代码分割功能 ,每个按需加载的组件打包后都会被单独分割成一个文件
import React from 'react'
import loadable from 'react-loadable'
import Loading from '../loading'
const LoadableComponent = loadable({
loader: () => import('../Test/index.jsx'),
loading: Loading,
});
class Assets extends React.Component {
render() {
return (
<div>
<div>这即将按需加载</div>
<LoadableComponent />
</div>
)
}
}
export default Assets
* 加入html-loader
识别html
文件
{
test: /\.(html)$/,
loader: 'html-loader'
}
配置别名
resolve: {
modules: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname,'node_modules'),
],
alias: {
components: path.resolve(__dirname, '/src/components'),
},
}
加入eslint-loader
{
enforce:'pre',
test:/\.js$/,
exclude:/node_modules/,
include:resolve(__dirname,'/src/js'),
loader:'eslint-loader'
}
resolve
解析配置,为了为了给所有文件后缀省掉 js jsx json
,加入配置
resolve: {
extensions: [".js", ".json", ".jsx"]
}
加入HTML
文件压缩,自动将入门的js
文件注入html
中,优化HTML
文件
new HtmlWebpackPlugin({
template: './public/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
}
}),
SSR
同构直出热调试
- , 采用
webpack watch+nodemon
结合的模式实现对SSR
热调试的支持。node
服务需要的html/js
通过webpack
插件动态输出,当nodemon
检测到变化后将自动重启,html
文件中的静态资源全部替换为dev
模式下的资源,并保持socket
连接自动更新页面。 - 实现热调试后,调试流程大幅缩短,和普通非直出模式调试体验保持一致。下面是SSR热调试的流程图:
加入 babel-loader
还有 解析JSX ES6
语法的 babel preset
-
@babel/preset-react
解析jsx语法
-
@babel/preset-env
解析es6
语法 -
@babel/plugin-syntax-dynamic-import
解析react-loadable
的import
按需加载,附带code spliting
功能 -
["import", { libraryName: "antd-mobile", style: true }],
Antd-mobile的按需加载
{
loader: 'babel-loader',
options: { //jsx语法
presets: ["@babel/preset-react",
//tree shaking 按需加载babel-polifill
["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],
plugins: [
//支持import 懒加载
"@babel/plugin-syntax-dynamic-import",
//andt-mobile按需加载 true是less,如果不用less style的值可以写'css'
["import", { libraryName: "antd-mobile", style: true }],
//识别class组件
["@babel/plugin-proposal-class-properties", { "loose": true }],
],
cacheDirectory: true
},
}
特别提示,如果电脑性能不高,不建议开启babel
缓存索引,非常吃内存,记得每次开发完了清理内存
加入thread-loader
,在babel
首次编译后开启多线程
const os = require('os')
{
loader: 'thread-loader',
options: {
workers: os.cpus().length
}
}
加入单独抽取CSS
文件的loader
和插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
test: /\.(less)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader', options: {
modules: true,
localIdentName: '[local]--[hash:base64:5]'
}
},
{loader:'postcss-loader'},
{ loader: 'less-loader' }
]
}
new MiniCssExtractPlugin({
filename:'[name].[contenthash:8].css'
}),
CSS
的tree shaking
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
// 清除无用 css
new PurifyCSS({
paths: glob.sync([
// 要做 CSS Tree Shaking 的路径文件
path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对 html 文件进行 tree shaking
path.resolve(__dirname, './src/*.js')
])
})
]
对小图片进行base64
处理,减少http
请求数量,并对输出的文件统一打包处理
{
test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[name].[hash:8].[ext]',
}
}, {
exclude: /\.(js|json|less|css|jsx)$/,
loader: 'file-loader',
options: {
outputPath: 'media/',
name: '[name].[hash].[ext]'
}
}
]
}]
},
加入单独抽取CSS
文件的loader
和插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
test: /\.(less)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader', options: {
modules: true,
localIdentName: '[local]--[hash:base64:5]'
}
},
{loader:'postcss-loader'},
{ loader: 'less-loader' }
]
}
new MiniCssExtractPlugin({
filename:'[name].[contenthash:8].css'
}),
加入压缩css
的插件
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
new OptimizeCssAssetsWebpackPlugin({
cssProcessPluginOptions:{
preset:['default',{discardComments: {removeAll:true} }]
}
}),
加入每次打包输出文件清空上次打包文件的插件
const CleanWebpackPlugin = require('clean-webpack-plugin')
new CleanWebpackPlugin()
加入图片压缩
{
test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
use:[
{loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[name].[hash:8].[ext]',
outputPath:'/img'
}},
{
loader: 'img-loader',
options: {
plugins: [
require('imagemin-gifsicle')({
interlaced: false
}),
require('imagemin-mozjpeg')({
progressive: true,
arithmetic: false
}),
require('imagemin-pngquant')({
floyd: 0.5,
speed: 2
}),
require('imagemin-svgo')({
plugins: [
{ removeTitle: true },
{ convertPathData: false }
]
})
]
}
}
]
}
加入代码混淆,反编译
var JavaScriptObfuscator = require('webpack-obfuscator');
// ...
// webpack plugins array
plugins: [
new JavaScriptObfuscator ({
rotateUnicodeArray: true
}, ['excluded_bundle_name.js'])
],
加入 PWA
的插件 , WorkboxPlugin
-
pwa
这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的lavas
框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
clientsClaim: true, //让浏览器立即servece worker被接管
skipWaiting: true, // 更新sw文件后,立即插队到最前面
importWorkboxFrom: 'local',
include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
}),
加入预加载preload
new PreloadWebpackPlugin({
rel: 'preload',
as(entry) {
if (/\.css$/.test(entry)) return 'style';
if (/\.woff$/.test(entry)) return 'font';
if (/\.png$/.test(entry)) return 'image';
return 'script';
},
include: 'allChunks'
//include: ['app']
}),
加入预渲染
const PrerenderSPAPlugin = require('prerender-spa-plugin')
new PrerenderSPAPlugin({
routes: ['/','/home','/shop'],
staticDir: resolve(__dirname, '../dist'),
}),
我这套webpack
配置,无论多复杂的环境,都是可以搞定的
-
webpack
真的非常非常重要,如果用不好,就永远是个初级前端 - 只要
webpack
不更新到5,以后就不出webpack
的文章了 -
webpack4
大结局,谢谢 - 以后会出一些偏向跨平台技术,原生
javascript
,TS
,Golang
等内容的文章
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。