webpack4 - loader 的执行过程和部分常用loader原理实现
1.loader 运行的总体流程
1.Compiler.js中会将用户配置与默认配置合并,其中就包括了loader部分
2.webpack就会根据配置创建两个关键的对象 ———— NormalModuleFactory 和 ContextModuleFactory , 他们相当于是两个类工厂,通过其可以创建相应的 NormalModule 和 ContextModule
3.在工厂创建NormalModule实例之前还要通过loader的resolver来解析loader路径
4.在NormalModule实例创建之后,则会通过其build方法来进行模块的构建,构建模块的第一步就是使用loader来加载并处理模块内容,而loader-runner这个库就是webpack中loader的运行器
5.最后,将loader处理完毕的模块内容输出,进入后续的编译流程
开始编译 => webpack默认配置 => 创建NormalModuleFactory => 创建NormalModule[使用resolver解析loader路径] => 编译模块[loader-runner]
2.loader 匹配和用法
loader 是导出为一个函数的 node 模块,该函数在 loader 转换资源的时候调用,给定的函数将调用 loaderApi,并通过 this 上下文访问
访问方式可以是直接给 use 数组一个个对象,loader 指定一个绝对路径,而不是直接用 xxx-loader 提供,也可以是 resolveLoader 配置的 module 的查找目录(文件夹),或者使用 npm link 发布到本地 npm 的模块
还有一种方式就是配置 resolveLoader.alias 对象,配置一一 key-value 对应的关系,key 就是可以拿来使用的 loader 别名
/** loaders/loader1 */
// 最后到给loader1
function loader(inputSource) {
// console.log('index') // loader3 // loader2
return inputSource + ' // loader1';
}
module.exports = loader;
/** loaders/loader2 */
// 第二个给loader2
function loader(inputSource){
// console.log('index') // loader3
return inputSource + ' // loader2';
}
module.exports = loader;
/** loaders/loader3 */
// 先给loader3
function loader(inputSource) {
// 文件内容 inputSource
// return inputSource + ' // loader3';
// 异步写法如下
const callback = this.async();
setTimeout(() => {
callback(null, inputSource + ' // loader3');
}, 1000);
}
module.exports = loader;
// webpack.config.js
module.exports = {
...
resolveLoader: {
// [制定loader查找的时候]
// 方式2,直接定义查找模块,node_modules找不到再去loaders目录找
modules: [path.resolve('node_modules'), path.resolve('loaders')]
// 方式3 配置别名
// alias: {
// 'loader1': path.resolve('./loaders/loader1.js'),
// 'loader2': path.resolve('./loaders/loader2.js'),
// 'loader3': path.resolve('./loaders/loader3.js'),
// }
},
module: {
rules: [
{
test: /\.js$/,
// 方式1
// use: [
// {
// loader: path.resolve('loaders/loader1.js')
// }
// ],
use: ['loader1', 'loader2', 'loader3']
}
]
}
}
3.用法准则
3.1 简单 [单一独立原则]
- loader 应该只做单一任务,这不仅使每个 loader 容易维护,也可以在更多场景链式调用
3.2 链式(Chaining)
- 利用 loader 可以链式调用的优势,写五个简单的 loader 实现五项任务,而不是一个 loader 实现五项任务
3.3 模块化(Modular)
- 保证输出模块化,loader 生成的模块与普通模块遵循相同的设计原则。
3.4 无状态(Stateless)
确保 loader 在不同模块转换之间不保持状态,每次运行都应该独立与其他编译模块以及相同模块之间的编译结果
3.5 loader 工具库
- loader-utils 包,它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
- schema-utils 包,它配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结果一致的校验.
3.6 loader 依赖(Loader Dependencies)
- 如果一个 loader 使用外部资源(例如从文件系统读取),必须声明它,这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重新编译
3.7 模块依赖(Module Dependencies)
- 根据模块类型,可能会有不同的模式制定依赖关系,例如在 CSS 中,使用@import 和 url(…)语句来声明依赖,这些依赖关系应该由模块系统解析
3.8 绝对路径(Absolute Paths)
- 不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会发生变化,loader-utils 中的 stringifyRequest 方法,可以讲绝对路径转化程相对路径。
3.9 同等依赖(Peer Dependencies)
- 如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入
- 这种方式允许应用程序开发者在必要情况下,在 package.json 中指定所需的确定版本。
4.loader 的实现
4.1 babel-loader 实现
// loaders/babel-loader.js
const babel = require('@babel/core');
const loaderUtils = require('loader-utils');
const path = require('path');
/** loader只是一个函数 */
module.exports = function loader(inputSource) {
// 获取配置参数options的数据
const options = loaderUtils.getOptions(this);
// 默认配置
const baseOptions = {
...options, // 合并配置
// presets: ['@babel/preset-env'],
sourceMaps: true, // 告诉babel我要生成sourceMao
filename: path.basename(this.resourcePath)
};
// 代码 map文件 ast语法树
const {
code, map, ast } = babel.transform(inputSource, baseOptions);
// 我们可以把source-map ast 都传递给webpack,这样webpack就不需要自己把源代码转语法树,也不需要自己生成source-map
return this.callback(null, code, map, ast);
};
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
// webpack loader的选项配置
presets: ['@babel/preset-env']
}
}
]
}
]
}
// ...
};
4.2 pitch 实现 (上面实现的 babel-loader,babel1-3 是普通函数,不是 pitch 函数)
pitch function 是先执行的,从左往右执行,执行完毕后执行normal function,从右往左回去,不过执行条件如下
- 比如 a!b!c!module,正常调用顺序应该是 c、b、a,但是真正调用顺序是 a(pitch)、b(pitch)、c(pitch)、c、b、a,如果其中任何一个 pitching loader 返回了值,就相当于在它以及它右边的 loader 已经执行完毕
- 比如如果 b 返回了字符串’result b’,接下来只有 a 会被系统执行,且 a 的 loader 收到的参数是’result b’
- loader 根据返回值可以分成两种,一种是返回 s 代码(一个 module 的代码,含有类似 module export 语句)的 loader,还有不能作为最左边 loader 的其他 loader
- 有时候我们想把两个第一种 loader chain 起来,比如 style-loader!css-loader,问题是 css-loader 的返回值是一串代码,如果按正常方式写 style-loader 的参数就是一串代码字符串
- 为了解决这种问题,我们需要在 style-loader 里执行 require(css-loader!resources)
/** loaders/loader1 */
// 最后到给loader1
function loader(inputSource) {
// console.log('index') // loader3 // loader2
console.log('loader1');
return inputSource + ' // loader1';
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
console.log('pitch1');
};
module.exports = loader;
/** loaders/loader2 */
// 第二个给loader2
function loader(inputSource) {
// console.log('index') // loader3
console.log('loader2');
return inputSource + ' // loader2';
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
console.log('pitch2');
};
module.exports = loader;
/** loaders/loader3 */
// 先给loader3
function loader(inputSource) {
// 文件内容 inputSource
// return inputSource + ' // loader3';
// 异步写法如下
const callback = this.async();
setTimeout(() => {
console.log('loader3');
callback(null, inputSource + ' // loader3');
}, 1000);
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
console.log(