簡單易懂的 webpack 打包後 JS 的執行過程

sea_ljf發表於2017-12-09

hello~親愛的看官老爺們大家好~ 最近一直在學習 webpack 的相關知識,當清晰地領悟到 webpack 就是不同 loaderplugin 組合起來打包之後,只作為工具使用而言,算是入門了。當然,在過程中碰到數之不盡的坑,也產生了想要深入一點了解 webpack 的原理(主要是掉進坑能靠自己爬出來)。因而就從簡單的入手,先看看使用 webpack 打包後的 JS 檔案是如何載入吧。

友情提示,本文簡單易懂,就算沒用過 webpack 問題都不大。如果已經瞭解過相關知識的朋友,不妨快速閱讀一下,算是溫故知新 ,其實是想請你告訴我哪裡寫得不對

簡單配置

既然需要用到 webpack,還是需要簡單配置一下的,這裡就簡單貼一下程式碼,首先是 webpack.config.js:

const path = require('path');
const webpack = require('webpack');
//用於插入html模板
const HtmlWebpackPlugin = require('html-webpack-plugin');
//清除輸出目錄,免得每次手動刪除
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
    index: path.join(__dirname, 'index.js'),
  },
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'js/[name].[chunkhash:4].js'
  },
  module: {},
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
    }),
    //持久化moduleId,主要是為了之後研究載入程式碼好看一點。
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
    })
  ]
};
複製程式碼

這是我能想到近乎最簡單的配置,用到的兩個額外下載的外掛都是十分常用的,也已經在註釋中簡單說明了。

之後是兩個簡單的 js 檔案:

// test.js
const str = 'test is loaded';
module.exports = str;

// index.js
const test = require('./src/js/test');
console.log(test);
複製程式碼

這個就不解釋了,貼一下打包後,專案的目錄結構應該是這樣的:

簡單易懂的 webpack 打包後 JS 的執行過程

至此,我們的配置就完成了。

index.js 開始看程式碼

先從打包後的 index.html 檔案看看兩個 JS 檔案的載入順序:

<body>
	<script type="text/javascript" src="js/manifest.2730.js"></script>
	<script type="text/javascript" src="js/index.5f4f.js"></script>
</body>
複製程式碼

可以看到,打包後 js 檔案的載入順序是先 manifest.js,之後才是 index.js,按理說應該先看 manifest.js 的內容的。然而這裡先賣個關子,我們先看看 index.js 的內容是什麼,這樣可以帶著問題去了解 manifest.js,也就是主流程的邏輯到底是怎樣的,為何能做到模組化。

// index.js

webpackJsonp([0], {
  "JkW7": (function(module, exports, __webpack_require__) {
    const test = __webpack_require__("zFrx");
    console.log(test);
  }),
  "zFrx": (function(module, exports) {
    const str = 'test is loaded';
    module.exports = str;
  })
}, ["JkW7"]);
複製程式碼

刪去各種奇怪的註釋後剩下這麼點內容,首先應該關注到的是 webpackJsonp 這個函式,可以看見是不在任何名稱空間下的,也就是 manifest.js 應該定義了一個掛在 window 下的全域性函式,index.js 往這個函式傳入三個引數並呼叫。

第一個引數是陣列,現在暫時還不清楚這個陣列有什麼作用。

第二個引數是一個物件,物件內都是方法,這些方法看起來至少接受兩個引數(名為 zFrx 的方法只有兩個形參)。看一眼這兩個方法的內部,其實看見了十分熟悉的東西, module.exports,儘管看不見 require, 但有一個樣子類似的 __webpack_require__,這兩個應該是模組化的關鍵,先記下這兩個函式。

第三個引數也是一個陣列,也不清楚是有何作用的,但我們觀察到它的值是 JkW7,與引數2中的某個方法的鍵是一致的,這可能存在某種邏輯關聯。

至此,index.js 的內容算是過了一遍,接下來應當帶著問題在 manifest.js 中尋找答案。

manifest.js 程式碼閱讀

由於沒有配置任何壓縮 js 的選項,因此 manifest.js 的原始碼大約在 150 行左右,簡化後為 28 行(已經跑過程式碼,實測沒問題)。鑑於精簡後的程式碼真的不多,因而先貼程式碼,大家帶著剛才提出的問題,先看看能找到幾個答案:

(function(modules) {
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    var moduleId, result;
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (executeModules) {
      for (i = 0; i < executeModules.length; i++) {
        result = __webpack_require__(executeModules[i]);
      }
    }
    return result;
  };
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }
})([]);
複製程式碼

首先應該看到的是,manifest.js 內部是一個 IIFE,就是自執行函式咯,這個函式會接受一個空陣列作為引數,該陣列被命名為 modules。之後看到我們在 index.js 中的猜想,果然在 window 上掛了一個名為 webpackJsonp 的函式。它接受的三個引數,分別名為chunkIds, moreModules, executeModules。對應了 index.js 中呼叫 webpackJsonp 時傳入的三個引數。而 webpackJsonp 內究竟是有怎樣的邏輯呢?

先不管定義的引數,webpackJsonp 先是 for in 遍歷了一次 moreModules,將 moreModules 內的所有方法都存在 modules, 也就是自執行函式執行時傳入的陣列。

之後是一個條件判斷:

if (executeModules) {
  for (i = 0; i < executeModules.length; i++) {
    result = __webpack_require__(executeModules[i]);
  }
}
複製程式碼

判斷 executeModules, 也就是第三個引數是否存在,如存在即執行 __webpack_require__ 方法。在 index.js 呼叫 webpackJsonp 方法時,這個引數當然是存在的,因而要看看 __webpack_require__ 方法是什麼了。

__webpack_require__ 接受一個名為 moduleId 的引數。方法內部首先是一個條件判斷,先不管。接下來看到賦值邏輯

var module = installedModules[moduleId] = {
  exports: {}
};
複製程式碼

結合剛才的條件判斷,可以推測出 installedModules 是一個快取的容器,那麼前面的程式碼意思就是如果快取中有對應的 moduleId,那麼直接返回它的 exports,不然就定義並賦值一個吧。接著先偷看一下 __webpack_require__ 的最後的返回值,可以看到函式返回的是 module.exports,那麼 module.exports 又是如何被賦值的呢? 看看之後的程式碼:

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
複製程式碼

剛才我們知道 modules[moduleId] 就是 moreModules 中的方法,此處就是將 this 指定為 module.exports,再把module, module.exports, __webpack_require__ 傳入去作為引數呼叫。這三個引數是不是很熟悉?之前我們看 index.js 裡面程式碼時,有一個疑問就是模組化是如何實現的。這裡我們已經看出了眉目。

其實 webpack 就是將每一個 js 檔案封裝成一個函式,每個檔案中的 require 方法對應的就是 __webpack_require____webpack_require__ 會根據傳入的 moduleId 再去載入對應的程式碼。而當我們想匯出 js 檔案的值時,要麼用 module.exports,要麼用 exports,這就對應了module, module.exports兩個引數。少接觸這塊的童鞋,應該就能理解為何匯出值時,直接使用 exports = xxx 會匯出失敗了。簡單舉個例子:

const module = {
  exports: {}
};

function demo1(module) {
  module.exports = 1;
}

demo1(module);
console.log(module.exports); // 1

function demo2(exports) {
  exports = 2;
}

demo2(module.exports);
console.log(module.exports); // 1
複製程式碼

貼上這段程式碼去瀏覽器跑一下,可以發現兩次列印出來都是1。這和 wenpack 打包邏輯是一模一樣的。

梳理一下打包後程式碼執行的流程,首先 minifest.js 會定義一個 webpackJsonp 方法,待其他打包後的檔案(也可稱為 chunk)呼叫。當呼叫 chunk 時,會先將該 chunk 中所有的 moreModules, 也就是每一個依賴的檔案也可稱為 module (如 test.js)存起來。之後通過 executeModules 判斷這個檔案是不是入口檔案,決定是否執行第一次 __webpack_require__。而 __webpack_require__ 的作用,就是根據這個 modulerequire 的東西,不斷遞迴呼叫 __webpack_require____webpack_require__函式返回值後供 require 使用。當然,模組是不會重複載入的,因為 installedModules 記錄著 module 呼叫後的 exports 的值,只要命中快取,就返回對應的值而不會再次呼叫 modulewebpack 打包後的檔案,就是通過一個個函式隔離 module 的作用域,以達到不互相汙染的目的。

小結

以上就是 webpack 打包後 js 檔案是載入過程的簡單描述,其實還是有很多細節沒有說完的,比如如何非同步載入對應模組, chunkId 有什麼作用等,但由於篇幅所限 (還沒研究透),不再詳述。相關程式碼會放置 github 中,歡迎隨時查閱順便點star

感謝各位看官大人看到這裡,知易行難,希望本文對你有所幫助~謝謝!

相關文章