资讯专栏INFORMATION COLUMN

webpack模块化原理-Code Splitting

Winer / 2084人阅读

摘要:的模块化不仅支持和,还能通过实现模块的动态加载。根据官方文档,实现动态加载的方式有两种和。如果你对如何实现和感兴趣,可以查看我的前两篇文章模块化原理和模块化原理。

webpack的模块化不仅支持commonjs和es module,还能通过code splitting实现模块的动态加载。根据wepack官方文档,实现动态加载的方式有两种:importrequire.ensure

那么,这篇文档就来分析一下,webpack是如何实现code splitting的。

PS:如果你对webpack如何实现commonjs和es module感兴趣,可以查看我的前两篇文章:webpack模块化原理-commonjs和webpack模块化原理-ES module。

准备

首先我们依然创建一个简单入口模块index.js和两个依赖模块foo.jsbar.js

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

webpack配置如下:

var path = require("path");

module.exports = {
    entry: path.join(__dirname, "index.js"),
    output: {
        path: path.join(__dirname, "outs"),
        filename: "index.js",
        chunkFilename: "[name].bundle.js"
    },
};

这是一个最简单的配置,指定了模块入口和打包文件输出路径,值得注意的是,这次还指定了分离模块的文件名[name].bundle.js(不指定会有默认文件名)。

在根目录下执行webpack,得到经过webpack打包的代码如下(去掉了不必要的注释):

(function(modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
    var parentJsonpFunction = window["webpackJsonp"];
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
        // add "moreModules" to the modules object,
        // then flag all "chunkIds" as loaded and fire callback
        var moduleId, chunkId, i = 0, resolves = [], result;
        for(;i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            if(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(chunkIds, moreModules, executeModules);
        while(resolves.length) {
            resolves.shift()();
        }
    };
    // The module cache
    var installedModules = {};
    // objects to store loaded and loading chunks
    var installedChunks = {
        2: 0
    };
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = function requireEnsure(chunkId) {
        var installedChunkData = installedChunks[chunkId];
        if(installedChunkData === 0) {
            return new Promise(function(resolve) { resolve(); });
        }
        // a Promise means "currently loading".
        if(installedChunkData) {
            return installedChunkData[2];
        }
        // setup Promise in chunk cache
        var promise = new Promise(function(resolve, reject) {
            installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        installedChunkData[2] = promise;
        // start chunk loading
        var head = document.getElementsByTagName("head")[0];
        var script = document.createElement("script");
        script.type = "text/javascript";
        script.charset = "utf-8";
        script.async = true;
        script.timeout = 120000;
        if (__webpack_require__.nc) {
            script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.src = __webpack_require__.p + "" + ({"0":"foo","1":"bar"}[chunkId]||chunkId) + ".bundle.js";
        var timeout = setTimeout(onScriptComplete, 120000);
        script.onerror = script.onload = onScriptComplete;
        function onScriptComplete() {
            // avoid mem leaks in IE.
            script.onerror = script.onload = null;
            clearTimeout(timeout);
            var chunk = installedChunks[chunkId];
            if(chunk !== 0) {
                if(chunk) {
                    chunk[1](new Error("Loading chunk " + chunkId + " failed."));
                }
                installedChunks[chunkId] = undefined;
            }
        };
        head.appendChild(script);
        return promise;
    };
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if(!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ?
            function getDefault() { return module["default"]; } :
            function getModuleExports() { return module; };
        __webpack_require__.d(getter, "a", getter);
        return getter;
    };
    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    // __webpack_public_path__
    __webpack_require__.p = "";
    // on error function for async loading
    __webpack_require__.oe = function(err) { console.error(err); throw err; };
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
([
(function(module, exports, __webpack_require__) {
    "use strict";
    __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(foo => {
        console.log(foo());
    })
    __webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 2)).then(bar => {
        console.log(bar());
    })
})
]);
分析

编译后的代码,整体跟前两篇文章中使用commonjs和es6 module编写的代码编译后的结构差别不大,都是通过IFFE的方式启动代码,然后使用webpack实现的requireexports实现的模块化。

而对于code splitting的支持,区别在于这里使用__webpack_require__.e实现动态加载模块和实现基于promise的模块导入。

所以首先分析__webpack_require__.e函数的定义,这个函数实现了动态加载:

__webpack_require__.e = function requireEnsure(chunkId) {
    // 1、缓存查找
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData === 0) {
        return new Promise(function(resolve) { resolve(); });
    }
    if(installedChunkData) {
        return installedChunkData[2];
    }
    // 2、缓存模块
    var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    installedChunkData[2] = promise;
    // 3、加载模块
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.charset = "utf-8";
    script.async = true;
    script.timeout = 120000;
    if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
    }
    script.src = __webpack_require__.p + "" + ({"0":"foo"}[chunkId]||chunkId) + ".bundle.js";
    // 4、异常处理
    var timeout = setTimeout(onScriptComplete, 120000);
    script.onerror = script.onload = onScriptComplete;
    function onScriptComplete() {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if(chunk !== 0) {
            if(chunk) {
                chunk[1](new Error("Loading chunk " + chunkId + " failed."));
            }
            installedChunks[chunkId] = undefined;
        }
    };
    head.appendChild(script);
    // 5、返回promise
    return promise;
};

代码大致逻辑如下:

缓存查找:从缓存installedChunks中查找是否有缓存模块,如果缓存标识为0,则表示模块已加载过,直接返回promise;如果缓存为数组,表示缓存正在加载中,则返回缓存的promise对象

如果没有缓存,则创建一个promise,并将promiseresolvereject缓存在installedChunks

构建一个script标签,append到head标签中,src指向加载的模块脚本资源,实现动态加载js脚本

添加script标签onload、onerror 事件,如果超时或者模块加载失败,则会调用reject返回模块加载失败异常

如果模块加载成功,则返回当前模块promise,对应于import()

以上便是模块加载的过程,当资源加载完成,模块代码开始执行,那么我们来看一下模块代码的结构:

webpackJsonp([0],[
/* 0 */,
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
exports.foo = function () {
    return 2;
}
/***/ })
]);

可以看到,模块代码不仅被包在一个函数中(用来模拟模块作用域),外层还被当做参数传入webpackJsonp中。那么这个webpackJsonp函数的作用是什么呢?

其实这里的webpackJsonp类似于jsonp中的callback,作用是作为模块加载和执行完成的回调,从而触发importresolve

具体细看webpackJsonp代码来分析:

window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    var moduleId, chunkId, i = 0, resolves = [], result;
    // 1、收集模块resolve
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    // 2、copy模块到modules
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
    // 3、resolve import
    while(resolves.length) {
        resolves.shift()();
    }
};

代码大致逻辑如下:

根据chunkIds收集对应模块的resolve,这里的chunkIds为数组是因为require.ensure是可以实现异步加载多个模块的,所以需要兼容

把动态模块添加到IFFE的modules中,提供其他CMD方案使用模块

直接调用resolve,完成整个异步加载

总结

webpack通过__webpack_require__.e函数实现了动态加载,再通过webpackJsonp函数实现异步加载回调,把模块内容以promise的方式暴露给调用方,从而实现了对code splitting的支持。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88846.html

相关文章

  • webpack学习(四)— code splitting

    摘要:支持定义分割点,通过进行按需加载。若按照中做,则会造成通用模块重复打包。下文将详细说明。同样是利用和来处理的。如下在中添加入口其中模块为通用功能模块在中对应和这样则会打包出和两个文件。为通用功能模块。希望有更好方案的同学能够不吝赐教。 什么是code splitting 首先说,code splitting指什么。我们打包时通常会生成一个大的bundle.js(或者index,看你如...

    lsxiao 评论0 收藏0
  • 代码分割与懒加载情况下(code-splitting+lazyload)抽离懒加载模块的公用模块代码

    摘要:但是同时,抽离到父模块,也意味着如果有一个懒加载的路由没有用到模块,但是实际上引入了父模块,也为这也引入了的代码。 前言 我们清楚,在 webpack 中通过CommonsChunkPlugin 可以将 entry 的入口文件中引用多次的文件抽离打包成一个公用文件,从而减少代码重复冗余 entry: { main: ./src/main.js, ...

    zebrayoung 评论0 收藏0
  • 翻译webpack3.5.5 - code splitting - 上半部分

    摘要:澄清一个共同的误解代码分离不仅仅是抽出公共代码把它们放进一个共享的块中。让我们来使用来移除这个重复的部分。插件将会注意到我们已经将分割成一个单独的块。并且从我们的主中删除了这部分。 对于大型web app来说,如果把所有的文件都打包到一个文件中是非常低效的,特别是当一些代码块只在某些特定的条件下被调用。webpack可以让你的代码库分割成不同的块(chucks),仅仅在需要的时候再加载...

    Bryan 评论0 收藏0
  • webpack Code Splitting浅析

    摘要:不知大家是不是跟大雄一样之前从未看过编译产出的代码。前文大雄给了一个粗陋的动态加载的方法说白了就是动态创建标签。大雄看完至少大概知道了原来编出来的代码是那样执行的原来可以那么灵活的使用。 Code Splitting是webpack的一个重要特性,他允许你将代码打包生成多个bundle。对多页应用来说,它是必须的,因为必须要配置多个入口生成多个bundle;对于单页应用来说,如果只打包...

    Amos 评论0 收藏0
  • webpack源码分析之二:code-splitting

    摘要:前言是最引人瞩目的特性之一此特性将代码分离到不同的文件中。功能分析官网上有三种方式实现入口起点使用选项手动分离代码。防止重复使用去重和分离。本质则是多个入口的,则在以为入口文件将多入口的切分为按切割文件通过加载。 前言 code-splitting是webpack最引人瞩目的特性之一,此特性将代码分离到不同的bundle文件中。详细介绍官网code-split,这次实现则在笔者上次文件...

    wudengzan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<