webpack | 動態匯入語法import

語言: CN / TW / HK

前言

  • 文章結構採用【指出階段目標,然後以需解決問題為入口,以解決思路為手段】達到本文目標,若使諸君稍有啟發,不枉此文心力^-^

目標

理解webpack的重要概念-【程式碼分割】,以及其實現import函式

關鍵點

對於隨著功能而使用的程式碼,可以先拆分出來打包到一個單獨的js檔案中(程式碼分割),然後在使用時動態建立script標籤進行引入。

import語法的實現

先看使用
## index.js
btn.addEventListener('click',()=>{
    import(
        /* webpackChunkName: "title" */ "./title.js"
    ).then((result)=>{
        console.log(result);
    })
})
複製程式碼
</-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">非同步載入</button>
</body>
</html>
複製程式碼
核心問題

如何讓import語法包裹的模組在執行時才引入

解決思路

採用JSONP的思路,首先,將動態引入模組單獨打成一個js檔案;其次,在import執行時建立script標籤傳入src為引入模組地址;從而實現動態載入的效果,注意,JSONP必然是非同步的,所以必須要結合Promise;

前置知識

JSONP是以前很流行的一種跨域手段(面試問跨域必答),其核心就在於利用script標籤的src屬性不收瀏覽器安全協議的限制(同源策略,域名埠協議三者必須相同,否則即跨域),再具體而言,就是向伺服器發起具體特定(比如這就是非同步載入模組的邏輯程式碼)js檔案的請求,然後獲得其結果(此處就是模組匯出值,會預設放到window下名為webpackJsonp的陣列中)

實現邏輯

核心:在引入非同步載入模組後再執行使用者自定義邏輯,promise實現訂閱釋出,promise收集使用者

思路
  1. 觸發時將非同步模組以jsonp進行引入,此處必然是非同步,所以需要利用promise進行訂閱釋出,先訂閱收集內部對引入模組的處理

  2. 在引入模組的程式碼執行時完成模組的安裝(加入到模組源上),同時釋出模組處理操作獲得模組返回值(呼叫對應promise的resolve方法)

    在動態引入的模組中需實現

    1. 將被引入模組標為已載入,並記錄此非同步引入模組
    2. 將被引入模組物件存入初始的modules中,統一管理
    3. 執行使用者邏輯(即將promise標為成功態,從而執行then中的回撥)
  3. 將返回值交給使用者定義函式,完成引入

具體實現

定義方法 __webpack_require__.e 其核心是通過jsonp(建立script標籤)載入動態引入程式碼,其次兩點 快取 + 非同步 ;

  1. installedChunks物件用於記錄模組狀態,實現快取
// 用於存放載入過的和載入中的程式碼塊 
		// key :程式碼塊名 chunkId 
		// value : undefined 未載入  null 預載入  Promise  程式碼塊載入中   0 載入完成
	var installedChunks = {
		0: 0
	};
複製程式碼
  1. 若模組未載入,則通過chunkId拼接src並建立script標籤非同步載入模組
// 根據模組名獲得引入路徑
script.src = jsonpScriptSrc(chunkId);
。。。
	// 將指令碼插入文件  開始獲取依賴模組內容
document.head.appendChild(script);
複製程式碼
  1. 動態引入的模組中需實現邏輯,
    • 因為要記錄,而且可能有多個非同步引入模組,所以可以採用陣列;
    • 因為在記錄的同時還有執行【存入初始modules】【改變模組狀態】等邏輯,所以可以用裝飾者設計模式,重寫此陣列例項的push方法,從而在存入同時執行其餘邏輯;(此寫法在vue原始碼實現資料劫持也有應用,可見【xxxxx】)
## 自執行函式中預設執行
// 重寫陣列push
	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
	// 獲取原push方法
	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
	// 將push指向自定義的函式	
jsonpArray.push = webpackJsonpCallback;
。。。
/**
	 * 非同步載入模組中會執行這個函式進行模組安裝
	 * @param {Array} data 
	 * [
	 * 		chunkIds:Array,  模組id陣列  個人感覺其實只會有一個id,因為非同步載入時自然只會載入一個chunk;沒想明白為什麼要設計成陣列,如有知道的請解惑 
	 * 				[string|number]
	 * 		modules:Array   模組函式陣列  即之前說到的包裹使用者自定義邏輯的函式,採用陣列是因為在webpac4.441版本後將陣列下標作為chunkId了,所以main的chunkId是0,在此例中title的chunkId是1,那麼0處就需要empty去佔位;
	 * 				[fn]
	 * ]
	 */
	function webpackJsonpCallback(data) {
		// 模組id陣列 
		var chunkIds = data[0];
		// 模組函式陣列
		var moreModules = data[1];
		// 模組ID(被安裝進modules時的標識)   程式碼塊ID   迴圈索引  這個非同步載入模組對應的promise會有resolve函式,會被存在resolves中  
		var moduleId, chunkId, i = 0, resolves = [];
		// 【將被引入模組標為已載入,並記錄此非同步引入模組】
		// 1. 迴圈儲存非同步載入模組對應的promise的resolve函式 末尾會執行以將promise標為成功態,從而執行then中的第一個回撥(promise規範中稱為onFullFinished)
		// 2. 將被引入模組標為已載入
		for(;i < chunkIds.length; i++) {
			chunkId = chunkIds[i];
			if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
				resolves.push(installedChunks[chunkId][0]);
			}
			installedChunks[chunkId] = 0;
		}
		// 將被引入模組物件存入初始的modules中,統一管理
		for(moduleId in moreModules) {
			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
				modules[moduleId] = moreModules[moduleId];
			}
		}
		// parentJsonpFunction指的是陣列原始的push方法,執行以保證webpackJsonp陣列的狀態
		if(parentJsonpFunction) parentJsonpFunction(data);
		// 執行使用者邏輯(即將promise標為成功態,從而執行then中的回撥)
		while(resolves.length) {
			resolves.shift()();
		}
	};

複製程式碼
  1. 在非同步載入的模組中,就會執行webpackJsonp的push方法(其實是webpackJsonpCallback方法),從而完成安裝和引入,至此,我們的模組源modules找那個就有了我們的title模組;下一步只要複用之前邏輯,進行模組的安裝就好
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    // chunkId
    [1],
    [
      // 佔位 代表main模組
        /* 0 */,
        /* 1 */ // title模組對應的模組安裝函式
        /***/ (function(module, exports) {

        module.exports = "title"

        /***/ })
    ]
]);
複製程式碼
  1. 此時,我們的modules上已經有非同步載入的模組資訊了,該以什麼方式匯出呢(commonJs還是es),webpack中採用了預設es但也支援commonJs的方式,此邏輯在__webpack_require__.t中實現,__webpack_require__.e返回promise的回撥中會執行t方法;在在實現此方法前,我們需要了解js的位運算&
A & B  先將A B轉為二進位制,如果兩位數的同位都是 1 則設定每位為 1 否則為0;
複製程式碼

​ 要區別處理匯出方式,自然要進行判斷,在webpack中採用的就是位運算&;

十進位制二進位制判斷優先順序為true時執行邏輯
100011執行__webpack_require__方法,進行模組安裝
200104將模組物件的屬性和值拷貝到ns上
401003會繼續判斷是不是es模組,如果是則直接返回,如果不是則向下執行定義一個es模組物件ns(此為預設返回值)
810002直接返回,注意判斷此優先順序是2

舉例解釋:在本例中,傳遞的是7(第一位是chunkId,第二位是判斷標識)

 __webpack_require__.e(/* import() | title */ 1).then(__webpack_require__.t.bind(null, 1, 7)).then((result)=>{
        console.log(result);
    })
複製程式碼

7轉為二進位制是0111,所以執行為

  • 執行__webpack_require__方法
  • 不直接返回(注意判斷優先順序)
  • 不是es模組物件,向下執行定義一個es模組物件ns
  • 將模組物件的屬性和值拷貝到ns上,返回此ns物件
__webpack_require__.t = function(value, mode) {
	if(mode & 1) value = __webpack_require__(value);
	if(mode & 8) return value;
	if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
	var ns = Object.create(null);
	__webpack_require__.r(ns);
	Object.defineProperty(ns, 'default', { enumerable: true, value: value });
	if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
	return ns;
};
複製程式碼
簡化版測試用例,可自行debuge加深理解

// The module cache
var installedModules = {},modules = {
	moduleA(module,exports){
		exports.value = "moduleA"
	},
	moduleB(module,exports){
		exports.__esModule = true;
		exports.default = {value:"moduleB"}
	}
};
// 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;
}
__webpack_require__.r = function(exports) {
	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
	}
	Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, { enumerable: true, get: getter });
	}
};
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
__webpack_require__.t = function(value, mode) {
	if(mode & 1) value = __webpack_require__(value);
	if(mode & 8) return value;
	if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
	var ns = Object.create(null);
	__webpack_require__.r(ns);
	Object.defineProperty(ns, 'default', { enumerable: true, value: value });
	if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
	return ns;
};
let result1 = __webpack_require__.t("moduleA",1); // 0b0001
console.log("result1",result1);
let result2 = __webpack_require__.t("moduleA",9); // 0b1001
console.log("result2",result2);
let result3 = __webpack_require__.t("moduleA",5); // 0b0101
console.log("result3",result3);
let result4 = __webpack_require__.t("moduleA",7); // 0b0111
console.log("result4",result4);
複製程式碼
show the code (編譯後加註釋版原始碼)
main.js
(function(modules) { // webpackBootstrap
	/**
	 * 非同步載入模組中會執行這個函式進行模組安裝
	 * @param {Array} data 
	 * [
	 * 		chunkIds:Array,  模組id陣列  個人感覺其實只會有一個id,因為非同步載入時自然只會載入一個chunk;沒想明白為什麼要設計成陣列,如有知道的請解惑 
	 * 				[string|number]
	 * 		modules:Array   模組函式陣列  即之前說到的包裹使用者自定義邏輯的函式,採用陣列是因為在webpac4.441版本後將陣列下標作為chunkId了,所以main的chunkId是0,在此例中title的chunkId是1,那麼0處就需要empty去佔位;
	 * 				[fn]
	 * ]
	 */
	function webpackJsonpCallback(data) {
		// 模組id陣列 
		var chunkIds = data[0];
		// 模組函式陣列
		var moreModules = data[1];
		// 模組ID(被安裝進modules時的標識)   程式碼塊ID   迴圈索引  這個非同步載入模組對應的promise會有resolve函式,會被存在resolves中  
		var moduleId, chunkId, i = 0, resolves = [];
		// 【將被引入模組標為已載入,並記錄此非同步引入模組】
		// 1. 迴圈儲存非同步載入模組對應的promise的resolve函式 末尾會執行以將promise標為成功態,從而執行then中的第一個回撥(promise規範中稱為onFullFinished)
		// 2. 將被引入模組標為已載入
		for(;i < chunkIds.length; i++) {
			chunkId = chunkIds[i];
			if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
				resolves.push(installedChunks[chunkId][0]);
			}
			installedChunks[chunkId] = 0;
		}
		// 將被引入模組物件存入初始的modules中,統一管理
		for(moduleId in moreModules) {
			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
				modules[moduleId] = moreModules[moduleId];
			}
		}
		// parentJsonpFunction指的是陣列原始的push方法,執行以保證webpackJsonp陣列的狀態
		if(parentJsonpFunction) parentJsonpFunction(data);
		// 執行使用者邏輯(即將promise標為成功態,從而執行then中的回撥)
		while(resolves.length) {
			resolves.shift()();
		}
	};
	// The module cache
	var installedModules = {};
	// 用於存放載入過的和載入中的程式碼塊 
		// key :程式碼塊名 chunkId 
		// value : undefined 未載入  null 預載入  Promise  程式碼塊載入中   0 載入完成
	var installedChunks = {
		0: 0
	};
	// script path function
	function jsonpScriptSrc(chunkId) {
		// __webpack_require__.p 是指 publicPath   
		return __webpack_require__.p + "" + ({"1":"title"}[chunkId]||chunkId) + ".js"
	}
	// 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;
	}
	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = function requireEnsure(chunkId) {
		// 因為載入程式碼塊是非同步的,所以需要用到Promise
		var promises = [];
		
		var installedChunkData = installedChunks[chunkId];
		// 判斷此程式碼塊是否被安裝過
		if(installedChunkData !== 0) { // 0 means "already installed".
			// 如果不為0且是一個數組(其第二項是一個promise,前兩項是promise的resolve和reject函式)
			if(installedChunkData) {
				// 則存入promise佇列
				promises.push(installedChunkData[2]);
			} else {
				// 如果不為0且不存在 即 undefined 未載入  null 預載入 
				var promise = new Promise(function(resolve, reject) {
					installedChunkData = installedChunks[chunkId] = [resolve, reject];
				});
				// 將promise存入第二項
				promises.push(installedChunkData[2] = promise);
				// 開始程式碼塊匯入邏輯  建立script標籤
				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);
				//此處是用於載入超時提示  當模組載入超過120000ms時,則會在瀏覽器中丟擲異常,提示使用者
				var error = new Error();
				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;
							error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
							error.name = 'ChunkLoadError';
							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);
	};
	// expose the modules object (__webpack_modules__)
	__webpack_require__.m = modules;
	// expose the module cache
	__webpack_require__.c = installedModules;
	// define getter function for harmony exports
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};
	// define __esModule on exports
	__webpack_require__.r = function(exports) {
		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
		}
		Object.defineProperty(exports, '__esModule', { value: true });
	};
	// create a fake namespace object
	// mode & 1: value is a module id, require it
	// mode & 2: merge all properties of value into the ns
	// mode & 4: return value when already ns object
	// mode & 8|1: behave like require
	__webpack_require__.t = function(value, mode) {
		if(mode & 1) value = __webpack_require__(value);
		if(mode & 8) return value;
		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
		var ns = Object.create(null);
		__webpack_require__.r(ns);
		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
		return ns;
	};
	// getDefaultExport function for compatibility with non-harmony modules
	__webpack_require__.n = function(module) {
		var getter = module && module.__esModule ?
			function getDefault() { return module['default']; } :
			function getModuleExports() { return module; };
		__webpack_require__.d(getter, 'a', getter);
		return getter;
	};
	// Object.prototype.hasOwnProperty.call
	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
	// __webpack_public_path__
	__webpack_require__.p = "";
	// on error function for async loading
	__webpack_require__.oe = function(err) { console.error(err); throw err; };
	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]);
	var parentJsonpFunction = oldJsonpFunction;
	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
([
/* 0 */
	(function(module, exports, __webpack_require__) {

btn.addEventListener('click',()=>{
    __webpack_require__.e(/* import() | title */ 1).then(__webpack_require__.t.bind(null, 1, 7)).then((result)=>{
        console.log(result);
    })
})

	})
]);

複製程式碼
title.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    
    [1],
    [
        /* 0 */,
        /* 1 */
        /***/ (function(module, exports) {

        module.exports = "title"

        /***/ })
    ]
]);
複製程式碼