我想這兩年,應該是「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 過程
寫做不易,若是你以爲有收穫的話,能夠愛心三連擊!!!