如何编写好一个Webpack Loader

本文是对于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在链式调用加载的时候,顺序是从右到左或者是从上到下的。例:代码中给出了fooLoaderbarLoader,执行的顺序将会是先执行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})
    ]
  }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值