核心概念
webpack是js的模块打包工具,通过分析模块之间的依赖,将所有模块打包成一份或者多分代码块,供HTML直接引用。webpack仅提供了打包功能和文件处理机制,可以通过生态中的各种Loader和Plugin对代码进行预编译和打包。
- 概念
- Entry: 入口文件,webpack会从该文件开始进行分析和编译
- Output: 出口文件,打包后创建的bundler文件的路径以及文件名
- Module: 模块,在webpack中任何文件都可以作为一个模块,根据配置的不同Loader进行加载和打包
- Chunk: 代码块,可以根据配置将所有模块代码合并成一个或者多个代码块,以便按需加载,提高性能。是 webpack 处理过程中的一组模块
- bundle:捆绑好的最终文件。如果说,chunk 是各种片段,那么 bundle 就是一堆 chunk 组成的“集大成者”,比如上面说的 main.js 就属于 bundle。当然它也类似于电路上原先是各种散乱的零件,最终组成一个集成块的感觉。它经历了加载和编译的过程,是源文件的最终版本。bundle 是一个或多个 chunk 组成的集合。
- Loader: 模块加载器,进行各种文件类型的加载与转换
- Plugin: 拓展插件,可以通过webpack响应的事件钩子,介入到打包过程中的任意环节,从而对代码按需修改
- optimization: 优化
- Compiler
- 可以理解为Webpack的实例,全局唯一,从启动生存到结束
- 包含当前的所有配置信息
- options, loaders, plugins
- Compilation
- 当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译
- 包含了当前的输入资源,输出资源,变化的文件等
- 通过它提供的 api,可以监听每次编译过程中触发的事件钩子
- 通过 Compilation 也能读取到 Compiler 对象
- 工作流程
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;执行配置文件中的插件实例化语句 new Plugin()
- 开始编译:用上一步得到的参数初始化 Compiler 实例,调用插件的apply方法,挂载插件监听,执行对象的 run 方法从入口文件开始执行编译
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
- 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
- 特性
- 通过链式调用,按顺序串起一个个Loader
- 通过事件流机制,让Plugin可以插入到整个过程中的每个步骤中
使用 tree shaking
- 使用 ES2015 模块语法(即 import 和 export):因为es6模块的依赖关系是确定的是固定的,不依靠运行时的静态分析。而对于commonJS类型的模块需要依赖第三方的插件才能实现动态删除无效代码
lodash使用commonJS所以需要替换为lodash-es6 - 以default方式引入的模块,无法treeShaking。引入单个导出对象的方式,使用import * as xxx 的语法,还是import {xxx} 的语法
- webpackconfig 中 rule.sideEffects 默认为false,指代在要处理的模块中是没有有副作用。通过import 引入的css文件会被 tree shaking,可以通过设置 rules.sudeEffects: true 告诉webpack 这些文件有副作用
- optimzation.sideEffects 默认为true,指代在优化过程中是否遵循依赖模块的副作用描述。
- 在 Babel 7 之前的 babel-env中,modules 的默认项为 'commonjs';在 Babel 7 之后的 @babel/preset-env 中,modules 的选项默认为 'auto'
- 配置
- 开发环境:
mode: 'development',optimization: { usedExports: true} - 生产环境:
mode: 'production' - package.json 中的 sideEffects 属性来确认对应的依赖包是否会产生副作用,默认为 true, 告诉 Webpack ,所有文件都有副作用,他们不能被 Tree Shaking。sideEffects 为一个数组时,告诉 Webpack ,数组中那些文件不要进行 Tree Shaking,其他的可以 Tree Shaking。
- 开发环境:
- 相应的操作是在优化阶段进行的,并不能减少编辑模块编译阶段的构建时间,消除那些被引用了但未被使用的模块代码 :参考
Loader
webpack是基于Node,因此只能识别js模块,如css/html /图片等类型的文件无法加载。而Loader就是文件转换器,对webpack传入的字符串进行按需修改
- 步骤
- 对代码进行分析,构建AST(抽象语法树)
- 遍历进行定向的修改
- 生成新的代码字符串
- 特性
- 链式传递: 从右到左顺序执行
- 基于Node环境,拥有较高权限,比如文件增删改查
- 可同步也可异步
- 常用loader
file-loader: 加载文件资源,如字体/图片等url-loader: 加载图片,可将小图片直接转换为Date Url,减少请求babel-loader: 加载js/jsx文件,将ES6/ES7代码转换为ES5style-loader: 将css代码以<style>标签的形式插入到html中css-loader: 分析@import和url(),引用css文件与对应的资源postcss-loader: 用于css的兼容性处理,例如添加前缀,单位转换等less-loader/sass-loader: css预处理器,在css中新增了新语法,提高开发效率
- 编写原则
- 单一原则: 每个 Loader 只做一件事
- 链式调用: Webpack 会按顺序链式调用每个 Loader
- 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用
Plugin
- 原理
- 在 Webpack 运行的生命周期中会广播出许多事件
- Plugin 可以监听这些事件
- 在合适的时机通过 Webpack 提供的 API 改变输出结果
- 常用 Plugin:
- uglifyjs-webpack-plugin: 压缩、混淆代码
- clean-webpack-plugin: 每次打包前自动清理输入文件夹
- CommonsChunkPlugin: 代码分割
- html-webpack-plugin: 加载 html 文件,并引入 css / js 文件
- extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件
- 常用API
- emit 修改输出资源 源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容
- after-compile 监听文件变化 默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件
- done 在成功构建并且输出了文件后,Webpack 即将退出时发生
- failed 在构建出现异常导致构建失败,Webpack 即将退出时发生
事件流机制
- 核心是基础类Tapable,观察者模式通过事件的订阅与广播
- Compiler 和 Compilation 都继承自 Tapable。可以直接在 Compiler 和 Compilation 对象上广播和监听事件
- 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用,在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件
- 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程
编译流程
- Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例
- 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler)给插件实例传入 compiler 对象
- 插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件
- 并且可以通过 compiler 对象去操作 Webpack
plugin 代码
/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);
/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {
});
module.exports = {
plugins:[
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
new EndWebpackPlugin(() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
}, (err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err);
})
]
}
class EndWebpackPlugin {
constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
compiler.plugin('done', (stats) => {
// 在 done 事件中回调 doneCallback
this.doneCallback(stats);
});
compiler.plugin('failed', (err) => {
// 在 failed 事件中回调 failCallback
this.failCallback(err);
});
}
}
// 导出插件
module.exports = EndWebpackPlugin;
编译优化
- 代码优化
- 无用代码消除: UglifyJs 生产环境中删除不可能被执行的代码
- 摇树优化(tree shaking):
-
- import, ESM:摇树的关键
-
- sideEffects mode: 'development',optimization: { usedExports: true} :开发环境下的基础配置
- mode: 'production':生产环境
- sideEffects:摇树的作用范围
-
- package.json 中的配置:sideEffects: false(全都摇)
-
- 规则配置中的字段:sideEffects: true(控制全局文件不被摇掉)
-
-
- code-spliting: 代码分割
- split chunks 是指在chunk生成之后将原先以入口点来划分的chunks根据一定的规则分离出子chunk的过程
- 路由分割: React Router
- 剥离公共组件: Loading
- 项剥离第三方库: 使用 optimization.splitChunks 配置
- 剥离wenpack运行态: 使用 optimization.runtimeChunk 配置项
- 使用chunkhash,利用浏览器缓存
- resolve 在构建时指定查找文件的规则
- resolve.modules 指定查找模块的目录范围
- resolve.extensions 指定查找模块的文件类型范围
- resolve.mainFields 指定查找模块的package.json中主文件的属性名
- resolve.symlinks 指定在查找模块时是否处理软连接
- 编译性能优化
- 减少编译模块
- ignorePlugin 忽略不需要编译的模块,比如引入moment时不需要local语言包
- lodash 导入时声明特定的模块,babel-plugin-lodash
- DLLPlugin
- Externals
- 缩小编译范围
- modules: 指定模块路径,减少递归搜索
- mainFields: 指定入口文件描述字段,减少搜索
- includes/exclude: 指定搜索范围/排除不必要的搜索范围
- alias: 缓存目录,避免重复寻址
- babel-loader
- 忽略node_moudles,避免编译第三方库中已经被编译过的代码
- 使用cacheDirectory,可以缓存编译结果,避免多次重复编译
- 多进程并发
- webpack-parallel-uglify-plugin: 压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算比较好使,可使用此插件多进程并发压缩 js 文件,提高压缩速度
- HappyPack :本身就是对node开启了一个多进程打包,将不再被维
- thread-loader: 多进程并发文件的 Loader 解析,只要把 thread-loader 放置在其他 loader 之前即可
- 使用分析
- speed-measure-webpack-plugin 分析构建过程每个生命周期的用时时间,是一个基于时间的分析工具
- Webpack Analyse / webpack-bundle-analyzer 对打包后的文件进行分析,寻找可优化的地方
- 配置profile:true,对各个编译阶段耗时进行监控,寻找耗时最多的地方
- source-map
- 开发: cheap-module-eval-source-map
- 生产: hidden-source-map
- 减少编译模块