webpack源碼學習系列之一:如何實現一個簡單的webpack
更多關於 webpack 源碼的文章,請移步我的個人博客。
前言
在上一篇 #98 中,我們通過實現requireJS,對模塊化有了一些認識。今天我們更進一步,看看如何實現一個簡單的webpack,實現的源碼參考這裡。
目標
現在的webpack是一個龐然大物,我們不可能實現其所有功能。
那麼,應該將目光聚焦在哪兒呢?
從webpack的第一個commit可以看出,其當初最主要的目的是在瀏覽器端復用符合CommonJS規範的代碼模塊。這個目標不是很難,我們努力一把還是可以實現的。注意:在此我們不考慮插件、loaders、多文件打包等等複雜的問題,僅僅考慮最基本的問題:如何將多個符合CommonJS規範的模塊打包成一個JS文件,以供瀏覽器執行。
bundle.js
顯然,瀏覽器沒法直接執行CommonJS規範的模塊,怎麼辦呢?
答案:將其轉換成一個自執行表達式
注意:此處涉及到webpack構建出來的bundle.js的內部結構問題,如果不了解bundle.js具體是如何執行的,請務必搞清楚再往下閱讀。可以參考 #64 或者這裡例子
我們實際要處理的例子是這個:example依賴於a、b和c,而且c位於node_modules文件夾中,我們要將所有模塊構建成一個JS文件,就是這裡的output.js
思路
仔細觀察output.js,我們能夠發現:
- 不管有多少個模塊,頭部那一塊都是一樣的,所以可以寫成一個模板,也就是templateSingle.js。
- 需要分析出各個模塊間的依賴關係。也就是說,需要知道example依賴於a、b和c。
- c模塊位於node_modules文件夾當中,但是我們調用的時候卻可以直接require("c"),這裡肯定是存在某種自動查找的功能。
- 在生成的output.js中,每個模塊的唯一標識是模塊的ID,所以在拼接output.js的時候,需要將每個模塊的名字替換成模塊的ID。也就是說,
// 轉換前let a = require("a");let b = require("b");let c = require("c");// 轉換後let a = require(/* a */1);let b = require(/* b */2);let c = require(/* c */3);
ok,下面我們來逐一看看這些問題。
分析模塊依賴關係
CommonJS不同於AMD,是不會在一開始聲明所有依賴的。CommonJS最顯著的特徵就是用到的時候再require,所以我們得在整個文件的範圍內查找到底有多少個require。
怎麼辦呢?最先蹦入腦海的思路是正則。然而,用正則來匹配require,有以下兩個缺點:- 如果require是寫在注釋中,也會匹配到。
- 如果後期要支持require的參數是表達式的情況,如require("a"+"b"),正則很難處理。
因此,正則行不通。
一種正確的思路是:使用JS代碼解析工具(如esprima或者acorn),將JS代碼轉換成抽象語法樹(AST),再對AST進行遍歷。這部分的核心代碼是parse.js。在處理好了require的匹配之後,還有一個問題需要解決。那就是匹配到require之後需要幹什麼呢?
舉個例子:// example.jslet a = require("a");let b = require("b");let c = require("c");
這裡有三個require,按照CommonJS的規範,在檢測到第一個require的時候,根據require即執行的原則,程序應該立馬去讀取解析模塊a。如果模塊a中又require了其他模塊,那麼繼續解析。也就是說,總體上遵循深度優先遍歷演算法。這部分的控制邏輯寫在buildDeps.js中。
找到模塊
在完成依賴分析的同時,我們需要解決另外一個問題,那就是如何找到模塊?也就是模塊的定址問題。
舉個例子:
// example.jslet a = require("a");let b = require("b");let c = require("c");
在模塊example.js中,調用模塊a、b、c的方式都是一樣的。
但是,實際上他們所在的絕對路徑層級並不一致:a和b跟example同級,而c位於與example同級的node_modules中。所以,程序需要有一個查找模塊的演算法,這部分的邏輯在resolve.js中。目前實現的查找邏輯是:
- 如果給出的是絕對路徑/相對路徑,只查找一次。找到?返回絕對路徑。找不到?返回false。
- 如果給出的是模塊的名字,先在入口js(example.js)文件所在目錄下尋找同名JS文件(可省略擴展名)。找到?返回絕對路徑。找不到?走第3步。
- 在入口js(example.js)同級的node_modules文件夾(如果存在的話)查找。找到?返回絕對路徑。找不到?返回false。
當然,此處實現的演算法還比較簡陋,之後有時間可以再考慮實現逐層往上的查找,就像nodejs默認的模塊查找演算法那樣。
拼接output.js
這是最後一步了。
在解決了模塊依賴和模塊查找的問題之後,我們將會得到一個依賴關係對象depTree,此對象完整地描述了以下信息:都有哪些模塊,各個模塊的內容是什麼,他們之間的依賴關係又是如何等等。具體的結構如下:{ "modules": { "/Users/youngwind/www/fake-webpack/examples/simple/example.js": { "id": 0, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/example.js", "name": "/Users/youngwind/www/fake-webpack/examples/simple/example.js", "requires": [ { "name": "a", "nameRange": [ 16, 19 ], "id": 1 }, { "name": "b", "nameRange": [ 38, 41 ], "id": 2 }, { "name": "c", "nameRange": [ 60, 63 ], "id": 3 } ], "source": "let a = require("a");
let b = require("b");
let c = require("c");
a();
b();
c();
" }, "/Users/youngwind/www/fake-webpack/examples/simple/a.js": { "id": 1, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/a.js", "name": "a", "requires": [], "source": "// module a
module.exports = function () {
console.log("a")
};" }, "/Users/youngwind/www/fake-webpack/examples/simple/b.js": { "id": 2, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/b.js", "name": "b", "requires": [], "source": "// module b
module.exports = function () {
console.log("b")
};" }, "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js": { "id": 3, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js", "name": "c", "requires": [], "source": "module.exports = function () {
console.log("c")
}" } }, "mapModuleNameToId": { "/Users/youngwind/www/fake-webpack/examples/simple/example.js": 0, "a": 1, "b": 2, "c": 3 }}
根據這個depTree對象,我們便能完成這最後的一步:**output.js文件的拼接。**其控制邏輯無非是一層循環,寫在writeChunk.js中。
但是這裡有一個需要注意的地方,那就是本文思路章節提到的第4點:要把模塊名轉換成模塊ID,這是writeSource.js所要完成的功能。至此,我們就實現了一個非常簡單的webpack了。
遺留問題
- 尚未支持require("a" + "b")這種情況。
- 如何實現自動 watch 的功能?
- 其loader或者插件機制又是怎樣的?
- ……
參考資料
- webpack 源碼解析
- webpack源碼分析(一)— Tapable插件架構
- DDFE/DDFE-blog#12
- http://hao.jser.com/archive/13881/
- 細說 webpack 之流程篇
========EOF===========
推薦閱讀: