webpack-loader原理

webpack loader原理

loader是什么

官网解释:所谓 loader 只是一个导出为函数的 JavaScript 模块。loader runner 会调用这个函数,然后把上一个 loader 产生的结果或者资源文件(resource file)传入进去。函数的 this 上下文将由 webpack 填充,并且 loader runner 具有一些有用方法,可以使 loader 改变为异步调用方式,或者获取 query 参数。
个人理解:loader本质上是一个函数,这个函数会在加载正则匹配到的文件时执行,一个loader类似一个过滤器,给loader一个东西,返回一个函数中处理过的东西,并由loader runnder传递给下一个loader。

markdown-loader

简单看下arkdown-loader源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";

const marked = require("marked");
const loaderUtils = require("loader-utils");

module.exports = function (markdown) {
// merge params and default config
const options = loaderUtils.getOptions(this);

this.cacheable();

marked.setOptions(options);

return marked(markdown);
};

可以看到markdown-loader的源码只有几行,loaderUtils为webpack官方插件,用来获取loder的配置项。这个函数接受一个markdown参数,然后中间用marked包做了一些事情,最后返回marked包处理的参数。注意:当我们编写loader时,这个函数必须返回一个buffer或者string。

markdown参数是上一个loader传递过来的文件的字符串,一般来说,在loader内部会使用一些包来把这个字符串生成ast抽象语法树,或者正则匹配一些特定字符,做一些处理后,再将其转成字符串传递给下一个loader。这些也是webpack一些loader慢的原因,因为每个loader都需要将上一个loader传递过来的内容生成语法树,然后做一些处理,最后吐出。

自己简单实现一个loader

初始化一个项目,安装webpack和webpack-cli,结构大致如下:

1
2
3
4
5
-- node_modules
-- src
--index.js
-- package.json
-- webpack.config.js

内容如下:

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
//index.js
console.log('🏆🏆🏆')

// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}

//package.json
{
"name": "webpack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^5.3.1",
"webpack-cli": "^4.1.0"
}
}

接着src下面新建一个myloader.js:

1
2
3
4
5
// myloader.js
module.exports = function (content) {
let res = `console.log("🚀🚀🚀");${content};`;
return res
}

配置myloader:

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

// webpack.config.js

const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: path.resolve('./src/myloader.js'),
}
}
]
}
}

执行打包命令:npm run build,可以看到打包后的js多了小火箭🚀,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is not neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: */
eval("console.log(\"🚀🚀🚀\");console.log('🏆🏆🏆');\n\n//# sourceURL=webpack://webpack/./src/index.js?");
/******/ })()
;

这里只是简单demo,具体操作可根据业务需要变更,例如可以编写一个国际化语言的loader等。

官方demo

this.callback 方法则更灵活,可传递多个参数,官网示例:

1
2
3
4
module.exports = function(content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
return; // 当调用 callback() 时总是返回 undefined
};

对于异步 loader,使用 this.async 来获取 callback 函数,官网示例:

1
2
3
4
5
6
7
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 最初被设计为可以在同步 loader pipeline(如 Node.js ,使用 enhanced-require),与异步 pipeline(如 webpack )中运行。然而在 Node.js 这样的单线程环境下进行耗时长的同步计算不是个好主意,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。

查看评论