几个概念
我在学习 webpack 的时候,Chunk 的理解感觉比较困难,对概念的深刻理解是更好的理解 webpack 运行的基石。
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。webpack 只能处理 javascript,所以我们需要对一些非 js 文件处理成 webpack 能够处理的模块,比如 sass、图像等文件
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
webpack 运行流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
- 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
webpack 构建流程分解
Webpack 执行一次构建流程会有一下三个阶段,如果开启监听模式,则流程会在初始化阶段后,反复执行编译、输出这两个阶段。即监听模式下的流程是:初始化->(编译->输出)->(编译->输出)->(编译->输出)…
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
- 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
- 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
输出文件分析
灵魂拷问:webpack 输出的 bundle.js 是什么样子的? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 bundle.js 能直接运行在浏览器中?
打包后的 bundle.js 文件内容大致模样是这样的:
1 | (function (modules) { |
bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。
原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。并且 Webpack 做了缓存优化:执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。
分割代码时的输出
提取公共代码和异步加载本质上都是代码分割
1 | // 异步加载 show.js |
重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和 异步加载文件 0.bundle.js。
其中 0.bundle.js 内容如下:
1 | (window["webpackJsonp"] = window["webpackJsonp"] || []).push([ |
Loader
Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。
一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。
Loader 基础
一个 Loader 其实就是一个 Node.js 模块,可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用
1
2
3
4const sass = require("node-sass");
module.exports = function (source) {
return sass(source);
};获得 Loader 的 options
1
2
3
4
5
6const loaderUtils = require("loader-utils");
module.exports = function (source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};让 Loader 返回除了内容之外的东西。this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信
1
2
3
4
5
6
7
8module.exports = function (source) {
// 通过 this.callback 告诉 Webpack 返回的结果
// this.callback(err: Error | null, content: string | Buffer, sourceMap?: SourceMap, abstractSyntaxTree?: AST)
this.callback(null, source, sourceMaps);
// 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};Loader 有同步和异步之分,有些场景下转换的步骤只能是异步完成的,如通过网络请求才能得出结果的转换, 异步转换方法如下:
1
2
3
4
5
6
7
8module.exports = function (source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function (err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};Loader 处理二进制数据。在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,但是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据
1
2
3
4
5
6
7
8
9module.exports = function (source) {
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true;Loader 的缓存加速。Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。不想让 webpack 缓存 Loader 的处理结果方法如下:
1
2
3
4
5module.exports = function (source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};
加载本地 Loader
1、Npm link
Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。
完成 Npm link 的步骤如下:
- 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
- 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
- 在项目根目录下执行 npm link loader-name,把第 2 步注册到全局的本地 Npm 模块链接到项目的
- node_moduels 下,其中的 loader-name 是指在第 1 步中的 package.json 文件中配置的模块名称。
2、ResolveLoader
ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules。
假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:
1 | module.exports = { |
加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。
webpack 打包相关错误
Error: Plugin/Preset files are not allowed to export objects, only functions.
这个报错是因为 babel 的版本冲突。两者版本要对应
babel-loader 8.x | babel 7.x
npm install -D babel-loader @babel/core @babel/preset-env
babel-loader 7.x | babel 6.x
npm install -D babel-loader@7 babel-core babel-preset-env
解决方法:
1、升级 babel 到 babel7.0
1 | "@babel/core": "^7.2.2", |
并且修改.babel 文件
{ "presets":["@babel/react","@babel/env",]}
2、降级到 babel6.0 版本
1 | "babel-core": "^6.26.3", |
对应修改.babelrc 文件
{ "presets": ["react", "env"]}
参考资料: