转载

webpack2异步加载套路

webpack 提供的一个非常强大的功能就是 code spliting(代码切割)

webpack 1.x 中提供了

require.ensure([], () => {
        let module = require('./page1/module');
        // do something
    }, 'module1')

利用 require.ensure 这个 API 使得 webpack 单独将这个文件打包成一个可以异步加载的 chunk .

具体的套路见我写的另一篇blog: webpack分包及异步加载套路

一句话总结就是:

在输出的 runtime 代码中,包含了异步 chunkidchunk name 的映射关系。需要异步加载相应的 chunk 时,通过生成 script 标签,然后插入到 DOM 中完成 chunk 的加载。通过 JSONP , runtime 中定义好函数, chunk 加载完成后即会立即执行这个函数。

从编译生成后的代码来看, webpack 1.xchunk 的加载到执行的过程处理的比较粗糙,仅仅是通过添加 script 标签,异步加载 chunk 后,完成函数的执行。

这个过程当中,如果出现了 chunk 加载不成功时,这种情况下应该如何去容错呢?

webpack2 中相比于 webpack1.x 在这个点的处理上是将 chunk 的加载包裹在了 promise 当中,那么这个过程变的可控起来。具体的 webpack2 实现套路也是本文想要去说明的地方。

webpack 提供的异步加载函数是

/******/     // This file contains only the entry chunk.
/******/     // The chunk loading function for additional chunks
            // runtime代码里面只包含了入口的chunk
            // 这个函数的主要作用:
            // 1. 异步加载chunk
            // 2. 提供对于chunk加载失败或者处于加载中的处理
            // 其中chunk加载状态的判断是根据installedChunks对象chunkId是数字0还是数组来进行判断的
/******/     __webpack_require__.e = function requireEnsure(chunkId) {
                // 数字0代表chunk加载成功
/******/         if(installedChunks[chunkId] === 0)  
/******/             return Promise.resolve();

/******/         // an Promise means "currently loading".
                // 如果installedChunks[chunkId]为一个数组
/******/         if(installedChunks[chunkId]) {
                    // 返回一个promise对象
/******/             return installedChunks[chunkId][2];
/******/         }
/******/         // start chunk loading
                // 通过生成script标签来异步加载chunk.文件名是根据接受的chunkId来确认的
/******/         var head = document.getElementsByTagName('head')[0];
/******/         var script = document.createElement('script');
/******/         script.type = 'text/javascript';
/******/         script.charset = 'utf-8';
/******/         script.async = true;
                // 超时时间为120s
/******/         script.timeout = 120000;

/******/         if (__webpack_require__.nc) {
/******/             script.setAttribute("nonce", __webpack_require__.nc);
/******/         }
                // 需要加载的文件名
/******/         script.src = __webpack_require__.p + "js/register/" + ({"2":"index"}[chunkId]||chunkId) + ".js";
                // 120s的定时器,超时后触发onScriptComplete回调
/******/         var timeout = setTimeout(onScriptComplete, 120000);
                // chunk加载完毕后的回调
/******/         script.onerror = script.onload = onScriptComplete;
/******/         function onScriptComplete() {
/******/             // avoid mem leaks in IE.
/******/             script.onerror = script.onload = null;
                    // 清空定时器
/******/             clearTimeout(timeout);
                    // 获取这个chunk的加载状态
                    // 若为数字0,表示加载成功
                    // 若为一个数组, 调用数组的第2个元素(第二个元素为promise内传入的reject函数),使得promise捕获抛出的错误。reject(new Error('xxx'))
/******/             var chunk = installedChunks[chunkId];
/******/             if(chunk !== 0) {
/******/                 if(chunk) chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
/******/                 installedChunks[chunkId] = undefined;
/******/             }
/******/         };
                
                // 每次需要进行异步加载chunk时,会将这个chunk的加载状态进行初始化为一个数组,并以key/value的形式保存在installedChunks里
                // 这个数组为[resolve, reject, promise];
/******/         var promise = new Promise(function(resolve, reject) {
/******/             installedChunks[chunkId] = [resolve, reject];
/******/         });
/******/         installedChunks[chunkId][2] = promise;

/******/         head.appendChild(script);
                //返回promise
/******/         return promise;
/******/     };

我们再来看看路由配置文件编译后生成的代码 index.js , 特别注意下 __webpack_require__.e 这个异步加载函数:

Router
.home('path1')
.addRoute({
    path: 'path1',
    animate: 'zoomIn',
    viewBox: '.public-path1-container',
    template: __webpack_require__(5),
    //  挂载controller
    pageInit: function pageInit() {
        var _this = this;

        console.time('route async path1');
        // 异步加载0.js(这个文件是webpack通过code spliting自己生成的文件名)
        // 具体异步加载代码的封装见:point_up_2:分析
        // 其中0.js包含了包含了path1这个路由下的业务代码
        // __webpack_require__.e(0) 起的作用仅为加载chunk以及提供对于chunk加载失败错误的抛出
        // 具体的业务代码的触发是通过__webpack_require_e(0).then(__webpack_require__.bind(null, 8)).then(function(module) { ... })进行触发
        // __webpack_require__.bind(null, 8) 返回的是module[8]暴露出来的module
        // 这段代码执行时,首先初始化一个module对象
        // module = {
        //        i: moduleId,  // 模块id
        //        l: false,     // 加载状态
        //        exports: {}   // 需要暴露的对象
        //    }
        // 通过异步加载的chunk最后暴露出来的对象是作为了module.exports.default属性
        // 因此在第二个方法中传入的对象的default属性才是你模块8真正所暴露的对象
        __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 8)).then(function (module) {
            var controller = module.default;
            Router.registerCtrl('path1', new controller(_this.viewBox));
        // 添加错误处理函数,用以捕获前面可能抛出的错误
        }).catch(function (e) {
            return console.log('chunk loading failed');
        });
    },

    //  进入路由跳转之前
    beforeEnter: function beforeEnter() {},

    //  路由跳转前
    beforeLeave: function beforeLeave() {}
})
.addRoute({
    path: 'path2',
    viewBox: '.public-path2-container',
    animate: 'zoomIn',
    template: __webpack_require__(6),
    pageInit: function pageInit() {
        var _this2 = this;

        __webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 9)).then(function (module) {
            console.time('route async path2');
            var controller = module.default;
            Router.registerCtrl('path2', new controller(_this2.viewBox));
        }).catch(function (e) {
            return console.log('chunk loading failed');
        });
    },
    beforeEnter: function beforeEnter() {},
    beforeLeave: function beforeLeave() {}
});

Router.bootstrap();

总结一下就是:

webpack2 相比于 webpack1.x 将异步加载 chunk 的过程封装在了 promise 当中,如果 chunk 加载超时或者失败会抛出错误,这时我们可以针对抛出的错误做相应的错误处理。

此外还应该注意下, webpack2 异步加载 chunk 是基于原生的 promise 。如果部分环境暂时还不支持原生 promise 时需要提供 polyfill 。另外就是 require.ensure 可以接受第三个参数用以给 chunk 命名,但是 import 这个 API 没有提供这个方法

更多的细节大家可以运行 demo 看下编译后的代码

原文  https://segmentfault.com/a/1190000008279471
正文到此结束
Loading...