本文是对于github上文档how to write a loader(https://github.com/webpack/docs/wiki/how-to-write-a-loader)的翻译加个人理解。如有错漏,敬请指出。
什么是loader
Loader是暴露一个函数的node模块。当资源需要被loader转换的时候,就会调用这个函数。
简单情况下,只有一个loader应用于某个资源的时候,loader将会使用一个参数:被转换成字符串的资源文件内容。在这个函数中,loader可以通过this指向的上下文中来调用loader API。一个只需要给出一个值的简单同步loader,可以直接通过return
返回这个值。在别的情况下,loader如果需要返回任意数量的值,可以使用this.callback(err, values...)
函数。程序错误会通过this.callback
进行传递或者在某个同步loader中直接抛出。
loader可以返回一或两个值,第一个值是字符串或buffer形式的Javascript代码,第二个可选值是一个Javascript对象形式的SourceMap。
在复杂情况下,多个loader是链式调用的,只有最后一个loader能够获得资源文件,也只有第一个loader能够返回一个或者两个值(Javascript代码和SourceMap)。其他任意一个loader的返回值都被传递到前一个loader。换句话说,就是loader在链式调用加载的时候,顺序是从右到左或者是从上到下的。例:代码中给出了fooLoader
和barLoader
,执行的顺序将会是先执行fooLoader
再执行barLoader
。
module: {
rules: [{
test: /\.js/,
use: [{ loader: 'barLoader' }, { loader: 'fooLoader' }]
]
}
注意:webpack只会在你的node_modules
文件夹中搜索你使用的loader。所以,如果loader被定义在node_modules
文件夹外,你需要使用resolveLoader
属性来让webpack找到你的loader文件。比如,你将一个
自定义的loader放到了名为loaders
的文件夹中,那么你需要在配置文件中这样配置:
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
}
如何编写loader
依据上文所说,一个loader就是暴露一个函数的node模块,编译器在调用它的时候,将会把上一个loader返回的结果或者资源文件传递给它。该函数的this
上下文将会被编译器用一些有用的方法填充。单个结果可以由一个同步模块直接返回,对于多个结果,则必须调用this.callback
。在异步loader中,this.async
也必须被调用。如果允许异步,loader应该返回this.callback
,然后,loader将会返回undefined
并且调用callback
函数。
例子
同步loader
module.exports = function(content) {
return someSyncOperation(content);
};
有两个参数的同步loader
module.exports = function(source, map) {
this.callback(null, source, map);
};
异步loader
module.exports = function(content) {
var callback = this.async();
if(!callback) return someSyncOperation(content);
someAsyncOperation(content, function(err, result) {
if(err) return callback(err);
callback(null, result);
});
};
一个在代码之前加上module.exports=的loader
// raw-loader's source - just converts your file to a string with "module.exports=" appended
// This is basically the simplest real world loader.
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
module.exports = function(content) {
this.cacheable && this.cacheable();
this.value = content;
return "module.exports = " + JSON.stringify(content);
}
module.exports.seperable = true;
编写指南
按优先级排序
只专注于单一任务
loader是被链式调用的。为每一个步骤单独创建一个loader,而不是一个loader中做完全部任务。这也说明了如果没有必要,不该将它们转换成Javascript。
例如:通过查询参数来将字符串模板渲染为HTML。
这个任务可以通过一个从源代码中编译模板,执行它会返回一个暴露HTML代码的字符串的模块。但这样做不明智。
正确的做法是,为每一个步骤创建一个loader:
- jade-loader:将模板转换为一个暴露出函数的模块,
- apply-loader:接收一个暴露函数的模块,并返回一个含有查询参数的原始结果
- html-loader:接收原始html,并返回一个暴露原始html字符串的模块
创建模块化的模块
loader生成的模块也应该遵循普通模块的设计原则。
下图的代码就是没有遵循模块化设计的例子:
require("any-template-language-loader!./xyz.atl");
var html = anyTemplateLanguage.render("xyz");
模块缓存
模块都是可缓存的,在loader中调用cacheable函数进行缓存。
// Cacheable identity loader
module.exports = function(source) {
this.cacheable();
return source;
};
不要在运行的时候以及模块之间保存状态
- loader在其他模块编译的时候不应该受影响(除非是当前loader执行的)
- loader应该独立于同个模块的之前执行过的编译
标记依赖
如果loader使用了外部资源(比如调用文件系统进行读取),那么需要在运行时声明。这些信息可以让缓存的loader失效,并且在监视模式下重新编译。
// Loader adding a header
var path = require("path");
module.exports = function(source) {
this.cacheable();
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来解析路径
例1:css-loader:通过将@import
替换为对其他样式表的引入(require),url(...)
替换为对其他文件的引入(require)。
例2:less-loader:less-loader不能直接将@import转换为require,因为less文件需要进行编译来追踪变量和mixins。所以less-loader使用自定义的路径解析逻辑扩展了less编译器。这个自定义逻辑使用了this.resolve
来解析有模块系统配置的文件(别名、自定义模块和文件等)。
如果这种语言只支持相对路径(如CSS中的url(file)
通常是指./file
),可以使用~
符号来指定为引用模块。
url(file) -> require("./file")
url(~module) -> require("module")
抽离公共代码
不要在loader中生成过多的其他模块中都会出现的公共代码,最好的方法是在loader中创建一个runtime文件,把要用的公共代码放到里面再引入。
不要使用绝对路径
不要在模块的代码中使用绝对路径。如果项目根目录改变的时候会破坏哈希(hashing)过程。loader-utils
中有一个stringifyRequest
方法可以将绝对路径变为相对路径。
var loaderUtils = require("loader-utils");
return "var runtime = require(" +
loaderUtils.stringifyRequest(this, "!" + require.resolve("module/runtime")) +
");";
使用peerDependencies指定依赖库
使用peerDependencies允许应用开发人员在package.json中指定依赖的具体版本。这个依赖关系是相对开放的,以便于在依赖库更新的时候不需要发布新的loader版本。
"peerDependencies": {
"library": "^1.3.5"
}
将可编程对象作为查询选项
某些情况下,loader可能需要含有函数的但不能被解析为查询字符串的可编程对象。例如less-loader
,就提供了指定less-plugin
的选项。在这个情况下,loader需要扩展webpack的options对象来获得具体的选项。为了避免命名冲突,需要遵循loader下的驼峰命名格式标准。
// webpack.config.js
module.exports = {
...
lessLoader: {
lessPlugins: [
new LessPluginCleanCSS({advanced: true})
]
}
};