话不多说,直接上代码吧!
准备工作
bundler项目目录结构如下:
srcindex.jsmessage.jsword.js
bundler.js
src/index.js
import message from "./message.js";
console.log(message);
src/message.js
import { world } from './word.js'
const message = `say ${world}`
export default message
src/word.js
export const world = 'hello';
以上是 src 目录下对应文件中的代码。
解析入口文件
项目进行打包,首先要读取项目入口文件,并且对入口文件分析。
分析文件,则需要先拿到文件中的内容以及依赖。
const fs = require("fs");
const paser = require("@babel/parser");
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8"); // 读取出的文件设置为utf-8
// 生成抽象语法树 AST
const ast = paser.parse(content, {
sourceType: "module",
});
// 通过抽象语法树分析代码,查看对应的依赖关系
console.log(ast.program.body);
};
moduleAnalyser("./src/index.js");
运行
node bundler.js命令查看index.js相关代码
接下来对 AST 抽象语法树进行遍历,找出 “引入” 的语法,同时转换为浏览器可识别的代码。
const fs = require("fs");
const path = require("path");
const paser = require("@babel/parser");
// 默认导出是ES Module的导出,使用 export/import,需要.default
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, "utf-8"); // 读取出的文件设置为utf-8
// 生成抽象语法树 AST
const ast = paser.parse(content, {
sourceType: "module",
});
// 对抽象语法树进行遍历,需要找出 type: 'ImportDeclaration' 这样的元素
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
// 转换成基于bundler相对路径,打包时才不会出错
const newFile = "./" + path.join(dirname, node.source.value);
dependencies[node.source.value] = newFile;
},
});
// 将抽象语法树转换为浏览器可识别的代码
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return {
filename,
dependencies,
code,
};
};
const moduleInfo = moduleAnalyser("./src/index.js");
console.log(moduleInfo);
运行
node bundler.js命令查看相关输出,理解更加直观!
以上是对
index.js 入口文件进行的分析
生成依赖图谱
接下来要对项目中所有的模块进行分析。
创建 "依赖图谱",其作用是分析项目中除入口文件外的其它文件信息(模块信息)
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
const graphArry = [entryModule];
// 递归其它模块
for (let i = 0; i < graphArry.length; i++) {
const item = graphArry[i];
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
graphArry.push(moduleAnalyser(dependencies[j]));
}
}
}
const graph = {};
graphArry.forEach(({ filename, dependencies, code } = item) => {
graph[filename] = { dependencies, code };
});
return graph;
};
const graghInfo = makeDependenciesGraph("./src/index.js");
console.log(graghInfo);
运行
node bundler.js命令查看依赖图谱,可以直观看到每个模块中对应的依赖关系。
生成可执行 JS
其实就是要把依赖图谱中的code放到一起,返回一个可执行的js,其实也就是返回了一个js字符串。
注意:在code中有一个require方法和一个exports对象,如果没定义的话,js执行的时候一定会报错的。
在闭包内以require作入口,再定义一个闭包把各个模块划分开防止内部变量污染。同时我们注意到code中使用的是相对路径,所以定义了一个localRequire基于bundler文件的相对路径的转化,才能找到依赖的模块。
const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry));
// 避免全局污染 使用闭包
return `
(function(graph) {
function require (module) {
function localRequire(relativePath) {
// relativePath 等于 ./message.js 相对路径
// 根据相对路径找到基于 bundler 文件的相对路径(./src/message.js)
return require(graph[module].dependencies[relativePath])
}
var exports = {};
// 找到对应的code代码
(function(require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code)
return exports;
};
require('${entry}')
})(${graph});
`;
};
运行
node bundler.js命令查看相关代码