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

hello~親愛的看官老爺們你們好~ 最近一直在學習 webpack 的相關知識,當清晰地領悟到 webpack 就是不一樣 loaderplugin 組合起來打包以後,只做爲工具使用而言,算是入門了。固然,在過程當中碰到數之不盡的坑,也產生了想要深刻一點了解 webpack 的原理(主要是掉進坑能靠本身爬出來)。於是就從簡單的入手,先看看使用 webpack 打包後的 JS 文件是如何加載吧。javascript

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

簡單配置

既然須要用到 webpack,仍是須要簡單配置一下的,這裏就簡單貼一下代碼,首先是 webpack.config.js:java

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',
    })
  ]
};
複製代碼

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

以後是兩個簡單的 js 文件:git

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

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

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

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

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

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

相關文章
相關標籤/搜索