Loader 是一个 NodeJS 的普通模块,通过 module.exports 挂载出一个纯函数。
/**
*
* @param {string} content
* @returns
*/
module.exports = function(content) {
// 对content进行处理,eg:
return content.replace('hello', '你好');
};
content 通常是字符串类型,如果设置了 module.exports.raw = true,content就是Buffer类型。
Loader有什么用
本质就是接受输入,可以对输入进行各式各样的修改后返回。
解决兼容性问题
我们可以随意写ES6的语法,CSS的语法,通过 Loader 可以帮助我们完成兼容性问题处理。
使用 JSX
为了提升开发效率,我们使用 JSX 编写模板更加简单快速, Loader 可以将编写的 JSX 转化成框架API。
// JSX
<div onClick={sayHello}>
<h1>hello</h1>
</div>
// 通过 babel-loader 转成 React API,也是实际上在浏览器运行的JS代码。
React.createElement("div", {
onClick: sayHello
}, React.createElement("h1", null, "hello"));
Loader之间如何协作
这里直接引用官方的例子,非常直观。
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};
那么执行的顺序应该是
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
可以看到 loader 是倒序执行的,如果我们想要顺序的时候去做一些事情怎么办?
我们可以给 loader对象挂载一个 pitch 方法。
/**
*
* @param {string} content
* @returns
*/
module.exports = function(content) {
// 对content进行处理,eg:
return content.replace('hello', '你好');
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log("pitch");
};
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
如果 b-loader的pitch方法返回了值,则忽略c-loader,从a-loader开始倒序执行 normal execution。
module.exports = function(content) {
return someSyncOperation(content);
};
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
if (someCondition()) {
return 'cache content';
}
};
一旦 someCondition() 满足,执行的顺序如下:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
基于 Pitch 的机制,我们可以缓存之前 loader 处理的结果,比如已存在b-loader、c-loader处理后的数据,在a-loader中判断满足缓存条件,直接返回缓存数据,跳过b、c的处理,提升构建效率。
更改loader执行顺序
如果我们希望改变loader的执行顺序,可以通过 rule.enforce 来实现:
module.exports = {
//...
module: {
rules: [
{
//...
use: [
{
loader: 'a-loader',
enforce: 'pre'
},
{
loader: 'b-loader',
enforce: 'post'
},
'c-loader'
]
}
]
}
};
那么实际的顺序就是
|- b-loader `pitch`
|- c-loader `pitch`
|- a-loader `pitch`
|- requested module is picked up as a dependency
|- a-loader normal execution
|- c-loader normal execution
|- b-loader normal execution
那为什么不直接调整loader的顺序,非要通过enforce来指定呢?如果只是单纯调整loader顺序,那么所有的loader还是会被执行,但是当我们指定enforce,我们可以将loader分类,控制各种类型loader的执行:
- 所有普通 loader 可以通过在请求中加上 ! 前缀来忽略(覆盖)。
- 所有普通和前置 loader 可以通过在请求中加上 -! 前缀来忽略(覆盖)。
- 所有普通,后置和前置 loader 可以通过在请求中加上 !! 前缀来忽略(覆盖)。
// 禁用普通 loaders
import { a } from '!./file1.js';
// 禁用前置和普通 loaders
import { b } from '-!./file2.js';
// 禁用所有的 laoders
import { c } from '!!./file3.js';
Loader的工作方式
Loader 支持同步和异步的处理方式
同步
module.exports = function(content, map, meta) {
return content;
};
// 或者
module.exports = function(content) {
this.callback(null, content);
return;
};
异步
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);
});
};
Loader 和 Webpack 之间如何协作
直接上图,省略了较多步骤。 流程图👇
执行 loader 主要是 loader-runner 这个库,作为独立的npm包,可以单独使用。
Loader 和 Plugin 的区别
Loader 是对一个个的文件进行处理,它是一个转换器,将A文件进行编译成B文件。
Plugin 是贯穿在整个构建生命周期,可以对不同阶段的构建产物进行处理。
如何写一个高质量的 Loader
对于比较复杂的Loader,通常需要我们设置一些参数,比如babel-loader设置预设和插件:
module: {
rules: [
{
test: /.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-object-rest-spread']
}
}
}
]
}
为了保证参数的准确性,Webpack提供了 loader-utils 和 schema-utils 这两个库进行校验。
import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';
// loader所需的options类型
const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
};
export default function(source) {
// 获取参数
const options = getOptions(this);
// 进行验证
validate(schema, options, {
name: 'Example Loader',
baseDataPath: 'options'
});
// Apply some transformations to the source...
return `export default ${ JSON.stringify(source) }`;
}
另外,官方也提出了一下建议:
- 功能单一,尽量简单。
- 保证链接性,也就是接受前一个loader返回的,输出给下一个loader。
- 输出的代码也需要满足模块化。
- 满足无状态,FP的核心,每次运行应该始终独立于其他已编译模块以及同一模块的以前编译。
- 显示声明依赖,通过 this.addDependency,保证依赖修改后缓存失效。
- 不要在Loader中使用绝对路径,避免修改目录名称后hash失效。