webpack Code Splitting浅析

Code Splitting是webpack的一个主要特征,他许可你将代码打包天生多个bundle。对多页运用来讲,它是必需的,由于必须要设置多个进口天生多个bundle;关于单页运用来讲,假如只打包成一个bundle能够体积很大,致使没法应用浏览器并行下载的才能,且白屏时候长,也会致使下载许多能够用不到的代码,每次上线用户都得下载悉数代码,Code Splitting能够将代码支解,完成按需加载或并行加载多个bundle,可应用并发下载才能,削减初次接见白屏时候,能够只上线必要的文件。

三种Code Splitting体式格局

webpack供应了三种体式格局来切割代码,分别是:

  1. 多entry体式格局
  2. 大众提取
  3. 动态加载

本文将简朴引见多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("\n\nconsole.log('1');\n\n//# 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("\n\nconsole.log('b');\n\n//# sourceURL=webpack:///./b.js?");
  }),

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

    "use strict";
    eval("\n\n__webpack_require__(/*! ./b.js */ \"./b.js\");\n\nconsole.log('1');\n\n//# 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.\n(' + 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("\n\nconsole.log('b');\n\n//# 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 Code Splitting浅析》

总结

本文对webpack打包出的代码的构造和实行历程作了简朴剖析,引见了webpack中code splitting的几种体式格局,重点剖析了一下动态加载的流程。剖析的不一定完全正确,人人能够自身运用webpack打包产出代码进行研究,一定会有所收成。大雄看完最少也许知道了本来webpack编出来的代码是那样实行的、Promise本来能够那末天真的运用。

大雄在进修web开辟或在项目中遇到题目时经常须要做一些试验, 在react出了什么新的特征时也经常经由过程做试验来相识一下. 最最先经常直接在公司的项目做试验, 直接拉个test分支就开搞, 如许做有以下瑕玷:

  • 在公司的项目去做试验自身就是一件不好的事变
  • 公司的项目里边只要前端的部份, 想要做接口有关的试验不轻易. 比方想测试跨域的相应头Access-Control-Allow-Origin就得再启一个web服务器
  • 试验过的东西零星, 过一段时候想查找却找不到了

基于以上缘由, 特搭建了个基于react,webpack,express的用于web开辟相干试验的项目web-test.迎接运用。

    原文作者:Bigbear
    原文地址: https://segmentfault.com/a/1190000019021519
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞