栗子🌰
我们在 html 页面上加一个 load 按钮,点击时动态加载模块。
<!-- ./public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="main.js"></script></head>
<body>
<button id="load">load</button>
</body>
</html>
/** ./src/index.js **/ //打包后变成 main.js
const load = document.getElementById('load');
load.addEventListener('click', ()=>{
import(/* webpackChunkName: 'title' */'./title').then(result => {
console.log(result.default);
})
})
/** ./src/title.js **/
module.exports = 'title';
在这个例子中,每点击一次按钮,就会打印一遍 title.js 中导出的内容。
打包后与以往不同,会生成两个打包文件,如下:
main.js 源码
模块定义
模块定义这边,我们在 require 上新增一个 m 参数用来保存所有的 modules。
模块定义刚开始是空的,因为引入的 title.js 还没有被加载进来。
var modules = {};
require.m = modules;
require 方法和 cache
require 方法和 cache 还是老样子,如下:
var modules = {};
var cache = {};
function require(moduleId){
var cacheModule = cache[moduleId];
if(cacheModule){
return cacheModule.exports;
}
var module = cache[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.m = modules;
主流程
接下来是我们的主流程(也就是我们加载模块的过程)。思路如下:
- 懒加载或者说动态加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。
- 代码的主是一块模块,每个代码块里面又有诺干个模块
- 通过 require.e(‘title’) 动态加载 title 代码块,通过 jsonp加载 title.main.js 文件,取得对应的代码块,然后把代码块里的模块定义合并到当前文件的 modules 里。
然后通过 require 加载 ‘./src/title.js’ 模块,得到返回值,不管原来是 commonjs 还是 es,都会转成 es。 ```javascript var modules = {}; var cache = {}; function require(moduleId){ var cacheModule = cache[moduleId]; if(cacheModule){ return cacheModule.exports; }
var module = cache[moduleId] = { exports: {} };
modulesmoduleId; return modules.exports; } require.m = modules;
const load = document.getElementById(‘load’); load.addEventListener(‘click’, () => { // 懒加载或者说动态加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。 // 代码的主是一块模块,每个代码块里面又有诺干个模块 // 通过 require.e(‘title’) 动态加载 title 代码块,通过 jsonp加载 title.main.js 文件,取得对应的代码块, // 然后把代码块里的模块定义合并到当前文件的 modules 里。 // 然后通过 require 加载 ‘./src/title.js’ 模块,得到返回值,不管原来是 commonjs 还是 es,都会转成 es。 require.e(“title”).then(require.bind(require, “./src/title.js”)).then(result => { console.log(result.default); }) })
<a name="maSxo"></a>
### require.e 方法
此方法的主要作用是调用加载函数,然后返回一个 promise 对象。
1. 声明一个 promise 的空数组 promises。
1. 调用 require.f.j 方法给 promises 赋值。
1. 通过 Promise.all 执行所有 promises,并且返回一个 promise 对象。
```javascript
require.e = (chunkId) => {
var promises = [];
require.f.j(chunkId, promises);
return Promise.all(promises);
}
installedChunks 对象
在使用 require.f.j 方法前,必须定义一个 installedChunks 对象来存储已经安装或者说已经加载好的模块。
- key 是代码块的名字,入口默认是 main。
- 值 0 表示已经就绪,加载完成
- 如果没有加载完成(值不是 0)的话,是一个数组,第一个是 promise 的 resolve 方法,第二个是 reject 方法,第三个是 promise 本身。 ```javascript // chunkId title promises=[] // 已经安装好的或者说加载好的代码块 // key代码块的名字,入口默认是main // 值0表示已经就绪 加载完成
var installedChunks = { main: 0, // title: [resolve, reject, promise] }
<a name="UD4jt"></a>
### require.f.j 方法
f 是一个 对象,包含了一些处理数据的方法,j 方法就是实现 jsonp 的方法。
1. 创建一个 installedChunkData 数组,后面用来保存 installedChunks 中的 value 对象。
1. 创建一个 promise 对象,将resolve, reject 赋值给 installedChunkData 的 第 1,2 个元素
1. 然后再把 promise 对象放入到 promises 数组中,并且把 installedChunkData 的第 3 个元素赋值为这个promise对象。
1. 通过 require.p 和 require.u 生成资源的路径。
1. 通过 require.l 加载资源。
```javascript
require.f = {};
require.f.j = (chunkId, promises) => {
var installedChunkData;
var promise = new Promise((resolve, reject)=>{
installedChunkData = installedChunks[chunkId] = [resolve, reject]
})
promises.push(installedChunkData[2] = promise);
var url = require.p + require.u(chunkId);
require.l(url);
}
require.p 方法
获取文件的路径,默认是 /
require.p = '/';
require.u 方法
获取文件名
require.u = chunkId => chunkId + '.main.js';
require.l 方法
主要是用来生成 script 标签,并且引入动态导入的资源路径,如下:
require.l = (url) => {
var script = document.createElement('script');
script.src = url;
document.head.append(script);
}
挂载在window 上的方法
在执行 require.l 方法后,会立刻执行导入的代码,这块导入模块会调用 window 上的一个方法,实现加载完成后的回调,这个方法需要我们自己定义,如下:
- 创建一个 chunkLoadingGlobal 全局方法,把它挂在window 的 webpackChunkt4 属性上,这个属性名是自动生成的。
- 重写 chunkLoadingGlobal 的 push 方法。
push 方法接收一个数组为参数,这个数组固定只接收两个值
- chunkIds 第一个元素,表示 chunkId 的数组。
- moreModules 第二个元素,代表模块的定义,与主模块的定义是一样的。最终我们需要把moreModules 中的模块定义,与主模块的 modules 合并。 ```javascript var webpackJsonpCallback = (data) => { var [chunkIds, moreModules] = data; var moduleId, chunkId, i = 0, resolves = []; // 把返回的模块定义合并到当前的模块定义对象 modules 里 for (moduleId in moreModules) { modules[moduleId] = moreModules[moduleId]; }
for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; resolves.push(installedChunks[chunkId][0]); installedChunks[chunkId] = 0; }
while (resolves.length) { resolves.shift()(); } }
var chunkLoadingGlobal = window[“webpackChunkt4”] = []; chunkLoadingGlobal.push = webpackJsonpCallback;
<a name="lAo0k"></a>
## title.main.js 源码
```javascript
window["webpackChunkt4"].push([["title"], {
"./src/title.js":
((module) => {
module.exports = 'title';
})
}]);
这块代码就是动态导入的模块,调用 main.js 中的 webpackChunkt4 属性,实现了回调。
整理最终代码
上面的代码比较零碎,我们整理下,
/** main.js **/
var modules = {};
var cache = {};
function require(moduleId){
var cacheModule = cache[moduleId];
if(cacheModule){
return cacheModule.exports;
}
var module = cache[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.m = modules;
require.f = {};
require.p = '/';
require.u = chunkId => chunkId + '.main.js';
require.l = (url) => {
var script = document.createElement('script');
script.src = url;
document.head.append(script);
}
var installedChunks = {
main: 0,
// title: [resolve, reject, promise]
}
require.f.j = (chunkId, promises) => {
var installedChunkData;
var promise = new Promise((resolve, reject)=>{
installedChunkData = installedChunks[chunkId] = [resolve, reject]
})
promises.push(installedChunkData[2] = promise);
var url = require.p + require.u(chunkId);
require.l(url);
}
require.e = (chunkId) => {
var promises = [];
require.f.j(chunkId, promises);
return Promise.all(promises);
}
var webpackJsonpCallback = (data) => {
var [chunkIds, moreModules] = data;
var moduleId, chunkId, i = 0, resolves = [];
// 把返回的模块定义合并到当前的模块定义对象 modules 里
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
resolves.push(installedChunks[chunkId][0]);
installedChunks[chunkId] = 0;
}
while (resolves.length) {
resolves.shift()();
}
}
var chunkLoadingGlobal = window["webpackChunkt4"] = [];
chunkLoadingGlobal.push = webpackJsonpCallback;
var load = document.getElementById('load');
load.addEventListener('click', () => {
// 懒加载或者说动态加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。
// 代码的主是一块模块,每个代码块里面又有诺干个模块
// 通过 require.e('title') 动态加载 title 代码块,通过 jsonp加载 title.main.js 文件,取得对应的代码块,
// 然后把代码块里的模块定义合并到当前文件的 modules 里。
// 然后通过 require 加载 ‘./src/title.js’ 模块,得到返回值,不管原来是 commonjs 还是 es,都会转成 es。
require.e("title").then(require.bind(require, "./src/title.js")).then(result => {
console.log(result);
})
})
/** title.main.js **/
window["webpackChunkt4"].push([["title"], {
"./src/title.js":
((module) => {
module.exports = 'title';
})
}]);
简化代码
生成的源码中 方法定义的比较多,我们简化一下
var modules = ({});
var cache = {};
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = cache[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.m = modules;
require.f = {};
// chunkId title promises=[]
// 已经安装好的或者说加载好的代码块
// key代码块的名字,入口默认是main
// 值0表示已经就绪 加载完成
var installedChunks = {
main: 0,
// title: [resolve, reject, promise]
}
require.l = (url) => {
var script = document.createElement('script');
script.src = url;
document.head.append(script);
}
require.e = (chunkId) => {
let installedChunkData;
let promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
var url = chunkId + '.main.js'; // /title.main.js
require.l(url);
return promise;
}
var webpackJsonpCallback = (data) => {
let [chunkIds, moreModules] = data;
// 把返回的模块定义合并到当前的模块定义对象 modules 里
for (let moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
for (let i = 0; i < chunkIds.length; i++) {
let chunkId = chunkIds[i];
let resolve = installedChunks[chunkId][0];
installedChunks[chunkId] = 0;
resolve(); // 调用 promise 的 resolve 方法让 promise成功
}
}
var chunkLoadingGlobal = window["webpackChunkt4"] = [];
chunkLoadingGlobal.push = webpackJsonpCallback
const load = document.getElementById('load');
load.addEventListener('click', () => {
// 懒加载或者说董涛加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。
require.e("title").then(require.bind(require, "./src/title.js")).then(result => {
console.log(result);
})
})
实现效果
点击 load ,在 network 中就会加载 title.main.js 资源
同时会打印title