1. 简介
学习了前面的内容,我们本节讲一个非常简单的打包工具的实现。
2. 代码准备
我们准备如下三个文件,看看如何将其打包。
代码语言:javascript复制// src/index.js
import { sayHello, sayHi } from './say.js';
import message from './message.js';
sayHello(message);
sayHi(message);
代码语言:javascript复制// src/say.js
import { hello, hi } from './greeting.js';
export const sayHello = (message) => {
console.log(`${hello} ${message}`);
};
export const sayHi = (message) => {
console.log(`${hi} ${message}`);
};
代码语言:javascript复制// src/greeting.js
export default 'world';
代码语言:javascript复制// src/greeting.js
export const hello = 'hello';
export const hi = 'hi';
3. 模块分析
3.1 获取文件的文本内容
做模块分析,我们首先要获取源码内容。
代码语言:javascript复制// bundler.js
const fs = require('fs');
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
console.log(content);
};
moduleAnalyser('./src/index.js');
我们在 cli 运行一下该文件,为了展示更清楚,可以先安装一个包,
代码语言:javascript复制npm i cli-highlight -g
代码语言:javascript复制node bundler.js | highlight
如下:
可以看到,我们获取到了 src/index.js 中的文件内容。
3.2 利用 babel-parser 将文本转为 ast
我们获取到了文本以后,如果直接就拿来分析依赖当然也可以,但是处理起来非常麻烦,效率也低下,尤其是文件内容复杂的时候。所以我们需要将文本转化为 js 可直接操作的对象 ast。 前面我们讲到了 babel,它可以将 js 源文件根据我们的需要做内容变更,比如将我们的 es6 编写的源文件转成 es5,其实就是将我们的源文件内容先转为 ast 再去实现后续变更的。它有一个专门负责转换的模块,叫做 baben/parser,前身是 babylon。
代码语言:javascript复制// bundler.js
const fs = require('fs');
const parser = require('@babel/parser');
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
console.log(parser.parse(content, {
sourceType: 'module',
}));
};
moduleAnalyser('./src/index.js');
其实,如果大家想方便地查看文本和 ast 对应关系,可以直接访问 astexplorer。
3.3 ast 操作和转换成文本
我们要从 ast 获取信息,可以使用 babel-traverse 遍历 ast,这期间会有一些特定的钩子让我们能执行自己的操作。我们在遍历到 import 声明的时候,将 import 的文件名记录到依赖数组。最后我们再利用 babel-core 做源码的 es6 => es5 的转换。
代码语言:javascript复制// bundler.js
const fs = require('fs');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = {};
const dirPath = path.dirname(filename);
traverse(ast, {
ImportDeclaration({ node }) {
const fileRelativePath = node.source.value;
const srcRelativePath = `./${path.join(dirPath, fileRelativePath)}`;
dependencies[fileRelativePath] = srcRelativePath;
},
});
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
});
console.log('======dependencies', dependencies);
console.log('======code', code);
return {
filename,
dependencies,
code,
};
};
moduleAnalyser('./src/index.js');
4. 依赖图谱
前面我们将了如何获取单个文件的依赖和转换成 es5 的代码,这里我们讲一下如何对所有以来的文件做分析,生成一个依赖图谱。
代码语言:javascript复制// bundler.js
const fs = require('fs');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
// 模块分析
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = {};
const dirPath = path.dirname(filename);
traverse(ast, {
ImportDeclaration({ node }) {
const fileRelativePath = node.source.value;
const srcRelativePath = `./${path.join(dirPath, fileRelativePath)}`;
dependencies[fileRelativePath] = srcRelativePath;
},
});
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 { dependencies } = graphArr[i];
if (dependencies) {
Object.keys(dependencies).forEach((name) => {
graphArr.push(moduleAnalyser(dependencies[name]));
});
}
}
// 依赖数组转为一个对象,方便操作
const graph = {};
graphArr.forEach((item) => {
const { filename, dependencies, code } = item;
graph[filename] = {
dependencies,
code,
};
});
console.log(graph);
return graph;
};
makeDependenciesGraph('./src/index.js');
可以看到这个项目的依赖图谱。
5. 生成代码
代码语言:javascript复制// bundler.js
const fs = require('fs');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
// 模块分析
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = {};
const dirPath = path.dirname(filename);
traverse(ast, {
ImportDeclaration({ node }) {
const fileRelativePath = node.source.value;
const srcRelativePath = `./${path.join(dirPath, fileRelativePath)}`;
dependencies[fileRelativePath] = srcRelativePath;
},
});
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 { dependencies } = graphArr[i];
if (dependencies) {
Object.keys(dependencies).forEach((name) => {
graphArr.push(moduleAnalyser(dependencies[name]));
});
}
}
// 依赖数组转为一个对象,方便操作
const graph = {};
graphArr.forEach((item) => {
const { filename, dependencies, code } = item;
graph[filename] = {
dependencies,
code,
};
});
return graph;
};
// 生成代码
const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry));
return `
(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code)
return exports;
}
require('${entry}');
})(${graph});
`;
};
const code = generateCode('./src/index.js');
console.log(code);
运行后生成如下代码:
代码语言:javascript复制(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code)
return exports;
}
require('./src/index.js');
})({"./src/index.js":{"dependencies":{"./say.js":"./src/say.js","./message.js":"./src/message.js"},"code":""use strict";nnvar _say = require("./say.js");nnvar _message = _interopRequireDefault(require("./message.js"));nnfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }nn// src/index.jsn(0, _say.sayHello)(_message.default);n(0, _say.sayHi)(_message.default);"},"./src/say.js":{"dependencies":{"./greeting.js":"./src/greeting.js"},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n value: truen});nexports.sayHi = exports.sayHello = void 0;nnvar _greeting = require("./greeting.js");nn// src/say.jsnvar sayHello = function sayHello(message) {n console.log("".concat(_greeting.hello, " ").concat(message));n};nnexports.sayHello = sayHello;nnvar sayHi = function sayHi(message) {n console.log("".concat(_greeting.hi, " ").concat(message));n};nnexports.sayHi = sayHi;"},"./src/message.js":{"dependencies":{},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n value: truen});nexports.default = void 0;n// src/greeting.jsnvar _default = 'world';nexports.default = _default;"},"./src/greeting.js":{"dependencies":{},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n value: truen});nexports.hi = exports.hello = void 0;n// src/greeting.jsnvar hello = 'hello';nexports.hello = hello;nvar hi = 'hi';nexports.hi = hi;"}});
运行上面一段代码:
6. 生成后代码的执行过程分析
这里有些同学可能会对生成后的代码如何执行的过程不太清楚,我们来分析一遍。 step 1 执行 require('./src/index'.js) step 2 (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code) 这个闭包函数的执行环境中,require 被定义为 localRequire,而 exports 目前是一个外层定义的空对象 step 3 执行 eval(code),其实就是执行下面这段函数:
代码语言:javascript复制"use strict";
var _say = require("./say.js");
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// src/index.js
(0, _say.sayHello)(_message.default);
(0, _say.sayHi)(_message.default);
step4 碰到 require("./say.js") 会执行 localRequire('./say.js'),其实就是重复2,3 步骤执行到:
代码语言:javascript复制"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.sayHi = exports.sayHello = void 0;
var _greeting = require("./greeting.js");
// src/say.js
var sayHello = function sayHello(message) {
console.log("".concat(_greeting.hello, " ").concat(message));
};
exports.sayHello = sayHello;
var sayHi = function sayHi(message) {
console.log("".concat(_greeting.hi, " ").concat(message));
};
exports.sayHi = sayHi;
step5 碰到 require("./greeting.js") 会执行 localRequire('./greeting.js'),重复2,3,如下:
代码语言:javascript复制"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.hi = exports.hello = void 0;
// src/greeting.js
var hello = 'hello';
exports.hello = hello;
var hi = 'hi';
exports.hi = hi;
这里没有 require 了,会执行到最后,并且在 exports 里面导出模块想要抛出的内容。 step 6 回到 step4 中
代码语言:javascript复制var _greeting = require("./greeting.js");
现在 _greeting 就是
代码语言:javascript复制{
hello: 'hello',
hi: 'hi',
}
继续向下执行到代码结尾。exports 中抛出 sayHello 和 sayHi。 step7 回到 step3 中,_say 就是前面导出的 sayHello 和 sayHi 组成的对象。再往下,遇到 require("./message.js") 是同样的流程。 直到 index 中代码执行完毕。
7. 小结
本节只是演示了一个非常基本的打包器实现,其中很多功能我们都没去实现,比如遇到重复引用,循环引用等该怎么处理。
参考
docs/babel-parser docs/babel-traverse docs/babel-core