工作原理
基本概念
Entry:入口,Webpack 执行构建的第一步将从 Entry 开始。Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。Chunk:代码块,一个chunk由多个模块组合而成,用于代码合并与分割。Loader:模块转换器,用于将模块的原内容按照需求转换成新内容。Plugin:插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
流程概括
初始化参数 ——> 开始编译 ——> 确定入口 ——> 编译模块 ——> 完成模块编译 ——> 输出资源 ——> 输出完成
- 初始化参数:从配置文件(默认webpack.config.js)和shell语句中读取与合并参数,得出最终的参数
- 开始编译(compile):用上一步得到的参数初始化
Compiler对象,加载所有配置的插件Plugin,通过执行对象的run方法开始执行编译 - 确定入口:根据配置中的
entry找出所有的入口文件 - 编译模块:从入口文件出发,调用所有配置的
Loader对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过处理 - 完成编译模块:经过第四步之后,得到了每个模块被翻译之后的最终内容以及他们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会 - 输出完成:在确定好输出内容后,根据配置(webpack.config.js && shell)确定输出的路径和文件名,将文件的内容
写入文件系统中(fs)
总结一下,Webpack的构建流程可以分为以下三大阶段:
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
- 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
- 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
bundle.js
bundle.js其实是一个立即执行函数,bundle.js能直接运行在浏览器中的原因在于输出的文件中通过__webpack_require__函数定义了一个可以在浏览器中执行的加载函数来模拟Node.js中的require语句。
而且Webpack 做了缓存优化,执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。
(function(modules){
//模拟require语句
function __webpack_require__(){}
//执行存放所有模块数组中的第0个模块(main.js)
__webpack_require_[0]
})([/*存放所有模块的数组*/])
性能优化
减少Webpack打包时间
优化Loader:优化 Loader 的文件搜索范围(exclude掉node_modules)、将Babel编译过的文件缓存起来(loader: 'babel-loader?cacheDirectory=true')。 对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。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: 可以将特定的类库提前打包然后引入。极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包。
// 单独配置在一个文件中
// 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'),
})
]
}
代码压缩:webpack3中使用webpack-parallel-uglify-plugin来并行运行UglifyJS(单线程),webpack4中将mode设置为production则默认开启压缩。
减少Webpack打包后的文件体积
按需加载:每个路由页面单独打包为一个文件、loadash这种大型类库同样可以使用这个功能。Scope Hoisting: 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。Webpack4 中开启这个功能,只需要启用optimization.concatenateModules。
// test.js
export const a = 1
// index.js
import { a } from './test.js'
打包上面两个文件后,生成代码类似这样:
[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]
如果使用Scope Hositing,会生成这样的类似代码:
[
/* 0 */
function (module, exports, require) {
//...
}
]
Tree Shaking:可以实现删除项目中未被引用的代码。Webpack4的生产环境默认开启这个功能。
// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'
test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。