Skip to content

Commit 9483e32

Browse files
committed
simple webpack
1 parent 70e92b1 commit 9483e32

File tree

8 files changed

+1435
-0
lines changed

8 files changed

+1435
-0
lines changed

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# 什么是webpack
2+
可以引用官网的一幅图解释,我们可以看到webpack,可以分析各个模块的依赖关系,最终打包成我们常见的静态文件,.js 、 .css 、 .jpg 、.png。今天我们先不弄那么复杂,我们就分析webpack是怎么分析ES6的模块依赖,怎么把ES6的代码转成ES5的。
3+
4+
![](https://user-gold-cdn.xitu.io/2019/3/2/1693ed9df7905766?w=2124&h=928&f=png&s=153846)
5+
6+
# 实现
7+
由于ES6转ES5中需要用到babel,所以要用到一下插件
8+
9+
`npm install @babel/cord @babel/traverse @babel/core @babel/preset-env --save-dev`
10+
## 需要的文件
11+
到使用webpack肯定少不了原文件,我们会涉及三个需要打包的js文件(entry.js、message.js、name.js)
12+
13+
```
14+
// entry.js
15+
16+
import message from './message.js';
17+
console.log(message);
18+
```
19+
```
20+
// message.js
21+
22+
import {name} from './name.js';
23+
export default `hello ${name}!`;
24+
```
25+
```
26+
// name.js
27+
28+
export const name = 'world';
29+
```
30+
```
31+
//bundler.js
32+
// 读取文件信息,并获得当前js文件的依赖关系
33+
function createAsset(filename) {//代码略}
34+
// 从入口开始分析所有依赖项,形成依赖图,采用广度遍历
35+
function createGraph(entry) {//代码略}
36+
// 根据生成的依赖关系图,生成浏览器可执行文件
37+
function bundle(graph) {//代码略}
38+
```
39+
entry.js 就是我们的入口文件
40+
41+
bundler.js 是我们简易版的webpack
42+
43+
44+
目录结构
45+
```
46+
- example
47+
- entry.js
48+
- message.js
49+
- name.js
50+
- bundler.js
51+
```
52+
53+
## 如何分析依赖
54+
webpack分析依赖是从一个入口文件开始分析的,当我们把一个入口的文件路径传入,webpack就会通过这个文件的路径读取文件的信息(读取到的本质其实是字符串),然后把读取到的信息转成AST(抽象语法树),简单点来说呢,就是把一个js文件里面的内容存到某种数据结构里,里面包括了各种信息,**其中就有当前模块依赖了哪些模块**。我们暂时把通过传**文件路径**能返回文件信息的这个函数叫 `createAsset`
55+
56+
## createAsset返回什么
57+
第一步我们肯定需要先从 entry.js 开始分析,于是就有了如下的代码,我们先不关心createAsset具体代码是怎么实现的,具体代码我会放在最后。
58+
```
59+
createAsset("./example/entry.js");
60+
```
61+
当执行这句代码,`createAsset` 会返回下面的数据结构,这里包括了模块的id,文件路径,依赖数组(entry.js依赖了message.js,所以会返回依赖的文件名),code(这个就是entry.js ES6转ES5的代码)
62+
![](https://user-gold-cdn.xitu.io/2019/3/2/1693eee846b82ac0?w=1482&h=560&f=png&s=101080)
63+
好通过 `createAsset` 我们成功拿到了entry.js的依赖,就是 `dependencies` 数组。
64+
65+
## 继续找下一个依赖
66+
我们通过上面可以拿到entry.js依赖的模块,于是我们就可以接着去遍历`dependencies` 数组,循环调用`createAsset`这样就可以得到全部模块相互依赖的信息。得到全部依赖信息需要调用 `createGraph` 这个一个函数,它会进行广度遍历,最终返回下面的数据
67+
68+
![](https://user-gold-cdn.xitu.io/2019/3/2/1693efa64b43d73c?w=2320&h=1936&f=png&s=342355)
69+
70+
我们可以看到返回的是一个数据,字段之前都有和大家解析过,除了 `mapping``mapping`这个字段是把当前模块依赖的**文件名称** 和 模块的id 做一个映射,目的是为了更方便查找模块。
71+
72+
## 打包的最后步骤
73+
我们现在已经能拿到每个模块之前的依赖关系,我们再通过调用`bundle`函数,我们就能构造出最后的`bundle.js`,输出如下图
74+
75+
![](https://user-gold-cdn.xitu.io/2019/3/2/1693f0740a24a5b0?w=2220&h=1946&f=png&s=432836)
76+
77+
# 最后

bundle.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
(function (modules) {
3+
// 创建一个require()函数: 它接受一个 模块ID 并在我们之前构建的模块对象查找它.
4+
function require(id) {
5+
const [fn, mapping] = modules[id];
6+
function localRequire(relativePath) {
7+
// 根据mapping的路径,找到对应的模块id
8+
return require(mapping[relativePath]);
9+
}
10+
const module = { exports: {} };
11+
// 执行每个模块的代码。
12+
fn(localRequire, module, module.exports);
13+
return module.exports;
14+
}
15+
// 执行入口文件,
16+
require(0);
17+
})({
18+
0: [
19+
function (require, module, exports) {
20+
"use strict";
21+
22+
var _message = _interopRequireDefault(require("./message.js"));
23+
24+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25+
26+
console.log(_message.default);
27+
},
28+
{ "./message.js": 1 },
29+
], 1: [
30+
function (require, module, exports) {
31+
"use strict";
32+
33+
Object.defineProperty(exports, "__esModule", {
34+
value: true
35+
});
36+
exports.default = void 0;
37+
38+
var _name = require("./name.js");
39+
40+
var _default = "hello ".concat(_name.name, "!");
41+
42+
exports.default = _default;
43+
},
44+
{ "./name.js": 2 },
45+
], 2: [
46+
function (require, module, exports) {
47+
"use strict";
48+
49+
Object.defineProperty(exports, "__esModule", {
50+
value: true
51+
});
52+
exports.name = void 0;
53+
var name = 'world';
54+
exports.name = name;
55+
},
56+
{},
57+
],
58+
})

bundler.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const babylon = require("@babel/parser");
4+
const traverse = require("@babel/traverse").default;
5+
const babel = require("@babel/core");
6+
7+
let ID = 0;
8+
9+
// 读取文件信息,并获得当前js文件的依赖关系
10+
function createAsset(filename) {
11+
//获取文件,返回值是字符串
12+
const content = fs.readFileSync(filename, "utf-8");
13+
14+
// 讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
15+
//babylon 这个工具是是负责解析字符串并生产ast。
16+
const ast = babylon.parse(content, {
17+
sourceType: "module"
18+
});
19+
20+
//用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
21+
const dependencies = [];
22+
23+
// 遍历当前抽象语法树
24+
traverse(ast, {
25+
// 每当遍历到import语法的时候
26+
ImportDeclaration: ({ node }) => {
27+
// 把当前依赖的模块加入到数组中,其实这存的是字符串,
28+
//例如 如果当前js文件 有一句 import message from './message.js',
29+
//'./message.js' === node.source.value
30+
dependencies.push(node.source.value);
31+
}
32+
});
33+
34+
//模块的id 从0开始, 相当一个js文件 可以看成一个模块
35+
const id = ID++;
36+
37+
// 这边主要把ES6 的代码转成 ES5
38+
const { code } = babel.transformFromAstSync(ast, null, {
39+
presets: ["@babel/preset-env"]
40+
});
41+
42+
return {
43+
id,
44+
filename,
45+
dependencies,
46+
code
47+
};
48+
}
49+
50+
// 从入口开始分析所有依赖项,形成依赖图,采用广度遍历
51+
function createGraph(entry) {
52+
const mainAsset = createAsset(entry);
53+
54+
// 定义一个保存依赖项的数组
55+
const queue = [mainAsset];
56+
57+
for (const asset of queue) {
58+
const dirname = path.dirname(asset.filename);
59+
60+
// 定义一个保存子依赖项的属性
61+
asset.mapping = {};
62+
63+
asset.dependencies.forEach(relativePath => {
64+
const absolutePath = path.join(dirname, relativePath);
65+
66+
// 获得子依赖(子模块)的依赖项、代码、模块id,文件名
67+
const child = createAsset(absolutePath);
68+
69+
// 给子依赖项赋值,
70+
asset.mapping[relativePath] = child.id;
71+
72+
// 将子依赖也加入队列中,广度遍历
73+
queue.push(child);
74+
});
75+
}
76+
return queue;
77+
}
78+
79+
// 根据生成的依赖关系图,生成浏览器可执行文件
80+
function bundle(graph) {
81+
let modules = "";
82+
83+
// 把每个模块中的代码放在一个function作用域内
84+
graph.forEach(mod => {
85+
modules += `${mod.id}:[
86+
function (require, module, exports){
87+
${mod.code}
88+
},
89+
${JSON.stringify(mod.mapping)},
90+
],`;
91+
});
92+
93+
// require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以模拟了模块加载,执行,导出操作。
94+
const result = `
95+
(function(modules){
96+
// 创建一个require()函数: 它接受一个 模块ID 并在我们之前构建的模块对象查找它.
97+
function require(id){
98+
const [fn, mapping] = modules[id];
99+
function localRequire(relativePath){
100+
// 根据mapping的路径,找到对应的模块id
101+
return require(mapping[relativePath]);
102+
}
103+
const module = {exports:{}};
104+
// 执行每个模块的代码。
105+
fn(localRequire,module,module.exports);
106+
return module.exports;
107+
}
108+
// 执行入口文件,
109+
require(0);
110+
})({${modules}})
111+
`;
112+
113+
return result;
114+
}
115+
116+
const graph = createGraph("./example/entry.js");
117+
const result = bundle(graph);
118+
119+
// 打包生成文件
120+
fs.writeFileSync("./bundle.js", result);

example/entry.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import message from './message.js';
2+
3+
console.log(message);

example/message.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {name} from './name.js';
2+
3+
export default `hello ${name}!`;

example/name.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const name = 'world';

0 commit comments

Comments
 (0)