webpack Code Splitting淺析

来源:https://www.cnblogs.com/floor/archive/2019/04/29/10788304.html
-Advertisement-
Play Games

Code Splitting是webpack的一個重要特性,他允許你將代碼打包生成多個bundle。對多頁應用來說,它是必須的,因為必須要配置多個入口生成多個bundle;對於單頁應用來說,如果只打包成一個bundle可能體積很大,導致無法利用瀏覽器並行下載的能力,且白屏時間長,也會導致下載很多可能... ...


Code Splitting是webpack的一個重要特性,他允許你將代碼打包生成多個bundle。對多頁應用來說,它是必須的,因為必須要配置多個入口生成多個bundle;對於單頁應用來說,如果只打包成一個bundle可能體積很大,導致無法利用瀏覽器並行下載的能力,且白屏時間長,也會導致下載很多可能用不到的代碼,每次上線用戶都得下載全部代碼,Code Splitting能夠將代碼分割,實現按需載入或並行載入多個bundle,可利用併發下載能力,減少首次訪問白屏時間,可以只上線必要的文件。


三種Code Splitting方式

webpack提供了三種方式來切割代碼,分別是:

  1. 多entry方式
  2. 公共提取
  3. 動態載入
    本文將簡單介紹多entry方式和公共提取方式,重點介紹的是動態載入。這幾種方式可以根據需要組合起來使用。這裡是官方文檔,中文 英文

多entry方式

這種方式就是指定多個打包入口,從入口開始將所有依賴打包進一個bundle,每個入口打包成一個bundle。此方式特別適合多頁應用,我們可以每個頁面指定一個入口,從而每個頁面生成一個js。此方式的核心配置代碼如下:

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    page1: './src/page1.js',
    page2: './src/page2.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

上邊的配置最終將生成兩個bundle, 即page1.bundle.js和page2.bundle.js。

公共提取

這種方式將公共模塊提取出來生成一個bundle,公共模塊意味著有可能有很多地方使用,可能導致每個生成的bundle都包含公共模塊打包生成的代碼,造成浪費,將公共模塊提取出來單獨生成一個bundle可有效解決這個問題。這裡貼一個官方文檔給出的配置示例:

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    // 關鍵
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    }
  };

這個示例中index.js和another-module.js中都import了loadsh,如果不配置optimization,將生成兩個bundle, 兩個bundle都包含loadsh的代碼。配置optimization後,loadsh代碼被單獨提取到一個vendors~another~index.bundle.js。

動態載入

動態載入的含義就是講代碼打包成多個bundle, 需要用到哪個bundle時在載入他。這樣做的好處是可以讓用戶下載需要用到的代碼,避免無用代碼下載。確定是操作體驗可能變差,因為操作之後可能還有一個下載代碼的過程。關於動態載入,後面詳解。


實現一個簡單的動態載入

動態載入就是要實現可以在代碼裡邊去載入其他js,這個太簡單了,新建script標簽插入dom就可以了,如下:

function loadScript(url) {
    const script = document.createElement('script');
    script.src = url;
    document.head.appendChild(script);
}

只需要在需要載入某個js時調用即可,例如需要點擊按鈕時載入js可能就如下邊這樣。

btn.onClick = function() {
    console.log('1');
    loadScript('http://abc.com/a.js');
}

看上去非常簡單,事實上webpack也是這麼做的,但是他的處理更加通用和精細。


webpack動態載入

webpak打包出來的代碼怎麼執行

現有一個文件test2.js, 其中代碼為

console.log('1')

此文件通過webpack打包後輸出如下,刪除了部分代碼,完整版可自己嘗試編譯一個,也可查看web-test(這個項目是基於react,express,webpack的用於web相關實驗的項目,裡邊使用了code splitting方案來基於路由拆分代碼,與code splitting相關的實驗放在test-code-split分支)。

(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }
  return __webpack_require__(__webpack_require__.s = "./test2.js");
})
  ({

    "./test2.js":
      (function (module, exports, __webpack_require__) {

        "use strict";
        eval("\n\nconsole.log('1');\n\n//# sourceURL=webpack:///./test2.js?");

      })

  });

不知大家是不是跟大雄一樣之前從未看過webpack編譯產出的代碼。其實看一下還是挺有趣的,原來我們的代碼是放在eval中執行的。細看下這段代碼,其實並不複雜。他是一個自執行函數,參數是一個對象,key是模塊id(moduleId), value是函數,這個函數是裡邊是執行我們寫的代碼,在自執行函數體內是直接調用了一個__webpack_require__,參數就是入口moduleId, __webpack_require__方法里值執行給定模塊id對應的函數,核心代碼是modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

上面是沒有import命令的情況,對於有import命令的情況,產出和上邊類似,只是自執行函數的參數有變化。例如:

// 入口文件test2.js
import './b.js'
console.log('1')
// b.js
console.log('b')

這段代碼產出的自執行函數裡邊的參數如下:

// 自執行函數裡邊的參數
{

  "./b.js":
  (function (module, exports, __webpack_require__) {

    "use strict";
    eval("\n\nconsole.log('b');\n\n//# sourceURL=webpack:///./b.js?");
  }),

    "./test2.js":
  (function (module, exports, __webpack_require__) {

    "use strict";
    eval("\n\n__webpack_require__(/*! ./b.js */ \"./b.js\");\n\nconsole.log('1');\n\n//# sourceURL=webpack:///./test2.js?");
  })
}

./test2.js這個moduleId對應的函數的eval裡邊調用了__webpack_require__方法,為了看起來方便,將eval中的字元串拿出來,如下

__webpack_require__("./b.js");
console.log('1');

原來import命令在webpack中就是被轉換成了__webpack_require__的調用。太奇妙了,但是話說為啥模塊裡邊為啥要用eval來執行我們寫的代碼,大雄還是比較困惑的。

webpack動態code splitting方案

經過一番鋪墊,終於到主題了,即webpack是如何實現動態載入的。前文大雄給了一個粗陋的動態載入的方法--loadScript, 說白了就是動態創建script標簽。webpack中也是類似的,只是他做了一些細節處理。本文只介紹主流程,具體實現細節大家可以自己編譯產出一份代碼進行研究。

首先需要介紹在webpack中如何使用code splitting,非常簡單,就像下邊這樣

import('lodash').then(_ => {
    // Do something with lodash (a.k.a '_')...
  });

我們使用了一個import()方法, 這個import方法經過webpack打包後類似於前文提到的loadScript, 大家可以參看下邊的代碼:

__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];


    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 means "already installed".

        // a Promise means "currently loading".
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            // setup Promise in chunk cache
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var script = document.createElement('script');
            var onScriptComplete;

            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.src = jsonpScriptSrc(chunkId);

            onScriptComplete = function (event) {
                // avoid mem leaks in IE.
                script.onerror = script.onload = null;
                clearTimeout(timeout);
                var chunk = installedChunks[chunkId];
                if(chunk !== 0) {
                    if(chunk) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            var timeout = setTimeout(function(){
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete;
            document.head.appendChild(script);
        }
    }
    return Promise.all(promises);
};

是不是非常熟悉,代碼中也調用了document.createElement('script')來創建script標簽,最後插入到head里。這段代碼所做的就是動態載入js,載入失敗時reject,載入成功resolve,這裡並不能看到resolve的情況,resolve是在拆分出去的代碼里調用一個全局函數實現的。拆分出的js如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

/***/ "./b.js":
/*!**************!*\
  !*** ./b.js ***!
  \**************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("\n\nconsole.log('b');\n\n//# sourceURL=webpack:///./b.js?");

/***/ })

}]);

在webpackJsonp方法里調用了對應的resolve,具體如下:

function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];


    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(data);

    while(resolves.length) {
        resolves.shift()();
    }

};

這裡的掛到全局的webpackJsonp是個數組,其push方法被改為webpackJsonpCallback方法的數組。所以每次在執行webpackJsonp時實際是在調用webpackJsonpCallback方法。

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i])

總結起來,webpack的動態載入流程大致如下:

webpack動態載入原理圖

總結

本文對webpack打包出的代碼的結構和執行過程作了簡單分析,介紹了webpack中code splitting的幾種方式,重點分析了一下動態載入的流程。分析的不一定完全正確,大家可以自己使用webpack打包產出代碼進行研究,一定會有所收穫。大雄看完至少大概知道了原來webpack編出來的代碼是那樣執行的、Promise原來可以那麼靈活的使用。


大雄在學習web開發或在項目中遇到問題時經常需要做一些實驗, 在react出了什麼新的特性時也常常通過做實驗來瞭解一下. 最開始常常直接在公司的項目做實驗, 直接拉個test分支就開搞, 這樣做有如下缺點:

  • 在公司的項目去做實驗本身就是一件不好的事情
  • 公司的項目裡邊只有前端的部分, 想要做介面有關的實驗不方便. 例如想測試跨域的響應頭Access-Control-Allow-Origin就得再啟一個web伺服器
  • 實驗過的東西零散, 過一段時間想查找卻找不到了

基於以上原因, 特搭建了個基於react,webpack,express的用於web開發相關實驗的項目web-test.歡迎使用。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 註意:每次退出前導出自己的項目到本地做備份。 單機版特點: 1.同步官方最新版本,沒有對java源代碼進行修改,僅修改war\login.jsp及\war\WEB-INF\appengine-web.xml; 2.無需聯網,已經設置Rendezvous伺服器為127.0.0.1:8888; 3.帶有 ...
  • 路由大家應該都知道,在微信小程式也是有的,畢竟它是單頁面應用程式。在WeChat中有五種跳轉方式,分別是wx.switchTab、wx.reLaunch、wx.redirectTo、wx.navigateTo、wx.navigateBack。今天我們就說一說 如何使用這幾個API來跳轉頁面,並且我們 ...
  • Intent:即意圖,一般是用來啟動新的Activity,按照啟動方式分為兩類:顯式Intent 和 隱式Intent 顯示Intent就是直接以“類名稱”來指定要啟動哪一個Activity:Intent intent = new Intent(this , activity.class); 其中a ...
  • 插敘:之前電腦一直遇到VPN登錄不了的問題,試了幾臺電腦都能正常連接,只有我的電腦不可以,VPN大佬建議我直接重裝系統,索性就直接重新裝了系統,結果就能連接了。昨天開始上傳包的時候,發現用Application Loader登錄的時候,填寫了正確的開發者賬號一直登錄不上去,如下圖。 點了圖上的地址 ...
  • 我們的目標:搭建一個本地多用戶的App Inventor 2 伺服器 演示: http://ai2.fsyz.net [舊 win] http://ai2n.fsyz.net [新 Centos] 目的:課堂教學,社團活動,興趣學習 優勢:管理許可權(用戶管理,賬號切換,資源打包),網路鏈接速度快,擁 ...
  • 表單類控制項承載了一個網頁數據的錄入與交互,本章將介紹如何使用指令v-model完成表單的數據雙向綁定。 ...
  • 一、前言 在之前的前端開發中,為了實現我們的需求,通常採用的方案是通過 JS/Jquery 直接操縱頁面的 DOM 元素,得益於 Jquery 對於 DOM 元素優異的操作能力,我們可以很輕易的對獲取到的 DOM 元素進行操作。但是,當我們開始在前端項目中使用 Vue 這類的 MVVM 框架之後,對 ...
  • 學習筆記 1.jQuery動畫的淡入淡出 2.jQuery廣告彈窗 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...