4

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自己生成的文件名)
        // 具体异步加载代码的封装见?分析
        // 其中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看下编译后的代码


苹果小萝卜
5.1k 声望356 粉丝

Github: [链接]