Loader 作为 Webpack 的核心机制,内部的工作原理却非常简单。接下来我们一起来开发一个自己的 Loader,通过这个开发过程再来深入了解 Loader 的工作原理。
我们的需求是开发一个可以加载 markdown 文件的加载器,以便可以在代码中直接导入 md 文件。我们都应该知道 markdown 一般是需要转换为 html 之后再呈现到页面上的,所以我希望导入 md 文件后,直接得到 markdown 转换后的 html 字符串,如下图所示:
由于这里需要直观地演示,我们就不再单独创建一个 npm 模块,而是就直接在项目根目录下创建一个 markdown-loader.js 文件,完成后可以把这个模块发布到 npm 上作为一个独立的模块使用。
about.md
<!-- ./src/about.md --># Aboutthis is a markdown file.
index.js
...// 引入解析 MD 文件import about from './about.md'console.log(about)// 希望 about 输出 => '<h1>About</h1><p>this is a markdown file.</p>'...
我们先执行打包命令 npx webpack,会发现打包报错,.md 文件没有对应的 loader处理,如下图所示:
下面我们来实现 md 的 loader。
每个 Webpack 的 Loader 都需要导出一个函数,这个函数就是我们这个 Loader 对资源的处理过程,它的输入就是加载到的资源文件内容,输出就是我们加工后的结果。我们通过 source 参数接收输入,通过返回值输出。这里我们先尝试打印一下 source,然后在函数的内部直接返回一个字符串 hello loader ~,具体代码如下所示:
module.exports = (source) => {
console.log(source);
return 'hello loader ~';
};
完成以后,我们回到 Webpack 配置文件中添加一个加载器规则,这里匹配到的扩展名是 .md,使用的加载器就是我们刚刚编写的这个 markdown-loader.js 模块,具体代码如下所示:
rules: [
...
{
test: /\.md$/,
use: [
// 直接使用相对路径
{ loader: './loaders/markdown-loader.js' },
],
},
...
]
TIPS:这里的 use 中不仅可以使用模块名称,还可以使用模块文件路径,这点与 Node 中的 require 函数是一样的。
配置完成后,我们再次打开命令行终端运行打包命令,如下图所示:
打包过程中命令行确实打印出来了我们所导入的 Markdown 文件内容,这就意味着 Loader 函数的参数确实是文件的内容。
但同时也报出了一个解析错误,说的是: You may need an additional loader to handle the result of these loaders.(我们可能还需要一个额外的加载器来处理当前加载器的结果)。
那这究竟是为什么呢?其实 Webpack 加载资源文件的过程类似于一个工作管道,你可以在这个过程中依次使用多个 Loader,但是最终这个管道结束过后的结果必须是一段标准的 JS 代码字符串。
所以我们这里才会出现上面提到的错误提示,那解决的办法也就很明显了:
直接在这个 Loader 的最后返回一段 JS 代码字符串;
再找一个合适的加载器,在后面接着处理我们这里得到的结果。
先来尝试第一种办法。回到 markdown-loader 中,我们将返回的字符串内容修改为 console.log(‘hello loader~’),然后再次运行打包,此时 Webpack 就不再会报错了,代码如下所示:
那此时打包的结果是怎样的呢?我们打开输出的 bundle.js,找到最后一个模块(因为这个 md 文件是后引入的),如下图所示:
运行效果如下图所示:
实现 Loader 的逻辑
了解了 Loader 大致的工作机制过后,我们再回到 markdown-loader.js 中,接着完成我的需求。这里需要安装一个能够将 Markdown 解析为 HTML 的模块,叫作 marked。
安装完成后,我们在 markdown-loader.js 中导入这个模块,然后使用这个模块去解析我们的 source。这里解析完的结果就是一段 HTML 字符串,如果我们直接返回的话同样会面临 Webpack 无法解析模块的问题,正确的做法是把这段 HTML 字符串拼接为一段 JS 代码。
此时我们希望返回的代码是通过 module.exports 导出这段 HTML 字符串,这样外界导入模块时就可以接收到这个 HTML 字符串了。如果只是简单地拼接,那 HTML 中的换行和引号就都可能会造成语法错误,所以我这里使用了一个小技巧,具体操作如下所示:
const marked = require('marked')
module.exports = (source) => {
// 1. 将 markdown 转换为 html 字符串
const html = marked(source);
// 2. 将 html 字符串拼接为一段导出字符串的 JS 代码
const code = `module.exports = ${JSON.stringify(html)}`;
return code;
};
先通过 JSON.stringify() 将字段字符串转换为标准的 JSON 字符串,然后再参与拼接,这样就不会有问题了。
我们回到命令行再次运行打包,打包后的结果就是我们所需要的了。
除了 module.exports 这种方式,Webpack 还允许我们在返回的代码中使用 ES Modules 的方式导出,例如,我们这里将 module.exports 修改为 export default,然后运行打包,结果同样是可以的,Webpack 内部会自动转换 ES Modules 代码。
const marked = require('marked')
module.exports = (source) => {
const html = marked(source);
const code = `export default ${JSON.stringify(html)}`
return code;
};
此时在打包项目,运行后的 md 输出结果就是我们想要的字符串内容了,如下图所示:
多个 Loader 的配合
我们还可以尝试一下刚刚说的第二种思路,就是在我们这个 markdown-loader 中直接返回 HTML 字符串,然后交给下一个 Loader 处理。这就涉及多个 Loader 相互配合工作的情况了。
我们回到代码中,这里我们直接返回 marked 解析后的 HTML,代码如下所示:
const marked = require('marked')
module.exports = (source) => {
// 1. 将 markdown 转换为 html 字符串
const html = marked(source)
return html
}
然后我们再安装一个处理 HTML 的 Loader,叫作 html-loader。然后配置 webpack.config.js ,代码如下所示:
{
test: /\.md$/,
use: [
'html-loader',
'./loaders/markdown-loader.js'
],
},
安装完成过后回到配置文件,这里同样把 use 属性修改为一个数组,以便依次使用多个 Loader。不过同样需要注意,这里的执行顺序是从后往前,也就是说我们应该把先执行的 markdown-loader 放在后面,html-loader 放在前面。
完成以后我们回到命令行终端再次打包,这里的打包结果仍然是可以的。
至此,我们就完成了这个 markdown-loader 模块,其实整个过程重点在于 Loader 的工作原理和实现方式。
最后
总体来说,Loader 机制是 Webpack 最核心的机制,因为正是有了 Loader 机制,Webpack 才能足以支撑整个前端项目模块化的大梁,实现通过 Webpack 去加载任何你想要加载的资源。
换个角度来说,也正是有了 Loader 这种扩展机制,社区才能不断地为 Webpack 添砖加瓦,形成今天 Webpack 在前端工程化中不可撼动的地位。
如果我们想要玩转 Webpack,就必须加深对 Loader 机制和原理的理解。我们这里只是抛砖引玉,也希望你可以通过更多的尝试继续探索。
拓展(获取配置参数)
我们看到有一些 loader 配置的时候可以配置参数,那么我们在自定义 loader 的时候,如何获取这些参数呢?
// webpack.config.js
use: [
'html-loader',
{
loader: './loaders/markdown-loader.js',
options: {
name: 'md-loader',
minimize: true,
}
},
],
Webpack 5
const marked = require('marked')
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
console.log('options', options);
// 1. 将 markdown 转换为 html 字符串
const html = marked(source)
return html
}
Webpack 4
需要借助 Webpack 内置的工具包 loader-utils 。除了 loader-utils 之外包还有 schema-utils 包,我们可以用 schema-utils 提供的工具,获取用于校验 options 的 JSON Schema 常量,从而校验 loader options。
const marked = require('marked')
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
console.log('options', options);
// 1. 将 markdown 转换为 html 字符串
const html = marked(source)
return html
}

