深入理解 Webpack 核心機制與編譯流程

在這里插入圖片描述

🤖 作者簡介:水煮白菜王,一位前端勸退師 👻
👀 文章專欄: 前端專欄 ,記錄一下平時在博客寫作中,總結出的一些開發技巧和知識歸納總結?。
感謝支持💕💕💕

目錄

  • Webpack核心機制
    • Tapable:Webpack 插件系統的“心臟”
    • Tapabel提供的鉤子及示例
    • 源碼解讀
    • 編譯構建
      • compile
      • make
        • 1. Module
        • 2. loader-runner
        • 3. acorn
        • 4. Chunk生成算法
      • eal
      • emit
    • 總結
  • Webpack打包機制
    • 簡單版打包模型步驟
    • 單個文件的依賴模塊Map
    • 單個文件的依賴模塊Map
    • 輸出立即執行函數
    • webpack打包流程概括
  • 實現一個丐版Webpack
    • 開始
    • 接下來我們再來逐行解析 bundle 函數
  • 如果你覺得這篇文章對你有幫助,請點贊 👍、收藏 👏 并關注我!👀

在這里插入圖片描述

Webpack核心機制

Webpack 本質上是一個高度可配置且可擴展的模塊捆綁器,它采用了一種基于事件流的編程范例。Webpack 的運作依賴于一系列插件來完成各種任務,從簡單的文件轉換到復雜的構建優化。

Webpack 主要使用 Compiler 和 Compilation 兩個類來控制整個生命周期。它們都繼承自 Tapable 并利用它注冊構建過程中的各個階段所需觸發的事件。

Tapable:Webpack 插件系統的“心臟”

Tapable 是一個類似于 Node.js 的 EventEmitter 的庫,主要用于管理鉤子函數的發布與訂閱。在 Webpack 的插件系統中,Tapable 扮演著核心調度者的角色。

Tapabel提供的鉤子及示例

Tapable 提供了多種類型的鉤子(Hook)以便掛載,適用于不同的執行場景(同步 / 異步、串行 / 并發、是否支持熔斷等):

const {SyncHook,                   // 同步鉤子:依次執行所有訂閱者SyncBailHook,               // 同步熔斷鉤子:一旦某個訂閱者返回非 undefined 值則停止執行SyncWaterfallHook,          // 同步流水鉤子:前一個訂閱者的返回值作為參數傳給下一個SyncLoopHook,               // 同步循環鉤子:重復執行訂閱者直到返回 undefinedAsyncParallelHook,          // 異步并發鉤子:并行執行所有訂閱者(不關心順序)AsyncParallelBailHook,      // 異步并發熔斷鉤子:任意一個訂閱者返回非 undefined 則立即結束AsyncSeriesHook,            // 異步串行鉤子:按順序依次執行每個訂閱者AsyncSeriesBailHook,        // 異步串行熔斷鉤子:同 SyncBailHook,但為異步模式AsyncSeriesWaterfallHook    // 異步串行流水鉤子:同 SyncWaterfallHook,但為異步模式
} = require("tapable");

Tabpack 提供了同步&異步綁定鉤子的方法對比如下:

類型綁定方法執行方法
同步 (Sync).tap(name, fn).call(args...)
異步 (Async).apAsync(name, fn) /.tapPromise(name, fn) .callAsync(args..., cb) / .promise(args...)

Tabpack 同步簡單示例:

const { SyncHook } = require("tapable");// 創建一個帶有三個參數的同步鉤子
const demohook = new SyncHook(["arg1", "arg2", "arg3"]);// 注冊監聽函數 綁定事件到webpack事件流
demohook.tap("hook1", (arg1, arg2, arg3) => {console.log("接收到參數:", arg1, arg2, arg3);
});// 觸發鉤子 執行綁定的事件
demohook.call(1, 2, 3);
// 輸出: 接收到參數:1 2 3

源碼解讀

  1. 初始化啟動之Webpack的入口文件
    ● 追本溯源,第一步我們要找到Webpack的入口文件。
    ● 當通過命令行啟動Webpack后,npm會讓命令行工具進入node_modules.bin 目錄。
    ● 然后查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就執行它們,不存在就會拋出錯誤。
    ● 實際的入口文件是:node_modules/webpack/bin/webpack.js,讓我們來看一下里面的核心函數。
// node_modules/webpack/bin/webpack.js
// 正常執行返回
process.exitCode = 0;    
// 運行某個命令                               
const runCommand = (command, args) => {...}
// 判斷某個包是否安裝
const isInstalled = packageName => {...}
// webpack可用的CLI:webpacl-cli和webpack-command
const CLIs = {...}
// 判斷是否兩個CLI是否安裝了
const installedClis = CLIs.filter(cli=>cli.installed);
// 根據安裝數量進行處理
if (installedClis.length === 0) {...} else if (installedClis.length === 1) {...} else {...}

啟動后,Webpack最終會找到 webpack-cli /webpack-command的 npm 包,并且 執行 CLI。

  1. webpack-cli
    搞清楚了Webpack啟動的入口文件后,接下來讓我們把目光轉移到webpack-cli,看看它做了哪些動作。
    ● 引入 yargs,對命令行進行定制分析命令行參數,對各個參數進行轉換,組成編譯配置項引用webpack,根據配置項進行編譯和構建
    ● webpack-cli 會處理不需要經過編譯的命令。
// node_modules/webpack-cli/bin/cli.js
const {NON_COMPILATION_ARGS} = require("./utils/constants");
const NON_COMPILATION_CMD = process.argv.find(arg => {if (arg === "serve") {global.process.argv = global.process.argv.filter(a => a !== "serve");process.argv = global.process.argv;}return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {return require("./utils/prompt-command")(NON_COMPILATION_CMD,...process.argv);
}

webpack-cli提供的不需要編譯的命令如下

// node_modules/webpack-cli/bin/untils/constants.js
const NON_COMPILATION_ARGS = ["init",                 // 創建一份webpack配置文件"migrate",              // 進行webpack版本遷移"add",                  // 往webpack配置文件中增加屬性"remove",               // 往webpack配置文件中刪除屬性"serve",                // 運行webpack-serve"generate-loader",      // 生成webpack loader代碼"generate-plugin",      // 生成webpack plugin代碼"info"                  // 返回與本地環境相關的一些信息
];

webpack-cli 使用命令行工具包yargs

// node_modules/webpack-cli/bin/config/config-yargs.js
const {CONFIG_GROUP,BASIC_GROUP,MODULE_GROUP,OUTPUT_GROUP,ADVANCED_GROUP,RESOLVE_GROUP,OPTIMIZE_GROUP,DISPLAY_GROUP
} = GROUPS;

● webpack-cli對配置文件和命令行參數進行轉換最終生成配置選項參數 options,最終會根據配置參數實例化webpack對象,然后執行構建流程。
● 除此之外,讓我們回到node_modules/webpack/lib/webpack.js里來看一下Webpack還做了哪些準備工作。

// node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {...options = new WebpackOptionsDefaulter().process(options);compiler = new Compiler(options.context);new NodeEnvironmentPlugin().apply(compiler);...compiler.options = new WebpackOptionsApply().process(options, compiler);...webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;webpack.WebpackOptionsApply = WebpackOptionsApply;...webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
}

WebpackOptionsDefaulter的功能是設置一些默認的Options(代碼比較多,可自行查看node_modules/webpack/lib/WebpackOptionsDefaulter.js)

// node_modules/webpack/lib/node/NodeEnvironmentPlugin.js
class NodeEnvironmentPlugin {apply(compiler) {...		compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();});}
}

從上面的代碼我們可以知道,NodeEnvironmentPlugin插件監聽了beforeRun鉤子,它的作用是清除緩存。

  1. WebpackOptionsApply
    WebpackOptionsApply會將所有的配置options參數轉換成webpack內部插件。
    使用默認插件列表:
    ● output.library -> LibraryTemplatePlugin
    ● externals -> ExternalsPlugin
    ● devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
    ● AMDPlugin, CommonJsPlugin
    ● RemoveEmptyChunksPlugin
// node_modules/webpack/lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
  1. EntryOptionPlugin
    下來讓我們進入EntryOptionPlugin插件,看看它做了哪些動作。
// node_modules/webpack/lib/EntryOptionPlugin.js
module.exports = class EntryOptionPlugin {apply(compiler) {compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {if (typeof entry === "string" || Array.isArray(entry)) {itemToPlugin(context, entry, "main").apply(compiler);} else if (typeof entry === "object") {for (const name of Object.keys(entry)) {itemToPlugin(context, entry[name], name).apply(compiler);}} else if (typeof entry === "function") {new DynamicEntryPlugin(context, entry).apply(compiler);}return true;});}
};

● 如果是數組,則轉換成多個entry來處理,如果是對象則轉換成一個個entry來處理。
● compiler實例化是在node_modules/webpack/lib/webpack.js里完成的。通過EntryOptionPlugin插件進行參數校驗。通過WebpackOptionsDefaulter將傳入的參數和默認參數進行合并成為新的options,創建compiler,以及相關plugin,最后通過
● WebpackOptionsApply將所有的配置options參數轉換成Webpack內部插件。
● 再次來到我們的node_modules/webpack/lib/webpack.js中

if (options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {const watchOptions = Array.isArray(options)? options.map(o => o.watchOptions || {}): options.watchOptions || {};return compiler.watch(watchOptions, callback);
}
compiler.run(callback);

實例compiler后會根據options的watch判斷是否啟動了watch,如果啟動watch了就調用compiler.watch來監控構建文件,否則啟動compiler.run來構建文件。

編譯構建

compile

首先會實例化NormalModuleFactory和ContextModuleFactory。然后進入到run方法。

// node_modules/webpack/lib/Compiler.js
run(callback) { ...// beforeRun 如上文NodeEnvironmentPlugin插件清除緩存this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);// 執行run Hook開始編譯this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);this.readRecords(err => {if (err) return finalCallback(err);// 執行compilethis.compile(onCompiled);});});});
}

在執行this.hooks.compile之前會執行this.hooks.beforeCompile,來對編譯之前需要處理的插件進行執行。緊接著this.hooks.compile執行后會實例化Compilation對象

// node_modules/webpack/lib/compiler.js
compile(callback) {const params = this.newCompilationParams();this.hooks.beforeCompile.callAsync(params, err => {if (err) return callback(err);// 進入compile階段this.hooks.compile.call(params);const compilation = this.newCompilation(params);// 進入make階段this.hooks.make.callAsync(compilation, err => {if (err) return callback(err);compilation.finish(err => {if (err) return callback(err);// 進入seal階段compilation.seal(err => {if (err) return callback(err);this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);return callback(null, compilation);})})})})})
}

make

● 一個新的Compilation創建完畢,將從Entry開始讀取文件,根據文件類型和配置的Loader對文件進行編譯,編譯完成后再找出該文件依賴的文件,遞歸的編譯和解析。
● 我們來看一下make鉤子被監聽的地方。
● 如代碼中注釋所示,addEntry是make構建階段真正開始的標志

// node_modules/webpack/lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync("SingleEntryPlugin",(compilation, callback) => {const { entry, name, context } = this;cosnt dep = SingleEntryPlugin.createDependency(entry, name);// make構建階段開始標志 compilation.addEntry(context, dep, name, callback);}
)

addEntry實際上調用了_addModuleChain方法,_addModuleChain方法將模塊添加到依賴列表中去,同時進行模塊構建。構建時會執行如下函數:

// node_modules/webpack/lib/Compilation.js
// addEntry -> addModuleChain
_addModuleChain(context, dependency, onModule, callback) {...this.buildModule(module, false, null, null, err => {...})...}

如果模塊構建完成,會觸發finishModules。

// node_modules/webpack/lib/Compilation.js
finish(callback) {const modules = this.modules;this.hooks.finishModules.callAsync(modules, err => {if (err) return callback(err);for (let index = 0; index < modules.length; index++) {const module = modules[index];			this.reportDependencyErrorsAndWarnings(module, [module]);}callback();})
}
1. Module

● Module包括NormalModule(普通模塊)、ContextModule(./src/a ./src/b)、ExternalModule(module.exports=jQuery)、DelegatedModule(manifest)以及MultiModule(entry:[‘a’, ‘b’])。
● 本文以NormalModule(普通模塊)為例子,看一下構建(Compilation)的過程。
使用 loader-runner 運行 loadersLoader轉換完后,使用 acorn 解析生成AST使用 ParserPlugins 添加依賴

2. loader-runner
// node_modules/webpack/lib/NormalModule.js
const { getContext, runLoaders } = require("loader-runner");
doBuild(){...runLoaders(...)...}
...
try {const result = this.parser.parse()
}

doBuild會去加載資源,doBuild中會傳入資源路徑和插件資源去調用loader-runner插件的runLoaders方法去加載和執行loader

3. acorn
// node_modules/webpack/lib/Parser.jsconst acorn = require("acorn");

使用acorn解析轉換后的內容,輸出對應的抽象語法樹(AST)。

// node_modules/webpack/lib/Compilation.js
this.hooks.buildModule.call(module);
...
if (error) {this.hooks.failedModule.call(module, error);return callback(error);
}
this.hooks.succeedModule.call(module);
return callback();

● 成功就觸發succeedModule,失敗就觸發failedModule。
● 最終將上述階段生成的產物存放到Compilation.js的this.modules = [];上。

完成后就到了seal階段。
這里補充介紹一下Chunk生成的算法

4. Chunk生成算法

● webpack首先會將entry中對應的module都生成一個新的chunk。
● 遍歷module的依賴列表,將依賴的module也加入到chunk中。
● 如果一個依賴module是動態引入的模塊,會根據這個module創建一個新的chunk,繼續遍歷依賴。
● 重復上面的過程,直至得到所有的chunk。

eal

● 所有模塊及其依賴的模塊都通過Loader轉換完成,根據依賴關系開始生成Chunk。
● seal階段也做了大量的的優化工作,進行了hash的創建以及對內容進行生成(createModuleAssets)。

// node_modules/webpack/lib/Compilation.jsthis.createHash();
this.modifyHash();
this.createModuleAssets();
// node_modules/webpack/lib/Compilation.js
createModuleAssets(){for (let i = 0; i < this.modules.length; i++) {const module = this.modules[i];if (module.buildInfo.assets) {for (const assetName of Object.keys(module.buildInfo.assets)) {const fileName = this.getPath(assetName);this.assets[fileName] = module.buildInfo.assets[assetName];this.hooks.moduleAsset.call(module, fileName);}}}
}

seal階段經歷了很多的優化,比如tree shaking就是在這個階段執行。最終生成的代碼會存放在Compilation的assets屬性上

emit

將輸出的內容輸出到磁盤,創建目錄生成文件,文件生成階段結束。

// node_modules/webpack/lib/compiler.js
this.hooks.emit.callAsync(compilation, err => {if (err) return callback(err);outputPath = compilation.getPath(this.outputPath);this.outputFileSystem.mkdirp(outputPath, emitFiles);
})

總結

Webpack在啟動階段對配置參數和命令行參數以及默認參數進行了合并,并進行了插件的初始化工作。完成初始化的工作后調用Compiler的run開啟Webpack編譯構建過程,構建主要流程包括compile、make、build、seal、emit等階段。

Webpack打包機制

webpack是一個打包模塊化 JavaScript 的工具,在 webpack里一切文件皆模塊,通過 Loader 轉換文件,通過 Plugin 注入鉤子,最后輸出由多個模塊組合成的文件。webpack專注于構建模塊化項目。

簡單版打包模型步驟

從簡單的入手看,當 webpack 的配置只有一個出口時,不考慮分包的情況,其實我們只得到了一個bundle.js的文件,這個文件里包含了我們所有用到的js模塊,可以直接被加載執行。那么,我可以分析一下它的打包思路,大概有以下4步:

  • 利用babel完成代碼轉換及解析,并生成單個文件的依賴模塊Map
  • 從入口開始遞歸分析,并生成整個項目的依賴圖譜
  • 將各個引用模塊打包為一個立即執行函數
  • 將最終的bundle文件寫入bundle.js

單個文件的依賴模塊Map

我們會可以使用這幾個包:

  • @babel/parser:負責將代碼解析為抽象語法樹
  • @babel/traverse:遍歷抽象語法樹的工具,我們可以在語法樹中解析特定的節點,然后做一些操作,如ImportDeclaration獲取通過import引入的模塊,FunctionDeclaration獲取函數
  • @babel/core:代碼轉換,如ES6的代碼轉為ES5的模式

由這幾個模塊的作用,其實已經可以推斷出應該怎樣獲取單個文件的依賴模塊了,轉為Ast->遍歷Ast->調用ImportDeclaration。代碼如下:

// exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')const exportDependencies = (filename)=>{const content = fs.readFileSync(filename,'utf-8')// 轉為Astconst ast = parser.parse(content, {sourceType : 'module'//babel官方規定必須加這個參數,不然無法識別ES Module})const dependencies = {}//遍歷AST抽象語法樹traverse(ast, {//調用ImportDeclaration獲取通過import引入的模塊ImportDeclaration({node}){const dirname = path.dirname(filename)const newFile = './' + path.join(dirname, node.source.value)//保存所依賴的模塊dependencies[node.source.value] = newFile}})//通過@babel/core和@babel/preset-env進行代碼的轉換const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return{filename,//該文件名dependencies,//該文件所依賴的模塊集合(鍵值對存儲)code//轉換后的代碼}
}
module.exports = exportDependencies

試跑工作:

//info.js
const a = 1
export a
// index.js
import info from'./info.js'
console.log(info)//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))

單個文件的依賴模塊Map

有了獲取單個文件依賴的基礎,我們就可以在這基礎上,進一步得出整個項目的模塊依賴圖譜了。首先,從入口開始計算,得到entryMap,然后遍歷entryMap.dependencies,取出其value(即依賴的模塊的路徑),然后再獲取這個依賴模塊的依賴圖譜,以此類推遞歸下去即可,代碼如下:

const exportDependencies = require('./exportDependencies')//entry為入口文件路徑
const exportGraph = (entry)=>{const entryModule = exportDependencies(entry)const graphArray = [entryModule]for(let i = 0; i < graphArray.length; i++){const item = graphArray[i];//拿到文件所依賴的模塊集合,dependencies的值參考exportDependenciesconst { dependencies } = item;for(let j in dependencies){graphArray.push(exportDependencies(dependencies[j]))//關鍵代碼,目的是將入口模塊及其所有相關的模塊放入數組}}//接下來生成圖譜const graph = {}graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}})//可以看出,graph其實是 文件路徑名:文件內容 的集合return graph
}
module.exports = exportGraph

輸出立即執行函數

首先,我們的代碼被加載到頁面中的時候,是需要立即執行的。所以輸出的bundle.js實質上要是一個立即執行函數。我們主要注意以下幾點:

  • 我們寫模塊的時候,用的是 import/export.經轉換后,變成了require/exports
  • 我們要讓require/exports能正常運行,那么我們得定義這兩個東西,并加到bundle.js里
  • 在依賴圖譜里,代碼都成了字符串。要執行,可以使用eval

因此,我們要做這些工作:

  • 定義一個require函數,require函數的本質是執行一個模塊的代碼,然后將相應變量掛載到exports對象上
  • 獲取整個項目的依賴圖譜,從入口開始,調用require方法。完整代碼如下:
const exportGraph = require('./exportGraph')
// 寫入文件,可以用fs.writeFileSync等方法,寫入到output.path中
const exportBundle = require('./exportBundle')const exportCode = (entry)=>{//要先把對象轉換為字符串,不然在下面的模板字符串中會默認調取對象的toString方法,參數變成[Object object]const graph = JSON.stringify(exportGraph(entry))exportBundle(`(function(graph) {//require函數的本質是執行一個模塊的代碼,然后將相應變量掛載到exports對象上function require(module) {//localRequire的本質是拿到依賴包的exports變量function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);}var exports = {};(function(require, exports, code) {eval(code);})(localRequire, exports, graph[module].code);return exports;//函數返回指向局部變量,形成閉包,exports變量在函數執行后不會被摧毀}require('${entry}')})(${graph})`)
}
module.exports = exportCode

至此,簡單打包完成,跑出結果。bundle.js的文件內容為:

(function(graph) {//require函數的本質是執行一個模塊的代碼,然后將相應變量掛載到exports對象上function require(module) {//localRequire的本質是拿到依賴包的exports變量function localRequire(relativePath) {returnrequire(graph[module].dependencies[relativePath]);}var exports = {};(function(require, exports, code) {eval(code);})(localRequire, exports, graph[module].code);return exports;//函數返回指向局部變量,形成閉包,exports變量在函數執行后不會被摧毀}require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})

webpack打包流程概括

輸出
編譯
初始化
輸出資源
輸出完成
開始編譯
確定入口
編譯模塊
完成模塊編譯
初始化參數

webpack的運行流程是一個串行的過程,從啟動到結束會依次執行以下流程:

  • 初始化參數:Webpack啟動時,依據命令行參數和配置文件設置編譯所需的各項基本參數,確保準備好開始構建。
  • 開始編譯: 用上一步得到的參數初始Compiler對象,加載所有配置的插件,通 過執行對象的run方法開始執行編譯
  • 確定入口: 根據配置中的 Entry 找出所有入口文件
  • 編譯模塊: 從入口文件出發,調用所有配置的 Loader 對模塊進行編譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經過了本步驟的處理
  • 完成模塊編譯: 在經過第4步使用 Loader 翻譯完所有模塊后, 得到了每個模塊被編譯后的最終內容及它們之間的依賴關系
  • 輸出資源 :根據入口和模塊之間的依賴關系,組裝成一個個包含多個模塊的 Chunk,再將每個 Chunk 轉換成一個單獨的文件加入輸出列表中,這是可以修改輸出內容的最后機會
  • 輸出完成: 在確定好輸出內容后,根據配置確定輸出的路徑和文件名,將文件的內容寫入文件系統中。

在以上過程中, Webpack 會在特定的時間點廣播特定的事件,插件在監聽到感興趣的事件后會執行特定的邏輯,井且插件可以調用 Webpack 提供的 API 改變 Webpack 的運行結果。其實以上7個步驟,可以簡單歸納為初始化、編譯、輸出,三個過程,而這個過程其實就是前面說的基本模型的擴展。

實現一個丐版Webpack

該工具可以實現以下兩個功能
● 將 ES6 轉換為 ES5
● 支持在 JS 文件中 import CSS 文件
通過這個工具的實現,可以更好地理解打包工具背后的運行原理。

開始

由于需要將 ES6 轉換為 ES5,我們首先需要安裝一些 Babel 相關的依賴包:

yarn add babylon babel-traverse babel-core babel-preset-env

接下來我們將這些工具引入文件中

const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

第一步:首先,實現如何使用 Babel 解析并轉換代碼

function readCode(filePath) {// 讀取文件內容const content = fs.readFileSync(filePath, 'utf-8')// 生成 ASTconst ast = babylon.parse(content, {sourceType: 'module'})// 尋找當前文件的依賴關系const dependencies = []traverse(ast, {ImportDeclaration: ({ node }) => {dependencies.push(node.source.value)}})// 通過 AST 將代碼轉為 ES5const { code } = transformFromAst(ast, null, {presets: ['env']})return {filePath,dependencies,code}
}

● 首先我們傳入一個文件路徑參數,通過 fs 模塊讀取其內容。
● 接下來我們通過 babylon 解析代碼生成抽象語法樹(AST),用于分析是否存在其他導入文件。
● 通過 babel-traverse 遍歷 AST,提取出所有依賴路徑。
● 通過 dependencies 來存儲文件中的依賴,最終調用 transformFromAst 將 AST 轉換為 ES5 代碼。
● 最后函數返回了一個對象,對象中包含了當前文件路徑、當前文件依賴和當前文件轉換后的代碼

接下來我們需要構建一個函數來處理整個依賴圖譜,這個函數的功能有以下幾點
● 調用 readCode 函數,傳入入口文件
● 分析入口文件的依賴
● 識別 JS 和 CSS 文件

function getDependencies(entry) {// 讀取入口文件const entryObject = readCode(entry)const dependencies = [entryObject]// 遍歷所有文件依賴關系for (const asset of dependencies) {// 獲得文件目錄const dirname = path.dirname(asset.filePath)// 遍歷當前文件依賴關系asset.dependencies.forEach(relativePath => {// 獲得絕對路徑const absolutePath = path.join(dirname, relativePath)// CSS 文件邏輯就是將代碼插入到 `style` 標簽中if (/\.css$/.test(absolutePath)) {const content = fs.readFileSync(absolutePath, 'utf-8')const code = `const style = document.createElement('style')style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, '')}document.head.appendChild(style)`dependencies.push({filePath: absolutePath,relativePath,dependencies: [],code})} else {// JS 代碼需要繼續查找是否有依賴關系const child = readCode(absolutePath)child.relativePath = relativePathdependencies.push(child)}})}return dependencies
}

● 首先我們讀取入口文件,然后創建一個數組,該數組的目的是存儲代碼中涉及到的所有文件
● 接下來我們遍歷這個數組,一開始這個數組中只有入口文件,在遍歷的過程中,如果入口文件有依賴其他的文件,那么就會被 push 到這個數組中
● 在遍歷的過程中,我們先獲得該文件對應的目錄,然后遍歷當前文件的依賴關系
● 在遍歷當前文件依賴關系的過程中,首先生成依賴文件的絕對路徑,然后判斷當前文件是 CSS 文件還是 JS 文件
● 如果是 CSS 文件的話,我們就不能用 Babel 去編譯了,只需要讀取 CSS 文件中的代碼,然后創建一個 <style> 標簽,將代碼插入進標簽并且放入 head 中即可
● 如果是 JS 文件的話,我們還需要分析 JS 文件是否還有別的依賴關系
● 最后將讀取文件后的對象 push 進數組中,此時已經獲取一個包含所有依賴項的對象數組。

● 現在我們已經獲取到了所有的依賴文件,接下來就是實現打包的功能了

第三步:打包依賴,模擬 CommonJS 運行環境
現在我們已經收集了完整的依賴圖,下一步是將這些模塊打包成一個可以在瀏覽器中運行的單文件。

function bundle(dependencies, entry) {let modules = ''// 構建函數參數,生成的結構為// { './entry.js': function(module, exports, require) { 代碼 } }dependencies.forEach(dep => {const filePath = dep.relativePath || entrymodules += `'${filePath}': (function (module, exports, require) { ${dep.code} }),`})// 構建 require 函數,目的是為了獲取模塊暴露出來的內容const result = `(function(modules) {function require(id) {const module = { exports : {} }modules[id](module, module.exports, require)return module.exports}require('${entry}')})({${modules}})`// 當生成的內容寫入到文件中fs.writeFileSync('./bundle.js', result)
}

這段代碼需要結合著 Babel 轉換后的代碼來看,這樣大家就能理解為什么需要這樣寫了,
代碼結構與 Babel 編譯后的 CommonJS 代碼相對應,目的是在瀏覽器端模擬模塊化運行環境。

示例:Babel 轉換后的代碼如下

// entry.js
var _a = require('./a.js')
var _a2 = _interopRequireDefault(_a)
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_a2.default)
// a.js
Object.defineProperty(exports, '__esModule', {value: true
})
var a = 1
exports.default = a

Babel 將我們 ES6的模塊化代碼轉換為了 CommonJS的代碼,但是瀏覽器是不支持 CommonJS 的,所以如果這段代碼需要在瀏覽器環境下運行的話,我們需要手動實現 CommonJS 相關的類似機制,這就是 bundle 函數做的大部分事情。bundle 函數正是為此而設計。

接下來我們再來逐行解析 bundle 函數

● 首先遍歷所有依賴文件,構建出一個函數參數對象
● 對象的屬性就是當前文件的相對路徑,屬性值是一個函數,函數體是當前文件下的代碼,函數接受三個參數 module、exports、 require
○ module 參數對應 CommonJS 中的 module
○ exports 參數對應 CommonJS 中的 module.export
○ require 參數對應我們自己創建的 require 函數
● 接下來就是構造一個使用參數的函數了,函數做的事情很簡單,就是內部創建一個 require函數,然后調用 require(entry),也就是 require(‘./entry.js’),這樣就會從函數參數中找到 ./entry.js 對應的函數并執行,最后將導出的內容通過 module.export 的方式讓外部獲取到
● 最后再將打包出來的內容寫入到單獨的文件中
如果你對于上面的實現還有疑惑的話,可以閱讀下打包后的部分簡化代碼

;(function(modules) {function require(id) {// 構造一個 CommonJS 導出代碼const module = { exports: {} }// 去參數中獲取文件對應的函數并執行modules[id](module, module.exports, require)return module.exports}require('./entry.js')
})({'./entry.js': function(module, exports, require) {// 這里繼續通過構造的 require 去找到 a.js 文件對應的函數var _a = require('./a.js')console.log(_a2.default)},'./a.js': function(module, exports, require) {var a = 1// 將 require 函數中的變量 module 變成了這樣的結構// module.exports = 1// 這樣就能在外部取到導出的內容了exports.default = a}// 省略
})

盡管這個“丐版 Webpack”僅用了不到百行代碼實現,但它涵蓋了現代打包工具的核心思想:
● 找出入口文件所有的依賴關系。
● 將不同類型的資源統一處理
● 然后通過構建 CommonJS 代碼來獲取 exports 導出的內容。

這為我們理解打包工具的工作原理提供了很好的入門視角。

如果你覺得這篇文章對你有幫助,請點贊 👍、收藏 👏 并關注我!👀

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/80011.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/80011.shtml
英文地址,請注明出處:http://en.pswp.cn/web/80011.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

概率相關問題

問題匯總 1. 貝葉斯定理&#xff08;貝葉斯公式和全概率公式&#xff09;2. 概率題2.1 隨機發生器的概率為1/2 1. 貝葉斯定理&#xff08;貝葉斯公式和全概率公式&#xff09; 定義&#xff1a;在信息和條件有限的情況下&#xff0c;基于過去的數據&#xff0c;通過動態調整的…

【系統架構師】2025論文《WEB系統性能優化技術》

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一個正在變禿、變強的文藝傾年。 &#x1f514;本文分享【系統架構師】2025論文《系統可靠性設計》&#xff0c;期待與你一同探索、學習、進步&#xff0c;一起卷起來叭&#xff01; 目錄 項目介紹背景介紹系統模塊技術棧性能…

ADS1220高精度ADC(TI)——應用 源碼

文章目錄 德州儀器ADS1220概述資料引腳&封裝布線寄存器配置寄存器0&#xff08;00h&#xff09;配置寄存器1&#xff08;01h&#xff09;配置寄存器2&#xff08;02h&#xff09;配置寄存器3&#xff08;03h&#xff09; 連續轉換流程驅動源碼ads1220.cads1220.h 德州儀器A…

Uniapp 安卓實現訊飛語音聽寫(復制即用)

在移動應用開發中&#xff0c;語音交互功能能夠極大提升用戶體驗&#xff0c;讓操作更加便捷自然。訊飛語音聽寫技術憑借其高準確率和穩定性&#xff0c;成為眾多開發者的選擇。本文將詳細介紹如何在 Uniapp 項目中&#xff0c;實現安卓端的訊飛語音聽寫功能&#xff0c;幫助你…

【golang】DNS 資源記錄(RR)接口

Go 中 miekg/dns 包對 DNS 資源記錄&#xff08;RR&#xff09;接口 的定義&#xff1a; type RR interface {Header() *RR_HeaderString() stringcopy() RRlen(off int, compression map[string]struct{}) intpack(...)unpack(...)parse(...)isDuplicate(r2 RR) bool }這個接…

16.2 VDMA視頻轉發實驗之模擬源

文章目錄 1 實驗任務2 系統框圖3 硬件設計3.1 IP核配置3.2 注意事項3.3 自定義IP核源碼 4 軟件設計4.1 注意事項4.2 工程源碼4.2.1 main.c文件 1 實驗任務 基于14.1&#xff0c;相較于16.1&#xff0c;使用自定義IP核vid_gen_motion替換Xilinx TPG IP核。 2 系統框圖 基于14…

深度學習之用CelebA_Spoof數據集搭建一個活體檢測-訓練好的模型用MNN來推理

一、模型轉換準備 首先確保已完成PyTorch到ONNX的轉換&#xff1a;深度學習之用CelebA_Spoof數據集搭建活體檢測系統&#xff1a;模型驗證與測試。這里有將PyTorch到ONNX格式的模型轉換。 二、ONNX轉MNN 使用MNN轉換工具進行格式轉換&#xff1a;具體的編譯過程可以參考MNN的…

JVM學習專題(一)類加載器與雙親委派

目錄 1、JVM加載運行全過程梳理 2、JVM Hotspot底層 3、war包、jar包如何加載 4、類加載器 我們來查看一下getLauncher&#xff1a; 1.我們先查看getExtClassLoader() 2、再來看看getAppClassLoader(extcl) 5、雙親委派機制 1.職責明確&#xff0c;路徑隔離?&#xff…

部署安裝gitlab-ce-17.9.7-ce.0.el8.x86_64.rpm

目錄 ?編輯 實驗環境 所需軟件 實驗開始 安裝部署gitlab171.配置清華源倉庫&#xff08;版本高的系統無需做&#xff09;vim /etc/yum.repos.d/gitlab-ce.repo 2.提前下載包dnf localinstall gitlab-ce-17.9.7-ce.0.el8.x86_64.rpm --rocklinux 3.修改配…

使用LoRA微調Qwen2.5-VL-7B-Instruct完成電氣主接線圖識別

使用LoRA微調Qwen2.5-VL-7B-Instruct完成電氣主接線圖識別 動機 任務適配需求 Qwen2.5-VL在視覺理解方面表現優異&#xff0c;但電氣主接線圖識別需要特定領域的結構化輸出能力&#xff08;如設備參數提取、拓撲關系解析&#xff09;。微調可增強模型對專業符號&#xff08;如…

系統集成項目管理工程師學習筆記

第九章 項目管理概論 1、項目基本要素 項目基礎 項目是為創造獨特的產品、服務或成果而進行的臨時性工作。 項目具有臨時性、獨特性、漸進明細的特點。項目的“臨時性”是指項目只有明確的起點和終點。“臨時性”并一定意味著項目的持續時間短。 項目可宣告結束的情況&…

Secs/Gem第七講(基于secs4net項目的ChatGpt介紹)

好的&#xff0c;那我們現在進入&#xff1a; 第七講&#xff1a;掉電重連后&#xff0c;為什么設備不再上報事件&#xff1f;——持久化與自動恢復的系統設計 關鍵詞&#xff1a;掉電恢復、狀態重建、初始化流程、SecsMessage 緩存機制、自動重連、事件再注冊 本講目標 你將理…

室內定位:熱門研究方向與未解難題深度解析

I. 引言:對普適性室內定位的持續探索 A. 室內定位在現代應用中的重要性 室內定位系統(IPS)正迅速成為眾多應用領域的基石技術,其重要性源于現代社會人們約70%至90%的時間在室內度過的事實 1。這些應用橫跨多個行業,包括應急響應 1、智能建筑與智慧城市 6、醫療健康(如病…

Android學習總結之Glide自定義三級緩存(實戰篇)

一、為什么需要三級緩存 內存緩存&#xff08;Memory Cache&#xff09; 內存緩存旨在快速顯示剛瀏覽過的圖片&#xff0c;例如在滑動列表時來回切換的圖片。在 Glide 中&#xff0c;內存緩存使用 LruCache 算法&#xff08;最近最少使用&#xff09;&#xff0c;能自動清理長…

Linux的文件查找與壓縮

查找文件 find命令 # 命令&#xff1a;find 路徑范圍 選項1 選項1的值 \[選項2 選項2 的值…]# 作用&#xff1a;用于查找文檔&#xff08;其選項有55 個之多&#xff09;# 選項&#xff1a;# -name&#xff1a;按照文檔名稱進行搜索&#xff08;支持模糊搜索&#xff0c;\* &…

python處理異常,JSON

異常處理 #異常處理 # 在連接MySQL數據庫的過程中&#xff0c;如果不能有效地處理異常&#xff0c;則異常信息過于復雜&#xff0c;對用戶不友好&#xff0c;暴露過多的敏感信息 # 所以&#xff0c;在真實的生產環境中&#xff0c; 程序必須有效地處理和控制異常&#xff0c;按…

線程的兩種實現方式

線程的兩種實現方式——內核支持線程&#xff08;kernal Supported Thread, KST&#xff09;&#xff0c; 用戶級線程&#xff08;User Level Thread, ULT&#xff09; 1. 內核支持線程 顧名思義&#xff0c;內核支持線程即為在內核支持下的那些線程&#xff0c;它們的創建&am…

vue3基礎學習(上) [簡單標簽] (vscode)

目錄 1. Vue簡介 2. 創建Vue應用 2.1 下載JS文件 2.2 引用JS文件 2.3 調用Vue方法?編輯 2.4 運行一下試試: 2.5 代碼如下 3.模塊化開發模式 3.1 Live Server插件 3.2 運行 4. 常用的標簽 4.1 reactive 4.1.1 運行結果 4.1.2 代碼: 4.2 ref 4.2.1 運行結果 4.2.2…

自定義分區器-基礎

什么是分區 在 Spark 里&#xff0c;彈性分布式數據集&#xff08;RDD&#xff09;是核心的數據抽象&#xff0c;它是不可變的、可分區的、里面的元素并行計算的集合。 在 Spark 中&#xff0c;分區是指將數據集按照一定的規則劃分成多個較小的子集&#xff0c;每個子集可以獨立…

深入解析HTTP協議演進:從1.0到3.0的全面對比

HTTP協議作為互聯網的基礎協議&#xff0c;經歷了多個版本的迭代演進。本文將詳細解析HTTP 1.0、HTTP 1.1、HTTP/2和HTTP/3的核心特性與區別&#xff0c;幫助開發者深入理解網絡協議的發展脈絡。 一、HTTP 1.0&#xff1a;互聯網的奠基者 核心特點&#xff1a; 短連接模式&am…