最近原創文章回顧:javascript
Webpack 是前端很火的打包工具,它本質上是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 Webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph
),其中包含應用程序須要的每一個模塊,而後將全部模塊打包成一個或多個 bundle
。前端
其實就是:Webpack 是一個 JS 代碼打包器。java
至於圖片、CSS、Less、TS等其餘文件,就須要 Webpack 配合 loader 或者 plugin 功能來實現~node
首先先簡單瞭解下 Webpack 構建過程:webpack
看完上面的構建流程的簡單介紹,相信你已經簡單瞭解了這個過程,那麼接下來開始詳細介紹 Webpack 構建原理,包括從啓動構建到輸出結果一系列過程:git
(1)初始化參數github
解析 Webpack 配置參數,合併 Shell 傳入和 webpack.config.js
文件配置的參數,造成最後的配置結果。web
(2)開始編譯面試
上一步獲得的參數初始化 compiler
對象,註冊全部配置的插件,插件監聽 Webpack 構建生命週期的事件節點,作出相應的反應,執行對象的 run
方法開始執行編譯。json
(3)肯定入口
從配置文件( webpack.config.js
)中指定的 entry
入口,開始解析文件構建 AST 語法樹,找出依賴,遞歸下去。
(4)編譯模塊
遞歸中根據文件類型和 loader 配置,調用全部配置的 loader 對文件進行轉換,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理。
(5)完成模塊編譯並輸出
遞歸完後,獲得每一個文件結果,包含每一個模塊以及他們之間的依賴關係,根據 entry
配置生成代碼塊 chunk
。
(6)輸出完成
輸出全部的 chunk
到文件系統。
注意:在構建生命週期中有一系列插件在作合適的時機作合適事情,好比 UglifyPlugin
會在 loader 轉換遞歸完對結果使用 UglifyJs
壓縮覆蓋以前的結果。
到這裏,相信你們對 Webpack 構建流程已經有所瞭解,可是這還不夠,咱們再來試着手寫 Webpack 構建工具,來將上面文字介紹的內容,應用於實際代碼,那麼開始吧~
在手寫構建工具前,咱們先初始化一個項目:
$ yarn init -y
並安裝下面四個依賴包:
@babel/parser
: 用於分析經過 fs.readFileSync
讀取的文件內容,並返回 AST (抽象語法樹) ; @babel/traverse
: 用於遍歷 AST, 獲取必要的數據;@babel/core
: babel 核心模塊,提供 transformFromAst
方法,用於將 AST 轉化爲瀏覽器可運行的代碼;@babel/preset-env
: 將轉換後代碼轉化成 ES5 代碼;$ yarn add @babel/parser @babel/traverse @babel/core @babel/preset-env
初始化項目目錄及文件:
代碼存放在倉庫:https://github.com/pingan8787/Leo-JavaScript/tree/master/Cute-Webpack/Write-Webpack
因爲本部分核心內容是實現 Webpack 構建工具,因此會從《2. Webpack 構建原理》的「(3)肯定入口」步驟開始下面介紹。
大體代碼實現流程以下:
從圖中能夠看出,手寫 Webpack 的核心是實現如下三個方法:
createAssets
: 收集和處理文件的代碼;createGraph
:根據入口文件,返回全部文件依賴圖;bundle
: 根據依賴圖整個代碼並輸出;首先在 ./src/index
文件中寫點簡單代碼:
// src/index.js import info from "./info.js"; console.log(info);
實現 createAssets
方法中的 文件讀取 和 AST轉換 操做:
// leo_webpack.js const fs = require("fs"); const path = require("path"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; // 因爲 traverse 採用的 ES Module 導出,咱們經過 requier 引入的話就加個 .default const babel = require("@babel/core"); let moduleId = 0; const createAssets = filename => { const content = fs.readFileSync(filename, "utf-8"); // 根據文件名,同步讀取文件流 // 將讀取文件流 buffer 轉換爲 AST const ast = parser.parse(content, { sourceType: "module" // 指定源碼類型 }) console.log(ast); } createAssets('./src/index.js');
上面代碼:
經過 fs.readFileSync()
方法,以同步方式讀取指定路徑下的文件流,並經過 parser
依賴包提供的 parse()
方法,將讀取到的文件流 buffer 轉換爲瀏覽器能夠認識的代碼(AST),AST 輸出以下:
另外須要注意,這裏咱們聲明瞭一個 moduleId
變量,來區分當前操做的模塊。
在這裏,不只將讀取到的文件流 buffer 轉換爲 AST 的同時,也將 ES6 代碼轉換爲 ES5 代碼了。
接下來聲明 dependencies
變量來保存收集到的文件依賴路徑,經過 traverse()
方法遍歷 ast
,獲取每一個節點依賴路徑,並 push
進 dependencies
數組中。
// leo_webpack.js function createAssets(filename){ // ... const dependencies = []; // 用於收集文件依賴的路徑 // 經過 traverse 提供的操做 AST 的方法,獲取每一個節點的依賴路徑 traverse(ast, { ImportDeclaration: ({node}) => { dependencies.push(node.source.value); } }); }
在收集依賴的同時,咱們能夠將 AST 代碼轉換爲瀏覽器可運行代碼,這就須要使用到 babel
,這個萬能的小傢伙,爲咱們提供了很是好用的 transformFromAstSync()
方法,同步的將 AST 轉換爲瀏覽器可運行代碼:
// leo_webpack.js function createAssets(filename){ // ... const { code } = babel.transformFromAstSync(ast,null, { presets: ["@babel/preset-env"] }); let id = moduleId++; // 設置當前處理的模塊ID return { id, filename, code, dependencies } }
到這一步,咱們在執行 node leo_webpack.js
,輸出以下內容,包含了入口文件的路徑 filename
、瀏覽器可執行代碼 code
和文件依賴的路徑 dependencies
數組:
$ node leo_webpack.js { filename: './src/index.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"]);', dependencies: [ './info.js' ] }
// leo_webpack.js const fs = require("fs"); const path = require("path"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; // 因爲 traverse 採用的 ES Module 導出,咱們經過 requier 引入的話就加個 .default const babel = require("@babel/core"); let moduleId = 0; function createAssets(filename){ const content = fs.readFileSync(filename, "utf-8"); // 根據文件名,同步讀取文件流 // 將讀取文件流 buffer 轉換爲 AST const ast = parser.parse(content, { sourceType: "module" // 指定源碼類型 }) const dependencies = []; // 用於收集文件依賴的路徑 // 經過 traverse 提供的操做 AST 的方法,獲取每一個節點的依賴路徑 traverse(ast, { ImportDeclaration: ({node}) => { dependencies.push(node.source.value); } }); // 經過 AST 將 ES6 代碼轉換成 ES5 代碼 const { code } = babel.transformFromAstSync(ast,null, { presets: ["@babel/preset-env"] }); let id = moduleId++; // 設置當前處理的模塊ID return { id, filename, code, dependencies } }
在 createGraph()
函數中,咱們將遞歸全部依賴模塊,循環分析每一個依賴模塊依賴,生成一份依賴圖譜。
爲了方便測試,咱們補充下 consts.js
和 info.js
文件的代碼,增長一些依賴關係:
// src/consts.js export const company = "平安"; // src/info.js import { company } from "./consts.js"; export default `你好,${company}`;
接下來開始實現 createGraph()
函數,它須要接收一個入口文件的路徑( entry
)做爲參數:
// leo_webpack.js function createGraph(entry) { const mainAsset = createAssets(entry); // 獲取入口文件下的內容 const queue = [mainAsset]; // 入口文件的結果做爲第一項 for(const asset of queue){ const dirname = path.dirname(asset.filename); asset.mapping = {}; asset.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath); // 轉換文件路徑爲絕對路徑 const child = createAssets(absolutePath); asset.mapping[relativePath] = child.id; // 保存模塊ID queue.push(child); // 遞歸去遍歷全部子節點的文件 }) } return queue; }
上面代碼:
首先經過 createAssets()
函數讀取入口文件的內容,並做爲依賴關係的隊列(依賴圖譜) queue
數組的第一項,接着遍歷依賴圖譜 queue
每一項,再遍歷將每一項中的依賴 dependencies
依賴數組,將依賴中的每一項拼接成依賴的絕對路徑(absolutePath
),做爲 createAssets()
函數調用的參數,遞歸去遍歷全部子節點的文件,並將結果都保存在依賴圖譜 queue
中。
注意, mapping
對象是用來保存文件的相對路徑和模塊 ID 的對應關係,在 mapping
對象中,咱們使用依賴文件的相對路徑做爲 key
,來存儲保存模塊 ID。
而後咱們修改啓動函數:
// leo_webpack.js - const result = createAssets('./src/index.js'); + const graph = createGraph("./src/index.js"); + console.log(graph);
這時咱們將獲得一份包含全部文件依賴關係的依賴圖譜:
這個依賴圖譜,包含了全部文件模塊的依賴,以及模塊的代碼內容。下一步只要實現 bundle()
函數,將結果輸出便可。
從前面介紹,咱們知道,函數 createGraph()
會返回一個包含每一個依賴相關信息(id / filename / code / dependencies)的依賴圖譜 queue
,這一步就將使用到它了。
在 bundle()
函數中,接收一個依賴圖譜 graph
做爲參數,最後輸出編譯後的結果。
咱們首先聲明一個變量 modules
,值爲字符串類型,而後對參數 graph
進行遍歷,將每一項中的 id
屬性做爲 key
,值爲一個數組,包括一個用來執行代碼 code
的方法和序列化後的 mapping
,最後拼接到 modules
中。
// leo_webpack.js function bundle(graph) { let modules = ""; graph.forEach(item => { modules += ` ${item.id}: [ function (require, module, exports){ ${item.code} }, ${JSON.stringify(item.mapping)} ], ` }) }
上面代碼:
在 modules
中每一項的值中,下標爲 0 的元素是個函數,接收三個參數 require
/ module
/ exports
,爲何會須要這三個參數呢?
緣由是:構建工具沒法判斷是否支持require
/ module
/ exports
這三種模塊方法,因此須要本身實現(後面步驟會實現),而後方法內的 code
才能正常執行。
接着,咱們來實現 bundle()
函數返回值的處理:
// leo_webpack.js function bundle(graph) { //... return ` (function(modules){ function require(id){ const [fn, mapping] = modules[id]; function localRequire(relativePath){ return require(mapping[relativePath]); } const module = { exports: {} } fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) ` }
上面代碼:
最終 bundle
函數返回值是一個字符串,包含一個自執行函數(IIFE),其中函數參數是一個對象, key
爲 modules
, value
爲前面拼接好的 modules
字符串,即 {modules: modules字符串}
。
在這個自執行函數中,實現了 require
方法,接收一個 id
做爲參數,在方法內部,分別實現了 localRequire
/ module
/ modules.exports
三個方法,並做爲參數,傳到 modules[id]
中的 fn
方法中,最後初始化 require()
函數(require(0);
)。
// leo_webpack.js function bundle(graph) { let modules = ""; graph.forEach(item => { modules += ` ${item.id}: [ function (require, module, exports){ ${item.code} }, ${JSON.stringify(item.mapping)} ], ` }) return ` (function(modules){ function require(id){ const [fn, mapping] = modules[id]; function localRequire(relativePath){ return require(mapping[relativePath]); } const module = { exports: {} } fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) ` }
當咱們上面方法都實現之後,就開始試試吧:
// leo_webpack.js const graph = createGraph("./src/index.js"); const result = bundle(graph); console.log(result)
這時候能夠看到終端輸出相似這樣的代碼,是字符串,這裏爲了方便查看而複製到控制檯了:
這就是打包後的代碼咯~
那麼如何讓這些代碼執行呢?用 eval()
方法咯:
// leo_webpack.js const graph = createGraph("./src/index.js"); const result = bundle(graph); eval(result);
這時候就能看到控制檯輸出 你好,平安
。那麼咱們就完成一個簡單的 Webpack 構建工具啦~
能看到這裏的朋友,爲你點個贊~
本文主要介紹了 Webpack 的構建流程和構建原理,並在此基礎上,和你們分享了手寫 Webpack 的實現過程,但願你們對 Webpack 構建流程能有更深瞭解,畢竟面試賊喜歡問啦~
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推薦 | https://github.com/pingan8787... |
ES小冊 | js.pingan8787.com |
語雀知識庫 | Cute-FrontEnd |