前言

在使用 webpack 时,我们一般都会知道 loader 与 plugin,因为在使用 webpack 时,必不可少,而这边主要了解和知道怎么实现 loader。
loader 是一种对指定内容的打包方案,如在 webpack 中的 rules 某一条配置的规则 test 是匹配 css 类的文件,当遇到这类 css 格式文件,webpack 就会执行这条 rules 里的 loader,实现我们期望的内容打包处理。而 Npm 上有各种各样的 loader 供我们使用,一般情况下,直接下载使用即可,没有必要自己动手实现,但由于项目的特殊性,npm 无法找到适合的包,那么只能自己实现。

什么是 Loader

本质上来说,loader 就是一个 node 模块,既然是 node 模块,那就一定会导出点什么。在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块(resource)的时候调用该函数。在这个函数内部,我们可以通过传入 this 上下文给 Loader API 来使用它们。
简单说,就是与写 node 没有很大区别,只是 webpack 那些 Loader API 都使用需要使用 this 调用。

如:

1
2
3
4
5
6
7
module.exports = function(content) {
if (this.query.isAdd) {
return content+"isAdd";
} else {
return content;
}
};

在 webpack config 里怎么使用自定 loader

一般有三种方式:

  1. 把编写好的 loader 模块发布到 npm 上或放到 package.json 能把包引入到 node_mudules 的地址上。
  2. npm link 方式关联起来。
  3. 使用 require.resolve 或 path.resolve 方式引入。
1
2
3
require.resolve("../loader/add-body-css-loader")
----
path.resolve(__dirname, "../loader/add-body-css-loader")

而 loader 的加载调用顺序是自下而上或自右到左的方式,而 source 是一层层 loader 去处理的,就像加工厂一样。

1
2
3
4
5
6
7
8
{
test: /\.module\.(scss|sass)$/,
use: [
require.resolve("style-loader"),
require.resolve("postcss-loader"),
require.resolve("../loader/add-body-css-loader")
]
}

从 add-body-css-loader->postcss-loader->style-loader 然后输出到文件里。

用正确的姿势开发 Loader

  1. 单一职责
    一个 loader 只做一件事,这样不仅可以让 loader 的维护变得简单,还能让 loader 以不同的串联方式组合出符合场景需求的搭配。

  2. 链式组合
    这一点是第一点的延伸。好好利用 loader 的链式组合的特型,可以收获意想不到的效果。具体来说,写一个能一次干 5 件事情的 loader ,不如细分成 5 个只能干一件事情的 loader,也许其中几个能用在其他你暂时还没想到的场景。

Loader 实用工具

loader-utils
schema-utils 使用来校验 Options。

例子

sass、less 等简单的生成主题处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
const log4js = require("../config/log4js"),
logger = log4js.getLogger("info");

function replace(source) {
const theme = this.query.theme;
let isBody = false;
let content = source;
let loader = this.query.loader;
Object.keys(theme).forEach(item => {
Object.keys(theme[item]).forEach(item => {
if (~source.indexOf(item)) {
isBody = true;
}
});
});
if (isBody) {
if (~loader.indexOf("less")) {
content = createThemeLessVar(theme, source, loader);
} else if (~loader.indexOf("sass")) {
content = createThemeSassVar(theme, source, loader);
}
}

return content;
}

function createThemeSassVar(theme, source, loader) {
let prefix = "$";
let reg = /(@import[^\n]*)/g;
let importCss = source.match(reg) || "";
if (importCss) {
importCss = importCss.join("\n");
}
let newSource = source.replace(reg, "");
let content = `${importCss}`;
Object.keys(theme).forEach(item => {
let allVar =
Object.entries(theme[item])
.map(item => {
return prefix + item[0] + ":" + item[1];
})
.join(";") + ";";

content += `
body[data-theme="${item}"]{
${allVar}
${newSource}
}
`;
});
return content;
}

function createThemeLessVar(theme, source, loader) {
let prefix = "@";
let reg = /(@import[^\n]*)/g;
let importCss = source.match(reg).join("\n");
let newSource = source.replace(reg, "");
let content = `${importCss}`;

Object.keys(theme).forEach(item => {
let allVar =
Object.entries(theme[item])
.map(item => {
return prefix + item[0] + ":" + item[1];
})
.join(";") + ";";
let cssFn = `
.${item}(){
${allVar}
${newSource}
}
`;

content += `
${cssFn}
body[data-theme="${item}"]{
.${item}();
}
`;
});
return content;
}

module.exports = function(content) {
if (this.query.isAdd) {
return replace.call(this, content);
} else {
return content;
}
};

当内部有异步处理时可以使用 this.async(),会返回一个 callback 函数,调用它,代表当前 loader 已经处理完,到下一个或结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 相当于异步版的 return
callback(null, header + "\n" + source);
});
};

async 返回的 callback 入参:

1
2
3
4
5
6
7
8
9
10
11
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);

第一个参数必须为Error或null
第二个参数是string或Buffer,既处理后的内容。
可选:第三个参数必须是可由此模块解析的源映射。
可选:webpack忽略的第四个选项可以是任何内容(例如某些元数据)。

例子项目地址