webpack深入学习

几个概念

我在学习 webpack 的时候,Chunk 的理解感觉比较困难,对概念的深刻理解是更好的理解 webpack 运行的基石。

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。webpack 只能处理 javascript,所以我们需要对一些非 js 文件处理成 webpack 能够处理的模块,比如 sass、图像等文件
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

webpack 运行流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
  8. 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

webpack 构建流程分解

Webpack 执行一次构建流程会有一下三个阶段,如果开启监听模式,则流程会在初始化阶段后,反复执行编译、输出这两个阶段。即监听模式下的流程是:初始化->(编译->输出)->(编译->输出)->(编译->输出)

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  2. 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  3. 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

输出文件分析

灵魂拷问:webpack 输出的 bundle.js 是什么样子的? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 bundle.js 能直接运行在浏览器中?
打包后的 bundle.js 文件内容大致模样是这样的:

1
2
3
4
5
6
7
8
9
(function (modules) {
// 模拟 require 语句
function __webpack_require__() {}

// 执行存放所有模块数组中的第0个模块
__webpack_require__(0);
})([
/*存放所有模块的数组*/
]);

bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。并且 Webpack 做了缓存优化:执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。

分割代码时的输出

提取公共代码和异步加载本质上都是代码分割

1
2
3
4
// 异步加载 show.js
import("./test").then(({ A }) => {
A();
});

重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和 异步加载文件 0.bundle.js。

其中 0.bundle.js 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
[0],
{
"./src/test.js": function (
module,
__webpack_exports__,
__webpack_require__
) {
// content
},
},
]);

Loader

Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。

一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。

Loader 基础

  • 一个 Loader 其实就是一个 Node.js 模块,可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用

    1
    2
    3
    4
    const sass = require("node-sass");
    module.exports = function (source) {
    return sass(source);
    };
  • 获得 Loader 的 options

    1
    2
    3
    4
    5
    6
    const 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
    8
    module.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
    8
    module.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
    9
    module.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
    5
    module.exports = function (source) {
    // 关闭该 Loader 的缓存功能
    this.cacheable(false);
    return source;
    };

加载本地 Loader

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。
完成 Npm link 的步骤如下:

  1. 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  2. 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  3. 在项目根目录下执行 npm link loader-name,把第 2 步注册到全局的本地 Npm 模块链接到项目的
  4. node_moduels 下,其中的 loader-name 是指在第 1 步中的 package.json 文件中配置的模块名称。

2、ResolveLoader

ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules。

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

1
2
3
4
5
6
module.exports = {
resolveLoader: {
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ["node_modules", "./loaders/"],
},
};

加上以上配置后, 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
2
3
4
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.5",

并且修改.babel 文件

{ "presets":["@babel/react","@babel/env",]}

2、降级到 babel6.0 版本

1
2
3
4
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-loader": "^7.1.5",

对应修改.babelrc 文件

{ "presets": ["react", "env"]}

参考资料: