圖解 webpack(模塊化 - CommonJS)

在前面一篇文章中 《模塊化系列》完全理清 AMD,CommonJS,CMD,UMD,ES6,咱們能夠學到了各類模塊化的機制。那麼接下里咱們就來分析一下 webpack 的模塊化機制。(主要講 JS 部分)前端

提到 webpack,能夠說是與咱們的開發工程很是密切的工具,不論是平常開發、進行面試仍是對於自個人提升,都離不開它,由於它給咱們的開發帶了極大的便利以及學習的價值。可是因爲webpack是一個很是龐大的工程體系,使得咱們望之卻步。本文想以這種圖解的形式可以將它慢慢地剝開一層一層複雜的面紗,最終露出它的真面目。如下是我列出的關於 webpack 相關的體系。webpack

webpack-2

webpack-2git

本文講得是 打包 - CommonJS 模塊,主要分爲兩個部分github

  • webpack 的做用
  • webpack 的模塊化機制與實現

webpack 的做用

在咱們前端多樣化的今天,不少工具爲了知足咱們日益增加的開發需求,都變得很是的龐大,例如 webpack 。在咱們的印象中,它彷佛集成了全部關於開發的功能,模塊打包,代碼降級,文件優化,代碼校驗等等。正是由於面對如此龐大的一個工具,因此才讓咱們望而卻步,固然了還有一點就是,webpack 的頻繁升級,周邊的生態插件配套版本混亂,也加重咱們對它的恐懼。web

那麼咱們是否是應該思考一下,webpack 的出現究竟給咱們帶來了什麼?咱們爲啥須要用它?而上面全部的一些代碼降級(babel轉化)、編譯SCSS 、代碼規範檢測都是得益於它的插件系統和loader機制,並非完徹底全屬於它。面試

因此在我看來,它的功能核心是「打包」,而打包則是可以讓模塊化的規範得以在瀏覽器直接執行。所以咱們來看看打包後所帶來的功能:數組

  • 模塊隔離
  • 模塊依賴加載

模塊隔離

若是咱們不用打包的方式,咱們全部的模塊都是直接暴露在全局,也就是掛載在 window/global 這個對象。也許代碼量少的時候還能夠接受,不會有那麼多的問題。特別是在代碼增多,多人協做的狀況下,給全局空間帶來的影響是不可預估的,若是你的每一次開發都得去一遍一遍查找是否有他們使用當前的變量名。瀏覽器

舉個例子(僅僅爲例子說明,實際工程會比如下複雜許多),一開始咱們的 user1 寫了一下幾個模塊,跑起來很是的順暢。babel

image-20200626231748187

image-20200626231748187閉包

├── bar.js    function bar(){}
├── baz.js    function baz(){}
└── foo.js function foo(){}

可是呢,隨着業務迭代,工程的複雜性增長,來了一個 user2,這個時候 user2,須要開發一個 foo 業務,裏面也有一個 baz 模塊,代碼也很快寫好了,變成了下面這個樣子。

├── bar.js    function bar(){}
├── baz.js    function baz(){}
├── foo
│   └── baz.js function baz(){}
└── foo.js function foo(){}

可是呢這個時候,老闆來找 user2 了,爲何增長了新業務後,原來的業務出錯了呢?這個時候發現原來是 user2 寫的新模塊覆蓋了 user1 的模塊,從而致使了這場事故。

image-20200626220806881

image-20200626220806881

所以,當咱們開發的時候將全部的模塊都暴露在全局的時候,想要避免錯誤,一切都得很是的當心翼翼,咱們很容易在不知情的偷偷覆蓋咱們之前定義的函數,從而釀成錯誤。

所以 webpack 帶來的第一個核心做用就是隔離,將每一個模塊經過閉包的形式包裹成一個個新的模塊,將其放於局部做用域,全部的函數聲明都不會直接暴露在全局。

image-20200626220851909

image-20200626220851909

原來咱們調用的 是 foo 函數,可是 webpack 會幫咱們生成獨一無二的模塊ID,徹底不須要擔憂模塊的衝突,如今能夠愉快地書寫代碼啦。

baz.js
module.exports = function baz (){}

foo/baz.js
module.exports = function baz (){}

main.js
var baz = require('./baz.js');
var fooBaz = require('./foo/baz.js');

baz();
fooBaz();

可能你說會以前的方式也能夠經過改變函數命名的方式,可是原來的做用範圍是整個工程,你得保證,當前命名在整個工程中不衝突,如今,你只須要保證的是單個文件中命名不衝突。(對於頂層依賴也是很是容易發現衝突)

image-20200627140818771

image-20200627140818771

模塊依賴加載

還有一種重要的功能就是模塊依賴加載。這種方式帶來的好處是什麼?咱們一樣先來看例子,看原來的方式會產生什麼問題?

User1 如今寫了3個模塊,其中 baz 是依賴於 bar 的。

image-20200627000240836

image-20200627000240836

寫完後 user1 進行了上線,利用了順序來指出了依賴關係。

<script src="./bar.js"></script>
<script src="./baz.js"></script>
<script src="./foo.js"></script>

但是過了不久 user2 又接手了這個業務。user 2 發現,他開發的 abc 模塊,經過依賴 bar 模塊,能夠進行快速地開發。但是 粗心的 user2 不太明白依賴關係。居然將 abc 的位置隨意寫了一下,這就致使 運行 abc 的時候,沒法找到 bar 模塊。

image-20200627000713100

image-20200627000713100

<script src="./abc.js"></script>
<script src="./bar.js"></script>
<script src="./baz.js"></script>
<script src="./foo.js"></script>

所以這裏 webpack 利用 CommonJS/ ES Modules 規範進行了處理。使得各個模塊之間相互引用無需考慮最終實際呈現的順序。最終會被打包爲一個 bunlde 模塊,無需按照順序手動引入。

baz.js
const bar = require('./bar.js');
module.exports = function baz (){
 ...
 bar();
 ...
}

abc.js
const bar = require('./bar.js');
module.exports = function baz (){
 ...
 bar();
 ...
}
<script src="./bundle.js"></script>

image-20200627003815071

image-20200627003815071

webpack 的模塊化機制與實現

基於以上兩項特性,模塊的隔離以及模塊的依賴聚合。咱們如今能夠很是清晰的知道了webpack所起的核心做用。

  • 爲了儘量下降編寫的難度和理解成本,我沒有使用 AST 的解析,(固然 AST 也不是什麼很難的東西,之後的文章中我會講解 AST是什麼以及 AST 解析器的實現過程。
  • 僅實現了 CommonJS 的支持

bundle工做原理

爲了可以實現 webpack, 咱們能夠經過反推的方法,先看webpack 打包後 bundle 是如何工做的。

「源文件」

// index.js
const b = require('./b');
b();
// b.js
module.exports = function () {
    console.log(11);
}

「build 後」(去除了一些干擾代碼)

(function(modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    module.l = true;
    return module.exports;
  }
  return __webpack_require__((__webpack_require__.s = 0));
})([
  /* 0 */
  function(module, exports, __webpack_require__) {
    var b = __webpack_require__(1);
    b();
  },
  /* 1 */
  function(module, exports) {
    module.exports = function() {
      console.log(11);
    };
  },
]);

image-20200627135324956

image-20200627135324956

以上就是 bundle 的運做原理。經過上述的流程圖咱們能夠看到,有四個關鍵點

  • 已註冊模塊(存放已經註冊的模塊)
  • 模塊列表(用來存放全部的包裝模塊)
  • 模塊查找(從原來的樹形的模塊依賴,變成了扁平查找)
  • 模塊的包裝(原有的模塊都進行了一次包裝)

webpack實現

經過 bundle 的分析,咱們只須要作的就是 4 件事

  • 遍歷出全部的模塊
  • 模塊包裝
  • 提供註冊模塊、模塊列表變量和導入函數
  • 持久化導出

模塊的遍歷

首先來介紹一下模塊的結構,能使咱們快速有所瞭解, 結構比較簡單,由內容和模塊id組成。

interface GraphStruct {
    context: string;
    moduleId: string;
}
{
 "context": `function(module, exports, require) {
    const bar = require('./bar.js');
  const foo = require('./foo.js');
  console.log(bar());
  foo();
  }`,
  "moduleId": "./example/index.js"
}

接下來咱們以拿到一個入口文件來進行講解,當拿到一個入口文件時,咱們須要對其依賴進行分析。說簡單點就是拿到 require 中的值,以便咱們去尋找下一個模塊。因爲在這一部分不想引入額外的知識,開頭也說了,通常採用的是 AST 解析的方式,來獲取 require 的模塊,在這裏咱們使用正則。

用來匹配全局的 require 
const REQUIRE_REG_GLOBAL = /require\(("|')(.+)("|')\)/g;
用來匹配 require 中的內容
const REQUIRE_REG_SINGLE = /require\(("|')(.+)("|')\)/;
const context = `
const bar = require('./bar.js');
const foo = require('./foo.js');
console.log(bar());
foo();
`;
console.log(context.match(REQUIRE_REG_GLOBAL));
// ["require('./bar.js')", "require('./foo.js')"]

image-20200627202427794

image-20200627202427794

因爲模塊的遍歷並非只有單純的一層結構,通常爲樹形結構,所以在這裏我採用了深度遍歷。主要經過正則去匹配出require 中的依賴項,而後不斷遞歸去獲取模塊,最後將經過深度遍歷到的模塊以數組形式存儲。(不理解深度遍歷,能夠理解爲遞歸獲取模塊)

image-20200627142130902

image-20200627142130902

如下是代碼實現

...
private entryPath: string
private graph: GraphStruct[]
...
createGraph(rootPath: string, relativePath: string) {
    // 經過獲取文件內容
    const context = fs.readFileSync(rootPath, 'utf-8');
    // 匹配出依賴關係
    const childrens = context.match(REQUIRE_REG_GLOBAL);
   // 將當前的模塊存儲下來
    this.graph.push({
        context,
        moduleId: relativePath,
    })
    const dirname = path.dirname(rootPath);
    if (childrens) {
       // 若有有依賴,就進行遞歸
        childrens.forEach(child => {
            const childPath = child.match(REQUIRE_REG_SINGLE)[2];
            this.createGraph(path.join(dirname, childPath), childPath);
        });
    }
}

模塊包裝

爲了可以使得模塊隔離,咱們在外部封裝一層函數, 而後傳入對應的模擬 requiremodule使得模塊能進行正常的註冊以及導入 。

function (module, exports, require){
    ...
},

提供註冊模塊、模塊列表變量和導入函數

這一步比較簡單,只要按照咱們分析的流程圖提供已註冊模塊變量、模塊列表變量、導入函數。

/* modules = {
  "./example/index.js": function (module, exports, require) {
    const a = require("./a.js");
    const b = require("./b.js");

    console.log(a());
    b();
  },
  ...
};*/
bundle(graph: GraphStruct[]) {
    let modules = '';
    graph.forEach(module => {
        modules += `"${module.moduleId}":function (module, exports, require){
        ${module.context}
        },`;
    });
    const bundleOutput = `
    (function(modules) {
        var installedModules = {};
        // 導入函數
        function require(moduleId) {
            // 檢查是否已經註冊該模塊
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
            }
           // 沒有註冊則從模塊列表獲取模塊進行註冊
            var module = (installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {},
            });
           // 執行包裝函數,執行後更新模塊的內容
            modules[moduleId].call(
                module.exports,
                module,
                module.exports,
                require
            );
            // 設置標記已經註冊
            module.l = true;
            // 返回實際模塊
            return module.exports;
        }
        require("${graph[0].moduleId}");
    })({${modules}})
    `;
    return bundleOutput;
}

持久化導出

最後將生成的 bundle 持久寫入到磁盤就大功告成。

fs.writeFileSync('bundle.js', this.bundle(this.graph))

完整代碼100行 代碼不到,詳情能夠查看如下完整示例。

github地址: https://github.com/hua1995116...

結尾

以上僅表明我的的理解,但願讓你對webpack的理解有所幫助, 若有講的很差的請多指出。

歡迎關注公衆號 「「秋風的筆記」」,主要記錄平常中以爲有意思的工具以及分享開發實踐,保持深度和專一度。回覆 webpack 獲取概覽圖 xmind 原圖

weixin-gongzhonghao

FAQ

Q: 爲何打算寫這篇文章?

R: 其實主要是爲了畫圖,純粹比較新奇。

Q: 還會有下一篇嗎?

R: 有的,下一篇暫定爲 ES module 和 code splitting 相關。

相關文章
相關標籤/搜索