# 底层原理
# 一、如何编写一个loader
1、创建一个loader
// replaceLoader.js
const loaderUtils = require('loader-utils');
// 编写一个loader,注意只能是function,不能是箭头函数,因为this需指向webpack
module.exports = function (source) {
//console.log(source) 源代码
/********************取参数******************/
// 方法1、从this.query取出从options里传递过来的参数,前提是options是一个对象
//console.log('this.query',this.query); // this.query包含了从options里传递过来的参数
// 方法2、使用loaderUtils的方法取(推荐)
const options = loaderUtils.getOptions(this); // options就是一个对象
//console.log('options.name',options.name)
/********************处理结果******************/
// 方法1,直接return
//return source.replace(new RegExp(this.query.name,'gm'), 'lensh')
// 方法2,使用this.callback,这种适合需要把sourceMap传递出去的情况
//const result = source.replace(this.query.name, 'lensh');
//this.callback(null, result);
// 方法3,使用this.async,这种适合有异步操作的逻辑
const callback = this.async();
setTimeout(() => {
const result = source.replace(this.query.name, 'lensh');
callback(null, result);
}, 1000)
}
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
说明:
如果webpack的配置文件里的options是一个对象,则直接使用this.query就可以取出options里面的内容。如果不是一个对象,则借助
loader-utils
来处理。如果除了要返回结果,还需要返回sourceMap之类的东西,则使用
this.callback(null, result)
。如果loader里面有异步操作,则使用
this.async()
来处理。如果需要在异步loader执行完后再执行同步loader,则把先要处理的loader放在数组后面。
例如,下面的replaceLoader.js负责把lensh换成dell,而replaceLoaderAsync.js负责异步把dell换成lensh,现在想要先处理异步loader,处理完后再处理同步loader,则需要修改webpack的配置,把先要处理的loader放在数组后面。
// replaceLoader.js
module.exports = function (source) {
return source.replace(new RegExp('lensh','gm'), 'dell');
}
// replaceLoaderAsync.js
module.exports = function (source) {
const callback = this.async();
setTimeout(() => {
const result = source.replace(new RegExp('dell','gm'), 'lensh');
callback(null, result);
}, 2000)
}
// webpack.config.js
rules: [{
test: /\.js$/,
// 先处理的loader放在数组后面
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
}, {
loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
options: {
name: 'dell'
}
}]
}]
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
- 如果我们不想使用
path.resolve(__dirname, './loaders/replaceLoader.js')
这样的方式引入自定义的loader,而是直接使用replaceLoader,则使用resolveLoader定义loader的寻找方式即可。
2、在webpack配置文件里配置这个loader。
// webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: "development",
entry: {
index: './src/index.js'
},
output: {
filename: '[name].[chunkhash:8].js',
path: path.resolve(__dirname, './dist')
},
// 定义loader的寻找方式
resolveLoader: {
modules: ['node_modules', './loaders'] // 如果定义的loader在node_modules下找不到,则还去loaders下面寻找
},
module: {
rules: [{
test: /\.js$/,
//use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
use: [{
loader: 'replaceLoader',
}, {
loader: 'replaceLoaderAsync',
options: {
name: 'dell'
}
}]
}]
},
plugins: [
new CleanWebpackPlugin()
]
}
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
# 二、如何编写一个plugin
loader处理模块,plugin在打包过程中某些时刻生效。
1、新建一个copyright-webpack-plugin.js
我们要做的功能是打包前把dist目录清空,然后打包后在dist文件夹下新增一个copyright.txt文件。
const fs = require('fs')
const path = require('path')
// 清空目录
const emptyDir = (fileUrl) => {
const files = fs.readdirSync(fileUrl)
files.map(file => {
const filePath = fileUrl + '/' + file;
if (fs.statSync(filePath).isDirectory()) {
emptyDir(filePath)
} else {
fs.unlinkSync(filePath)
console.log("删除文件" + filePath + "成功")
}
})
}
// 插件必须是一个类
class CopyrightWebpackPlugin {
constructor(options) {
// options是webpack.config.js里的plugin里面传递过来的 { name: 'Lee' }
console.log(options)
}
apply(compiler) { // compiler是webpack的一个实例
// 准备编译的时刻是compile,同步用tap,异步用tapAsync
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('准备打包');
const directory = path.resolve(__dirname, '../dist')
// 清空dist目录
emptyDir(directory);
});
// 在打包完准备放到dist目录下的时刻是emit
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
console.log('打包完了,准备放到dist目录里了');
//debugger; //这里加断点
console.log(compilation.assets); // compilation.assets包含了打包后的所有内容
// 往dist里加一个文件
compilation.assets['copyright.txt'] = {
source: function () { // source,即copyright.txt文件里面的内容
return 'copyright'
},
size: function () { // 说明文件的大小是20字节
return 20;
}
};
cb();
});
}
}
module.exports = CopyrightWebpackPlugin;
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
2、修改webpack.config.js
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin');
// 修改plugins
plugins: [
new CopyrightWebpackPlugin({
name: 'Lee'
})
]
2
3
4
5
6
7
8
如果我们需要打印出compilation下包含什么内容,我们可能会用console.log(compilation)
去看,但是这样不直观。我们可以用 node --inspect --inspect-brk node_modules/webpack/bin/webpack.js
命令去执行分析。
这个 --inspect
作用是开启node的调试,--inspect-brk
是在node的第一行打一个断点。
然后作以下配置:
// copyright-webpack-plugin.js (加一个debugger)
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
console.log('打包完了,准备放到dist目录里了');
debugger; // 在这里打断点
console.log(compilation.assets); // compilation.assets包含了打包后的所有内容
// 往dist里加一个文件
compilation.assets['copyright.txt'] = {
source: function () { // source,即copyright.txt文件里面的内容
return 'copyright'
},
size: function () { // 说明文件的大小是20字节
return 20;
}
};
cb();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// package.json(使用node命令)
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
},
2
3
4
运行npm run debug
,然后打开chrome浏览器的控制台,在Elements左边会有一个图标,点击这个图标,然后就可以看到打印出的内容了。
# 三、Bundler 源码编写
先进行模块分析,然后根据依赖关系生成Dependencies Graph(依赖图谱),最后生成打包后的代码。
打包步骤:
- 1、读取入口文件的内容,得到一个字符串
- 2、利用@babel/parser将字符串转成ast
- 3、使用@babel/traverse进行ast的分析
- 4、利用@babel/core将ast进行转义,得到的code就是可以在浏览器运行的代码
- 5、通过遍历入口文件的Dependencies生成依赖图谱
- 6、生成代码
安装相关模块,用于分析源代码,npm i -D @babel/parser @babel/traverse @babel/core @babel/preset-env
。
bundle.js
const fs = require('fs');
const path = require('path');
const parse = require('@babel/parser');
const babel = require('@babel/core');
const traverse = require('@babel/traverse').default;
// 分析模块
const moduleAnalyser = (filename) => {
// 读取入口文件的内容,得到一个字符串
const content = fs.readFileSync(filename, 'utf-8');
// 利用@babel/parser将字符串转成ast
const ast = parse.parse(content, {
sourceType: 'module' // 模块引入方式
});
//console.log(ast.program.body) // 分析后的内容
// 使用@babel/traverse进行ast的分析,得到对应的依赖
const dependencies = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename); // 当前文件所在的目录
const newFile = path.join(dirname, node.source.value); // 将文件目录和文件拼接起来
dependencies[node.source.value] = newFile;
}
});
// 利用@babel/core将ast进行转义,得到的code就是可以在浏览器运行的代码
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
return {
filename, // 入口文件
dependencies, // 入口文件对应的依赖
code // 翻译后的代码
}
}
// 生成依赖图谱
const makeDependenciesGraph = (entry) => {
// 入口模块的分析
const entryModule = moduleAnalyser(entry);
const graphArr = [entryModule];
for (let i = 0; i < graphArr.length; i++) {
const item = graphArr[i];
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
graphArr.push(moduleAnalyser(dependencies[j]));
}
}
}
const graph = {}
graphArr.map(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}
// 生成代码
const gengerateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry)); // 依赖图谱
// 因为需要生成打包代码,所以需要把代码放在闭包里面
const str = `
(function (graph) {
function require(module) {
// localRequire将相对路径转换成绝对路径
function localRequire(relativePath) {
// 依赖图谱的dependencies里面存储了每个模块的相对路径和绝对路径
return require(graph[module].dependencies[relativePath])
}
// 闭包解析code
var exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports; // 把exports导出,这样下一个模块才能拿到前一个模块的结果
};
require('${entry}');
})(${graph});
`
return str;
}
console.log(gengerateCode('./src/index.js'));
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
我们把打印出来的内容放在浏览器控制台运行:
(function (graph) {
function require(module) {
// localRequire将相对路径转换成绝对路径
function localRequire(relativePath) {
// 依赖图谱的dependencies里面存储了每个模块的相对路径和绝对路径
return require(graph[module].dependencies[relativePath])
}
// 闭包解析code
var exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports; // 把exports导出,这样下一个模块才能拿到前一个模块的结果
};
require('./src/index.js');
})({"./src/index.js":{"dependencies":{"./message.js":"src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"src\\message.js":{"dependencies":{"./word.js":"src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say dell \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = \"dell dell\";\nexports.word =word;"}});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
就会输出 say dell dell dell
,说明我们打包成功了!