blog
blog copied to clipboard
webpack源码学习系列之三:loader 机制
前言
在上一篇 #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 `less解析出现错误: ${e}, ${e.stack}`;
}
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', `./${identifier}-loader-fake`, `index${ext}`));
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 ------------
文中的depTree是从哪里生成的?@youngwind
请先看第一篇 #99 ,里面有提到。 @mosikoo
学习了
能别评论这么没有营养的东西吗,关注的也会收到邮件
在 2017年10月11日,下午2:52,aaawhz [email protected] 写道:
太难了, 看不太懂, 感觉前端也要变成算法了, 人工智能。 那得花多少时间掌握。。 麻烦。。 。。 还是炒股有意思啊。
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/youngwind/blog/issues/101#issuecomment-335700418, or mute the thread https://github.com/notifications/unsubscribe-auth/ALHp93fOzvQapHbgREBQdXu8yioHM8Ffks5srGWkgaJpZM4MN2-e.
get
博主,loader的pitch过程没说呀。。。
大佬,你打包出来的文件为什么没有那么多的换行符之类的东西,看起来很干净?
你好!我已经收到你的邮件啦!谢谢!