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

319 阅读2分钟

承接上文,先看下上次没搞明白的两个点,然后根据读到的源码写个简易版的异步加载;

上次代码中webpackJsonpCallck中有两个地方如下

webpackJsonCallback = (parentChunkLoadingFunction, data) {
    var [modules, moreModules, runtime] = data;
    // 。。。 此处省略模块安装
    if (runtime) runtime(__webpack_require__);
    if (parentChunkLoadingFunction) parentChunkLoadingFunction(data) 
    // 。。。 此处省略script loading resolve
}

这个函数的调用用到了柯里化,填充参数分为了两步;

  • 情况一: runtime先加载 step1:
chunkLoadingGloabl.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

step2(async module invoke):

// 作为入口的模块打包的时候会传入runtime,开始执行
(chunkLoadingGlobal.push([[chunkId], [modules], ?runtime]))();
  • 情况二: 模块先加载 从webpack4开始运行时模块可以在其他模块后加载的,详情可以点击链接;
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));

来看下chunkLoadingGloabl的结构:[chunkIds, moreModules, ?runtime];

  • chunkIds(加载的chunkId)用于resolve对应chunk load 的 Promise;
  • moreModules 存储实际模块内容,用于安装;
  • runtime用于执行入口模块; webpack4可以在控制台打印webpackJsonp查看,webpack5 打印 webpackChunkwebpack_build 查看;

然后我们来实现一个低配版模块加载,webpack 这段代码应该是解析完后生成的,解析过程还未深入了解,大家可以参考这个老哥写的babel import语法 js_深入原理:Babel原理与Babel插件实践; 所以先实现主要的模块加载,类似amd加载方式:

下面是一个简陋版本的实现:

(function (global) {
    let modules = {};
    let pathMap = {};
    function define (deps, fn) {
        if (typeof deps === 'function') {
            modules = {...modules, ...deps()};
            return
        }
        for (let [index, item] of deps.entries()) {
            if (modules[item]) {
                deps[index] = Promise.resolve(modules[item])
            } else {
                let structrue = [];
                deps[index] = new Promise((resolve, reject) => {
                    structrue = [() =>resolve(modules[item]), reject]
                });
                loadScript(item, structrue);
            }
        }
        Promise.all(deps).then(res => {
            let result = fn.apply(null, res);
            if (result) {
                modules = {...modules, ...result}
            }
        })
    }

    // 用于配置模块加载路径 
    define.config = function (config) {
        pathMap = config.paths
    }

    let inProgress = {};
    function loadScript (id, structure) {
        const url = pathMap[id];
        if (inProgress[url]) {
            // 该模块已经请求中;
            inProgress[url].push(structure);
            return;
        }
        const script = document.createElement('script');
        document.body.append(script);
        script.src = url;
        inProgress[url] = [structure];
        script.onload = function () {
            let callbacks = inProgress[url];
            delete inProgress[url];
            callbacks.forEach(([resolve, reject]) => {
                resolve();
            });
            document.body.removeChild(script);
        }
        script.onerror = function (err) {
            let callbacks = inProgress[url];
            delete inProgress[url];
            callbacks.forEach(([resolve, reject]) => {
                reject(err);
            });
            document.body.removeChild(script);
        }
    }
    global.define = define;
})(window);

使用方式如下:

<script src="./module.js"></script>
<script>
define.config({
    paths: {mutilple: './multiple.js'}
});

define(['mutilple'], function (mutilple) {
    console.log(mutilple);
    console.log(mutilple(2, 3));
})
</script>

感觉看了编译完的代码主要只是感觉这个promise 的 resolve 和 reject 还可以传到别的地方去执行,这个挺有趣;

好了,整体分析到此结束,其实看编译完的代码和源码,揣测作者的真实意图还是不太容易;有问题或者错误的地方大家可以留言讨论;