动手写一个简单的代码编译器,了解一下webpack的核心原理。

核心打包原理

  1. 获取入口文件的内容
  2. 分析入口文件,读取需要执行的模块内容
  3. 生成 AST 语法树
  4. 根据 AST 语法树生成浏览器能够正常运行的代码

撸代码

首先初始化一个项目

webpackTheory
├── src
│ ├── sum.js
│ ├── subtract.js
│ └── index.js
├── package.json
├── bundle.js
└── index.html
// sum.js
export default (a, b) => {
return a + b;
}
// subtract.js
export const minus = (a, b) => {
return a - b;
}
// index.js
import add from './sum.js';
import {
minus
} from './subtract.js';


const sum = add(2, 1);
const subtract = minus(2, 1);

document.write(`<h1>sum:${sum}</h1><h1>subtract:${subtract}</h1>`)

如果此时直接在index.html中引入index.js,页面是无法正常运行的,浏览器无法正常解析,就会抛出语法错误。

Uncaught SyntaxError: Cannot use import statement outside a module

所以需要一个编译器,把index.js编译成浏览器能够正常解析的语法。

创建一个bundle.js

// bundle.js
const fs = require("fs");

/**
* 2️⃣ https://babeljs.io/docs/en/babel-parser
* 用来把 JS 生成 AST 语法树
*/
const parser = require("@babel/parser");

/**
* 3️⃣ https://babeljs.io/docs/en/babel-traverse
* 用来遍历 AST,可以添加、删除和更新节点,这里主要用来获取依赖文件
*/
const traverse = require("@babel/traverse").default;

// 3️⃣
const path = require("path");

/**
* 4️⃣ https://babeljs.io/docs/en/babel-core
* Babel 的核心功能包,这里用来把 AST 语法树转换成可执行的 JS
*/
const babel = require("@babel/core");

const getModuleInfo = (file) => {
// 获取执行代码
const body = fs.readFileSync(file, "utf-8");

// 2️⃣ 生成 ast tree
const AST = parser.parse(body, {
sourceType: "module",
});

const deps = {};
// 3️⃣ 解析依赖
traverse(AST, {
ImportDeclaration({
node
}) {
const dirname = path.dirname(file);
const abspath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
},
});

// 4️⃣ 把代码转换成目标运行环境支持的语法
const {
code
} = babel.transformFromAst(AST, "", {
// 与 .babelrc 文件中 presets 的配置相同
presets: ["@babel/preset-env"],
});

return {
file, // 文件
deps, // 文件依赖
code // 可执行代码
}
};

// 5️⃣ 遍历解析所有的 module
const parseModules = file => {
const entry = getModuleInfo(file);
const temp = [entry];
for (let i = 0; i < temp.length; i++) {
const deps = temp[i].deps;
if (deps) {
for (let key in deps) {
temp.push(getModuleInfo(deps[key]))
}
}
}

const depsObject = {};
temp.forEach(t => {
depsObject[t.file] = {
deps: t.deps,
code: t.code
}
})

return depsObject
}

// 6️⃣ 打包,返回一个可执行的代码字符
// 1. 立即执行函数,传入相关依赖的JSON。
// 2. 自定义 require 函数, 浏览器不认识 require
// 3. 自定义 exports 对象,浏览器不认识 exports
// 4. 立即执行函数,传入 2中定义的 require、3中定义的 exports 和可以执行的代码字符 code,用 eval 函数执行
const bundle = file => {
const DEPSJSON = JSON.stringify(parseModules(file));
return `
;(function (deps) {
function require(f) {
const customRequire = function(path){
return require(deps[f]['deps'][path])
};
const customExports = {};
(function(require, exports, code){
eval(code);
})(customRequire, customExports, deps[f]['code'])
return customExports;
};
require('${file}');
})(${DEPSJSON})
`
}
const JSCONTENT = bundle("./src/index.js");


fs.unlinkSync("./dist/bundle.js");
fs.rmdirSync("./dist");
fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", JSCONTENT);

// (function() {
// (0,eval)("var foo = 123"); // indirect call to eval, creates global variable
// })();
// console.log(foo); // 123
// (function() {
// eval("var bar = 123"); // direct call to eval, creates local variable
// })();
// console.log(bar); // ReferenceError

在根目录下运行:

node bundle.js

dist目录下就生成了编译好的文件,在index.html中引入,就可以正常执行了。