webpack分片chunk加載原理


首先,使用create-react-app快速創建一個demo

npx create-react-app react-demo # npx命令需要npm5.2+ 
cd react-demo
npm start

通過http://localhost:3000/端口可以訪問頁面,接下來修改主應用組件App.js

import React, { Component } from 'react';
import './App.css';
class App extends Component {
  onButtonClick = () => {
    import(/* webpackChunkName: "alert" */ './Alert')
    .then((res) => {
      res.default()
    })
  }
  render() {
    return (
      <div className="App">
        <button onClick={this.onButtonClick}>點擊按需加載</button>
      </div>
    );
  }
}
export default App;

在App.js中,點擊按鈕會加載alert模塊,通過import()方法按需引入模塊webpack很早就支持了,什么?你還在用過時的require.ensure(),你一定是個假的前端。通過注釋里的webpackChunkName可以指定待加載模塊打包后的文件名。Alert模塊代碼很簡單,如下:

function showAlert() {
  alert('我們一起學喵叫,喵喵喵喵')
}
export default showAlert

為了方便研究源碼,我們需要修改npm build時,webpack的配置,為此可以執行npm eject“釋放”該項目的配置,然后修改config/webpack.config.prod.js,找到插件配置plugins數組里的webpack.optimize.UglifyJsPlugin注釋掉,再執行npm build,打包后的結果在build目錄下。

默認情況下,prd環境打出來的入口包是main.js,dev環境下是bundle.js,通過查看頁面源碼script標簽引用的文件可以看出來。下面的動圖演示了點擊按鈕時,頁面html結構的變化。
2018-09-30_16-43-48-1
從上圖可以清楚的看到,一開始頁面只加載了bundle.js文件,當點擊按鈕時在head標簽的末尾插入了一個script標簽,以此引入了alert.chunk.js並執行了代碼。本文即要分析此過程webpack是如何實現的。

_this.onButtonClick = function() {
              __webpack_require__
                .e(/* import() */ 0)
                .then(__webpack_require__.bind(null, 20))
                .then(function(res) {
                  res.default()
                })
            }

我們從按鈕點擊事件開始,上面的代碼即為打包后的點擊按鈕的事件處理函數,首先關注__webpack_require__.e(/* import() */ 0),其傳入參數是分片代碼的IdchunkId,值為0,返回結果是一個Promise

__webpack_require__.e = function requireEnsure(chunkId) {
    // installedChunks是在外層代碼中定義的一個對象,緩存了已加載chunk信息,鍵為chunkId
  var installedChunkData = installedChunks[chunkId]
    // installedChunkData為0表示此chunk已經加載過
  if (installedChunkData === 0) {
    return new Promise(function(resolve) {
      resolve()
    })
  }
    /* 如果此chunk正在加載中,則返回對應未fullfilled的Promise,
    此時installedChunkData是一個數組,數組的元素從后續的代碼中
    可以看出為[resolve, reject, Promise]。
    */
  if (installedChunkData) {
    return installedChunkData[2]
  } 
    /* 如果需要加載的chunk還未加載,則構造對應的Promsie並緩存在
    installedChunks對象中,從這里可以看出正在加載的chunk的緩存數據結構是一個數組
    */
  var promise = new Promise(function(resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject]
  })
  installedChunkData[2] = promise
  
    /*開始加載chunk*/
    // 構造script標簽
  var head = document.getElementsByTagName("head")[0]
  var script = document.createElement("script")
  script.type = "text/javascript"
  script.charset = "utf-8"
  script.async = true
  script.timeout = 120000
    // 如果設置了內容安全策略,則添加響應屬性值,默認情況下是不啟用的參考https://webpack.docschina.org/guides/csp/)
  if (__webpack_require__.nc) {
    script.setAttribute("nonce", __webpack_require__.nc)
  }
    // 根據chunkId設置src,__webpack_require__.p是配置的公共路徑
  script.src =
    __webpack_require__.p +
    "static/js/" +
    ({ "0": "alert" }[chunkId] || chunkId) +
    "." +
    { "0": "620d2495" }[chunkId] +
    ".chunk.js"
  var timeout = setTimeout(onScriptComplete, 120000)
  script.onerror = script.onload = onScriptComplete
  function onScriptComplete() {
    // avoid mem leaks in IE.
    script.onerror = script.onload = null
    clearTimeout(timeout)
    // 取出緩存中對應的chunk加載狀態
    var chunk = installedChunks[chunkId]
    // 如果加載失敗
    if (chunk !== 0) {
        // chunk加載超時
      if (chunk) {
        chunk[1](new Error("Loading chunk " + chunkId + " failed."))
      }
      // 將此chunk的加載狀態重置為未加載狀態
      installedChunks[chunkId] = undefined
    }
  }
    /* 將script標簽插入到head中就會立即加載該腳本,
    腳本加載成功或者失敗會執行上面的onScriptComplete方法
    */
  head.appendChild(script)
    // 返回待fullfilled的Promise
  return promise
}

補充說明

  • installedChunks在本例中的初始值為
  var installedChunks = {
    1: 0
  }

該對象用戶緩存已經加載和正在加載的chunk,在入口文件(把入口文件也當做一個chunk)中初始化,初始化后包含了入口chunk的狀態,此例中入口chunk的Id為1,webpack分配chunkId是0開始計數遞增的,實際上入口chunk的Id一定是最大的,從上面的代碼中值0表示當前的入口chunk已經加載了。

  • chunk在此過程中有三種狀態,在installedChunks分別對應三種值:未加載(undefined)->加載中([resolve, reject, Promise])->已加載(0)

以上代碼是chunk對應的js資源加載的方式,那么新加載的chunk是如何執行的呢?installedChunks中對應正在加載chunk狀態該如何變化呢?下面我們假設需要加載的chunk加載成功了,此時alert.xxx.chunk.js對應的代碼就會執行。alert.js打包后的代碼如下:

webpackJsonp([0], {
  20: function(module, __webpack_exports__, __webpack_require__) {
    "use strict"
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true })
    function showAlert() {
      alert("我們一起學喵叫,喵喵喵喵")
    }
    __webpack_exports__["default"] = showAlert
  }
})

這段代碼就執行了一個webpackJsonp方法,傳入了兩個參數,第一個參數是一個數組,數組的元素由需要加載的chunkId組成,第二個參數是一個對象,對象的鍵是moduleId,此例子中,當前chunk依賴的唯一模塊是自己本身的,如果當前代碼還有其他未加載的模塊,也會出現在這里,注意如果在其他已加載的chunk中已加載的模塊,這里就不會重新加載了。
下面我們只需要關注webpackJsonp方法是如何實現的,這個方法並不是在當前chunk中實現的,而是在入口chunk文件中實現的。從這里也可以看出webpack是通過jsonp的方式異步加載chunk的

var parentJsonpFunction = window["webpackJsonp"]
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
  // add "moreModules" to the modules object,
  // then flag all "chunkIds" as loaded and fire callback
  var moduleId,
    chunkId,
    i = 0,
    resolves = [];
    // 遍歷需要執行的chunk
  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i]
    // 如果該chunk正在加載中狀態
    if (installedChunks[chunkId]) {
        // 暫存該chunk對應Promise的resolve方法
      resolves.push(installedChunks[chunkId][0])
    }
        // 將該chunk的狀態置為加載完成
    installedChunks[chunkId] = 0
  }
   // 遍歷這些chunk依賴的模塊並緩存模塊到modules對象中,這個對象是在入口文件的最外層方法當做參數傳入的
  for (moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId]
    }
  }
  if (parentJsonpFunction)
    parentJsonpFunction(chunkIds, moreModules, executeModules)
    // 將加載的chunk對應的Promise fullfill掉
  while (resolves.length) {
    resolves.shift()()
  }
}

還記得之前點擊按鈕的代碼嗎,當加載的chunk對應的Promise變為fullfilled狀態,就會執行__webpack_require__.bind(null, 20)加載該chunk中對應主模塊。__webpack_require__是用來加載模塊的,它的實現非常簡單:

function __webpack_require__(moduleId) {
  // 檢查模塊緩存對象中是否已有該模塊,有的話直接返回
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports
  } 
  // 模塊緩存對象中沒有該模塊就創建一個新模塊並添加到緩存中
  var module = (installedModules[moduleId] = {
    i: moduleId, // 模塊Id
    l: false, // 是否已加載
    exports: {} // 模塊的導出
  }) 
  // 執行模塊對應的代碼(模塊的代碼是在webpackJsonp方法中緩存到modules對象中的)
  modules[moduleId].call(
    module.exports,
    module,
    module.exports,
    __webpack_require__
  ) 
    // 將模塊標志改為已加載
  module.l = true 
    // 返回模塊的導出對象
  return module.exports
}

上述代碼中modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)執行了下面的模塊代碼,執行上下文為module.exports對象,傳入了參數module、 module.exports、 __webpack_require____webpack_require__是用來加載其他模塊的,本例中並沒有。

function (module, __webpack_exports__, __webpack_require__) {
  "use strict"
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true })
  function showAlert() {
    alert("我們一起學喵叫,喵喵喵喵")
  }
  // __webpack_exports__就是傳入的module.exports對象
  __webpack_exports__["default"] = showAlert
}

返回按鈕點擊執行的事件處理程序,當__webpack_require__.bind(null, 20)執行后返回導出的模塊,再執行res.default()就相當於執行了showAlert方法。

_this.onButtonClick = function() {
              __webpack_require__
                .e(/* import() */ 0)
                .then(__webpack_require__.bind(null, 20))
                .then(function(res) {
                  res.default()
                })
            }

通過這個簡單的例子,基本了解了webpack加載chunk和module的原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM