手写一个自己的webpack

  目录

一个超级简化版的webpack打包工具

手写一个自己的webpack

webpack现在用的人非常多,我们在开发vue,react,angular的时候默认的打包工具就是webpack,作为一名前端开发者来说,webpack是现在必须掌握的技能之一。随着webpack版本的不断增加,功能和复杂度越来越高,会配置的话都得学一阵子,但是,抛开使用来说,想没想过它的原理是什么?如果自己写一个该从哪里入手呢,今天我就写一个简化版的webpack。

功能

简化版的webpack,实现了它的最基础的功能,也就是模块化的引用处理,比如,main.js文件依赖了a.js文件,使用es6的语法import语法,浏览器是不支持的,需要用工具打包成浏览器支持的语法,我就实现这个最基础的功能。

babel

打包文件,首先需要分析代码,比如,入口文件main.js,分析出import语句,都依赖哪些文件,再去读取这些依赖的文件,而分析js代码就需要babel这个工具库,babel工具库可以将es6代码转换成es5,es3代码,功能特别强大。

打包流程

这里,依赖babel的3个工具,分别是

1
2
3
"@babel/core": "^7.8.3",
"@babel/parser": "^7.8.3",
"@babel/traverse": "^7.8.3"

parser是用来将js代码解析成ast语法书。
traverse可以将ast语法书进行遍历,对相应的ast语法书节点做单独的获取或者处理。
babel-core是babel的核心,可以将es6的js代码或ast语法书转化成es5代码。

步骤1

用paser将es6的代码先转成ast语法树

步骤2

用traverse处理ast语法书中的import节点,获取依赖文件路径,并获取依赖文件的代码,重复做步骤1,步骤2的处理。

步骤3

用babel-core将获取到的文件全部转成es5语法。

步骤4

最后把这些文件拼接成一个可执行的js代码,保存起来,html直接引用就可以执行了。

源码

上面步骤都说了,但是还是有点抽象,下面贴出代码,一目了然。

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
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
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

let ID = 0;
function createAsset(fileName) {
// 这个文件里所依赖的其它文件路径
let dependencies = [];
const content = fs.readFileSync(fileName, 'utf8');
// 用babel的parser来解析读取的的文件内容content,获取ast抽象语法书
const ast = parser.parse(content, {
sourceType: 'module'
});
// 用babel的traverse功能来观察ast语法树的相应节点,进行相应的处理
traverse(ast, {
ImportDeclaration({ node }) {
// console.log(node.source.value);
dependencies.push(node.source.value);
}
});
// 用babel来讲es6语法转成es5语法
const { code } = babel.transformFromAstSync(ast, null, {
presets: ['@babel/preset-env']
})

let id = ID++;

return {
id,
fileName,
dependencies,
code,
};
}

function createGraph() {
const mainAsset = createAsset('./src/main.js');
// 队列存放单个文件资源,这里利用循环队列去搜寻依赖文件,而没有使用递归方法
const queue = [ mainAsset ];
for(const asset of queue) {
const dirname = path.dirname(asset.fileName);
asset.mapping = {};
asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath);
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
queue.push(child);
});
}
return queue;
}
// 打包生成处理后的js文件
function bundle() {
const graph = createGraph('./src/main.js');
let modules = ``;
graph.forEach(mod=> {
modules += `
${mod.id}: [
function(require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)}
],
`;
});
const result = `(function(modules) {
function require(id) {
const fn = modules[id][0],
mapping = modules[id][1];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
const module = {
exports: {}
};
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})`;

return result;
}

const result = bundle();
fs.writeFileSync('./dist/bundle.js', result);

demo地址

截这里