likes
comments
collection
share

webpack | 动态导入语法import

作者站长头像
站长
· 阅读数 10

前言

  • 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段】达到本文目标,若使诸君稍有启发,不枉此文心力^-^

目标

理解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"

        /***/ })
    ]
]);