所谓的加载器其实就是一个导出函数的node模块。如果有资源需要该加载器处理,webpack就会自动调用该加载器。这个返回的函数通过上下文提供的this
可以访问Loader API.
设置
在我们深入了解不同种类的加载器以及他们的用法、相关案例之前,我们先来看看在本地开发和测试加载器的三种方式吧。
想要测试一个加载器,你可以在rules
对象里面用path
去resolve
一个本地文件:
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
想要测试多个加载器,你可以通过resolveLoader.modules
配置项告诉webpack应该去哪儿寻找加载器。
比如项目里面有个目录/loaders
:
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
最后,如果你之前已经单独为加载器创建了一个仓库,不要忘了使用npm link
将加载器链接到你当前要测试的项目。
简单用法
加载器处理资源的时候只是传入一个字符串参数,也就是那个资源文件里面的内容。
同步加载器可以简单地返回一个代表被处理资源的值。不过更复杂情况下,也可以通过this.callback(err, values...)
返回任意数量的值。错误(Errors)要么被传入那个this.callback
函数里面,要么就在同步加载器里面抛出。
加载器应该返回1个或者2个值。第一个应该是String或者Buffer类型值,代表处理中的JS代码;第二个是一个JS对象,就是所谓的SourceMap
.
复杂用法
多个加载器链式调用时,值得注意的就是,他们的执行顺序是倒叙的---根据数组格式写法,要么从右向左,要么从下向上。
- 最后一个加载器,最先被调用,它要处理原始的资源。
- 第一个加载器,最后被调用,应该返回JS代码以及一个可选的
source map
. - 中间的加载器,每一个加载器处理的都是前一个加载器执行完的结果。
因此,下面的例子中,foo-loader会首先被调用,处理原始资源,然后返回值传给bar-loader.bar-loader执行完后返回最终的JS结果,有必要的话还会返回Source map.
{
test: /\.js/,
use: [
'bar-loader',
'foo-loader'
]
}
编写原则
加载器编写需要遵循以下原则。根据重要性排列如下。其中有些只在特定场景下使用。想了解更多可以查看随后的详情介绍。
- 尽可能简单
- 使用链式
- 模块化输出
- 确保无状态
- 利用加载器公共功能
- 标记加载器依赖
- 解析模块依赖
- 抽离公共代码
- 避免绝对路径
- 使用peer dependencies
简单
加载器应该只做某个单一的事情。这不但使得维护更容易,同时也允许在更多场景下去链式使用加载器。(其实就是功能比较单一的加载器在复杂的场景下可以被链式的组合使用)
链式调用
充分利用加载器可以链式调用这一特性。比如,我们应该编写5个加载器,让每个加载器去处理一项任务,而不是编写一个处理5项任务的加载器。这种分离不但使得加载器极其简单,而且有时候还能在你原先没有想到场景下使用。
考虑一下。使用加载器options选项或者query参数提供的数据去渲染一个模板文件的场景。这个加载器会首先读取源模板文件,执行,并最终返回一个包含全部html代码的字符串。然而为了符合上面准则(简单,单一原则),apply-loader
可以用来简单的链接其他开源加载器。
-
jade-loader
:将模板转换为一个导出函数的模块。 -
apply-loader
:使用加载器options执行那个函数,并返回原生的html。 -
html-loader
:传入html,并返回一个有效的JS模块。
事实上加载器可以链接也就意味着他们不一定都返回JS代码。只要加载器执行对列下一个加载器能够处理,加载器就能返回任意类型的模块。
模块化
保持模块化输出。加载器产生的模块应该遵循同样的设计原则。
无状态
保证加载器在模块转换的时候不要保持状态。买一次执行加载器都不应该收到其他已编译或者同一个模块往次编译加过的影响。
加载器实用工具
使用加载器工具包loader-utils
。它提供了很多有用的工具,尤其重要的是获取传入加载器的options的工具。同时使用schema-utils
包可以被用来保证加载器options校验的一致性。下面有一个简短的案列。
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
}
export default function(source) {
const options = getOptions(this);
validateOptions(schema, options, 'Example Loader');
// Apply some transformations to the source...
return `export default ${ JSON.stringify(source) }`;
};
加载器依赖
如果加载器使用了外部资源(比如,从文件系统读入),就必须要指明。在watch模式下,这个信息可以用来清除失效的加载器缓存,并重新编译。下面是使用addDependency
方法实现这一功能的一个简短案例:
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);
});
};
模块依赖
由于模块的类型各种各样,所以指定模块依赖的方式也各有不同。比如在css中,我们就使用@import
和url(....)
来指定相关依赖的。模块系统会解析这些依赖的。
下面的两种方法可以完成:
- 把他们全部转换为
require
语句。 - 通过
this.resolve
函数解析路径。
css-loader
加载器就是第一种方案很好的一个案例。它把样式表文件里面的@import和url全部转换为require语句去引入相关资源了。
而less-loader
由于要处理复杂的变量和mixins,所以根本没法把每一个@import给转换为require。因此,less-loader
在less编译器的基础上扩展了自定义的路径解析逻辑。然后它利用上述第二种方案里面的this.resolve
去解析相关依赖。
注意:如果语言只接受相对路径的url,你可以使用`~`去引用已安装的模块(比如`node_modules`里的模块)。这种情况下看起来是这样子的`url('~some-library/image.jpg')`.
公共代码
为了避免在加载器处理的模块里面生成重复的代码,应该在加载器里面生成一个运行时文件放在独立的模块里面,然后让每一个加载器处理的模块require那个共享的运行时文件。
绝对路径
不要在代码里面使用绝对路径,否则一旦项目根目录迁移,以前的哈希名称什么的会破坏掉。loader-utils
里面的stringifyRequest
方法可以把绝对路径转换为相对路径。
同伴依赖
如果你的加载器只是在别的包上封装而成的。你应该把那个包指定为一个peerDependency
.这样允许开发者在package.json
里面指定准确的版本号。
比如。sass-loader
指定node-sass
作为peer dependency
:
"peerDependencies": {
"node-sass": "^4.0.0"
}
测试
现在你已经根据上面的原则编写好自己的加载器了,并在本地运行起来了,接下来干嘛呢?咱们运行一个简单的单元测试以确保加载器符合我们的预期吧。这儿我们使用Jest
这个框架。为了使用import / export
和async / await
我们把babel-jest
以及一些babel预设都给安装上。现在开始安装并把他们保存为devDependencies
。
npm install --save-dev jest babel-jest babel-preset-env
.babelrc
{
"presets": [[
"env",
{
"targets": {
"node": "4"
}
}
]]
}
我们的加载器会处理txt格式的文件,并且仅仅只是使用传入加载器options里面的name
参数去替换txt文件里面的[name]
.然后就会输出转换好的JS代码咯。
src/loader.js
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]!
请注意接下来的这一步哦,我们要使用Node API和memory-fs
来执行webpack
。这样就避免了把输出结果输出到硬盘上了,通过访问stats
数据,我们就可以拿到转换好了的模块了。
npm install --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 = {}) => {
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: {
name: 'Alice'
}
}
}]
}
});
compiler.outputFileSystem = new memoryfs();
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);
resolve(stats);
});
});
}
上面,我们内联了webpack的配置项。但是你依然可以让导出的函数接受一个webpack配置项作为参数,这样就可以使用同一个编译器模块去测试不同的设置了。
现在,我们就可以编写测试案例,并添加npm脚本,跑起来咯:
test/loader.test.js
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
const stats = await compiler('example.txt');
const output = stats.toJson().modules[0].source;
expect(output).toBe(`export default "Hey Alice!\\n"`);
});
package.json
"scripts": {
"test": "jest"
}
一切就绪,现在就可以跑起来看看我们的加载器测试是否通过咯。
PASS test/loader.test.js
✓ Inserts name and outputs JavaScript (229ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.853s, estimated 2s
Ran all test suites.
成功了!现在你就可以去开发,测试,部署你自己的加载器咯。期待你的分享!