最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • webpack | 动态导入语法import

    正文概述 掘金(南方小菜)   2020-11-27   549

    前言

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

    目标

    理解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"
    
            /***/ })
        ]
    ]);
    

    起源地下载网 » webpack | 动态导入语法import

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元