资讯专栏INFORMATION COLUMN

webpack Code Splitting浅析

Amos / 2527人阅读

摘要:不知大家是不是跟大雄一样之前从未看过编译产出的代码。前文大雄给了一个粗陋的动态加载的方法说白了就是动态创建标签。大雄看完至少大概知道了原来编出来的代码是那样执行的原来可以那么灵活的使用。

Code Splitting是webpack的一个重要特性,他允许你将代码打包生成多个bundle。对多页应用来说,它是必须的,因为必须要配置多个入口生成多个bundle;对于单页应用来说,如果只打包成一个bundle可能体积很大,导致无法利用浏览器并行下载的能力,且白屏时间长,也会导致下载很多可能用不到的代码,每次上线用户都得下载全部代码,Code Splitting能够将代码分割,实现按需加载或并行加载多个bundle,可利用并发下载能力,减少首次访问白屏时间,可以只上线必要的文件。
三种Code Splitting方式

webpack提供了三种方式来切割代码,分别是:

多entry方式

公共提取

动态加载

本文将简单介绍多entry方式和公共提取方式,重点介绍的是动态加载。这几种方式可以根据需要组合起来使用。这里是官方文档,中文 英文

多entry方式

这种方式就是指定多个打包入口,从入口开始将所有依赖打包进一个bundle,每个入口打包成一个bundle。此方式特别适合多页应用,我们可以每个页面指定一个入口,从而每个页面生成一个js。此方式的核心配置代码如下:

const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    page1: "./src/page1.js",
    page2: "./src/page2.js"
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist")
  }
};

上边的配置最终将生成两个bundle, 即page1.bundle.js和page2.bundle.js。

公共提取

这种方式将公共模块提取出来生成一个bundle,公共模块意味着有可能有很多地方使用,可能导致每个生成的bundle都包含公共模块打包生成的代码,造成浪费,将公共模块提取出来多带带生成一个bundle可有效解决这个问题。这里贴一个官方文档给出的配置示例:

  const path = require("path");

  module.exports = {
    mode: "development",
    entry: {
      index: "./src/index.js",
      another: "./src/another-module.js"
    },
    output: {
      filename: "[name].bundle.js",
      path: path.resolve(__dirname, "dist")
    },
    // 关键
    optimization: {
      splitChunks: {
        chunks: "all"
      }
    }
  };

这个示例中index.js和another-module.js中都import了loadsh,如果不配置optimization,将生成两个bundle, 两个bundle都包含loadsh的代码。配置optimization后,loadsh代码被多带带提取到一个vendors~another~index.bundle.js。

动态加载

动态加载的含义就是讲代码打包成多个bundle, 需要用到哪个bundle时在加载他。这样做的好处是可以让用户下载需要用到的代码,避免无用代码下载。确定是操作体验可能变差,因为操作之后可能还有一个下载代码的过程。关于动态加载,后面详解。

实现一个简单的动态加载

动态加载就是要实现可以在代码里边去加载其他js,这个太简单了,新建script标签插入dom就可以了,如下:

function loadScript(url) {
    const script = document.createElement("script");
    script.src = url;
    document.head.appendChild(script);
}

只需要在需要加载某个js时调用即可,例如需要点击按钮时加载js可能就如下边这样。

btn.onClick = function() {
    console.log("1");
    loadScript("http://abc.com/a.js");
}

看上去非常简单,事实上webpack也是这么做的,但是他的处理更加通用和精细。

webpack动态加载 webpak打包出来的代码怎么执行

现有一个文件test2.js, 其中代码为

console.log("1")

此文件通过webpack打包后输出如下,删除了部分代码,完整版可自己尝试编译一个,也可查看web-test(这个项目是基于react,express,webpack的用于web相关实验的项目,里边使用了code splitting方案来基于路由拆分代码,与code splitting相关的实验放在test-code-split分支)。

(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // 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;
  }
  return __webpack_require__(__webpack_require__.s = "./test2.js");
})
  ({

    "./test2.js":
      (function (module, exports, __webpack_require__) {

        "use strict";
        eval("

console.log("1");

//# sourceURL=webpack:///./test2.js?");

      })

  });

不知大家是不是跟大雄一样之前从未看过webpack编译产出的代码。其实看一下还是挺有趣的,原来我们的代码是放在eval中执行的。细看下这段代码,其实并不复杂。他是一个自执行函数,参数是一个对象,key是模块id(moduleId), value是函数,这个函数是里边是执行我们写的代码,在自执行函数体内是直接调用了一个__webpack_require__,参数就是入口moduleId, __webpack_require__方法里值执行给定模块id对应的函数,核心代码是modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

上面是没有import命令的情况,对于有import命令的情况,产出和上边类似,只是自执行函数的参数有变化。例如:

// 入口文件test2.js
import "./b.js"
console.log("1")
// b.js
console.log("b")

这段代码产出的自执行函数里边的参数如下:

// 自执行函数里边的参数
{

  "./b.js":
  (function (module, exports, __webpack_require__) {

    "use strict";
    eval("

console.log("b");

//# sourceURL=webpack:///./b.js?");
  }),

    "./test2.js":
  (function (module, exports, __webpack_require__) {

    "use strict";
    eval("

__webpack_require__(/*! ./b.js */ "./b.js");

console.log("1");

//# sourceURL=webpack:///./test2.js?");
  })
}

./test2.js这个moduleId对应的函数的eval里边调用了__webpack_require__方法,为了看起来方便,将eval中的字符串拿出来,如下

__webpack_require__("./b.js");
console.log("1");

原来import命令在webpack中就是被转换成了__webpack_require__的调用。太奇妙了,但是话说为啥模块里边为啥要用eval来执行我们写的代码,大雄还是比较困惑的。

webpack动态code splitting方案

经过一番铺垫,终于到主题了,即webpack是如何实现动态加载的。前文大雄给了一个粗陋的动态加载的方法--loadScript, 说白了就是动态创建script标签。webpack中也是类似的,只是他做了一些细节处理。本文只介绍主流程,具体实现细节大家可以自己编译产出一份代码进行研究。

首先需要介绍在webpack中如何使用code splitting,非常简单,就像下边这样

import("lodash").then(_ => {
    // Do something with lodash (a.k.a "_")...
  });

我们使用了一个import()方法, 这个import方法经过webpack打包后类似于前文提到的loadScript, 大家可以参看下边的代码:

__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);

            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;
                        var error = new Error("Loading chunk " + chunkId + " failed.
(" + errorType + ": " + realSrc + ")");
                        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);
};

是不是非常熟悉,代码中也调用了document.createElement("script")来创建script标签,最后插入到head里。这段代码所做的就是动态加载js,加载失败时reject,加载成功resolve,这里并不能看到resolve的情况,resolve是在拆分出去的代码里调用一个全局函数实现的。拆分出的js如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

/***/ "./b.js":
/*!**************!*
  !*** ./b.js ***!
  **************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

"use strict";
eval("

console.log("b");

//# sourceURL=webpack:///./b.js?");

/***/ })

}]);

在webpackJsonp方法里调用了对应的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(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()();
    }

};

这里的挂到全局的webpackJsonp是个数组,其push方法被改为webpackJsonpCallback方法的数组。所以每次在执行webpackJsonp时实际是在调用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])

总结起来,webpack的动态加载流程大致如下:

总结

本文对webpack打包出的代码的结构和执行过程作了简单分析,介绍了webpack中code splitting的几种方式,重点分析了一下动态加载的流程。分析的不一定完全正确,大家可以自己使用webpack打包产出代码进行研究,一定会有所收获。大雄看完至少大概知道了原来webpack编出来的代码是那样执行的、Promise原来可以那么灵活的使用。

大雄在学习web开发或在项目中遇到问题时经常需要做一些实验, 在react出了什么新的特性时也常常通过做实验来了解一下. 最开始常常直接在公司的项目做实验, 直接拉个test分支就开搞, 这样做有如下缺点:

在公司的项目去做实验本身就是一件不好的事情

公司的项目里边只有前端的部分, 想要做接口有关的实验不方便. 例如想测试跨域的响应头Access-Control-Allow-Origin就得再启一个web服务器

实验过的东西零散, 过一段时间想查找却找不到了

基于以上原因, 特搭建了个基于react,webpack,express的用于web开发相关实验的项目web-test.欢迎使用。

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

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

相关文章

  • 翻译webpack3.5.5 - code splitting - 上半部分

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

    Bryan 评论0 收藏0
  • 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
  • webpack2.x 中文文档 翻译 之 分离库代码Code Splitting - Librari

    摘要:浏览器需要重新下载打包后的文件,即使文件的绝大部分都没有变化。分离并且以来命名新的入口能够缓和当前的问题。现在运行绑定的检查结果是只是被绑定到这个绑定文件中。 分离库代码Code Splitting - Libraries 这个在webpack2.x中文网已存在,点击这里 让我们想一个简单的应用——momentjs,他是一个事件格式化的库。安装moment. npm install -...

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

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

    wudengzan 评论0 收藏0

发表评论

0条评论

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