FrankKai.github.io
FrankKai.github.io copied to clipboard
[译]如何写一个webpack Loader?
原文:https://webpack.js.org/contribute/writing-a-loader/#setup demo仓库:https://github.com/FrankKai/webpack-loader-demo
- 什么是loader
- 安装
- 简单用法
- 复制用法
- 指南
- 简单
- 链式
- 模块化
- 无状态
- loader工具
- loader依赖
- 模块依赖
- 通用代码
- 绝对路径
- 对等依赖
- 测试
什么是loader
loader是导出了一个函数的node模块。 这个函数会在资源被这个loader转换时,调用。这个函数,通过this上下文,拥有Loader的API。
安装
在我们深入不同类型的loader之前,它们的用法,它们的示例前,先看看你可以在本地开发和测试的3种方式。
为了测试单个loader,你可以在rule对象中,使用path去解析一个本地文件:
// webpack.config.js
const path = require('path');
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve('path/to/loader.js')
}
]
}
]
}
}
如果想测试多个文件,可以利用resolveLoader.modules配置去升级webpack搜索多个loader。例如,如果你有本地的/loaders目录:
// webpack.config.js
const path = require('path');
module.exports = {
// ...
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')],
}
}
如果拆了仓库或者包出去,可以使用npm link测试。
简单用法
当一个loader应用到资源时,loader只会接收一个参数被调用-这个参数是一个包含了资源文件内容的字符串。
同步loader可以返回一个单值,这个值代表转化后的模块。在更复杂的场景中,loader可以通过this.callback(err, values...)函数返回任意数量的值。异常也可以传。
loader需要一个或者两个值。第一个值是string或者buffer类型的js代码。第二个值是SourceMap类型的js对象。
复杂用法
当多个loader链式处理时,有一点很重要,他们会以逆序执行,从右到左还是从下到上取决于数组的格式。
- 最后一个loader,首先调用,给它传入的是raw(生的)资源内容
- 第一个loader,最后调用,它返回的是js代码以及可选的js sourcemap
- 在中间的loader们,会接收上一个loader的输出,作为自己的输入
在下面这个例子中,foo-loader会被传入raw(生的)资源,并且bar-loader会接收foo-loader的输出,并且返回最终转化后的模块和source map。
// webpack.conifg.js
module.exports = {
module: {
rules: [
{
test: /\.js/,
use: ['bar-loader', 'foo-loader'],
}
]
}
}
指南
指南部分是写loader的详细部分。根据重要性排序,有一些仅在特定场景下使用,阅读详细章节去获得更多信息。
- 使loaders 简约
- 利用 chaining 链式转换
- 发射 modular 输出
- 确保 loaders 无状态
- 使用 loader 工具库
- 标记 loader 依赖
- 解析 模块依赖
- 抽离 通用代码
- 避免 绝对路径
- 使用 对等依赖
简约
loader应该只做一个任务。这不仅仅会让维护每个loader变得更容易,同样也可以允许它们被任意链式组合,在更多场景使用。
链式转化
用加载器可以链接在一起这一事实。不要在一个loader里对付5个任务,而是拆成5个简单loader各自分工。隔离它们不仅保持单个独立loader的简洁性,而且允许它们在一些你没有预料到的场景中使用。
假设现在有一个场景,通过loader选项或者查询参数渲染一个模板文件。可以通过写一个单loader,通过资源编译模板,执行它,并且返回一个模块,这个模块导出一个包含了HTML代码的字符串。
但是,根据指南,存在可以与其他开源加载器链接的应用加载器:
- pug-loader: 将模板转换为导出了一个函数的模块
- apply-loader:通过loader配置执行函数,返回生的HTML
- html-loader:接收HTML并且输出一个有效的js模块
模块化
将输出模块化。loader生成的模块,需要尊重和常规模块一样的设计原则。
无状态
确保loader在转换模块时,不保持状态。每次运行都应始终独立于其他已编译的模块,以及同一模块的先前编译。
loader工具库
loader-utils提供了一系列有用的工具。schema-utils可用于对持续的用于loader配置的JSON Schema做校验。这里有一个简单的使用utilizes的例子:
// loader.js
import { urlToRequest } from 'loader-utils';
import { validate } from 'schema-utils';
const shema = {
type: 'object',
properties: {
test: {
type: 'string',
}
}
}
export default function (source) {
const options = this.getOptions();
validate(schema, options, {
name: 'Example Loader',
baseDataPath: 'options',
})
console.log("The requrest path", urlToRequest(this.resourcePath));
return `export default ${JSON.stringify(source)}`;
}
loader 依赖
如果一loader使用了额外的资源,必须要标明它。此信息用于使可缓存加载程序无效并在监视模式下重新编译。这里有一个使用addDependency方法来添加loader依赖的例子:
// loader.js
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);
});
}
模块依赖
取决于模块的类型,可以使用不同的schema去指明依赖。例如在CSS,@import和url(...)语句会用到。这些依赖应该被模块系统解析。
有下面两种实现方式:
- 将它们转换为require语句
- 使用this.resolve函数去解析路径
css-loader就是第一种方法的典型例子。它将依赖转换为requires,通过替换@import语句为require另一个样式表以及url(...)也通过require去引用文件。
对于less-loader,它不能加个每个@import转换为require,因为所有的.less文件必须被编译一次,从而进行变量和mixin追踪。因此,less-loader使用自定义的路径解析逻辑,去扩展了less编译器。就是利用第二种方式,this.resolve去实现的,通过webpack去解析依赖。
抽离 通用代码
避免在每一个loader进行中都生成通用的代码。取而代之的是,在loader中创建一个运行时文件,并且为共享模块生成一个require:
// src/loader-runtime.js
const { someOtherModule } = require('./some-other-module');
module.exports = function runtime(params) {
const x = params.y * 2;
return someOtherModule(params, x);
}
铺垫了这么多,终于可以写loader了
// src/loader.js
import runtime from './loader-runtime.js';
export default function loader(source) {
// 自定义的loader逻辑
return ``${runtime({
source,
y: Math.random(),
})}
}
绝对路径
不要插入绝对路径,因为如果项目根路径发生变化时,会导致哈希崩溃。有一个叫做stringifyRequest方法在loader-utils中,可用于转换绝对路径为相对路径。
对等依赖
如果loader是另一个包的简单包装器,你需要将这个包裹作为对等依赖。这个方法允许应用的开发者在package.json去声明精确的版本。
例如sass-loader声明了node-sass为对等依赖
{
"peerDependencies": {
"node-sass": "^4.0.0"
}
}
测试
使用Jest测试loader,并且使用babel-jest是的我们可以用import/export以及async/await。
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
// babel.conifg.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
我们的loader将处理.txt类型的文件并且通过loader的name option去替换任意的[name]示例。然后它将输出一个包含了default export文本的有效的js模块:
// src/loader.js
export default function loader(source) {
const options = this.getOptions();
source = source.replace(/\[name\]/g, options.name);
return `export default ${JSON.stringify(source)}`;
}
我们将用这个loader去处理下面这个文件:
test/example.txt
Hey [name]!
请注意,我们后面将使用nodejs以及memfs去运行webpack。这可以允许我们不将output输出到磁盘上并且去观察状态。
npm install --save-dev webpack memfs
import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';
export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.txt$/,
use: {
loader: path.resolve(__dirname, '../src/loader.js'),
options,
}
}
]
}
})
// Volume可以理解为文件系统卷,内存上的文件系统卷
compiler.outputFileSystem = createFsFromVolume(new Volume());
compiler.outputFileSystem.join = path.join.bind(path);
return new Promise((resolve, reject)=>{
compiler.run((err, stats)=>{
if(err) reject(err)
if (stats.hasErrors()) reject(stats.toJson().errors);
resolve(stats);
})
})
}
现在,我们写一个测试和npm script去运行。
// test/loader.test.js
/**
* @jest-environment node
*/
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
const stats = await compiler('example.txt', { name: 'Alice' });
const output = stats.toJson({ source: true }).modules[0].source;
expect(output).toBe('export default "Hey Alice!\\n"');
});
package.json
{
"scripts": {
"test": "jest"
},
"jest": {
"testEnvironment": "node"
}
}
> [email protected] test
> jest
console.log
loader before: Hey [name]!
at Object.log (src/loader.js:4:11)
console.log
loader after: Hey Alice!
at Object.log (src/loader.js:8:11)
PASS test/loader.test.js
✓ Inserts name and outputs JavaScript (1226 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.859 s, estimated 2 s
Ran all test suites.
成功了! 现在开始,你可以去开发、测试、部署你自己的loader。我们期待在社区中分享你的灵感和创造!