背景
作为前端工程化的基石,webpack 随着不断地发展,功能越来越强大的同时,也变得越来越复杂。对很多开发者出现了不友好的局面,主要体现在学习成本高,上手难。本文将着重叙述 webpack 的基本构建流程,重新认识 webpack。
webpack 构建流程
At its core, webpack is a static module bundler for modern JavaScript applications.
通俗的讲,webpack 的主要作用是将各种类型的资源,包括图片、css、js 等,转译、组合、拼接、生成 JS 格式的 bundler 文件。 整个过程可以分为以下三个阶段(整个过程自上而下):
- 初始化:
- 初始化参数:从配置对象、 文件、Shell 中读取,与默认配置 merge 得出最终的参数
- 创建编译器对象:用上一步得到的参数再去创建 Compiler 对象
- 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
- 开始编译:执行 compiler 对象的 run 方法
- 确定入口:根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象
- 构建:
- 编译模块(make):根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:上一步递归处理所有的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图
- 生成:
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 写入系统:根据配置确定输出的路径和文件名,把文件内容写入到文件系统下面罗列一些整个过程中涉及到的技术名词: Entry:编译入口,编译过程的起点
Compiler:编译管理器,webpack 启动后会创建 compiler 对象,该对象一直存活知道结束退出
Compilation:单次编译过程的管理器,比如 watch = true 时,运行过程中只有一个 compiler 但每次文件变更触发重新编译时,都会创建一个新的 compilation 对象
Dependence:依赖对象,webpack 基于该类型记录模块间依赖关系
Module:webpack 内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的
Chunk:编译完成准备输出时,webpack 会将 module 按特定的规则组织成一个一个的 chunk
Loader:资源内容转换器 to B
Plugin:webpack 构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程
初始化:
- 将 process.args + webpack.config.js 合并成用户配置
- 调用 validateSchema 校验配置
- 调用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终配置
- 创建 compiler 对象
- 遍历用户定义的 plugins 集合,执行插件的 apply 方法
- 调用 new WebpackOptionsApply().process 方法,加载各种内置插件到这里,compiler 实例就被创建出来了,开始执行 compile 过程。
构建(module):
- 构建阶段从 entry 开始递归解析资源与资源的依赖,在 compilation 对象内逐步构建出 module 集合以及 module 之间的依赖关系
- 调用 handleModuleCreate ,根据文件类型构建 module 子类
- 调用 loader-runner 仓库的 runLoaders 转译 module 内容,通常是从各类资源类型转译为 JavaScript 文本
- 调用 acorn(webpack 内置 JS Parser) 将 JS 文本解析为 AST
- 遍历 AST,触发各种钩子
- 对于 module 新增的依赖,调用 handleModuleCreate ,控制流回到第一步
- 所有依赖都解析完毕后,构建阶段结束这个过程中数据流 module => ast => dependences => module: Webpack 遍历 AST 集合过程中,识别 require/ import 之类的导入语句,确定模块对其他资源的依赖关系
生成(chunk):
- entry 及 entry 触达到的模块,组合成一个 chunk
- 使用动态引入语句引入的模块,各自组合成一个 chunk 以上的规则会导致一个常见的问题:多页应用引入相同的库,那么 chunk 中就会有重复的内容。所以便有了 SplitChunksPlugin 、CommonsChunkPlugin(4.0 后不推荐使用了)等插件继续优化 chunk 结构。
loader 和 plugin 的区别:
- 工作内容不同:
- loader 负责转换源文件,翻译内容,往下传递:如 style/css/sass/less-loader 用来转换 css 并插入到 dom 中。而 plugin 用于提升 webpack 自身功能,作用于打包过程中,根据 webpack 提供的 hooks 进行额外的操作:如常见的 htmlwebpackplugin,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中。
- 作用时机不同
webpack 优化(SMW 分析速度、WBA 分析体积)
- 区分开发测试配置
- 缩⼩ loader ⽂件查找范围 Loader(include,not exclude)并且开启 cache
- CDN 引入。配置 externals(lodash、moment 等常见第三方库)
- 多线程 threadloader 替换 happypack
- 优化 resolve.extensions 配置(导入没后缀的文件,指定后缀名称,减小后缀尝试的可能)
- 压缩 css:css-minimizer-webpack-plugin
- webpack5 默认文件缓存(升级策略)
- terserWP 替换原来的 js 压缩插件 uglifyjWP
- 升级为 css-minimizer-webpack-plugin 压缩 css
- 推荐用 esbuild-loader 替换 babel-loader
- webpack module 优化配置-oneOf(优化匹配 loader 时间)
- ...