webpack | 动态导入语法import
前言
- 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段】达到本文目标,若使诸君稍有启发,不枉此文心力^-^
目标
理解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收集用户
思路
-
触发时将异步模块以jsonp进行引入,此处必然是异步,所以需要利用promise进行订阅发布,先订阅收集内部对引入模块的处理
-
在引入模块的代码执行时完成模块的安装(加入到模块源上),同时发布模块处理操作获得模块返回值(调用对应promise的resolve方法)
在动态引入的模块中需实现
- 将被引入模块标为已加载,并记录此异步引入模块
- 将被引入模块对象存入初始的modules中,统一管理
- 执行用户逻辑(即将promise标为成功态,从而执行then中的回调)
-
将返回值交给用户定义函数,完成引入
具体实现
定义方法 __webpack_require__.e
其核心是通过jsonp(创建script标签)加载动态引入代码,其次两点 缓存 + 异步 ;
- installedChunks对象用于记录模块状态,实现缓存
// 用于存放加载过的和加载中的代码块
// key :代码块名 chunkId
// value : undefined 未加载 null 预加载 Promise 代码块加载中 0 加载完成
var installedChunks = {
0: 0
};
- 若模块未加载,则通过chunkId拼接src并创建script标签异步加载模块
// 根据模块名获得引入路径
script.src = jsonpScriptSrc(chunkId);
。。。
// 将脚本插入文档 开始获取依赖模块内容
document.head.appendChild(script);
- 动态引入的模块中需实现逻辑,
- 因为要记录,而且可能有多个异步引入模块,所以可以采用数组;
- 因为在记录的同时还有执行【存入初始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()();
}
};
- 在异步加载的模块中,就会执行webpackJsonp的push方法(其实是webpackJsonpCallback方法),从而完成安装和引入,至此,我们的模块源modules找那个就有了我们的title模块;下一步只要复用之前逻辑,进行模块的安装就好
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
// chunkId
[1],
[
// 占位 代表main模块
/* 0 */,
/* 1 */ // title模块对应的模块安装函数
/***/ (function(module, exports) {
module.exports = "title"
/***/ })
]
]);
- 此时,我们的modules上已经有异步加载的模块信息了,该以什么方式导出呢(commonJs还是es),webpack中采用了默认es但也支持commonJs的方式,此逻辑在
__webpack_require__.t
中实现,__webpack_require__.e
返回promise的回调中会执行t方法;在在实现此方法前,我们需要了解js的位运算&
A & B 先将A B转为二进制,如果两位数的同位都是 1 则设置每位为 1 否则为0;
要区别处理导出方式,自然要进行判断,在webpack中采用的就是位运算&;
十进制 | 二进制 | 判断优先级 | 为true时执行逻辑 |
---|---|---|---|
1 | 0001 | 1 | 执行__webpack_require__方法,进行模块安装 |
2 | 0010 | 4 | 将模块对象的属性和值拷贝到ns上 |
4 | 0100 | 3 | 会继续判断是不是es模块,如果是则直接返回,如果不是则向下执行定义一个es模块对象ns(此为默认返回值) |
8 | 1000 | 2 | 直接返回,注意判断此优先级是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"
/***/ })
]
]);
转载自:https://juejin.cn/post/6899640414446780430