译文 编写一个loader
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"`); });