前言
在上一篇 #100 中,我们实现了 webpack 的 code-splitting 功能。今天,我们来探索 loader 机制,最终实现的代码版本参考这里。(参考的 webpack 版本是这个)
问题
以加载 less 为例。
// example.js
require('./style.less');
// style.less
@color: #000fff;
.content {
width: 50px;
height: 50px;
background-color: @color;
}
按照官方文档,想要加载 less 文件,我们需要配置三个 loader:style-loader!css-loader!less-loader。
该从什么地方着手研究呢? → 仔细观察最终生成的 output.js ,如下图所示。
由此我们进行以下思考:
- 既然最终 css 代码会被插入到 head 标签中,那么一定是模块2在起作用。但是,项目中并不包含这部分代码,经过排查,发现源自于 node-modules/style-loader/addStyle.js ,也就是说,是由 style-loader 引入的。(后面我们再考察是如何引入的)
- 观察模块3,那应该是 less 代码经过 less-loader 的转换之后,再包装一层 module.exports,成为一个 JS module。
-
style-loader 和 less-loader 的作用已经明了,但是,css-loader 发挥什么作用呢?虽然我一直按照官方文档配置三个 loader,但我从未真正理解为什么需要 css-loader。后来我在 css-loader 的文档中找到了答案。
@import and url() are interpreted like import and will be resolved by the css-loader.来源:https://github.com/webpack-contrib/css-loader#options
既然如此,为了降低实现的难度,我们暂时不予考虑 import 和 url 的情况,也就无需实现 css-loader 了。 - 观察模块1,
require(2)(require(3))
,很显然:”模块3的导出作为模块2的输入参数,执行模块2“,也就是说:“将模块3中的 css 代码插入到 head 标签中“。理解这个逻辑不难,难点在于:webpack 如何知道应该拼接成require(2)(require(3))
,而不是别的什么。也就说,如何控制拼接出require(2)(require(3))
?
思路
思路进行到这儿,似乎走不下去了。看来只分析 output.js 还不足以理清,那么,让我们更进一步,观察 depTree,如下图所示。(图片较大,请点击放大查看)
问题在于:为什么凭空多出来2个模块?到底是哪里起了作用呢?→ 我在 style-loader 的源码中找到了答案。
style-loader 的再 require
// style-loader/index.js
const path = require('path');
module.exports = function (content) {
// content 的值为:/Users/youngwind/www/fake-webpack/node_modules/style-loader-fake/index.js!/Users/youngwind/www/fake-webpack/node_modules/less-loader-fake/index.js!/Users/youngwind/www/fake-webpack/examples/loader/style.less
let loaderSign = this.request.indexOf("!");
let rawCss = this.request.substr(loaderSign);
// rawCss 的值为:/Users/youngwind/www/fake-webpack/node_modules/less-loader-fake/index.js!/Users/youngwind/www/fake-webpack/examples/loader/style.less
return "require(" + JSON.stringify(path.join(__dirname, 'addStyle')) + ")" +
"(require(" + JSON.stringify(rawCss) + "))";
};
观察源码,我们发现:style-loader 返回的字符串里面又包含了2个 require,分别 require 了 addStyle 和 less-loader!style.less,由此,我们终于找到了突破口。→ loader 本质上是一个函数,输入参数是一个字符串,输出参数也是一个字符串。当然,输出的参数会被当成是 JS 代码,从而被 esprima 解析成 AST,触发进一步的依赖解析。 这就是多引入2个模块的原因。
loaders 的拆解与运行
loaders 就像首尾相接的管道那样,从右到左地被依次运行。对应的代码如下:
// buildDep.js
/**
* 运算文件类型对应的 loaders,比如: less 文件对应 style-loader 和 less-loader
* 这些 loaders 本质上是一些处理字符串的函数,输入是一个字符串,输出是另一个字符串,从右到左串行执行。
* @param {string} request 相当于 filenamesWithLoader ,比如 /Users/youngwind/www/fake-webpack/node_modules/fake-style-loader/index.js!/Users/youngwind/www/fake-webpack/node_modules/fake-less-loader/index.js!/Users/youngwind/www/fake-webpack/examples/loader/style.less
* @param {array} loaders 此类型文件对应的loaders
* @param {string} content 文件内容
* @param {object} options 选项
* @returns {Promise}
*/
function execLoaders(request, loaders, content, options) {
return new Promise((resolve, reject) ={
// 当所有 loader 都执行完了,输出最终的字符串
if (!loaders.length) {
resolve(content);
return;
}
let loaderFunctions = [];
loaders.forEach(loaderName ={
let loader = require(loaderName);
// 每个loader 本质上是一个函数
loaderFunctions.push(loader);
});
nextLoader(content);
/***
* 调用下一个 loader
* @param {string} content 上一个loader的输出字符串
*/
function nextLoader(content) {
if (!loaderFunctions.length) {
resolve(content);
return;
}
// 请注意: loader有同步和异步两种类型。对于异步loader,如 less-loader,
// 需要执行 async() 和 callback(),以修改标志位和回传字符串
let async = false;
let context = {
request,
async: () ={
async = true;
},
callback: (content) ={
nextLoader(content);
}
};
// 就是在这儿逐个调用 loader
let ret = loaderFunctions.pop().call(context, content);
if(!async) {
// 递归调用下一个 loader
nextLoader(ret);
}
}
});
}
请注意:loader 也是分为同步和异步两种的,比如 style-loader 是同步的(看源码就知道,直接 return);而 less-loader 却是异步的,为什么呢?
异步的 less-loader
// less-loader
const less = require('less');
module.exports = function (source) {
// 声明此 loader 是异步的
this.async();
let resultCb = this.callback;
less.render(source, (e, output) ={
if (e) {
throw <code>less解析出现错误: ${e}, ${e.stack}</code>;
}
resultCb("module.exports = " + JSON.stringify(output.css));
});
}
由代码我们可以看出:less-loader 本质上只是调用了 less 本身的 render 方法,由于 less.render 是异步的,less-loader 肯定也得异步,所以需要通过回调函数来获取其解析之后的 css 代码。
node-modules 的逐级查找
还差最后一点,我们就能完成 loader 机制了。
试想以下情景:webpack 检测到当前为 less 文件,需要找到 style-loader 和 less-loader 运行。但是,webpack 怎么知道这两个 loader 藏在哪个目录下面呢?他们可能藏在 example.js 所在目录的任意上层文件夹的 node-modules 中。 说到底,我们还是得实现之前提到过的 node-modules 的逐级查找功能。 核心代码如下:
// resolve.js
/**
* 根据 loaders / 模块名,生成待查找的路径集合
* @param {string} context 入口文件所在目录
* @param {array} identifiers 可能是loader的集合,也可能是模块名
* @returns {Array}
*/
function generateDirs(context, identifiers) {
let dirs = [];
for (let identifier of identifiers) {
if (path.isAbsolute(identifier)) {
// 绝对路径
if (!path.extname(identifier)) {
identifier += '.js';
}
dirs.push(identifier);
} else if (identifier.startsWith('./') || identifier.startsWith('../')) {
// 相对路径
dirs.push(path.resolve(context, identifier));
} else {
// 模块名,需要逐级生成目录
let ext = path.extname(identifier);
if (!ext) {
ext = '.js';
}
let paths = context.split(path.sep);
let tempPaths = paths.slice();
for (let folder of tempPaths) {
let newContext = paths.join(path.sep);
dirs.push(path.resolve(newContext, './node_modules', <code>./${identifier}-loader-fake</code>, <code>index${ext}</code>));
paths.pop();
}
}
}
return dirs;
}
举个例子,对于 style-loader 来说,生成的查找路径集合如下:
[
"/Users/youngwind/www/fake-webpack/examples/loader/node_modules/style-loader-fake/index.js",
"/Users/youngwind/www/fake-webpack/examples/node_modules/style-loader-fake/index.js",
"/Users/youngwind/www/fake-webpack/node_modules/style-loader-fake/index.js",
"/Users/youngwind/www/node_modules/style-loader-fake/index.js",
"/Users/youngwind/node_modules/style-loader-fake/index.js",
"/Users/node_modules/style-loader-fake/index.js",
]
程序按照这个顺序依次查找,直到找到为止或者最终找不到抛出错误。
后话
至此,我们就完成了一个非常简单的 loader 机制,可以通过 style-loader 和 less-loader 处理加载 less 文件。当然,还有很多可以完善的地方,比如:
- 实现 css-loader,以处理 import 和 url 的情况
- 给 loader 传递选项参数,以控制是否压缩代码等等特性
- ……
———– EOF ————
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文来源于网络,其版权属原作者所有,如有侵权,请与小编联系,谢谢!
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com
链接:https://www.javascriptc.com/1354.html
原文链接:https://github.com/youngwind/blog/issues/101