webpack之模块异步加载

1,988 阅读3分钟

1、准备代码

//foo.js
'use strict';
exports.foo = function foo() {
    return 'foo';
}
//index.js
'use strict';
import(/* webpackChunkName: "foo" */ './foo').then(foo => {
    console.log(foo);
});

webpack配置文件

var path = require("path");
module.exports = {
    mode: 'development',
    entry: path.join(__dirname, './src/index.js'),
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'index.js',
        chunkFilename: '[name].bundle.js', //动态加载模块名称
        publicPath: path.join(__dirname, 'dist/'), //动态加载模块路径
    },
};

打包后的到的foo.bundle.js

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    ["foo"],
    {
        "./src/foo.js":
        (function(module, exports, __webpack_require__) {
            "use strict";
            eval("\nexports.foo = function foo() {\n    return 'foo';\n}\n\n\n//# sourceURL=webpack:///./src/foo.js?");
        })
    }
]);

打包后的index.js

(function(modules) { 
    ...
})({
    "./src/index.js":
    (function(module, exports, __webpack_require__) {
    "use strict";
    eval("\n\n__webpack_require__.e(/*! import() | foo */ \"foo\").then(__webpack_require__.t.bind(null, /*! ./foo */ \"./src/foo.js\", 7)).then(foo => {\n    console.log(foo);\n});\n\n//# sourceURL=webpack:///./src/index.js?");
    })
});

2、分析

index.js

import(/* webpackChunkName: "foo" */ './foo').then(foo => {
    console.log(foo);
});

打包后index.js

`__webpack_require__.e(/*! import() | foo */ "foo").then(__webpack_require__.t.bind(null, /*! ./foo */ "./src/foo.js", 7)).then(foo => {
    console.log(foo);
})

__webpack_require__.e实现异步加载模块。实际使用script标签加载foo.bundle.js文件,返回promise。

__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];

    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 means "already installed".

        // a Promise means "currently loading".
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            // setup Promise in chunk cache
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            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);

            // create error before stack unwound to get useful stacktrace later
            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);
};

foo.bundle.js文件加载完后,执行内部代码

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    ['foo'], 
    {"./src/foo.js": (function(module, exports, __webpack_require__) {...})}
]);

window["webpackJsonp"]执行的push方法。在打包后的index.js中,push被改写成webpackJsonpCallback方法

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

installedChunks是缓存chunk,注册模块到modules变量中。 在webpackJsonpCallback中调用resolve,更新异步加载的状态

function webpackJsonpCallback(data) {
   	var chunkIds = data[0];
   	var moreModules = data[1];

   	// add "moreModules" to the modules object,
   	// then flag all "chunkIds" as loaded and fire callback
   	var moduleId, chunkId, i = 0, resolves = [];
   	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;
   	}
   	for(moduleId in moreModules) {
   	    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
   	        modules[moduleId] = moreModules[moduleId];
   	    }
   	}
   	
   	if(parentJsonpFunction) parentJsonpFunction(data);
   	
   	while(resolves.length) {
   	    resolves.shift()();//更改异步加载的状态
   	}
};

3、疑惑分析

1、我们知道 import('./foo')会返一个 Promise 实例 promise,在 webpack 打包出来的最终文件中是如何处理这个 promise 的?

在加载 foo.bundle.js 之前会在全局 installedChunks 中先存入了一个 promise 对象。__webpack_require__.e执行中

installedChunks[chunkId] = [resolve, reject, promise]

resolve 这个值在 webpackJsonpCallback 中会被用到,这时就会进入到我们写的 import('./foo').then() 的 then 语句中了。

2、在 index.js 中处理 webpackJsonp 过程中还有一段特殊的逻辑

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;

也就是说如果之前已经存在全局的 window["webpackJsonp"] 那么在替换其 push 函数之前会将原有的 push 方法保存为 oldJsonpFunction,同时将已存在于 window["webpackJsonp"] 中的内容,一一执行 webpackJsonpCallback。并且在 webpackJsonpCallback 中也将异步加载的内容也会在 parentJsonpFunction 中同样执行一次

if(parentJsonpFunction) parentJsonpFunction(data);

总结

1、__webpack_require__.e

  • 根据 installedChunks 检查是否加载过该 chunk
  • 假如没加载过,则发起一个 JSONP 请求去加载 chunk
  • 设置一些请求的错误处理,然后返回一个 Promise。

3、window["webpackJsonp"]

用自定义的 webpackJsonpCallback 函数替换了 window["webpackJsonp"] 的 push 方法。所以说,我们之前 foo.js 执行的 push 其实就是执行了自定义的 webpackJsonpCallback 函数

2、webpackJsonpCallback

可以看到,webpackJsonpCallback 做了2件事情。

  • 执行 installedChunks 中的 resolve , 让 import() 得以继续执行。
  • 将 chunk 中含有的 模块全部注册到 modules 变量中。