webpack 按需加载原理探索之路(一)

826 阅读3分钟

开发过程中,一般我们为了提高单页面应用响应速度,一般会使用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/…]