深入Loader
Loader的作用就是用于解析处理那些webpack不能识别的非JavaScript模块资源。Loader对Webpack的构建起着至关重要的作用,增加了Webpack构建的可能行,本节将对loader进行详细的介绍。
Loader的使用
Loader有三种方式进行配置使用:
配置文件中配置(推荐):在webpack.config.js文件中指应Loader。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
]
}
};
内联:在每个import语句中显式指定Loader。
import 'style-loader!css-loader?modules!./styles.css';
CLI:在Shell命令中指定它们。
webpack --module-bind jade-loader --module-bind ‘css=style-loader!css-loader’
通过开发实践,推荐使用配置文件来配置Loader,该方式可以通过代码逻辑来控制相关模块的解析,更适用于复杂的场景。
Loader特性
Loader有用很多特性,了解Loader的特性,对使用好Loader有着极大的帮助。
- Loader支持链式传递。能够对资源使用流水线(pipeline)。一组链式的Loader将按照相反的顺序执行。Loader链中的第一个Loader返回值给下一个Loader。在最后一个Loader,返回Webpack所预期的JavaScript。
- Loader可以是同步的,也可以是异步的。
- Loader运行在Node.js中,并且能够执行任何可能的操作。
- Loader接收查询参数。用于对Loader传递配置。
- Loader也能够使用options对象进行配置。
- 除了使用package.json常见的main属性,还可以将普通的Npm模块导出为loader,做法是在package.json里定义一个Loader字段。
- 插件(Plugin)可以为Loader带来更多特性。 Loader能够产生额外的任意文件。
Loader的本质通过导出是一个预处理函数:
// loader基础结构
module.exports = function (content, map, meta) {
return content;
};
可以在该函数中更加灵活地引入细粒度逻辑,例如压缩、打包、语言翻译等等。
解析Loader
Loader遵循标准的模块解析。多数情况下,Loader将从模块路径(通常将模块路径认为是 node_modules)解析。
Loader 模块需要导出为一个函数,并且兼容Node.js的 JavaScript编写。通常使用Npm进行管理,但是也可以将自定义Loader作为应用程序中的文件。按照约定,Loader通常被命名为xxx-loader(例如 css-loader)。
Loader API
所谓的Loader就是一个导出结果为函数的JavaScript模块,Loader runner会调用这个函数,实现对应资源相关的转换操作。大量的Loader API供导出函数内部使用,以此来和Webpack结合实现许多魔法功能。
同步loader
使用return或者this.callback都可以同步地返回转换后的content内容。
使用return:
module.exports = function (content, map, meta) {
return someSyncOperation(content);
};
使用this.callback:
module.exports = function (content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
return; // 当调用 callback() 时总是返回 undefined
};
this.callback相对于return更加灵活,除了传递content,还可以传入其他参数,完整的传参如下:
this.callback(
err: error | null,
content: string | buffer,
sourceMap ?: sourceMap,
meta ?: any
);
- 第一个参数必须是error或者null
- 第二个参数是一个string或者buffer。
- 可选的:第三个参数必须是一个可以被这个模块解析的source map。
- 可选的:第四个选项,会被Webpack忽略,可以是任何东西。 可以将抽象语法树(abstract syntax tree -
AST)(例如 ESTree)作为第四个参数,如果你想在多个Loader之间共享通用的AST,这样做有助于加速编译时间。
异步Loader
过使用this.async来获取callback,可以实现一个异步Loader:
module.exports = function (content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function (err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
在Node.js这样的单线程环境下进行耗时长的同步计算不是个好主意,建议尽可能地使你的Loader异步化。但如果计算量很小,同步Loader也是可以的。
Row
默认情况下,资源文件会被转化为UTF-8字符串,然后传给Loader。通过设置Raw,Loader可以接收原始的Buffer。每一个Loader都可以用String或者Buffer的形式传递它的处理结果。Compiler将会把它们在Loader之间相互转换。
module.exports = function (content) {
// 类型断言
assert(content instanceof Buffer);
return someSyncOperation(content);
// 返回值也可以是一个 `Buffer`
// 即使不是 raw loader 也没问题
};
module.exports.raw = true;
Pitching loader
当使用多个Loader的时候,Loader的默认执行顺序是从右到左,从下到上的,可以通过配置Enforce指定Loader的执行顺序。实际在Loader执行之前,会从左到右调用Loader的pitch方法。
比如下面的Loader配置:
module.exports = {
module: {
rules: [
{
test: /\.render$/,
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};
涉及到的Loader运行步骤可以抽象成如下所示:
使用Loader的pitch的时候,传递给pitch方法的data,会在执行阶段暴露在this.data下面,并且可以用于在循环时,捕获和共享前面的信息
module.exports = function (content) {
return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = Render;
};
当在某个Loader在pitch方法中返回一个结果,那么当前Loader就会跳过执行剩下Loader的任何方法,比如在b-loader中的pitch返回了一些东西:
module.exports = function (content) {
return someSyncOperation(content);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return "module.exports = require(" + JSON.stringify("-!" + remainingRequest) + ");";
}
};
现在所有的Loader执行步骤抽象成如下所示
更多Loader API请前往https://webpack.js.org/api/loaders/查阅。
加载本地Loader
通过module.rules配置,我们可以使用通过Npm下载的Loader模块,当在本地编写好Loader,需要让Webpack识别并且使用该Loader,下面介绍两种Webpack中加载本地Loader的方法。
ResolveLoader
ResolveLoader用作模块解析规则,用于配置如何寻找Loader模块。为了让webpack去加载本地Loader,需要修改 resolveLoader.modules。
假设本地编写的Loader都放在项目的local-loaders目录下:
module.exports = {
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, './local-loaders')]
}
}
默认情况下modules的值为[‘node_modules’], 表示在node_modules目录下去寻找Loader模块。当前配置表示首先去node_modules目录下去寻找Loader,没有找到在去./local-loaders目录下去寻找。
Npm link
Npm link是专门用于快发开发和调试本地Npm模块的,可以将本地正在开发的一个模块的源码连接到项目的node_modules目录下。
实现Npm link的步骤如下:
- 需要正确配置本地开发模块的package.json。
- 在本地的开发模块目录下执行npm link,把该模块注册到全局。
- 在开发的项目下执行npm link
module-name,将第二步注册到全局的模块链接到本项目的node_modules下,module-name为第一步中开发模块的package.json文件中配置的模块名称。
如此以来就可以像使用npm下载的模块一样使用本地开发的模块。
编写自己的Loader
当现有Loader无法满足项目需求时,需要开发者自己编写解决问题的loader,本节将简单介绍一下如何编写Loader。
当我们解析Less文件的时候,我们需要安装less-loader,css-loader,style-loader这三个Loader模块,下面我们就简单实现一下上述的三个Loader。
实现less-loader
在这里,我们需要安装less模块,使用less模块来解析less文件:
var less = require("less");
function loader(content) {
var callback = this.async();
less.render(content, (err, result) => {
if (err) {
return callback(err);
}
callback(null, result.css)
})
}
module.exports = loader;
实现css-loader
function loader(source) {
let reg = /url\((.+?)\)/g;
let pos = 0;
let content;
let arr = ['let list=[]'];
// url地址引用替换成require引用,webpack才能处理
while (content = reg.exec(source)) {
let [matchUrl, g] = content;
let last = reg.lastIndex - matchUrl.length;
arr.push(`list.push(${
JSON.stringify(source.slice(pos, last))})`);
pos = reg.lastIndex;
arr.push(`list.push('url('+require(${
g})+')')`);
}
arr.push(`list.push(${
JSON.stringify(source.slice(pos))})`);
arr.push(`module.exports = list.join('')`);
return arr.join('\r\n');
}
module.exports = loader;
实现style-loader
const loaderUtils = require('loader-utils');
function loader(content) {
content = content.replace(/\n/g, '\\n');
let style = `
let style = document.createElement('style');
style.innerHTML = ${
JSON.stringify(content)};
document.head.appendChild(style);
`
return style;
}
loader.pitch = function (other) {
let style = `
let style = document.createElement('style');
style.innerHTML = require(${
loaderUtils.stringifyRequest(this,
'!!' + other)});
document.head.appendChild(style);
`
return style;
}
module.exports = loader;
现在使用编写的这三个Loader就可以实现less文件的转换,并正确显示效果。
本章节提供案例源码下载:https://gitee.com/mvc_ydb/webpack/blob/master/loader.zip