开发过程中,一般我们为了提高单页面应用响应速度,一般会使用webpack的import动态引入路由,那么webpack是怎样按需加载代码并且放到module里的呢?
我能想到的是动态加载script标签,但是执行完会影响全局环境,如果动态加载的script内容用立即执行函数包裹,那么其它模块用到它时又怎么取到模块导出的变量呢(两个IIFE函数相互隔离)?如果在考虑到使用到A的时候,按需加载B,同时B又要加载C,好难有么有,带着问题来看看webpack的实现:
先写个简单的webapck 配置(文中使用webpack5.0版本,使用npx 运行打包构建)
const path = require('path');
module.exports = {
entry: {
'index': './src/index.js'
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
},
plugins: [],
mode: 'none' // 设置为none 防止代码被uglify
}
我们先来看打包好的结构
(function () => {
var __webpack_modules__ = [];
var __webpack_module_cache__ = {};
function __webpack_require__ (moduleId) {
};
// 以下三个属性定义在编译完的代码中是用IIFE函数包裹的;为了看着方便省略了
__webpack_require__.d = (export, defination) => {};
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop));
__webpack_require__.r = (exports) => {};// 在exports对象上设置__esModule属性为true;
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
// 以下两行为 原import 部分的代码 例:import {add} from './add'
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
var _multiple__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
// dosomething(); // 入口模块的代码;
const setAddThenMulti = (a, b) => m => {
(0,_multiple__WEBPACK_IMPORTED_MODULE_1__.multiple)((0,_add__WEBPACK_IMPORTED_MODULE_0__.add)(a + b), m);
};
})();// 立即执行入口模块的代码
})()
里面比较重要的是require函数和modules里的成员结构,我们分别看一下:
__webpack_require__函数,整体逻辑是如果模块已加载过,则返回已被缓存的模块,否则将加载模块并将其加入到缓存,同时返回挂在好的模块访问对象
function __webpack_require__ (moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
__webpack_modules__里面的成员结构如下:
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
// 此处比较重要;在exports上挂在相应的属性; 打包前为 export const add
__webpack_require__.d(__webpack_exports__, {
"add": () => (/* binding */ add)
});
const add = (a, b) => a + b;
})
通过上面我们了解了一个非常简单的模块加载流程;
首先webpack会把非入口模块放到modules数组中;然后需要用到的时候通过require加载;并且只加载一次;后面会走缓存;那么按需加载的模块是如何进入这个流程呢?
我们再来看一下,代码中引入import();打包后发现按需引入的模块单独放到了一个文件中;并且打包后的原文件在require函数上也多了以下属性:
__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => {}; // 返回一个Promise
__webpack_require__.u = () => {}; // 生成异步模块的文件名
__webpack_require__.m = __webpack_modules__; // 暴露modules访问
__webpack_require__.g = (() => {})();
(() => {
var inProgress = {};
__webpack_require__.l = (url, done, key, chunkId) => {} // 动态创建script标签加载模块,里面做了如果模块在加载不会再次加载的逻辑
})();
(() => { // 提供加载模块的路径 与 __webpack_require__.u 拼接最终生成script的src
var scriptUrl;
// dosomething;
__webpack_require__.p = scriptUrl;
})();
(() => {
var installedChunks = {0: 0}; // 0 代表chunked已加载; 加载中的格式为{'chunckId': [resolve, reject, promise]}
__webpack_require__.f.j = (chunkId, promises) => {}
})();
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {};
var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
如下是引入出发流程: import('./multiple.js') 函数编译后为__webpack_require__.e(/* import() */ 1).then(_webpack_require_.bind(_webpack_require_, 2));
_webpack_require_.e会返回一个promise.all(),在所有模块加载完毕后会置为resolve状态,通过script加载完毕的代码会调用全局的webpackJsonpCallback函数来与内部模块沟通,调用push将自己加入到modules中;然后通过__webpack_require__即可拿到加载的代码模块的导出对象;
webpackJsonpCallback的代码
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [moduleIds, moreModules, runtime] = data;
var moduleId, chunkId, i = 0;
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId]; // 模块安装逻辑
}
}
if(runtime) runtime(__webpack_require__); // 暂时不确定,先不写注释
if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);// 暂时不确定,先不写注释
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0](); // promise 状态确定的逻辑,这个逻辑当时找了一会
}
installedChunks[chunkIds[i]] = 0; // 标注为已加载
}
}
被按需加载的模块的代码
(self["webpackChunk"] = self["webpackChunk"] || []).push(
[[1],
{ 2:
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"multiple": () => (/* binding */ multiple)
});
const multiple = (a, b) => a * b;
})
}]);
整个流程线先整理这么多,后面更复杂的场景和未看明白的逻辑后面继续研究;
看代码中少用的API
Symbol.toStringFlag 为对象添加类型标识属性 [developer.mozilla.org/zh-CN/docs/…]