共计 6319 个字符,预计需要花费 16 分钟才能阅读完成。
前言
我想这两年,应该是「Webpack」受冲击最显著的时间段。前有「Snowpack」基于浏览器原生ES Module
提出,后有「Vite」站在「Vue3」肩膀上的迅猛发展,真的是后浪推前浪,前浪 ….
并且,「Vite」主推的实现技术 不是一点点新 ,典型的一点应用「esbuild」来充当「TypeScript」的解释器,这一点是和目前社区内 绝大多数打包工具 是不同的。
在下一篇文章,我将会介绍什么是「esbuild」,以及其带来的价值。
然而,虽说后浪的确很强,不过起码近两年来看「Webpack」所处的位置是依然 不可撼动 的。所以,更好地理解「Webpack」相干的原理,能够增强咱们的集体竞争力。
那么,回到明天的正题,咱们就来从零实现一个「Webpack」的 Bundler
打包机制。
1 Bundler 打包背景
Bundler
打包背景,即它是什么?Bundler
打包指的是咱们能够将模块化的代码通过 构建模块依赖图 、 解析代码 、 执行代码 等一系列伎俩来将模块化的代码聚合成 可执行的代码。
在平时的开发中,咱们常常应用的就是 ES Module
的模式进行模块间的援用。那么,为了实现一个 Bundler
打包,咱们筹备这样一个例子:
目录
|—— src
|-- person.js
|-- introduce.js
|-- index.js ## 入口
|—— bundler.js ## bundler 打包机制
代码
// person.js
export const person = 'my name is wjc'
// introduce.js
import {person} from "./person.js";
const introduce = `Hi, ${person}`;
export default introduce;
// index.js
import introduce from "./introduce.js";
console.log(introduce);
除开 bundler.js
打包机制实现文件,另外咱们创立了三个文件,它们别离进行了模块间的援用,最终它们会被 Bundler
打包机制解析生成可执行的代码。
接下来,咱们就来一步步地实现 Bundler
打包机制。
2 单模块解析
Bundler
的打包实现第一步,咱们须要晓得每个模块中的代码,而后对模块中的代码进行依赖剖析、代码转化,从而保障代码的失常执行。
首先,从入口文件 index.js
开始,获取其文件的内容(代码):
const fs = require("fs")
const moduleParse = (file = "") => {const rawCode = fs.readFileSync(file, 'utf-8')
}
获取到模块的代码后,咱们须要晓得它依赖了哪些模块?这个时候,咱们须要借助两个 babel
的工具:@babel/parser
和 @babel/traverse
。前者负责将代码转化为「形象语法树 AST」,后者能够依据模块的援用构建依赖关系。
@babel/parser
将模块的代码解析成「形象语法树 AST」:
const rawCode = fs.readFileSync(file, 'utf-8')
const ast = babelParser(rawCode, {sourceType: "module"})
@babel/traverse
依据模块的援用标识 ImportDeclaration
来构建依赖:
const dependencies = {};
traverse(ast, {ImportDeclaration({ node}) {const dirname = path.dirname(file);
const absoulteFile = `./${path
.join(dirname, node.source.value)
.replace("\\", "/")}`;
dependencies[node.source.value] = absoulteFile;
},
});
这里,咱们通过 @babel/traverse
来将入口 index.js
依赖的模块放到 dependencies
中:
// dependencies
{'./intro.js' : './src/intro.js'}
然而,此时 ast
中的代码还是初始 ES6
的代码,所以,咱们须要借助 @babel/preset-env
来将其转为 ES5
的代码:
const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],
});
index.js
转化后的代码:
"use strict";
var _introduce = _interopRequireDefault(require("./introduce.js"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ?
obj : {"default": obj};
}
console.log(_introduce["default"]);
到此,咱们就实现了对 单模块的解析,残缺的代码如下:
const moduleParse = (file = "") => {const rawCode = fs.readFileSync(file, "utf-8");
const ast = babelParser.parse(rawCode, {sourceType: "module",});
const dependencies = {};
traverse(ast, {ImportDeclaration({ node}) {const dirname = path.dirname(file);
const absoulteFile = `./${path
.join(dirname, node.source.value)
.replace("\\", "/")}`;
dependencies[node.source.value] = absoulteFile;
},
});
const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],
});
return {
file,
dependencies,
code,
};
};
接下来,咱们就开始模块依赖图的构建。
2 构建模块依赖图
家喻户晓,「Webpack」的打包过程会构建一个模块依赖图,它的造成无非就是从入口文件登程,通过它的援用模块,进入该模块,持续单模块的解析,一直反复这个过程。大抵的逻辑图如下:
所以,在代码层面,咱们须要从入口文件登程,先调用 moduleParse()
解析它,而后再遍历获取其对应的依赖 dependencies
,以及调用 moduleParse()
:
const buildDependenceGraph = (entry) => {const entryModule = moduleParse(entry);
const rawDependenceGraph = [entryModule];
for (const module of rawDependenceGraph) {const { dependencies} = module;
if (Object.keys(dependencies).length) {for (const file in dependencies) {rawDependenceGraph.push(moduleParse(dependencies[file]));
}
}
}
// 优化依赖图
const dependenceGraph = {};
rawDependenceGraph.forEach((module) => {dependenceGraph[module.file] = {
dependencies: module.dependencies,
code: module.code,
};
});
return dependenceGraph;
};
最终,咱们构建好的模块依赖图会放到 dependenceGraph
。当初,对于咱们这个例子,构建好的依赖图会是这样:
{
'./src/index.js':
{dependencies: { './introduce.js': './src/introduce.js'},
code: '"use strict";\n\nvar...'},'./src/introduce.js':{
dependencies: {'./person.js': './src/person.js'},
code: '"use strict";\n\nObject.defineProperty(exports,...'},'./src/person.js':
{dependencies: {},
code: '"use strict";\n\nObject.defineProperty(exports,...'}
}
3 生成可执行代码
构建完模块依赖图后,咱们须要依据依赖图将模块的代码转化成能够执行的代码。
因为 @babel/preset-env
解决后的代码用到了两个不存在的变量 require
和 exports
。所以,咱们须要定义好这两个变量。
require
次要做这两件事:
- 依据模块名,获取对应的代码并执行。
eval(dependenceGraph[module].code)
- 解决模块名,因为援用的时候是相对路径,这里须要转成绝对路径,并且递归执行依赖模块代码
function _require(relativePath) {return require(dependenceGraph[module].dependencies[relativePath]);
}
而 export
则用于存储定义的变量,所以咱们定义一个对象来存储。残缺的生成代码函数 generateCode
定义:
const generateCode = (entry) => {const dependenceGraph = JSON.stringify(buildDependenceGraph(entry));
return `
(function(dependenceGraph){function require(module) {function localRequire(relativePath) {return require(dependenceGraph[module].dependencies[relativePath]);
};
var exports = {};
(function(require, exports, code) {eval(code);
})(localRequire, exports, dependenceGraph[module].code);
return exports;
}
require('${entry}');
})(${dependenceGraph});
`;
};
4 残缺的 bundler 打包机制实现代码
残缺的 Bunlder
打包实现代码:
const fs = require("fs");
const path = require("path");
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
const moduleParse = (file = "") => {const rawCode = fs.readFileSync(file, "utf-8");
const ast = babelParser.parse(rawCode, {sourceType: "module",});
const dependencies = {};
traverse(ast, {ImportDeclaration({ node}) {const dirname = path.dirname(file);
const absoulteFile = `./${path
.join(dirname, node.source.value)
.replace("\\", "/")}`;
dependencies[node.source.value] = absoulteFile;
},
});
const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],
});
return {
file,
dependencies,
code,
};
};
const buildDependenceGraph = (entry) => {const entryModule = moduleParse(entry);
const rawDependenceGraph = [entryModule];
for (const module of rawDependenceGraph) {const { dependencies} = module;
if (Object.keys(dependencies).length) {for (const file in dependencies) {rawDependenceGraph.push(moduleParse(dependencies[file]));
}
}
}
// 优化依赖图
const dependenceGraph = {};
rawDependenceGraph.forEach((module) => {dependenceGraph[module.file] = {
dependencies: module.dependencies,
code: module.code,
};
});
return dependenceGraph;
};
const generateCode = (entry) => {const dependenceGraph = JSON.stringify(buildDependenceGraph(entry));
return `
(function(dependenceGraph){function require(module) {function localRequire(relativePath) {return require(dependenceGraph[module].dependencies[relativePath]);
};
var exports = {};
(function(require, exports, code) {eval(code);
})(localRequire, exports, dependenceGraph[module].code);
return exports;
}
require('${entry}');
})(${dependenceGraph});
`;
};
const code = generateCode("./src/index.js");
最终,咱们拿到的 code
就是 Bundler
打包后生成的 可执行代码。接下来,咱们能够将它间接复制到浏览器的 devtool
中执行,查看后果。
写在最初
尽管,这个 Bundler
打包机制的实现,只是简易版的,它只是大抵地实现了整个「Webpack」的 Bundler
打包流程,并不是实用于所有用例。然而,在我看来很多货色的学习都应该是从易到难,这样的排汇效率才是最高的。
往期文章回顾
深度解读 Vue3 源码 | 组件创立过程
深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?
深度解读 Vue3 源码 | compile 和 runtime 联合的 patch 过程
❤️爱心三连击
写作不易,如果你感觉有播种的话,能够爱心三连击!!!