前言
前端工程化主要解决效率
和质量
的问题;webpack是一种实现前端工程化的有效方案。
基本配置思路图
基本概念
webpack类似一个加工处理器,通过灵活的配置文件实现复杂需求;主要配置有入口、出口、装在器、插件
资料
- entry/output: 出口/入口
- loader/plugin: 装载器/插件 加工处理
-
moudle/chunk/bundle:
- moudle: 源码每个文件可称为一个模块
- chunk: 通过webpack处理后的文件为chunk代码块,一般由多个模块构成
- bundle: 最终产品,一般为chunk处理后的代码块
主要任务
自动化处理js,css,html,图片等静态资源
脚本处理
主要让javascript适应宿主环境,竟可能的压缩体积和复用
es6
ployfill : 全局垫片,注入全局对象,污染全局
runtime : 局部垫片,按需局部注入,不会污染全局
presets
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage|entry|false" // usage 按需加载polyfill
}]
]
}
browerslist : 兼容到目标浏览器标准
ts
// ts.config.json
{
"compilerOptions": {
"module": "commonjs",
"target": "es5"
},
"exclude": \["./node\_modules"\]
}
样式处理
css处理
style-loader (合并style标签,transform插入页面前对其修改)
css-loader (css模块化功能)(importLoaders
解决css中@import未能处理loader的问题)
mini-css-extract-plugin(提取css至单独文件)
css预处理
less-loader
sass-loader
stylus-loader
css后处理
postcss
autoprefixer : 兼容浏览器样式
postcss-preset-env : 可设置目标css规范
cssnano是一个强大的 PostCss 插件,在 CSS 压缩优化中会经常被用到,它有别于常规的 CSS 压缩工具只是去除空格注释,还支持根据 CSS 语法解析结果智能压缩代码
静态资源处理
html处理
html-webpack-plugin : 打包并处理html文件至目标文件夹
文件处理
file-loader : 允许js加载其他资源;如, 图片,字体,媒体 等
url-loader : 在file-loader基础上封装;如, 允许将图片转成base64格式
tips: 注意3.0版本 esMoudle属性
图片
svg-url-loader 的工作原理类似于 url-loader
,除了它利用 URL encoding 而不是 Base64 对文件编码。
可以借助img-webpack-loader来对使用到的图片进行优化。它支持 JPG、PNG、GIF 和 SVG 格式的图片。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// 这会应用该 loader,在其它之前
enforce: 'pre'
}
]
}
};
通过enforce: 'pre'
我们提高了 img-webpack-loader 的优先级,保证在url-loader
和svg-url-loader
之前就完成了图片的优化。
另外img-webpack-loader默认的配置就已经适用于日常开发图片的压缩优化需求了,但是如果你想更进一步去配置它,参考插件选项。要选择指定选项,请查看国外牛人写的一个图像优化指南。
字体/媒体
{
// 文件解析
test: /\.(eot|woff|ttf|woff2|appcache|mp4|pdf)(\?|$)/,
loader: 'url-loader',
query: {
// 这么多文件,ext不同,所以需要使用[ext]
name: 'assets/[name].[hash:7].[ext]'
}
},
数据
要导入 CSV,TSV 和 XML,可以使用csv-loader和xml-loader。
{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},
{
test: /\.xml$/,
use: [
'xml-loader'
]
}
CDN部署
一般静态资源上线的时候都会放到 CDN,假设我们的 CDN 域名和路径为:http://bd.xxx.com/img/
,这时候只需要修改output.publicPath
即可
module.exports = {
//..
output: {
publicPath: 'http://bd.xxx.com/img/'
}
//..
};
框架处理
vue
react
jsx
hot-module-replacement-plugin
// babelrc
presets: [
// 添加 preset-react
require.resolve('@babel/preset-react'),
[require.resolve('@babel/preset-env'), {modules: false}]
],
// index.js
if (module.hot) {
module.hot.accept(err => {
if (err) {
console.error('Cannot apply HMR update.', err);
}
});
}
辅助任务
本地开发调试
webapck-dev-server(错误遮罩,代理请求,热更新,重定向)
webpack.HotModuleReplacementPlugin 插件来开启全局的 HMR 能力
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 9000,
// 开启 hmr 支持
hot: true,
overlay: true, // 错误遮罩
proxy: { // 接口代理
...
}
},
plugins: [
// 添加 hmr plugin
new webpack.HotModuleReplacementPlugin()
]
};
// 在入口文件index.js最后添加如下代码
if (module.hot) {
// 通知 webpack 该模块接受 hmr
module.hot.accept(err => {
if (err) {
console.error('Cannot apply HMR update.', err);
}
});
}
错误定位
devtool开启sourceMap方便定位错误 : evl-source-map(开发) / source-map(生产)
module.exports = {
...
devtool: 'evl'
}
代码风格检查
- 检查js : eslint-loader / eslint-friendly-formatter 让 ESLint 报错更加好看一些 / ts-lint
- 检查css : stylelint-webpack-plugin
{
test: /\.js$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [path.resolve(__dirname, 'src')], // 指定检查的目录
options: { // 这里的配置项参数将会被传递到 eslint 的 CLIEngine
formatter: require('eslint-friendly-formatter') // 指定错误报告的格式规范
}
}
const StyleLintPlugin = require('stylelint-webpack-plugin');
module.exports = {
// ...
plugins: [new StyleLintPlugin(options)]
// ...
};
优化方案
代码质量方向
代码分割
- 多入口分割思路: 主业务代码 + 公共依赖 + 第三方依赖 + webapck运行代码
- 单入口分割思路: 主业务代码 + 异步依赖 + 第三方依赖 + webpack运行代码
splitChunks
由于 Webpack 做到了开箱即用,所以splitChunks
是有默认配置的:
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'async', // 三选一: "initial" | "all" | "async" (默认)
minSize: 30000, // 最小尺寸,30K,development 下是10k,越大那么单个文件越大,chunk 数就会变少(针对于提取公共 chunk 的时候,不管再大也不会把动态加载的模块合并到初始化模块中)当这个值很大的时候就不会做公共部分的抽取了
maxSize: 0, // 文件的最大尺寸,0为不限制,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize
minChunks: 1, // 默认1,被提取的一个模块至少需要在几个 chunk 中被引用,这个值越大,抽取出来的文件就越小
maxAsyncRequests: 5, // 在做一次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了
maxInitialRequests: 3, // 针对一个 entry 做初始化模块分隔的时候的最大文件数,优先级高于 cacheGroup,所以为 1 的时候就不会抽取 initial common 了
automaticNameDelimiter: '~', // 打包文件名分隔符
name: true, // 拆分出来文件的名字,默认为 true,表示自动生成文件名,如果设置为固定的字符串那么所有的 chunk 都会被合并成一个
/** =========核心配置cacheGroups======= **/
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk
priority: -10 // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级
},
default: {
minChunks: 2,
priority: -20, // 优先级
reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
}
}
}
}
};
splitChunks默认配置对应的就是 chunk 生成的第二种情况:通过写代码时主动使用import()或者require.ensure来动态加载。
Tips:除了 JavaScript,splitChunks
也适用于使用mini-css-extract-plugin插件的 css 配置。
runtimeChunk
Webpack 打包时,除了模块代码之外,Webpack 的 bundle 中还包含了 Runtime(运行时),这部分代码是一小段用来管理模块执行和加载的代码。
// webpack.config.js
module.exports = {
optimization: {
runtimeChunk: true
}
};
// 分离webapck 运行时代码
打包分析
-
官方版本
-
社区版本
体积优化
js压缩
在mode=production
下,Webpack 会自动压缩代码,我们可以自定义自己的压缩工具,这里推荐 terser-webpack-plugin,它是 Webpack 官方维护的插件
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new TerserPlugin({
// 使用 cache,加快二次构建速度
cache: true,
terserOptions: {
comments: false,
compress: {
// 删除无用的代码
unused: true,
// 删掉 debugger
drop_debugger: true, // eslint-disable-line
// 移除 console
drop_console: true, // eslint-disable-line
// 移除无用的代码
dead_code: true, // eslint-disable-line
parallel: true // 多线程
}
}
})]
}
};
作用域提升(Scope Hoisting)是指 webpack 通过 ES6 语法的静态分析,分析出模块之间的依赖关系,尽可能地把模块放到同一个函数中。
Tips:其实 webpack 4 中,在 production 模式下已经根据大多数项目的优化经验做了通用的配置,类似 Tree-Shaking、Scope Hoisting 都是默认开启的,而且最新版本的 Webpack 使用的压缩工具就是 terser-webpack-plugin。
tree-shaking
在 Webpack 中,Tree-Shaking 是需要配合mode=production
来使用的,这是因为 Webpack 的 Tree-Shaking 实际分了两步来实现
- Webpack 自己来分析 ES6 Modules 的引入和使用情况,去除不使用的
import
引入; - 借助工具(如
uglifyjs-webpack-plugin
或terser-webpack-plugin
)进行删除,这些工具只在mode=production
中会被使用。
// babelrc
presets: [
[require.resolve('@babel/preset-env'), {modules: false}] // 防止babel转义模块,导致tree-shaking实效
],
css压缩
首先我们的 CSS 文件应该是导出到单独的 CSS 文件中,而不要直接打包到 JavaScript 文件中,然后通过style-loader
的 addStyles
方法添加进去,导出 CSS 文件就需要使用mini-css-extract-plugin这个插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].[contenthash:8].css'
})
],
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
hmr: process.env.NODE_ENV === 'development'
}
},
'css-loader'
]
}
]
}
};
cssnano是基于 postcss 的一款功能强大的插件包,它集成了 30 多个插件,只需要执行一个命令,就可以对我们的 CSS 做多方面不同类型的优化,
在 Webapck 中,css-loader 已经集成了 cssnano,我们还可以使用optimize-css-assets-webpack-plugin来自定义 cssnano 的规则。optimize-css-assets-webpack-plugin 是一个 CSS 的压缩插件,默认的压缩引擎就是 cssnano。我们来看下怎么在 Webpack 中使用这个插件:
// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
plugins: [
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'), // 这里制定了引擎,不指定默认也是 cssnano
cssProcessorPluginOptions: {
preset: ['default', {discardComments: {removeAll: true}}]
},
canPrint: true
})
]
};
图片资源压缩
如果我们的项目中小图片特别多,例如有很多 icon 类的图标,这时候则推荐使用雪碧图(CSS Sprite)来合并这些小图到一张大图中,然后使用background-position
来设置图片的位置,通过这样的方式可以节省多次小图片的请求。
// postcss.config.js
const postcssSprites = require('postcss-sprites');
module.exports = {
plugins: [
postcssSprites({
// 在这里制定了从哪里加载的图片被主动使用css sprite
// 可以约定好一个目录名称规范,防止全部图片都被处理
spritePath: './src/assets/img/'
})
]
};
// tips: 雪碧图的位置时根据图片原图的位置而确定的,在多端适配问题上可能会有问题
对于大图片来说,可以使用image-webpack-loader来压缩图片,image-webpack-loader 它支持 JPG、PNG、GIF 和 SVG 格式的图片,因此我们在碰到所有这些类型的图片都会使用它。
缓存优化
webpack.mode介绍
浏览器只会在文件名发生改变(或者浏览器缓存策略失效)时才会请求网络。在使用 Webpack 构建项目的时候,同样可以做到自动更新,但 Webpack 使用的不是版本号,而是指定哈希值(hash),Webpack 的 hash 值有三种:
-
hash
:每次编译 Compilation 对象的 hash,全局一致,跟单次编译有关,跟单个文件无关,不推荐使用; -
chunkhash
:chunk 的 hash,根据不同的 chunk 及其包含的模块计算出来的 hash,chunk 中包含的任意模块发生变化,则 chunkhash 发生变化,推荐使用; -
contenthash
:CSS 文件特有的 hash 值,是根据 CSS 文件内容计算出来的,CSS 发生变化则其值发生变化,推荐 CSS 导出中使用。
Javascript运行时优化
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
const configuration = {};
module.exports = {
// ...
plugins: [
new PrepackWebpackPlugin(configuration)
]
};
开发效率方向
项目本身相关
- 减少依赖嵌套深度
- 合理使用 polyfill,防止多余的代码
- 使用 ES6 语法,尽量不使用具有副作用的代码,以加强 Tree-Shaking 的效果
Webpack相关
减少查找过程
使用 resolve.alias
减少查找过程
使用 resolve.extensions
优先查找
合理配置 rule 的查找范围,设置inclue,exclude范围
module.exports = {
resolve: {
// 顺序从前到后查找
extensions: \['.js', '.jsx', '.ts', '.tsx', '.vue'\],
// 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
// 减少耗时的递归解析操作
alias: {
react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
'@assets': resolve('./src/assets')
}
},
// 排除不需要解析的模块
module: {
noParse: /node_modules\/(jquey\.js)/;
}
};
利用多线程提升构建速度
thread-loader : 是针对 loader 进行优化的,它会将 loader 放置在一个 worker 池里面运行,以达到多线程构建
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader'
// 你的高开销的loader放置在此 (e.g babel-loader)
]
}
]
}
};
happypack : 利用多线程模型来提高构建速度 / 支持列表
// webpack.config.js
const os = require('os');
const HappyPack = require('happypack');
// 根据 cpu 数量创建线程池
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'happypack/loader?id=jsx'
},
{
test: /\.less$/,
use: 'happypack/loader?id=styles'
}
]
},
plugins: [
new HappyPack({
id: 'jsx',
// 多少个线程
threads: happyThreadPool,
loaders: ['babel-loader']
}),
new HappyPack({
id: 'styles',
// 自定义线程数量
threads: 2,
loaders: ['style-loader', 'css-loader', 'less-loader']
})
]
};
预编译处理
预先编译和打包不会变动存在的文件,在业务代码中直接引入,加快 Webpack 编译打包的速度,但是并不能减少最后生成的代码体积。
wepack.DllPlugin
add-asset-html-webpack-plugin可将dll处理文件自动插入html中
// webpack.config.dll.js
const webpack = require('webpack');
// 这里是第三方依赖库
const vendors = ['react', 'react-dom'];
module.exports = {
mode: 'production',
entry: {
// 定义程序中打包公共文件的入口文件vendor.js
vendor: vendors
},
output: {
filename: '[name].[chunkhash].js',
// 这里是使用将 verdor 作为 library 导出,并且指定全局变量名字是[name]_[chunkhash]
library: '[name]_[chunkhash]'
},
plugins: [
new webpack.DllPlugin({
// 这里是设置 mainifest.json 路径
path: 'manifest.json',
name: '[name]_[chunkhash]',
context: __dirname
})
]
};
// webpack.config.js
const webpack = require('webpack');
module.exports = {
output: {
filename: '[name].[chunkhash].js'
},
entry: {
app: './src/index.js'
},
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// 这里导入 manifest配置内容
manifest: require('./manifest.json')
})
]
};
压缩优化
terser-webpack-plugin : 开启多线程(parallel)和缓存(cache)
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
cache: true, // 开启缓存
parallel: true // 多线程
})
]
}
};
缓存优化
如babel-loader中cacheDirectory
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
cacheDirectory: true
},
// 排除路径
exclude: /node_modules/,
// 查找路径
include: [path.resolve('.src')]
}
];
其他优化
适当选择sourceMap的devtool值
切换一些 loader 或者插件,比如:fast-sass-loader可以并行处理 sass 文件,要比 sass-loader 快 5~10 倍;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。