# webpack源码(一)
# loader
手写一个简单loader
loader/index.js
// webpack自带包
const loaderUtils = require('loader-utils');
module.exports = function(content) {
// 获取配置文件
const options = loaderUtils.getOptions(this);
console.log(options.data);
return content + 'console.log(1);';
}
2
3
4
5
6
7
8
9
10
安装一下 loader-utils
,用来获取loader参数
yarn add loader-utils --dev
简单的写一个webpack文件,引入一下刚写的loader
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: path.resolve('./loader/index.js'),
options: {
data: '这是一个options'
}
}
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
运行一下
看到控制台输出了我们的options.data
,说明我们通过 loaderUtils.getOptions(this);
拿到了loader中的参数
再看一下通过 Webpack
打包出来的文件,在最下面找到
eval("console.log('京程一灯');console.log(1);\n\n//# sourceURL=webpack:///./src/index.js?");
我们在 loader
中最后返回的钩子中加入的 return content + 'console.log(1);';
挂在了原本内容的最后
再添加一个运行前钩子 pitch
// webpack自带包
const loaderUtils = require('loader-utils');
module.exports = function(content) {
// 获取配置文件
const options = loaderUtils.getOptions(this);
console.log(this.data.value);
console.log(options.data);
return content + 'console.log(1);';
}
module.exports.pitch = function(r, pre, data) {
data.value = 'pitch';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到也可以在运行loader的时候拿到
# 单文件打包
接下来来分析一下单文件打包
我们把loader什么的都去掉,单纯的打包我们的index.js文件
console.log('hello webpack');
然后看一下打包后的文件
一个清理干净的main.js
(function (modules) { // webpackBootstrap
// 缓存
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__("./src/index.js");
})
({
"./src/index.js":
(function (module, exports) {
eval("console.log('hello webpack');");
})
});
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
- 先把没用的注释都删掉,可以看到整个是一个大的闭包
installedModules
是个缓存function __webpack_require__(){}
是webpack自己实现的一套引入规则- 然后我们把下面一些什么 .m .n .r 巴拉巴拉的东西都先删掉,后面再说
return __webpack_require__(__webpack_require__.s = "./src/index.js");
相当于直接传了一个入口函数return __webpack_require__("./src/index.js");
接下来我们分析一下webpack自己实现的 __webpack_require__
:
- 首先
__webpack_require__
的参数其实就是入口文件的路径"./src/index.js"
- 进入后定义一个缓存,先判断是否有缓存,当然第一次肯定是没有的┓( ´∀` )┏
- 没有缓存那我们就把参数也就是入口地址存到缓存中去,相当于定义了
module.exports = {}
- 接下来
modules
也就是我们通过闭包传入的入口地址:执行函数
的键值对 - 通过
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
来执行 - 将前面定义的
module.exports
绑定,然后传入module
和module.exports
做参数 - 我们把这段代码直接放到浏览器执行可以直接运行
# 同步文件加载
在同级目录下导出一个 sync.js
const data = 'sync';
export default data;
2
index.js
import sync from './sync';
console.log(sync);
console.log('hello webpack');
2
3
打包一下看看干净的main.js
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__("./src/index.js");
})
({
"./src/index.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony import */
var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync */ "./src/sync.js");
console.log(_sync__WEBPACK_IMPORTED_MODULE_0__["default"]);
console.log('hello webpack');
}),
"./src/sync.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
const data = 'sync';
/* harmony default export */
__webpack_exports__["default"] = (data);
})
});
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
- 同样的我们删除了没用的代码
- 顺着代码,通过
__webpack_require__
调用了入口文件 - 进入 index.js 中后往下走,
/* harmony import */
和谐的导入,也就是webpack自己的导入 - 又通过
__webpack_require__
调用了 sync.js - 进入sync中后,定了变量,然后用传入的
__webpack_exports__
通过["default"]
把内容导出 - 又回到index 中,console 了刚刚导进来的
["default"]
- 最后console了 "hello webpack"
# 异步文件导入
async.js
const data = 'async';
export default data;
2
index.js
import('./async').then(_ => {
console.log(_);
})
console.log('hello webpack');
2
3
4
打包后会生成两个文件:0.js 和 main.js
0.js
(window["webpackJsonp"] = []).push([
[0],
{
"./src/async.js":
/*! exports provided: default */
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
const data = 'async';
/* harmony default export */
__webpack_exports__["default"] = (data);
})
}
]);
2
3
4
5
6
7
8
9
10
11
12
13
14
- 可以看到清理干净后的0.js实际上是在一个数组中push了一个二维数组
- [[0], {"key": (function(){})}]
- 里面的内容就是 async 中的内容
main.js
(function (modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
resolves.shift()();
}
};
// The module cache
var installedModules = {};
var installedChunks = {
"main": 0
};
// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".js"
}
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
script.src = jsonpScriptSrc(chunkId);
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
// __webpack_public_path__
__webpack_require__.p = "";
// on error function for async loading
__webpack_require__.oe = function (err) { console.error(err); throw err; };
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
// Load entry module and return exports
return __webpack_require__("./src/index.js");
})
({
"./src/index.js":
(function (module, exports, __webpack_require__) {
__webpack_require__.e(/*! import() */ 0)
.then(__webpack_require__.bind(null, /*! ./async */ "./src/async.js"))
.then(_ => { console.log(_); })
console.log('hello webpack');
})
});
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
main.js 的内容明显要复杂很多
先看 webpackJsonpCallback
方法
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
resolves.shift()();
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 从下面的代码中可以看出,
webpackJsonpCallback
传入的是从 0.js 中push进去的数组 chunkIds
就是[0]
,moreModules
是存入的函数- 接下来做了一个循环把chunk都加进来
modules[moduleId] = moreModules[moduleId];
把函数绑定
接下来就 __webpack_require__
这个东西跟之前的都一样
然后就是 webpack_require.e
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
script.src = jsonpScriptSrc(chunkId);
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
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
- 从下面的入口文件可以看出,webpack_require.e 是一个关键的东西,传入 0 的chunk值
- 进入分支后创建promise往里面push东西
- 往script中加了一设定
然后就是最后一段
__webpack_require__.p = "";
var jsonpArray = window["webpackJsonp"] = [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 文件绑定modules
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
2
3
4
5
6
7
8
9
10
- 其实就是把0.js中的东西引过来,然后调用
# AST语法树
为什么说webpack慢,因为loader慢,为什么loader慢,因为每个loader都要先把代码转化为AST然后再把AST转化成代码,所以就会很长时间
安装几个包
yarn add acorn acorn-walk magic-string --dev
src/index.js
const data = '这是一个data';
console.log('hello webpack');
2
loader/index.js
// webpack自带包
const loaderUtils = require('loader-utils');
const acorn = require('acorn');
const acornWalk = require('acorn-walk');
const magicString = require('magic-string');
module.exports = function(content) {
// 获取配置文件
const options = loaderUtils.getOptions(this);
const ast = acorn.parse(content);
console.log('ast', ast);
return content + 'console.log(1);';
}
module.exports.pitch = function(r, pre, data) {
data.value = 'pitch';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
运行一下看看ast拿到了什么东西
接下来要遍历语法树,去找到我们要替换的东西
const ast = acorn.parse(content);
// 遍历语法树
acornWalk.simple(ast, {
VariableDeclaration(node) {
console.log('node', node);
}
})
2
3
4
5
6
7
- 用
acornWalk.simple()
方法遍历语法树 - 找到我们刚才从ast中看到的
VariableDeclaration
类型看看能输出什么
接下来就要开始替换 const 了
const ast = acorn.parse(content);
// 把ast转成string
const code = new magicString(content);
// 遍历语法树
acornWalk.simple(ast, {
VariableDeclaration(node) {
// 拿到node中的start
const {start} = node;
code.overwrite(start, start + 5, 'var');
}
})
return code.toString();
2
3
4
5
6
7
8
9
10
11
12
13
- 遍历语法树
- 用
VariableDeclaration
类型拿到节点的 start 开始位置 code.overwrite
重写这段代码code.overwrite
三个参数,起始位置,终止位置,和要替换成的字符串- 最后再把code转成字符串
看看打包后变成了什么,把const
换成了 var
┓( ´∀` )┏
eval("var data = '这是一个data';\nconsole.log('hello webpack');\n\n//# sourceURL=webpack:///./src/index.js?");
# 手写一个简单webpack
准备好打包的文件 src/index.js
import sync from './sync';
console.log(sync);
console.log('hello webpack');
2
3
换一种新的ast解析的包
yarn add babylon @babel/traverse
创建 my-webpack.js
// 解析ast
const babylon = require('babylon');
// 遍历ast
const traverse = require('@babel/traverse').default;
const magicString = require('magic-string');
const entry = './src/index.js';
const fs = require('fs');
function parse(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType:'module'
})
traverse(ast, {
ImportDeclaration({node}) {
console.log(node);
}
})
}
parse(entry);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输出看一下找到的 ImportDeclaration
下的信息
把内容中的 import
重写掉
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType: 'module'
})
const code = new magicString(content);
traverse(ast, {
ImportDeclaration({ node }) {
const { start, end, specifiers, source } = node;
const newfile = path.join('./src/', `${source.value}.js`)
code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`);
const _code = code.toString();
console.log(_code);
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
继续
const arrGroup = [];
const dep = [];
function parse(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType: 'module'
})
const code = new magicString(content);
traverse(ast, {
ImportDeclaration({ node }) {
const { start, end, specifiers, source } = node;
const newfile = path.join('./src/',`${source.value}.js`);
code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`);
arrGroup.push(newfile);
}
})
const _code = code.toString();
dep.push({
filename,
_code
})
return arrGroup;
}
const arrfile = parse(entry);
console.log('arrfile',arrfile);
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
- 定义了两个数组
dep
和arrGroup
,arrGroup
用来存放文件 - 在
ImportDeclaration
中就完成了import
的转换,在外面转成字符串传到dep中
继续
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const magicString = require('magic-string');
const entry = './src/index.js';
const fs = require('fs');
const path = require('path');
const dep = [];
function parse(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const arrGroup = [];
const ast = babylon.parse(content, {
sourceType: 'module'
})
const code = new magicString(content);
traverse(ast, {
ExportDeclaration({node}) {
const {start, end, declaration} = node;
code.overwrite(start,end,`__webpack_exports__["default"] = ${declaration.name};`)
},
ImportDeclaration({ node }) {
const { start, end, specifiers, source } = node;
const newfile = path.join('./src/',`${source.value}.js`);
code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`)
arrGroup.push(newfile)
}
})
const _code = code.toString();
dep.push({
filename,
_code
})
return arrGroup;
}
const arrfile = parse(entry);
for (let item in arrfile) {
parse(arrfile[item]);
}
console.log(dep);
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
- 第一轮执行了
parse(entry)
之后,把入口文件的代码替换完了 - 将需要导入的文件放在数组中进行遍历,又进入ast中遍历,这次要把
exports
替换掉
接下来就要用模板替换,安装
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const magicString = require('magic-string');
const entry = './src/index.js';
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const dep = [];
function parse(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const arrGroup = [];
const ast = babylon.parse(content, {
sourceType: 'module'
})
const code = new magicString(content);
traverse(ast, {
ExportDeclaration({node}) {
const {start, end, declaration} = node;
code.overwrite(start,end,`__webpack_exports__["default"] = ${declaration.name};`)
},
ImportDeclaration({ node }) {
const { start, end, specifiers, source } = node;
const newfile = path.join('./src/',`${source.value}.js`);
code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`)
arrGroup.push(newfile)
}
})
const _code = code.toString();
dep.push({
filename,
_code
})
return arrGroup;
}
const arrfile = parse(entry);
for (let item in arrfile) {
parse(arrfile[item]);
}
const template = `
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
return __webpack_require__("${entry}");
})
({
<% for(let i = 0; i < dep.length; i++){ %>
"<%- dep[i]['filename'] %>":(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
<%- dep[i]['_code'] %>
}),
<% } %>
});
`
const result = ejs.render(template, {
dep
})
console.log(result);
fs.writeFileSync('./dist/yd-main.js', result);
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
- 引入ejs模板引擎,将webpack中的那部分不动的代码写进去,将我们刚写的东西替换掉
- 写入到 yd-main.js 文件中