参考 minipack
目录结构
├─example
│ entry.js
│ message.js
│ name.js
│
└─src
minipack.js
example 代码
// entry.js
import message from "./message.js";
console.log(message);
// message.js
import { name } from "./name.js";
export default `hello ${name}!`;
// name.js
export const name = "world";
minipack 代码
const fs = require("fs"); // 文件读取
const path = require("path"); // 文件路径
const { parse } = require("@babel/parser"); // 解析代码生成 AST
const transformFromAst = require("@babel/core").transformFromAst; // 将 AST 解析生成代码
const traverse = require("@babel/traverse").default; // 对 AST 进行遍历
let ID = 0;
/**
* @description: 生成文件依赖信息
* @param {*} filename 文件的绝对路径
* @return {*}
*/
function createAsset(filename) {
/* 读取文件内容 */
// 字符串形式读取文件内容.
const content = fs.readFileSync(filename, "utf-8");
// 现在我们需要搞清楚文件的依赖关系,可以通过寻找文件内容中的 import 字符串,为此我们就需要一个 JavaScript 解析器
/* 生成 AST */
// JavaScript 解析器可以帮助我们来理解代码,生成一个抽象的数据结构称为 AST(抽象语法树)
// 可以在这里看看抽象语法树是什么样子
// AST Explorer (https://astexplorer.net)
//
// 一个 AST 包含了代码中的大量信息,我们可以通过它来了解代码想要做什么
const ast = parse(content, {
sourceType: "module"
});
// 这个数组用来保存当前模块依赖的其他模块的路径
const dependencies = [];
/* 遍历 AST */
// 遍历 AST 来查询当前模块依赖哪些模块,也就是检查 AST 上每个 import 语句
traverse(ast, {
// ES 模块都很简单,因为它们都是静态的,所以看到 import 语句可以直接把值作为依赖
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
}
});
// 用 ID 来标识当前模块
const id = ID++;
// ES 模块和其他 js 特性可能不是所有浏览器都支持,为了能够在所有浏览器上运行,我们使用 Babel 来转译
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
// 返回该模块的所有信息.
return {
id,
filename,
dependencies,
code
};
}
/**
* @description: 创建依赖结构图
* @param {*} entry 入口文件的相对路径
* @return {*}
*/
function createGraph(entry) {
// 先解析入口文件的依赖信息.
const mainAsset = createAsset(entry);
// 用一个队列来保存所有的依赖信息
const queue = [mainAsset];
// 遍历队列
for (const asset of queue) {
// 每个 asset 都有一组它依赖模块的相对路径
// 我们用 createAsset 来迭代解析
asset.mapping = {};
// 当前模块的目录.
const dirname = path.dirname(asset.filename);
// 迭代访问依赖的相对路径.
asset.dependencies.forEach(relativePath => {
// 生成绝对路径
const absolutePath = path.join(dirname, relativePath);
// 提取依赖信息.
const child = createAsset(absolutePath);
// asset 保存 id 和 相对路径 的映射关系
asset.mapping[relativePath] = child.id;
// 加入队列
queue.push(child);
});
}
// queue 保存的就是当前应用的所有模块的依赖结构图
return queue;
}
/**
* @description: 文件打包
* @param {*} graph 依赖结构图
* @return {*}
*/
function bundle(graph) {
let modules = "";
graph.forEach(mod => {
// 每个模块用 id 来标识都是数组类型
// 每个模块有两个值
// 第一个是用函数包装每个模块的代码,是为了限定每个模块的作用域
// 第二个是模块的 相对路径和id 的依赖关系
modules += `${mod.id}: [
function (require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
});
// 最后实现自动执行函数的主题
// 1. 创建 require 函数接收模块 id
// 2. 通过 id 来获取模块代码和映射关系
// 3. 根据不同的模块导入方式生成代码
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
// 返回的结果就是打包完成的内容了
return result;
}
const graph = createGraph("./example/entry.js");
const result = bundle(graph);
console.log(result);
fs.writeFile("dist.js", result, e => {
if (e) throw e;
console.log("打包完成");
});
package.json
{
"name": "webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.12.3",
"@babel/generator": "^7.12.1",
"@babel/parser": "^7.12.3",
"@babel/preset-env": "^7.12.1",
"@babel/traverse": "^7.12.1"
}
}
使用说明
# 安装依赖
yarn install
# 执行
node src/minipack.js
# 会在最外层目录下生成 dist.js,执行该文件
node dist.js
# 输出 hello world!