0%

如何自己实现一个简单的tree shaking

tree shaking 简单理解就是在代码打包时将项目代码中没有用到的代码剔除掉,比如在一个文件中申明了一个工具函数,但是并没有调用它,把这样的代码剔除掉,以减少代码打包体积。

acron

acron

A tiny, fast JavaScript parser, written completely in JavaScript。

这是用js写的js语言的解析器。用它可以将js代码进行词法分析,语法分析,进而得到ast,处理ast得到我们想要的结果。 这里就用acron的能力,进行js的语法分析。

解决思路

整体思路是,读取一个文件中的代码,调用acron进行语法分析,剔除没有调用过的函数,结果再输出到一个新的文件中。

需要剔除的源代码

源代码 test.js

1
2
3
4
5
6
7
8
const add = (a, b) => a + b;

const subtract = (a, b) => a - b; // 这个函数没有被调用,在shaking后,会剔除这个函数

const num1 = 9;
const num2 = 100;

const result = add(num1, num2);

文件结构

整个项目的文件结构是

1
2
3
4
5
project
src
|--- index.js // 入口文件
|--- test.js // 源代码
|--- visitor.js // 处理ast的代码

文件内容

处理ast的代码 visitor.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
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
class Visitor {
// 访问变量申明
visitVariableDeclaration(node) {
let str = '';
str += node.kind + ' ';
str += this.visitNodes(node.declarations);
return str + '\n';
}

// 访问定义的变量名
visitVariableDeclarator(node, kind) {
let str = '';
str += kind ? kind + ' ' : str;
str += this.visitNode(node.id) + ' ';
str += '= ';
str += this.visitNode(node.init);
return str + ';' + '\n';
}
// 访问标识符
visitIdentifier(node) {
let str = '';
str += node.name;
return str;
}
// 访问箭头函数
visitArrowFunctionExpression(node) {
let str = '';
str += '(';
node.params.forEach((param, index) => {
str += this.visitNode(param);
str += (index === node.params.length - 1) ? '' : ','
})
str += ')';
str += '=>';
str += this.visitNode(node.body);
return str + '\n';
}
// 访问字符常量
visitLiteral(node) {
let str = '';
str += node.raw;
return str;
}
// 访问操作符
visitBinaryExpression(node) {
let str = '';
str += this.visitNode(node.left);
str += node.operator;
str += this.visitNode(node.right);
return str + '\n';
}

visitCallExpression(node) {
let str = '';
str += this.visitNode(node.callee);
str += '(';
node.arguments.forEach((param, index) => {
str += this.visitNode(param);
str += (index === node.arguments.length - 1) ? '':',';
})
str += ')';
return str;
}
// 访问多个节点
visitNodes(nodes) {
let str = ''
nodes.forEach(node => {
str += this.visitNode(node);
});
return str;
}
// 访问单个节点
visitNode(node) {
let str = '';
switch (node.type) {
case 'VariableDeclaration':
str += this.visitVariableDeclaration(node);
break;
case 'VariableDeclarator':
str += this.visitVariableDeclarator(node);
break;
case 'Identifier':
str += this.visitIdentifier(node);
break;
case 'ArrowFunctionExpression':
str += this.visitArrowFunctionExpression(node);
break;
case 'BinaryExpression':
str += this.visitBinaryExpression(node);
break;
case 'Literal':
str += this.visitLiteral(node);
break;
case 'CallExpression':
str += this.visitCallExpression(node);
break;
default:
break;
}
return str;
}
// 访问body元素
run(body) {
let str = '';
// 遍历
str += this.visitNodes(body);
return str;
}
}

module.exports = Visitor;

入口文件 index.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const fs = require('fs');
const acron = require('acorn');
const Visitor = require('./visitor');
const visitor = new Visitor();

// 获取命令行参数
const args = process.argv[2];
// 读取文件缓存
const buffer = fs.readFileSync(args).toString();
// 解析成抽象语法树
const body = acron.parse(buffer).body;

const decls = new Map(); // 记录所有申明过的变量
const calledDecls = []; // 记录调用过的函数
let code = []; // 存放源代码

body.forEach(node => {
if (node.type === 'VariableDeclaration') {
const kind = node.kind;
for (const decl of node.declarations) {
// todo
decls.set(visitor.visitNode(decl.id), visitor.visitVariableDeclarator(decl, kind));
if (decl.init.type === 'CallExpression') {
calledDecls.push(visitor.visitIdentifier(decl.init.callee));
const args = decl.init.arguments;
for (const arg of args) {
if (arg.type === 'Identifier') {
calledDecls.push(visitor.visitNode(arg));
}
}
}
}
}
if (node.type === 'Identifier') {
calledDecls.push(node.name);
}
code.push(visitor.run([node]));
});

code = calledDecls.map(c => {
return decls.get(c);
}).join('');

fs.writeFileSync(__dirname + '/test.shaked.js', code);

执行项目

在命令行中执行

1
node src/index.js src/test.js

得到test.shaked.js 文件

1
2
3
4
const add = (a, b)=>a+b;

const num1 = 9;
const num2 = 100;

至此,一个简单的tree shaking就做完了。

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道