# Webpack 基础知识

此处记录 webpack 中的一些入门知识点, 略微细碎, 主要用于回顾
# Module & Chunk & Bundle
module: 模块就是模块可以是 es 模块也可以是 commonJS 或者 AMD 模块
chunk: 打包过程中被操作的模块文件叫做 chunk,例如异步加载一个模块就是一个 chunk
bundle: bundle 是最后打包后的文件,最终文件可以和 chunk 长的一模一样,但是大部分情况下他是多个 chunk 的集合
另外的理解(摘至网络):
module:
就是 js 的模块化 webpack 支持 commonJS、ES6 等模块化规范,简单来说就是你通过 import 语句引入的代码。
chunk:
chunk 是 webpack 根据功能拆分出来的,包含三种情况:
1、你的项目入口(entry)
2、通过 import()动态引入的代码
3、通过 splitChunks 拆分出来的代码
chunk 可以包含多个 module, 是由 module 组成的 。
bundle:
bundle 是 webpack 打包之后的各个文件,一般就是和 chunk 是一对一的关系,bundle 就是对 chunk 进行编译压缩打包等处理之后的产出。
# 库(Library)
如果想将自己的工程作为一个第三方库提供给其他人使用,可以参考配置方法 (opens new window)
var path = require('path')
module.exports = {
// mode: "development || "production",
entry: {
alpha: './alpha',
beta: './beta'
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'MyLibrary.[name].js',
// 征对多页面,可以配置为数组,MyLibrary作为整体的命名空间,[name]作为该空间的属性调用
// 示例地址:
//https://github.com/webpack/webpack/tree/master/examples/multi-part-library
library: ['MyLibrary', '[name]'],
libraryTarget: 'umd'
}
}
# 入口(Entry)
// 对象式
{
entry:{
app:'./file.js',
vendor: './file1.js', //多页面
normal: ['jquery','lodash'], //两个chunk打包到一个bundle
'js/home': './files2.js' // 会生成到js/home文件夹下
}
}
// 字符串式
{
entry:'app' // 等同于 entry:{ main:'app.js' }
}
# Npm3 的 peerDependency
假如 项目 project-main 依赖的 package-a(dependency) 的 package.json 中声明了 peerDependency 是 package-apeer@^1.0.0,而 project-main 中没有任何 package-apeer 的配置,此时在 project-main 下使用 npm3 执行 npm install,控制台就会告警 UNMET PEER DEPENDENCY package-apeer@^1.0.0,意思就是说使用到 package-a 的项目必须安装同时安装 package-apeer@^1.0.0 ,否则程序就可能会有异常,而在 npm@1 和 npm@2 下,就不会报错而是自动把 package-apeer@^1.0.0 安装上,因为很多用户反应这样很困惑,我没声明这个包,你为什么要给我安装呢?所以在 npm@3 中这个 peerDependencies 如果没装就变成了控制台告警。
npm3 的官方文档 中 记录到:
通常是在插件开发的场景下,你的插件需要某些依赖的支持,但是你又没必要去安装,因为插件的宿主会去安装这些依赖,你就可以用 peerDependencies 去声明一下需要依赖的插件和版本,如果出问题 npm 就会有警告来提醒使用者去解决版本冲突问题。
# Html-webpack-plugin 疑惑选项
chunks
chunks 选项的作用主要是针对多入口(entry)文件。当你有多个入口文件的时候,对应就会生成多个编译后的 js 文件。那么 chunks 选项就可以决定是否都使用这些生成的 js 文件。 chunks 默认会在生成的 html 文件中引用所有的 js 文件,当然你也可以指定引入哪些特定的文件。 看一个小例子。
// webpack.config.js entry: { index: path.resolve(__dirname, './src/index.js'), index1: path.resolve(__dirname, './src/index1.js'), index2: path.resolve(__dirname, './src/index2.js') } ... plugins: [ new HtmlWebpackPlugin({ ... chunks: ['index','index2'] }) ]
执行 webpack 命令之后,你会看到生成的 index.html 文件中,只引用了 index.js 和 index2.js, 而如果没有指定 chunks 选项,默认会全部引用。
... <script type=text/javascript src=index.js></script> <script type=text/javascript src=index2.js></script>
excludeChunks
弄懂了 chunks 之后,excludeChunks 选项也就好理解了,跟 chunks 是相反的,排除掉某些 js 文件。 比如上面的例子,其实等价于下面这一行
... excludeChunks: ['index1.js']
# 加载器(Loaders)
loaders 配置项 use 和 loader 的区别参考链接-webpack1 升级 webpack2 (opens new window)
webpack1 是使用 loader 选项,而 webpack2 以上的版本都建议直接使用 use 选项,具体的更新如下:
- 外层 loaders 改为 rules
- 内层 loader 改为 use,也可以用 loader
- 所有插件必须加上 -loader,不再允许缩写
- 不再支持使用!连接插件,改为数组形式
- json-loader 模块移除,不再需要手动添加,webpack2 会自动处理
Webpack1 中如下配置
module: {
loaders: [
{
test: /\.(less|css)$/,
loader: 'style!css!less!postcss'
},
{
test: /\.json$/,
loader: 'json'
}
]
}
Webpack2 却使用如下配置
module: {
rules: [
{
test: /\.(less|css)$/,
use: ['style-loader', 'css-loader', 'less-loader', 'postcss-loader']
}
]
}
使用示例
module: {
rules: [
{
test: /\.jsx$/,
loader: "babel-loader", // Do not use "use" here
options: {
// ...
}
},
{
test: /\.less$/,
// 可以配置成字符串
loader: "style-loader!css-loader!less-loader"
// 也可以配置成数组,并通过queryString来设定选项(传参给loader)
use: ["style-loader", "css-loader?minimize", "less-loader"],
// 使用options来设定选项
use: [
{
loader: "css-loader",
options: {
minimize:true
}
}
]
}
];
}
补充说明一下, 正如 Webpack 2 迁移教程所述,两者之间的区别在于,如果我们想要一个加载器数组,我们必须使用 use,如果它只是一个加载器,那么我们必须使用 loader:
module: {
rules: [
{
test: /\.jsx$/,
loader: "babel-loader", // Do not use "use" here
options: {
// ...
}
},
{
test: /\.less$/,
loader: "style-loader!css-loader!less-loader"
use: [
"style-loader",
"css-loader",
"less-loader"
]
}
]
}
```
loader 也可以使用 import 或者 require 直接指定:
require('style-loader!css-loader?minimize!./main.css')
import Styles from 'style-loader!css-loader?modules!./styles.css'
// 选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?{"key":"value","foo":"bar"}
Loader 的几种用法归纳
第一种: use: ['xxx-loader', 'xxx-loader']
第二种: loader: ['style-loader', 'css-loader']
第三种: use: [{ loader: 'style-loader' }, { loader: 'css-loader' }]
# 常用 loaders
css-loader
css-laoder 是解释 @import 'a.css' 和 @import url(a.css)等引入的.css 文件, 将其加载到 js 文件中,便于 webpack 的其他 loader 处理
style-loader
style-loaders 用于处理将所有的样式文件插入到<style></style>
中
url-loader
url-loader 是用于处理文件(css/js)中的中引用的资源. 例如图片引入, 小图片转换成 base64...
postcss-loader
postcss-loader 的 autoprefixer 实现将 css3 属性添加上厂商前缀
# 常用 plugins
DllPlugin & DllReferencePlugin (动态链接库插件)
dll 插件一般只应用于开发环境,正式环境中不建议这么做
{ plugins: [ // 接入 DllPlugin new webpack.DllPlugin({ // 动态链接库的全局变量名称,需要和 output.library 中保持一致 // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值 name: '[name]_dll_[hash]', // 描述动态链接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, 'dist/dll', '[name].manifest.json') }) ] }
plugins: [ // 接入 DllPlugin new DllReferencePlugin({ context: __dirname, // 描述 react 动态链接库的文件内容 manifest: require('./dist/react.manifest.json') }) ]
webpack-bundle-analyzer (性能分析插件)
WebpackMd5Hash
注意: webpack-md5-hash 有相关问题请查看 (opens new window)
使用该插件后,模块与公共代码的映射关系文件(manifest),将不会 随着模块的改动来重新 计算公共代码的 chunkhash
因此 webpack-md5-hash 并没有解决之前的问题:
如何生成稳定的模块 ID? 如何避免频繁的 chunk 内容变动?
使用 NamedModulesPlugin 来解决
NamedChunksPlugin
webpack4.25 以上有两个选项来控制这个插件: namedChunks 和 chunkIds
这两个插件在更高的版本已经合并为了 chunkIds,详见链接 (opens new window)
固化 runtime 代码 内以及在使用动态加载时分离出的 chunk 的 chunk id
NamedModulesPlugin(webpack4 开发模式默认值,不需要单独配置了)
当开启 HMR 的时候使用该插件会显示模块的相对路径,建议用于开发环境。 开发环境下使用来固化 module id webpack.NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块,external 模块是不会起作用的。 征对上述的问题解决办法:
异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:
import(/* webpackChunkName: "lodash" */ 'lodash').then()
这样就有 Name 了所以我们需要再使用一个插件:
name-all-modules-plugin
这个插件中用到一些老的 API,Webpack 4 会发出警告,这个 pr (opens new window) 有新的版本,不过作者不一定会 merge。我们使用的时候可以直接 copy 这个插件的代码到我们的 Webpack 配置里面。//pr的核心代码 class NameAllModulesPlugin { apply(compiler) { compiler.hooks.compilation.tap("NameAllModulesPlugin", compilation => { compilation.hooks.beforeModuleIds.tap("NameAllModulesPlugin", modules => { for (const module of modules) { if (module.id === null) { module.id = module.identifier(); } } } }) } }
HashedModuleIdsPlugin
在生产环境下使用来固化 module id,就是在 namedModulesPlugin 的基础上做了路径的 hash,简化路径值, 减小 chunk 的
CommonsChunkPlugin(webpack3 中的公共代码提取插件)
HashedModuleIdsPlugin: 它是根据模块相对路径生成模块标识,如果模块没有改变,那模块标识也不会改变, 改变 webpack->entry 的顺序也将不改变模块的 ID, 也就不会影响 hash 和 chunkhash 的改变
{ plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: ['react', 'common'], // 用于提取manifest minChunks: Infinity // Infinity不会打包任何多余的代码 }), new webpack.HashedModuleIdsPlugin(), new WebpackMd5Hash() ] }
splitChunks (webpack4)
默认的分包策略:(条件需要全满足)
- 新的 chunk 是否被共享2次以上或者是来自 node_modules 的模块 - 新的 chunk 体积在压缩之前是否大于 30kb - 按需加载 chunk 的并发请求数量小于等于 5 个 - 页面初始加载时的并发请求数量小于等于 3 个
默认配置:
module.exports = {
optimization: {
// 提取webpack运行时的代码块单独引入到入口文件
// 而不是直接混入每个chunk,可以减小入口的代码体积
// 存储着 webpack 对 module 和 chunk 的解析信息
// 主要作用: 把entry中不相关的 module id 或者说内容摒除在外
//
runtimeChunk: true,
splitChunks: {
//范围:异步加载的模块中的引入import才进行拆分
chunks: 'async', // inital(入口) all(所有) async(异步)
minSize: 30000, // 大于30kb的
minChunks: 1, // 引用一次就拆分
maxAsyncRequests: 5, // 异步加载模块最多可以拆分的块数量
maxInitialRequests: 3, // 一个入口模块最多可以拆分的块数量
automaticNameDelimiter: '~', // 模块拆分名称的连接符
// 值为true:webpack会基于代码块和缓存组的key自动选择一个名称
// 当一个名称匹配到相应的入口名称,这个入口会被移除。
name: true,
//命中以下规则将被代码拆分
cacheGroups: {
vendors: {
// node_modules中的模块
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
// default值改为false则关闭这个匹配条件
minChunks: 2, // 页面引用2次以上的模块
priority: -20, // 同优先级的规则,以从上到下来匹配
// 复用已经存在的代码块
// 需要在精确匹配到对应模块时候才会生效
reuseExistingChunk: true
}
}
}
}
}
maxInitialRequests: 一个入口模块最多可以拆分的代码块 chunk 数量
* 规则:
- 入口文件本身算一个请求
- 如果入口里面有动态加载得模块这个不算在内
- 通过 runtimeChunk 拆分出的 runtime 不算在内
- 只算 js 文件的请求,css 不算在内
- 如果同时又两个模块满足 cacheGroup 的规则要进行拆分,
但是 maxInitialRequests 的值只能允许再拆分一个模块,
那尺寸更大的模块会被拆分出来
maxAsyncRequests: 异步按需加载模块最多可拆分的代码块 chunk 数量
* 规则:
- import()文件本身算一个请求
- 并不算 js 以外的公共资源请求比如 css
- 如果同时又两个模块满足 cacheGroup 的规则要进行拆分,
但是 maxInitialRequests 的值只能允许再拆分一个模块,
那尺寸更大的模块会被拆分出来
ParallelUglifyPlugin(并行插件)
注意: 该插件已经无人 维护,可以用 terser-webpack-plugin
这个插件可以帮助有很多入口点的项目加快构建速度。把对 JS 文件的串行压缩变为开启多个子进程并行进行 uglify。
// webpck.config.js const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); plugins: [ new ParallelUglifyPlugin({ workerCount: 4, uglifyJS: { output: { beautify: false, // 不需要格式化 comments: false // 保留注释 }, compress: { // 压缩 warnings: false, // 删除无用代码时不输出警告 drop_console: true, // 删除console语句 // 内嵌定义了但是只有用到一次的变量 collapse_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 reduce_vars: true } } }); ]
speed-measure-webpack-plugin
用来检测 webpack 打包过程中各个部分所花费的时间
webpack 动态引入资源, 使用 prefetch 和 preload
// webpackChunkName: 打包后的文件名,防止出现1.js,2.js这种 // webpackPrefetch: 开启prefetch (页面空闲时请求, 优先级中等) // webpackPreload: 开启preload (更高的优先级,不阻塞onload事件) import( /* webpackPrefetch: true */ /*webpackChunkName: 'topic'*/ '../topic' )
ParallelUglifyPlugin(并行多进程压缩 js 文件)
module.exports = { plugins: [ new ParallelUglifyPlugin({ // 传递给 UglifyJS 的参数 uglifyJS: { output: { // 最紧凑的输出 beautify: false, // 删除所有的注释 comments: false }, compress: { // 在UglifyJs删除没有用到的代码时不输出警告 warnings: false, // 删除所有的 `console` 语句,可以兼容ie浏览器 drop_console: true, // 内嵌定义了但是只用到一次的变量 collapse_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 reduce_vars: true } } }) ] }
ModuleConcatenationPlugin(开启 scope hosting 支持, 生产模式默认值)
对于使用 ES6 的代码,分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin') module.exports = { resolve: { // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件 mainFields: ['jsnext:main', 'browser', 'main'] }, plugins: [ // 开启 Scope Hoisting new ModuleConcatenationPlugin() ] }
webpack.DefinePlugin(定义环境变量)
该插件用于替换代码中的部分字段,例如定义了下面代码中的方式,那么在客户端中遇到 process.env.NODE_ENV 就会被替换成后面的值(webpack.DefinePlugin 将替换 process.env.NODE_ENV 为您定义的 development)
new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"development"' } }) new webpack.DefinePlugin({ PRODUCTION: JSON.stringify(true), VERSION: JSON.stringify('5fa3b9'), BROWSER_SUPPORTS_HTML5: true, TWO: '1+1', 'typeof window': JSON.stringify('object') })
DefinePlugin
可能会被误认为其作用是在 webpack 配置文件中为编译后的代码上下文环境设置全局变量,但其实不然。它真正的机制是:
DefinePlugin
的参数是一个 object,那么其中会有一些 key-value 对。在 webpack 编译的时候,会把业务代码中没有定义(使用var/const/let
来预定义的)而变量名又与 key 相同的变量(直接读代码的话的确像是全局变量)替换成 value。例如上面的官方例子,PRODUCTION 就会被替换为 true;VERSION 就会被替换为'5fa3b9'(注意单引号);BROWSER_SUPPORTS_HTML5 也是会被替换为 true;TWO 会被替换为 1+1(相当于是一个数学表达式);typeof window 就被替换为'object'了。
再举个例子,比如你在代码里是这么写的:
if (!PRODUCTION) console.log('Debug info') if (PRODUCTION) console.log('Production log')
那么在编译生成的代码里就会是这样了:
if (!true) console.log('Debug info') if (true) console.log('Production log')
而如果你用了 UglifyJsPlugin,则会变成这样:
console.log('Production log')
terser-webpack-plugin
terser-webpack-plugin (opens new window) 是一个使用 terser 压缩 js 的 webpack 插件
压缩是发布前处理最耗时间的一个步骤,如果是你是在 webpack 4 中,只要几行代码,即可加速你的构建发布速度
const TerserPlugin = require('terser-webpack-plugin') module.exports = { optimization: { minimizer: [ new TerserPlugin( (parallel: true) // 多线程 ) ] } }
# 常用优化手段
webpack 的优化手段主要从两个方向来进行:
- 减少 Webpack 的打包时间
- 减小 Webpack 打包体积
# 减少打包时间策略
- 缩小搜索范围
webpack 的各种路径搜索递归查找很耗时间,因此对部分选项进行明确的路径配置,减少搜索时间
module.exports = {
module: {
// 由于 Loader 对文件的转换操作很耗时,需要让尽可能少的文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src')
}
],
//一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义
noParse: [/react\.min\.js$/]
},
/*
resolve.modules 的默认值是 ['node_modules'],含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。
当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下
*/
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
// 入口文件配置
mainFields: ['main'],
alias: {
/* 默认情况下 Webpack 会从入口文件 ./node_modules/react/react.js 开始递归的解析和处理依赖的几十个文件,这会时一个耗时的操作。 通过配置 resolve.alias 可以让 Webpack 在处理 React 库时,直接使用单独完整的 react.min.js 文件,从而跳过耗时的递归解析操作 */
react: path.resolve(__dirname, './node_modules/dist/react.min.js')
},
/* 如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions 的配置也会影响到构建的性能。 在配置 resolve.extensions 时你需要遵守以下几点,以做到尽可能的优化构建性能:
后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把 require('./data') 写成 require('./data.json')。 */
extension: ['js']
}
}
- 使用多进程加速 loader
在 webpack 构建的过程中,最耗时的操作就是 loader 处理文件这一步, 受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]
- 使用动态链接库 DllPlugin
注意:
主要用于开发环境
DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
接下来我们就来学习如何使用 DllPlugin
// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin 将依赖文件引入项目中
// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json')
})
]
}
- babel-loader 设置缓存
设置 babel 的 cacheDirectory 为 true
babel 编译代码的过程太慢了,不仅要使用 exclude、include,尽可能准确的指定要转化内容的范畴,而且要充分利用缓存,进一步提升性能。babel-loader 提供了 cacheDirectory 特定选项(默认 false):设置时,给定的目录将用于缓存加载器的结果
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory=true',
exclude: /node_modules/,
include: [resolve('src'), resolve('test')]
}
]
}
}
- 使用 terser-webpack-plugin 加速代码压缩
注意:
webpack4 使用 terser-webpack-plugin (opens new window)
webpack3 使用 webpack-parallel-uglify-plugin (opens new window)
该插件主要用于线上环境
压缩是发布前处理最耗时间的一个步骤,因此也需要采用并行多进程的方式来开启加速,如果是你是在 webpack 4 中,只要几行代码,即可加速你的构建发布速度
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()]
//或者使用ugifyjs开启parallel
/* minimizer: [
new UglifyJsPlugin({
include: /\/includes/,
parallel: true // 或者写核心数
})
] */
}
}
- 开发环境开启热更新
为了加速本地开发环境的构建速度, 开启热模块替换能节省更多的时间,在 webpack 中只需要开启 devServer 的 hot 选项; 此时 webpack.HotModuleReplacementPlugin 自动会添加到 webpack 中
module.exports = {
//...
devServer: {
hot: true
}
}
# 减小打包体积策略
- 使用按需加载
一般按需加载都是 PWA 页面路由的懒加载,使用 esmodule 的 import()来进行加载, 此处使用 vue 的路由配置作为示例
// 如果是首页可以开启preload选项来加速显示
{
path: '/',
name: '',
meta: { title: '温馨提示' },
component: () => import(
/* webpackChunkName: "index" */
/* webpackPreload: true */
'./views/index.vue')
}
- 使用作用域提升 Scope Hoisting
Scope Hoisting 尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并,从而减小了代码体积
在 webpack4 只需要开启选项 concatenateModules
即可
module.exports = {
//...
optimization: {
concatenateModules: true
}
}
- 使用 Tree Shaking
注意
webpack 4 正式版本扩展了副作用检测能力,通过 package.json
的 "sideEffects"
属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。使用 Tree Shaking
, 应该明确标识出代码的 sideEffects
sideEffects 配置如下
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css", "..."],
// 无副作用直接设置为false
"sideEffects": false
}
TreeShaing 的开启如下
module.exports = {
//...
optimization: {
usedExports: true,
sideEffects: true // 有副作用时此处也要配置为true
}
}
- 压缩代码
webpack4 配置方式需要只需要开启 minimizer 选项即可
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new UglifyJsPlugin({
include: /\/includes/,
parallel: true,
sourceMap: true, //使用源映射将错误消息位置映射到模块
cache: true //启用文件缓存
})
]
}
}
- 提取公共代码
多个页面公共的代码抽离成单独的文件, 可以减少网络传输流量,降低服务器成本,也解决了重复代码导致的包体积变大, 再加上缓存策略;能够加速页面加载速度
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
- 使用 prepack
警告
该功能仍然是实验性的, 在生产环境不建议使用
直接上代码吧
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default
module.exports = {
plugins: [new PrepackWebpackPlugin()]
}
- 拷贝静态文件
在前文 Webpack 打包优化之体积篇中提到,引入 DllPlugin 和 DllReferencePlugin 来提前构建一些第三方库,来优化 Webpack 打包。而在生产环境时,就需要将提前构建好的包,同步到 dist 中;这里拷贝静态文件,你可以使用 copy-webpack-plugin 插件:把指定文件夹下的文件复制到指定的目录;其配置如下:
var CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
// ......
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
}
# 核心选项 optimization
optimization 包含了 webpack 关键的优化配置选项, 以下几个选项跟浏览器缓存息息相关, 此处做一个记录
namedModules
:在开发环境的打包结果中,使用路径作为模块的名称, 固化 moduleId
namedChunks
:在开发环境的打包结果中,固化 runtime 代码以及动态加载时分离出的 chunk 的 chunkId
moduleIds
:生产环境固化 moduleId
选项 描述 natural 按使用顺序的数字 ID named 可读的 ID,以进行更好的调试。 hashed 短哈希作为 id,可以更好地进行长期缓存 size 数字 ID 专注于最小的初始下载大小 total-size 数字 ID 专注于最小的总下载大小 chunkIds
:生产环境固化 runtime 和异步代码 chunkId
选项参照
moduleIds
的配置项nodeEnv
:webpack 将 process.env.NODE_ENV 设置为一个给定的字符串。如果 optimization.nodeEnv 不是 false,可以使用 DefinePlugin,配置为 optimization.nodeEnv 的值,如果为 falsy 值,可以返回退到"production"
# 部分疑惑选项记录
- include/exclude/test 的区别
test:必须满足的条件(正则表达式,不要加引号,匹配要处理的文件) exclude:不能满足的条件(排除不处理的目录) include:导入的文件将由加载程序转换的路径或文件数组(把要处理的目录包括进来) loader:一串“!”分隔的装载机(2.0 版本以上,”-loader”不可以省略) loaders:作为字符串的装载器阵列
module.exports = {
module: {
rules: [
{
// "test" is commonly used to match the file extension
test: /\.jsx$/,
// "include" is commonly used to match the directories
include: [
path.resolve(__dirname, "app/src"),
path.resolve(__dirname, "app/test")
],
// "exclude" should be used to exclude exceptions
// try to prefer "include" when possible
// the "loader"
loader: "babel-loader" // or "babel" because webpack adds the '-loader' automatically
}
];
}
}
- output 选项的 chunkFilename
作用: 配置无入口的 Chunk 在输出时的文件名称
常见的会在运行时生成 Chunk 场景有在使用 CommonChunkPlugin、使用 import('path/to/module') 动态加载等时。 chunkFilename 支持和 filename 一致的内置变量
- splitChunks 与 dllplugin 的区别:
都是提取公共代码插件
总而言之,它们看起来很相似,但它们可以让你击中不同的目标。这么多,你可以考虑在开发环境中使用 DllPlugin(优点:编译时间短),同时使用 splitChunks 进行生产(优点:app 更改时的加载时间短)。同样,您也可以在生产中使用 DllPlugin,只需要连续运行两个版本的小麻烦:一个用于 DLL,另一个用于应用程序。
附上 stackoverflow 中的分析连接 (opens new window)
- output 选项中的[hash]以及[chunkhash]
chunkhash 只能用于生产环境, 而 hash 一般用于开发环境,因为 chunkhash 与 HMR 冲突
- runtime && manifest
runtime: 就是帮助 webpack 编译构建后的打包文件在浏览器运行的一些辅助代码段,换句话说,打包后的文件,除了你自己的源码和 npm 库外,还有 webpack 提供的一点辅助代码段
manifest: 则是 webpack 用以查找 chunk 真实路径所使用的一份关系表,简单来说,就是 chunk 名对应 chunk 路径的关系表
# 注入全局变量
注入全局变量可以使用三种途径来完成
ProvidePlugin
exposed-loader
那他们有什么区别呢?
# ProvidePlugin
ProvidePlugin 用来自动加载模块,而不必到处 import 或 require
它的机制是当 webpack 加载到某个 js 模块里,出现了未定义且名称符合(字符串完全匹配)配置中 key 的变量时,会自动 require 配置中 value 所指定的 js 模块
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
# exposed-loader
看名称可以知道这个一个暴露全局变量的 loader,当某个 js 模块显式地调用 import $ from 'jquery'
的时候,就会将$注入到 window 中
module.exports = {
module: {
rules: [
{
test: require.resolve('jquery'),
use: [
{
loader: 'expose-loader',
options: '$'
}
]
}
]
}
}
// 在应用代码中使用
import $ from 'jquery'
// 就能直接读取到window.$
提示
html 已经通过 script 引入了一些外部 CDN 模块(例如 vue.min.js
), 在代码中就不要再次引入
import Vue from 'vue'
在 webpack 配置中, 使用 external 选项,将 Vue 给排除在外,以免引起模块多次打包,体积增大
# Long Term Cache
webpack
的长效缓存总结如下:
# SourceMap
在Webpack
中常见的devtool
的配置如下:
- devtool: source-map
- devtool: eval-source-map
- devtool: cheap-module-source-map
- devtool: cheap-module-eval-source-map
# devtool: source-map
这种方式的特点是大而全, 会单独生成一个sourcemap
文件, 在源码中会标识当前的行和列
# devtool: eval-source-map
这种方式不会产生单独的文件, 但是可以显示行和列
# cheap-module-source-map
这种不会产生列,列信息对于调试不是那么重要, 但是会产生一个单独的映射文件, 监控系统中上报sourcemap
首选当前模式
# cheap-module-eval-source-map
这种不会产生文件, 集成在打包后的文件中, 同时也没有列信息。可以在开发环境中使用
# Webpack 主要流程
Webpack
底层的工作流程大致可以总结为这么几个阶段:
# 初始化阶段
- 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数;
- 创建编译器对象:用上一步得到的参数创建
Compiler
对象; - 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等;
- 开始编译:执行
compiler
对象的run
方法,创建 Compilation 对象; - 确定入口:根据配置中的
entry
找出所有的入口文件,调用compilation.addEntry
将入口文件转换为dependence
对象。
# 构建阶段
- 编译模块(make):从
entry
文件开始,调用loader
将模块转译为标准 JS 内容,调用 JS 解析器将内容转换为AST
对象,从中找出该模块依赖的模块,再 递归 处理这些依赖模块,直到所有入口依赖的文件都经过了本步骤的处理; - 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的依赖关系图。
# 封装阶段
- 合并(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk;
- 优化(optimization):对上述
Chunk
施加一系列优化操作,包括:tree-shaking``、terser、``scope-hoisting
、压缩、Code Split
等; - 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
# Git 提交钩子(husky 和 yorkie)
husky 和 yorkie 都是提交钩子, 两者区别参考原文地址 (opens new window)
客户端钩子包括:pre-commit、prepare-commit-msg、commit-msg、post-commit 等,主要用于控制客户端 git 的提交工作流。服务端钩子:pre-receive、post-receive、update,主要在服务端接收提交对象时、推送到服务器之前调用
husky 可以让 git hooks 的使用变得更简单方便。运行 npm install husky@next --save-dev 安装最新版本,它会在我们项目根目录下面的.git/hooks 文件夹下面创建 pre-commit、pre-push 等 hooks。这些 hooks 可以让我们直接在 package.json 的 script 里运行我们想要在某个 hook 阶段执行的命令
husky 使用注意
husky 对应属性名已经改为 HUSKY_GIT_PARAMS , 而不是原始的 GIT_PARAMS 环境变量
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": ["eslint --fix", "git add"]
}
}
在 vue 最新的版本中,已经使用尤大改写的 youkie, youkie 实际是 fork husky,然后做了一些定制化的改动, 使得钩子能从 package.json 的 "gitHooks"属性中读取
{
"gitHooks": {
"pre-commit": "lint-staged",
"commit-msg": "node scripts/verify-commit-msg.js" //t比提交信息检查 [连接](https://github.com/vuejs/vue/blob/dev/scripts/verify-commit-msg.js)
}
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
}
}
关于 lint-staged (opens new window)
只 lint 当前改动的文件,lint-staged 就非常准确的解决了这一问题,从这个包名,就可以看出,Run linters on git staged files,只针对改动的文件进行处理