目录
一、什么是Bunlder?
Bundler其实是一个项目依赖管理器。
二:被打包的项目介绍
整个演示项目的目录结构如下所示:
├── bundler.js
├── package.json
└── src
├── index.js
├── msg.js
└── word.js
word.js
const word = 'world';
export default word;
msg.js
import word from './word.js'
const msg = `hello ${word}`;
export default msg;
index.js
import msg from './msg.js'
console.log(msg)
export default index;
三、实现bundler.js
要实现bundler,我们需要实现3部分功能:
1. moduleAnalyser:模块分析。分析模块,得到模块的依赖、代码等信息。
2. makeDependenciesGraph:生成依赖图谱。遍历打包项目,得到所有需要的模块的分析结果 。
3. generateCode:生成可执行代码。提供require()函数和exports对象,生成可以在浏览器执行的代码。
1、模块分析
使用fs
模块读取module的内容;使用@babel/parser
将文件内容转换成抽象语法树AST;使用遍历了@babel/traverse
AST ,对每个ImportDeclaration节点(保存的相对于module的路径信息)做映射,把依赖关系拼装在 dependencies对象里;使用@babel/core
结合@babel/preset-env
预设,将AST转换成了浏览器可以执行的代码。
1.1 获取文件的文本内容
// bundler.js
const fs = require('fs');
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
console.log(content);
};
moduleAnalyser('./src/index.js');
打印结果:
为了更清楚展示,我们对代码进行高亮,需要安装一个依赖 npm i cli-highlight -g
执行命令:
node bunlder.js | highlight
1.2、利用 babel-parser 将文本转为 ast
我们获取到了文本以后,如果直接就拿来分析依赖当然也可以,但是处理起来非常麻烦,效率也低下,尤其是文件内容复杂的时候。所以我们需要将文本转化为 js 可直接操作的对象 ast。
前面我们讲到了 babel
,它可以将 js 源文件根据我们的需要做内容变更,比如将我们的 es6 编写的源文件转成 es5,其实就是将我们的源文件内容先转为 ast 再去实现后续变更的。它有一个专门负责转换的模块,叫做 baben/parser,前身是 babylon。
先安装依赖 npm install @babel/parser --save
// 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');
执行 node bunlder.js | highlight
1.3、ast 操作和转换成文本
我们要从 ast 获取信息,可以使用 babel-traverse
遍历 ast,这期间会有一些特定的钩子让我们能执行自己的操作。我们在遍历到 import 声明的时候,将 import 的文件名记录到依赖数组。最后我们再利用 做源码的 babel-core
es6 => es5 的转换。
先安装这两个依赖:
npm install @babel/core --save
npm install @babel/preset-env --save
此时代码如下:
// 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');
执行 node bunlder.js | highlight
2、依赖图谱
前面我们将了如何获取单个文件的依赖和转换成 es5 的代码,这里我们讲一下如何对所有以来的文件做分析,生成一个依赖图谱。
// 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');
执行 node bunlder.js | highlight
3、生成代码
// 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);
执行 node bunlder.js | highlight
生成的代码:
(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":{"./msg.js":"./src\\msg.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\nvar _msg = _interopRequireDefault(require(\"./msg.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nconsole.log(_msg[\"default\"]);\nvar _default = index;\nexports[\"default\"] = _default;"},"./src\\msg.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\nvar _word = _interopRequireDefault(require(\"./word.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nvar msg = \"hello \".concat(_word[\"default\"]);\nvar _default = msg;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\nvar word = 'world';\nvar _default = word;\nexports[\"default\"] = _default;"}});
直接复制,在浏览器控制台执行,成功打印出: hello world
四、小结
这只是简单的基础的打包工具实现,主要技术点在于文件解析,函数回调,递归的使用。仅供参考。