手写 loader
什么是 loader?
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 “load(加载)” 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS 文件!
通俗等说:对于 webpack 来说,一切资源皆是模块,但由于 webpack 默认只支持 es5 的 js 以及 json,像是 es6+, react,css 等都要由 loader 来转化处理。
loader 代码结构
loader 只是一个导出为函数的 js 模块。
module.exports = function(source, map) {
return source;
}
复制代码
其中 source 表示匹配上的文件资源字符串,map 表示 SourceMap。
注意: 不要写成箭头函数,因为 loader 内部的属性和方法,需要通过 this 进行调用,比如默认开启 loader 缓存,配制 this.cacheable(false) 来关掉缓存
同步 loader
需求: 替换 js 里的某个字符串
实现:
新建个 replaceLoader.js:
module.exports = function (source) {
return `${source.replace('hello', 'world')} `;
};
复制代码
webpack.config.js:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'main.js',
},
module: {
rules: [{ test: /\.js$/, use: './loaders/replaceLoader.js' }],
},
};
复制代码
传递参数
上面的 replaceLoader 是固定将某个字符串(hello)替换掉,实际场景中更多的是通过参数传入,即
webpack.config.js:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.js$/,
// 通过 options 参数传参
use: [
{
loader: './loaders/replaceLoader.js',
options: {
name: 'hello',
},
},
],
// 通过字符串来传参
// use: './loaders/replaceLoader.js?name=hello'
},
],
},
};
复制代码
以上两种传参方式,如果使用 query 属性来获取参数,就会出现字符串传参获取到的是字符串, options 传参获取到的是对象格式,不好处理。这里推荐使用 loader-utils 库来处理。
这里使用 getOptions 来接收参数
const { getOptions } = require('loader-utils');
module.exports = function (source) {
const params = getOptions(this);
return `${source.replace(params.name, 'world')} `;
};
复制代码
异常处理
第一种: loader 内直接通过 throw 抛出
const { getOptions } = require('loader-utils');
module.exports = function (source) {
const params = getOptions(this);
throw new Error('出错了');
};
复制代码
第二种: 通过 this.callback 传递错误
this.callback({
//当无法转换原内容时,给 Webpack 返回一个 Error
error: Error | Null,
//转换后的内容
content: String | Buffer,
//转换后的内容得出原内容的Source Map(可选)
sourceMap?: SourceMap,
//原内容生成 AST语法树(可选)
abstractSyntaxTree?: AST
})
复制代码
第一个参数就是错误信息,当传递 null 里,作用跟前面的直接 return 个字符串作用类似,更建议采用这种方式返回内容
const { getOptions } = require('loader-utils');
module.exports = function (source) {
const params = getOptions(this);
this.callback(new Error("出错了"), `${source.replace(params.name, 'world')} `);
};
复制代码
异步处理
当遇到要处理异步需求时,比如获取文件,此时通过 this.async() 告知 webpack 当前 loader 是异步运行。
const fs = require('fs');
const path = require('path');
module.exports = function (source) {
const callback = this.async();
fs.readFileSync(
path.resolve(__dirname, '../src/async.txt'),
'utf-8',
(error, content) => {
if (error) {
callback(error, '');
}
callback(null, content);
}
);
};
复制代码
其中 callback 是跟上面 this.callback 一样的用法。
文件输出
通过 this.emitFile 进行文件写入。
const { interpolateName } = require('loader-utils');
const path = require('path');
module.exports = function (source) {
const url = interpolateName(this, '[name].[ext]', { source });
this.emitFile(url, source);
this.callback(null, source);
};
复制代码
resolveLoader
上述设置 loader 时将整个文件路径都配置了,这样写多了,是有些麻烦的,可以通过 resolveLoader 定义 loader 的查找文件路径。
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'main.js',
},
resolveLoader: { modules: ['./loaders/', 'node_modules'] },
module: {
rules: [
{
test: /\.js$/,
// 通过 options 参数传参
use: [
{
loader: 'asyncLoader.js',
},
{
loader: 'emitLoader.js',
},
{
loader: 'replaceLoader.js',
options: {
name: 'hello',
},
},
],
// 通过字符串来传参
// use: './loaders/replaceLoader.js?name=hello'
},
],
},
};
复制代码
plugin 工作机制
在手写 plugin 之前,先讲下 webpack 里 plugin 的工作机制,方便后续的讲解。
webpack 在对参数处理完之后,会进入 compiler 里的 run 函数,在该函数内部会
在 webpack.js 有如下代码:
compiler = new Compiler(options.context);
compiler.options = options;
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
复制代码
可以看到会遍历 options.plugins 并依次调用 apply 方法,当然如果 plugin 是个函数的话,会调用 call,但现在推荐将 plugin 定义个类。
手写 plugin
什么是 plugin
插件是 webpack 的 支柱 功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上!
插件目的在于解决 loader 无法实现的其他事。
插件类似于 React, Vue 里的生命周期,就是个某个时间点会触发,比如 emit 钩子:输出 asset 到 output 目录之前执行;done 钩子:在编译完成时执行。
plugin 代码结构
plugin 就是个类,该类里有个 apply 方法,方法会接收 compiler 参数