安装依赖
npm i @babel/parser @babel/traverse babel-types tapable -D
src/index.js
let title = require('./title.js');
console.log(title);
src/title.js
module.exports = 'title';
my-webpack.js
const fs = require("fs");//读文件
const path = require("path");//处理路径的
/*
@babel/types 是一款作用于 AST 的类 lodash 库,其封装了大量与 AST 有关的方法,大大降低了转换 AST 的成本。@babel/types 的功能主要有两种:一方面可以用它验证 AST 节点的类型,例如使用 isClassMethod 或 assertClassMethod 方法可以判断 AST 节点是否为 class 中的一个 method;另一方面可以用它构建 AST 节点,例如调用 classMethod 方法,可生成一个新的 classMethod 类型 AST 节点 。
*/
const types = require("babel-types");//处理类型
const parser = require("@babel/parser");//把源代码转成抽象语法树
const traverse = require("@babel/traverse").default;//遍历语法树
const generate = require("@babel/generator").default;//把语法树生成源代码
// /Users/sun/Desktop/base/webpack-interview/my-webpack
const baseDir = process.cwd().replace(/\\/g,path.posix.sep);//根目录 将\换成/
// /Users/sun/Desktop/base/webpack-interview/my-webpack/src/index.js
const entry = path.posix.join(baseDir, "src/index.js");//打包的入口文件
let modules = [];//数组里面放着本次编译的所有模块
function buildModule(absolutePath){
/*
index.js 文件的内容
let title = require('./title.js');
console.log(title);
title.js文件中的内容
module.exports = 'title';
*/
// 读取入口文件
const body = fs.readFileSync(absolutePath, "utf-8");//读取文件内容
// 将文件内容转为AST抽象语法树
const ast = parser.parse(body, {
sourceType: "module",//源代码转成抽象语法树
});
const moduleId = "./" + path.posix.relative(baseDir, absolutePath);// 第一调用值为./src/index.js;第二次调用值为./src/title.js 因为在webpack中moduleId都是相对于根目录的相对路径
const module = { id: moduleId, deps:[] };//声明一个模块对象
// @babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块
traverse(ast, {
CallExpression({ node }) {
if (node.callee.name === 'require') { // 是否存在依赖
node.callee.name = "__webpack_require__"; // 因为打包后的require都会变成__webpack_require__
let moduleName = node.arguments[0].value; // 获取模块名字 ./title.js
// /Users/sun/Desktop/base/webpack-interview/my-webpack/src
const dirname = path.posix.dirname(absolutePath);
// /Users/sun/Desktop/base/webpack-interview/my-webpack/src/title.js
const depPath = path.posix.join(dirname, moduleName);
// ./src/title.js
const depModuleId = "./" + path.posix.relative(baseDir, depPath);
// [ { type: 'StringLiteral', value: './src/title.js' } ]
node.arguments = [types.stringLiteral(depModuleId)];
// 递归处理所有模块
module.deps.push(buildModule(depPath));
}
}
});
// 将 AST 语法树转换为浏览器可执行代码
let { code } = generate(ast); // 重新生成新的代码
module._source = code;//module._source存放着重新生成后的代码
modules.push(module);
return module;
}
// 所有模块编译完成后
let entryModule = buildModule(entry);
// 重写 require函数 (浏览器不能识别commonjs语法)
let content = `
(function (modules) {
function __webpack_require__(moduleId) {
var module = {
i: moduleId,
exports: {}
};
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
return module.exports;
}
return __webpack_require__("${entryModule.id}");
})(
{
${modules
.map(
(module) =>
`"${module.id}": function (module, exports,__webpack_require__) {${module._source}}`
)
.join(",")}
}
);
`;
// 把文件内容写入到文件系统
fs.writeFileSync("./dist/bundle.js", content);
简单总结下
第一步,解析入口文件通过@babel/parser分析内部的语法,返回AST抽象语法树
第二步,通过@babel/traverse找出依赖模块
第三步,将 AST 语法树转换为浏览器可执行代码,
第四步,递归解析所有依赖项,生成依赖关系图
第五步,重写 require 函数,输出 bundle
参考: