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,我們能夠發現:

  1. 不管有多少個模塊,頭部那一塊都是一樣的,所以可以寫成一個模板,也就是templateSingle.js。
  2. 需要分析出各個模塊間的依賴關係。也就是說,需要知道example依賴於a、b和c。
  3. c模塊位於node_modules文件夾當中,但是我們調用的時候卻可以直接require("c"),這裡肯定是存在某種自動查找的功能。
  4. 在生成的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,有以下兩個缺點:

  1. 如果require是寫在注釋中,也會匹配到。
  2. 如果後期要支持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中。

目前實現的查找邏輯是:

  1. 如果給出的是絕對路徑/相對路徑,只查找一次。找到?返回絕對路徑。找不到?返回false。
  2. 如果給出的是模塊的名字,先在入口js(example.js)文件所在目錄下尋找同名JS文件(可省略擴展名)。找到?返回絕對路徑。找不到?走第3步。
  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了。

遺留問題

  1. 尚未支持require("a" + "b")這種情況。
  2. 如何實現自動 watch 的功能?
  3. 其loader或者插件機制又是怎樣的?
  4. ……

參考資料

  1. webpack 源碼解析
  2. webpack源碼分析(一)— Tapable插件架構
  3. DDFE/DDFE-blog#12
  4. hao.jser.com/archive/13
  5. 細說 webpack 之流程篇

========EOF===========


推薦閱讀:

React學習資源匯總

TAG:webpack | 前端开发 |