前端工程化
- 技术选型
- 对立标准——eslint、husky
- 测试、部署、监控——ut、e2e、mock
- 性能优化
- 模块化重构
webpack 流程
webpack 的构建流程能够分为以下三大阶段:
- 初始化:启动构建,读取与合并配置参数,加载
Plugin
,实例化Compiler
。 - 编译:从 Entry 登程,针对每个 Module 串行调用对应 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译解决。
- 输入:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输入到文件系统中。
Loader
Loader 就像一个翻译员,能将源文件通过转化后输入新的后果,并且一个文件还能够链式地通过多个翻译员翻译。
在开发一个 Loader 时,请确保其职责的单一性,咱们只须要关怀输出和输入。
根底
Webpack 是运行在 Node.js 上的,一个 Loader 其实就是一个 Node.js 模块,这个模块须要导出一个函数。这个导出的函数的工作就是取得解决前的原内容,对原内容执行解决后,返回解决后的内容。
一个最简的的 Loader 的源码如下:
// source 为 compiler 传递给 Loader 的一个文件的原内容
module.exports = function(source) {
// TODO: 对文件内容进行解决
return source;
}
因为 Loader 运行在 Node.js 中,所以咱们能够调用任意 Node.js 自带的 API,或者装置第三方模块进行调用:
const sass = require('node-sass');
module.exports = function(source) {return sass(source);
}
进阶
取得 Loader 的 options
const loaderUtils = require('loader-utils'); // getOptions 办法要求 loader-utils 版本为 2.x
module.exports = function(source) {
// 获取用户为以后 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
}
返回其余后果
下面的 Loader 都只是返回了原内容转换后的内容,然而在某些场景下还须要返回除了内容之外的货色。
以用 babel-loader 转换 ES6 为例,它还须要输入转换后的 ES5 代码对应的 Source Map,以不便调试源码。
module.exports = function(source) {this.callback(null, source, sourceMaps);
return;
}
其中的 this.callback
是 Webpack 向 Loader 注入的 API(The Loader Context),以不便 Loader 和 Webpack 之间通信。
this.callback
能够同步或者异步调用的并返回多个后果的函数。预期的参数是:
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
如果这个函数被调用的话,你应该返回 undefined 从而防止含混的 loader 后果。
异步
module.exports = function(source) {let callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, meta) {callback(err, result, sourceMaps, meta);
});
}
this.async
通知 loader-runner 这个 loader 将会异步地回调。返回this.callback
。
解决二进制数据
在默认状况下,Webpack 传给 Loader 的原内容都是 UTF- 8 格局编码的字符串。但在某些场景下 Loader 不会解决文本文件,而会解决二进制文件如 file-loader,这时就须要 Webpack 为 Loader 传入二进制格局的数据。
module.exports = function(source) {if(source instanceof Buffer === true) {// TODO: 解决二进制内容}
// Loader 返回的类型也能够是 Buffer 类型
return source;
}
// 通过 exports.raw 属性通知 webpack 该 Loader 是否须要二进制数据
module.exports.raw = true;
缓存减速
Webpack 会默认缓存所有 Loader 的处理结果,以防止每次构建都从新执行反复的转换操作,从而放慢构建速度。
敞开缓存性能:
module.exports = function(source) {this.cacheable(false);
return source;
}
其余 Loader API
详见 Loader Interface
加载本地 Loader
Npm link
Npm link 专门用于开发和调试本地的 Npm 模块,能做到在不公布模块的状况下,将本地的一个正在开发的模块的源码链接到我的项目的 node_modules
目录下,这让我的项目能够间接应用本地的 Npm 模块。
步骤如下:
- 确保正在开发的本地 Npm 模块的
package.json
已配置好; - 在本地的 Npm 根目录下执行 npm link,将本地模块注册到全局;
- 在我的项目根目录下执行 npm link loader-name,将第二步注册到全局的本地 Npm 模块链接到我的项目的
node_modules
下,其中 loader-name 是指在第一步的package.json
中配置的模块名称。
ResolveLoader
ResolveLoader 用于配置 Webpack 如何寻找 Loader,它在默认状况下只会去 node_modules
目录下寻找。
module.exports = {
//...
resolveLoader: {modules: ['node_modules'],
extensions: ['.js', '.json'],
mainFields: ['loader', 'main'],
},
};
Plugin
Webpack 通过 Plugin 机制让其更灵便,以适应各种场景。在 Webpack 运行的生命周期中会播送许多事件,Plugin 能够监听这些事件,在适合的机会通过 Webpack 提供的 API 扭转输入后果。
一个最根底的 Plugin 的代码是这样的:
// webpack3
class BasicPlugin {constructor(options) {}
apply(compiler) {compiler.plugin('compilation', function(compilation) {});
}
}
module.exports = BasicPlugin;
在 webpack5 的官网文档中对 plugin 的解释如下
plugin 的目标在于解决 loader 无奈实现的其余事,是对于 webpack 性能的扩大。
webpack plugin 是一个具备apply
办法的 JavaScript 对象。apply
办法会被 webpack compiler 调用,并且在整个编译生命周期都能够拜访 compiler 对象。
// webpack4,webpack5
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {constructor(options) {}
apply(compiler) {compiler.hooks.run.tap(pluginName, (compilation) => {console.log('webpack 构建正在启动!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
Compiler 和 Compilation
- Compiler 对象蕴含了 Webpack 环境的所有配置信息,蕴含 options、loaders、plugins 等信息。这个对象在 Webpack 启动时被实例化,它是全局惟一的,能够简略地将它了解为 Webpack 实例。
- Compilation 对象蕴含了以后的模块资源、编译生成资源、变动的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件发生变化,便有一次新的 Compilation 被创立。Compilation 对象也提供了很多事件回调供插件进行扩大。通过 Compilation 也能读取到 Compiler 对象。
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到敞开的生命周期,而 Compilation 只代表一次新的编译。
Compiler 钩子
Compiler
模块是 webpack 的次要引擎,它通过 CLI 或者 Node API 传递的所有选项创立出一个 compilation 实例。它扩大(extends)自Tapable
类,用来注册和调用插件。大多数面向用户的插件会首先在Compiler
上注册。
// 钩子函数调用形式
compiler.hooks.someHook.tap('MyPlugin', (params) => {/* ... */});
// 示例
compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {/* ... */});
Compilation 钩子
Compilation
模块会被Compiler
用来创立新的 compilation 对象(或新的 build 对象)。compilation 实例可能拜访所有的模块和它们的依赖(大部分是循环依赖)。它会对应用程序的依赖图中所有模块,进行字面上的编译 (literal compilation)。在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash) 和从新创立(restore)。
// 钩子函数调用形式
compilation.hooks.someHook.tap(/* ... */);
// 示例
compilation.hooks.buildModule.tap(
'SourceMapDevToolModuleOptionsPlugin',
(module) => {module.useSourceMap = true;}
);
事件流
Webpack 就像一条生产线,要通过一系列解决流程后能力将源文件转换成输入后果。Webpack 通过 Tabable
来组织这条简单的生产线。Webpack 在运行的过程中会播送事件,插件只须要监听它关怀的事件,就能退出这条生产线中,去扭转生产线的运作。
// webpack3
// 播送事件,留神不要和现有事件重名
compiler.apply('event-name', params);
compilation.apply('event-name', params);
// 监听事件
compiler.plugin('event-name', function(params) {});
compilation.plugin('event-name', function(params) {});
Tabable
这个小型库是 webpack 的一个外围工具,但也可用于其余中央,以提供相似的插件接口。在 webpack 中的许多对象都扩大自
Tapable
类。它对外裸露了tap
,tapAsync
和tapPromise
等办法,插件能够应用这些办法向 webpack 中注入自定义构建的步骤,这些步骤将在构建过程中触发。依据应用不同的钩子 (hooks) 和 tap 办法,插件能够以多种不同的形式运行。这个工作形式与 Tapable 提供的钩子 (hooks) 密切相关。compiler hooks 别离记录了 Tapable 外在的钩子,并指出哪些 tap 办法可用。
自定义钩子
// 须要简略的从 `tapable` 中 require 所需的 hook 类,并创立
const SyncHook = require('tapable').SyncHook;
if (compiler.hooks.myCustomHook) throw new Error('已存在该钩子');
compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c']);
// 在你想要触发钩子的地位 / 机会下调用……
compiler.hooks.myCustomHook.call(a, b, c);
All Hook constructors take one optional argument, which is a list of argument names as strings.
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
罕用的钩子
-
读取 Webpack 的处理结果 / 批改输入资源:
emit
钩子,输入 asset 到 output 目录之前执行compiler.hooks.emit.tap('MyPlugin', (compilation, callback) => { // do something callback();});
-
监听文件的变动:
afterCompile
钩子,compilation 完结和封印之后执行compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => { // 将指定文件增加到文件依赖列表中 commpilation.fileDependencies.push(filePath); callback();});