我想这两年,应该是「Webpack」受冲击最明显的时间段。前有「Snowpack」基于浏览器原生ES Module
提出,后有「Vite」站在「Vue3」肩膀上的迅猛发展,真的是后浪推前浪,前浪....javascript
而且,「Vite」主推的实现技术不是一点点新,典型的一点使用「esbuild」来充当「TypeScript」的解释器,这一点是和目前社区内绝大多数打包工具是不一样的。java
在下一篇文章,我将会介绍什么是「esbuild」,以及其带来的价值。
可是,虽然说后浪确实很强,不过起码近两年来看「Webpack」所处的地位是仍然不可撼动的。因此,更好地了解「Webpack」相关的原理,能够增强咱们的我的竞争力。node
那么,回到今天的正题,咱们就来从零实现一个「Webpack」的 Bundler
打包机制。segmentfault
Bundler
打包背景,即它是什么?Bundler
打包指的是咱们能够将模块化的代码经过构建模块依赖图、解析代码、执行代码等一系列手段来将模块化的代码聚合成可执行的代码。浏览器
在日常的开发中,咱们常常使用的就是 ES Module
的形式进行模块间的引用。那么,为了实现一个 Bundler
打包,咱们准备这样一个例子:babel
目录模块化
|—— 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
打包机制。学习
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, }; };
接下来,咱们就开始模块依赖图的构建。
众所周知,「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,...' } }
构建完模块依赖图后,咱们须要根据依赖图将模块的代码转化成能够执行的代码。
因为 @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}); `; };
完整的 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 源码 | 内置组件 teleport 是什么“来头”?
深度解读 Vue3 源码 | compile 和 runtime 结合的 patch 过程
写做不易,若是你以为有收获的话,能够爱心三连击!!!