译文 编写一个loader

2023-04-23,,

https://doc.webpack-china.org/contribute/writing-a-loader

  loader是一个导出了函数的node模块,当资源须要被这个loader所转换的时候,这个函数就会被执行,这个函数可以通过this访问loader api。有三种方式来本地开发和测试loader。

设置

  测试一个单独的loader,可以通过path.resolve一个本地文件来加载loader:

{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}

  测试多个loader的话,可以通过以下方式来添加webpack对loader的搜索路径:

resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}

  第三种方式:通过 npm link 从仓库中引入loader包进我们的项目中

简单用法

  当一个loader被调用时,被导出的函数会被执行,它有一个字符串参数,代表了文件的内容。函数应该返回一个或两个值,分别代表字符串js代码和一个可选的js对象sourceMap,对于复杂的情况,可以调用 this.callback(err, values...) 来返回两个以上的值,里面的err可以直接抛出或者传递给callback都可以,返回一个值时直接return 即可

复杂用法

  对于loader的链式调用,他们的调用次序是从后往前或者从下往上,loader接收上一个loader的处理结果作为参数

loader的书写方针

简单

  loader做单一简单的工作,这有利于使每个loader变得简单,而且使他们更方便地链式调用

链式调用

  使用loader可以被链式调用的优势,把一个任务分成多个简单步骤,让每个简单的loader分别去完成。

模块化

  使输出变得模块化。

无状态

  保证loader每次运行仅仅依赖于上一个loader的输出结果

loader的依赖

  如果一个loader须要读取外部的资源(如读取文件系统),则必须进行依赖声明(使用addDependency),这是为了在观察模式下使缓存无效以及重新编译

import path from 'path';

export default function(source) {
var callback = this.async();
var headerPath = path.resolve('header.js'); this.addDependency(headerPath); fs.readFile(headerPath, 'utf-8', function(err, header) {
if(err) return callback(err);
callback(null, header + "\n" + source);
});
};

模块内的依赖

  对于不同类型的模块,有不同的处理依赖的方式,如css,@import和url(...)会引入依赖,这些依赖须要被模块系统来处理,这有两种进行处理:

    转换为require
    使用this.resolve函数处理路径

  对于第一种方式,css-loader是一个好例子;而对less,不能仅仅对import或url()进行require替换,因为less须要被编译,所以less-loader须要使用第二种方式,通过webpack来处理依赖

抽取模块内的公共代码

  使用loader对模块进行处理时,避免生成公共代码,可以在loader进行处理的时候,生成一个运行时文件,然后通过require实现模块之间的代码共享,而不是在每个模块中都重复生成相同的代码

绝对路径

  不要在模块代码中使用绝对路径,因为当根目录发生变化(项目迁移),文件的hash会发生变化,在loader-utils中有一个函数stringifyRequest可以将绝对路径转为相对路径

peer 依赖

  可以把对其他包的依赖作为一个perrDependency,如sass-loader依赖于node-sass:

"peerDependencies": {
"node-sass": "^4.0.0"
}

测试

  使用jest对loader进行测试

npm i --save-dev jest babel-jest babel-preset-env

.babelrc

{
"presets": [[
"env",
{
"targets": {
"node": "4"
}
}
]]
}

创建一个src/loader.js实现功能:处理一个txt文件,将里面的[name]替换为options里的name值。返回将结果作为一个模块返回

import { getOptions } from 'loader-utils';

export default function loader(source) {
const options = getOptions(this); source = source.replace(/\[name\]/g, options.name); return `export default ${ JSON.stringify(source) }`;
};

要处理的文件test/example.txt如下:

Hey [name]!

下一步是使用nodeApi和memory-fs来启动webpack,这可以让我们把生成的文件提交到内存中,还可以获取文件stats数据。下面进行安装:

npm i --save-dev webpack memory-fs

test/compiler.js

import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs'; export default (fixture, options = {}) => {
// 生成一个webpack实例
const compiler = webpack({
context: __dirname,
// 这里的entry实际会被替换为要处理的文件,entry可以理解为是第一个要被处理的文件
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [{
test: /\.txt$/,
use: {
loader: path.resolve(__dirname, '../src/loader.js'),
options: {
name: 'Alice'
}
}
}]
}
}); // 将webpack的输出文件设置到内存中
compiler.outputFileSystem = new memoryfs(); // webpack 的run接收的函数的第二个参数代表webpack输出的文件信息
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err); resolve(stats);
});
});
}

开始进行测试,执行 test/loader.test.js

import compiler from './compiler.js';

test('Inserts name and outputs JavaScript', async () => {
// 这里指定的被测试文件会被替换为以上的entry文件
const stats = await compiler('example.txt');
const output = stats.toJson().modules[0].source; expect(output).toBe(`export default "Hey Alice!\\n"`);
});

译文 编写一个loader的相关教程结束。

《译文 编写一个loader.doc》

下载本文的Word格式文档,以方便收藏与打印。