Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读

本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析。

一 说明

本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理。

本文使用的Webpack版本是4.32.2版本。

注意:之前也分析过Webpack3.10.0版本构建出来的bundle.js,通过和这次的Webpack 4.32.2版本对比,核心的构建原理基本一致,只是将模块索引id改为文件路径和名字、模块代码改为了eval(moduleString)执行的方式等一些优化改造。

二 示例

1)Webpack.config.js文件内容:

 1 const path = require('path');
 2 const HtmlWebpackPlugin = require('html-webpack-plugin');
 3 const CleanWebpackPlugin = require('clean-webpack-plugin');
 4 
 5 module.exports = {
 6     entry: {
 7         app: './src/index.js'
 8     },
 9     output: {
10         filename: '[name].bundle.js',
11         chunkFilename: '[name].bundle.js',
12         path: path.resolve(__dirname, 'dist')
13     },
14     plugins: [
15         new CleanWebpackPlugin(['dist']),
16         new HtmlWebpackPlugin({
17             title: 'Output Management'
18         })
19     ],
20     mode: 'development' // 'production' 用于配置开发还是发布模式
21 };

2)创建src文件夹,添加入口文件index.js:

 1 function component() {
 2     var element = document.createElement('div');
 3     var button = document.createElement('button');
 4     var br = document.createElement('br');
 5 
 6     button.innerHTML = 'Click me and look at the console!';
 7     element.innerHTML = 'Hello webpack'; // _.join(['Hello', 'webpack'], ' ');
 8     element.appendChild(br);
 9     element.appendChild(button);
10 
11     button.onclick = (
12         e => {
13             // 注意:下边的注释不写的话,打包出来的print文件包名就不是print.bundle.js,而是0.bundle.js
14             import(/* webpackChunkName: "print" */'./print').then(
15                 module => {
16                     var print = module.default;
17                     print();
18                 }
19             )
20         }
21     );
22 
23     return element;
24 }
25 
26 document.body.appendChild(component());

3)在src目录下创建print.js文件:

1 export default () => {
2     console.log('Button Clicked: Here\'s "some text"!');
3 }

 

4)package.json文件内容:

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "webpack": "webpack",
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.18",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.32.2",
    "webpack-cli": "^3.3.2"
  },
  "dependencies": {
    "lodash": "^4.17.4"
  }
}

三 执行构建

执行构建命令:npm run webpack

在dist目录下生成了两个文件:app.bundle.js和print.bundle.js。

app.bundle.js源码如下(下边代码是将注释去掉、压缩的代码还原后的代码):

  1 (function (modules) {
  2     function webpackJsonpCallback(data) {
  3         var chunkIds = data[0];
  4         var moreModules = data[1];
  5 
  6         // add "moreModules" to the modules object,
  7         // then flag all "chunkIds" as loaded and fire callback
  8         var moduleId, chunkId, i = 0, resolves = [];
  9         for (; i < chunkIds.length; i++) {
 10             chunkId = chunkIds[i];
 11             if (installedChunks[chunkId]) {
 12                 resolves.push(installedChunks[chunkId][0]);
 13             }
 14             installedChunks[chunkId] = 0;
 15         }
 16         for (moduleId in moreModules) {
 17             if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 18                 modules[moduleId] = moreModules[moduleId];
 19             }
 20         }
 21         if (parentJsonpFunction) parentJsonpFunction(data);
 22 
 23         while (resolves.length) {
 24             resolves.shift()();
 25         }
 26     };
 27 
 28     // The module cache
 29     var installedModules = {};
 30 
 31     // object to store loaded and loading chunks
 32     // undefined = chunk not loaded, null = chunk preloaded/prefetched
 33     // Promise = chunk loading, 0 = chunk loaded
 34     var installedChunks = {
 35         "app": 0
 36     };
 37 
 38     // script path function
 39     function jsonpScriptSrc(chunkId) {
 40         return __webpack_require__.p + "" + ({"print": "print"}[chunkId] || chunkId) + ".bundle.js"
 41     }
 42 
 43     // The require function
 44     function __webpack_require__(moduleId) {
 45         // Check if module is in cache
 46         if (installedModules[moduleId]) {
 47             return installedModules[moduleId].exports;
 48         }
 49         // Create a new module (and put it into the cache)
 50         var module = installedModules[moduleId] = {
 51             i: moduleId,
 52             l: false,
 53             exports: {}
 54         };
 55 
 56         // Execute the module function
 57         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 58 
 59         // Flag the module as loaded
 60         module.l = true;
 61 
 62         // Return the exports of the module
 63         return module.exports;
 64     }
 65 
 66     // This file contains only the entry chunk.
 67     // The chunk loading function for additional chunks
 68     __webpack_require__.e = function requireEnsure(chunkId) {
 69         var promises = [];
 70 
 71         // JSONP chunk loading for javascript
 72 
 73         var installedChunkData = installedChunks[chunkId];
 74         if (installedChunkData !== 0) { // 0 means "already installed".
 75 
 76             // a Promise means "currently loading".
 77             if (installedChunkData) {
 78                 promises.push(installedChunkData[2]);
 79             } else {
 80                 // setup Promise in chunk cache
 81                 var promise = new Promise(function (resolve, reject) {
 82                     installedChunkData = installedChunks[chunkId] = [resolve, reject];
 83                 });
 84                 promises.push(installedChunkData[2] = promise);
 85 
 86                 // start chunk loading
 87                 var script = document.createElement('script');
 88                 var onScriptComplete;
 89 
 90                 script.charset = 'utf-8';
 91                 script.timeout = 120;
 92                 if (__webpack_require__.nc) {
 93                     script.setAttribute("nonce", __webpack_require__.nc);
 94                 }
 95                 script.src = jsonpScriptSrc(chunkId);
 96 
 97                 // create error before stack unwound to get useful stacktrace later
 98                 var error = new Error();
 99                 onScriptComplete = function (event) {
100                     // avoid mem leaks in IE.
101                     script.onerror = script.onload = null;
102                     clearTimeout(timeout);
103                     var chunk = installedChunks[chunkId];
104                     if (chunk !== 0) {
105                         if (chunk) {
106                             var errorType = event && (event.type === 'load' ? 'missing' : event.type);
107                             var realSrc = event && event.target && event.target.src;
108                             error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
109                             error.type = errorType;
110                             error.request = realSrc;
111                             chunk[1](error);
112                         }
113                         installedChunks[chunkId] = undefined;
114                     }
115                 };
116                 var timeout = setTimeout(function () {
117                     onScriptComplete({type: 'timeout', target: script});
118                 }, 120000);
119                 script.onerror = script.onload = onScriptComplete;
120                 document.head.appendChild(script);
121             }
122         }
123         return Promise.all(promises);
124     };
125 
126     // expose the modules object (__webpack_modules__)
127     __webpack_require__.m = modules;
128 
129     // expose the module cache
130     __webpack_require__.c = installedModules;
131 
132     // define getter function for harmony exports
133     __webpack_require__.d = function (exports, name, getter) {
134         if (!__webpack_require__.o(exports, name)) {
135             Object.defineProperty(exports, name, {enumerable: true, get: getter});
136         }
137     };
138 
139     // define __esModule on exports
140     __webpack_require__.r = function (exports) {
141         if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
142             Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
143         }
144         Object.defineProperty(exports, '__esModule', {value: true});
145     };
146 
147     // create a fake namespace object
148     // mode & 1: value is a module id, require it
149     // mode & 2: merge all properties of value into the ns
150     // mode & 4: return value when already ns object
151     // mode & 8|1: behave like require
152     __webpack_require__.t = function (value, mode) {
153         if (mode & 1) value = __webpack_require__(value);
154         if (mode & 8) return value;
155         if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
156         var ns = Object.create(null);
157         __webpack_require__.r(ns);
158         Object.defineProperty(ns, 'default', {enumerable: true, value: value});
159         if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) {
160             return value[key];
161         }.bind(null, key));
162         return ns;
163     };
164 
165     // getDefaultExport function for compatibility with non-harmony modules
166     __webpack_require__.n = function (module) {
167         var getter = module && module.__esModule ?
168             function getDefault() {
169                 return module['default'];
170             } :
171             function getModuleExports() {
172                 return module;
173             };
174         __webpack_require__.d(getter, 'a', getter);
175         return getter;
176     };
177 
178     // Object.prototype.hasOwnProperty.call
179     __webpack_require__.o = function (object, property) {
180         return Object.prototype.hasOwnProperty.call(object, property);
181     };
182 
183     // __webpack_public_path__
184     __webpack_require__.p = "";
185 
186     // on error function for async loading
187     __webpack_require__.oe = function (err) {
188         console.error(err);
189         throw err;
190     };
191 
192     var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
193     var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 复制一个数组的push方法,这个方法的this是jsonpArray
194     jsonpArray.push = webpackJsonpCallback; // TODO: 为什么要复写push,而不是直接增加一个新方法名?
195     jsonpArray = jsonpArray.slice(); // 拷贝一个新数组
196     for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
197     var parentJsonpFunction = oldJsonpFunction;
198 
199     // Load entry module and return exports
200     return __webpack_require__(__webpack_require__.s = "./src/index.js");
201 })
202 /************************************************************************/
203 ({
204     "./src/index.js": (function (module, exports, __webpack_require__) {
205         function component() {
206             var element = document.createElement('div');
207             var button = document.createElement('button');
208             var br = document.createElement('br');
209 
210             button.innerHTML = 'Click me and look at the console!';
211             element.innerHTML = 'Hello webpack'; // _.join(['Hello', 'webpack'], ' ');
212             element.appendChild(br);
213             element.appendChild(button);
214 
215             button.onclick = (
216                 e => {
217                     __webpack_require__.e("print")
218                         .then(__webpack_require__.bind(null, "./src/print.js"))
219                         .then(
220                             module => {
221                                 var print = module.default;
222                                 print();
223                             }
224                         )
225                 }
226             );
227 
228             return element;
229         }
230 
231         document.body.appendChild(component());
232     })
233 });

print.bundle.js的源码如下:

 1 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([ // 注意:这个push实际是webpackJsonpCallback方法
 2     ["print"],
 3     {
 4         "./src/print.js": (function(module, __webpack_exports__, __webpack_require__) {
 5             "use strict";
 6             __webpack_require__.r(__webpack_exports__);
 7             __webpack_exports__["default"] = (() => {
 8                 console.log('Button Clicked: Here\'s "some text"!');
 9             });
10         })
11     }
12 ]);

 四 源码解读

说明:懒加载构建和和上一篇的基础构建原理中有很多相同的代码,这里不再重复说明,本文主要详细说明其中增加的懒加载方面的内容。

app.bundle.js是构建好的入口文件,里边就是一个自执行函数,基本结构和上一篇基础构建源码中一致,这里不再详细说明。下边是使用懒加载模块构建后,增加的内容,这里详细说明这些内容:

 1 (function (modules) {
 2     function webpackJsonpCallback(data) {...};
 3 
 4     // The module cache
 5     var installedModules = {};
 6 
 7     // object to store loaded and loading chunks
 8     // undefined = chunk not loaded, null = chunk preloaded/prefetched
 9     // Promise = chunk loading, 0 = chunk loaded
10     var installedChunks = {
11         "app": 0
12     };
13 
14     // script path function
15     function jsonpScriptSrc(chunkId) {...}
16 
17     // The require function
18     function __webpack_require__(moduleId) {...}
19 
20     // This file contains only the entry chunk.
21     // The chunk loading function for additional chunks
22     __webpack_require__.e = function requireEnsure(chunkId) {...};
23     
24     // .... 
25     
26     // on error function for async loading
27     __webpack_require__.oe = function (err) {
28         console.error(err);
29         throw err;
30     };
31 
32     var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
33     var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 复制一个数组的push方法,这个方法的this是jsonpArray
34     jsonpArray.push = webpackJsonpCallback; // TODO: 为什么要复写push,而不是直接增加一个新方法名?
35     jsonpArray = jsonpArray.slice(); // 拷贝一个新数组
36     for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
37     var parentJsonpFunction = oldJsonpFunction;
38 
39     // Load entry module and return exports
40     return __webpack_require__(__webpack_require__.s = "./src/index.js");
41 })
42 /************************************************************************/
43 ({
44     "./src/index.js": (function (module, exports, __webpack_require__) {...})
45 });

我们详细分析下新增的这些代码。

4.1 installedChunks缓存变量

根据注释,该对象变量主要缓存各个独立的js文件模块的加载状态。

该对象的key就是chunkId,而chunkId实际就是文件名去掉.bundle.js后剩余的内容,例如:print.bundle.js的chunkId就是print。

根据值的不同标志着key对应的文件加载状态主要有以下几种:

undefined:key对应的文件未加载;

null:key对应的文件延迟加载;

数组:正在加载(注意,这里的注释有点不准确,这个数组实际存储的是一个promise的实例,以及对应的reject和resolve);

0:已经加载过了。

这个变量的核心作用:当一个懒加载模块被多个文件依赖时,如果该模块已经被加载过了,就不会被其它模块加载了。判断方法就是通过该缓存变量判断的。具体源码可以在__webpack_require__.e函数中看到:

 1 __webpack_require__.e = function requireEnsure(chunkId) {
 2     var promises = [];
 3 
 4     // JSONP chunk loading for javascript
 5     var installedChunkData = installedChunks[chunkId];
 6     if (installedChunkData !== 0) { // 0 means "already installed".
 7 
 8         // a Promise means "currently loading".
 9         if (installedChunkData) {
10             promises.push(installedChunkData[2]);
11         } else {
12             // ... 
13             // 创建一个<script>标签,将路径设置为懒加载文件路径,并插入HTML,实现该懒加载文件的加载。
14         }
15     }
16     return Promise.all(promises);
17 };

 

4.2 __webpack_require__.e函数

该函数主要作用就是创建一个<script>标签,然后将chunkId对应的文件通过该标签加载。

源代码如下:

 1 __webpack_require__.e = function requireEnsure(chunkId) {
 2         var promises = [];
 3 
 4         // JSONP chunk loading for javascript
 5 
 6         var installedChunkData = installedChunks[chunkId];
 7         if (installedChunkData !== 0) { // 0 means "already installed".
 8 
 9             // a Promise means "currently loading".
10             if (installedChunkData) {
11                 promises.push(installedChunkData[2]);
12             } else {
13                 // setup Promise in chunk cache
14                 var promise = new Promise(function (resolve, reject) {
15                     installedChunkData = installedChunks[chunkId] = [resolve, reject];
16                 });
17                 promises.push(installedChunkData[2] = promise);
18 
19                 // start chunk loading
20                 var script = document.createElement('script');
21                 var onScriptComplete;
22 
23                 script.charset = 'utf-8';
24                 script.timeout = 120;
25                 if (__webpack_require__.nc) {
26                     script.setAttribute("nonce", __webpack_require__.nc);
27                 }
28                 script.src = jsonpScriptSrc(chunkId);
29 
30                 // create error before stack unwound to get useful stacktrace later
31                 var error = new Error();
32                 onScriptComplete = function (event) {
33                     // avoid mem leaks in IE.
34                     script.onerror = script.onload = null;
35                     clearTimeout(timeout);
36                     var chunk = installedChunks[chunkId];
37                     if (chunk !== 0) {
38                         if (chunk) {
39                             var errorType = event && (event.type === 'load' ? 'missing' : event.type);
40                             var realSrc = event && event.target && event.target.src;
41                             error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
42                             error.type = errorType;
43                             error.request = realSrc;
44                             chunk[1](error);
45                         }
46                         installedChunks[chunkId] = undefined;
47                     }
48                 };
49                 var timeout = setTimeout(function () {
50                     onScriptComplete({type: 'timeout', target: script});
51                 }, 120000);
52                 script.onerror = script.onload = onScriptComplete;
53                 document.head.appendChild(script);
54             }
55         }
56         return Promise.all(promises);
57     };

主要做了如下几个事情:

1)判断chunkId对应的模块是否已经加载了,如果已经加载了,就不再重新加载;

2)如果模块没有被加载过,但模块处于正在被加载的过程,不再重复加载,直接将加载模块的promise返回。

为什么会出现这种情况?

例如:我们将index.js中加载print.js文件的地方改造为下边多次通过ES6的import加载print.js文件:

 1 button.onclick = (
 2     e => {
 3         
 4         import('./print').then(
 5             module => {
 6                 var print = module.default;
 7                 print();
 8             }
 9         );
10 
11         import('./print').then(
12             module => {
13                 var print = module.default;
14                 print();
15             }
16         )       
17     }
18 );

 从上边代码可以看出,当第一import加载print.js文件时,还没有resolve,就又执行第二个import文件了,而为了避免重复加载该文件,就通过将这里的判断,避免了重复加载。

3)如果模块没有被加载过,也不处于加载过程,就创建一个promise,并将resolve、reject、promise构成的数组存储在上边说过的installedChunks缓存对象属性中。然后创建一个script标签加载对应的文件,加载超时时间是2分钟。如果script文件加载失败,触发reject(对应源码中:chunk[1](error),chunk[1]就是上边缓存的数组的第二个元素reject),并将installedChunks缓存对象中对应key的值设置为undefined,标识其没有被加载。

4)最后返回promise

注意:源码中,这里返回的是Promise.all(promises),分析代码发现promises好像只可能有一个元素。可能还没遇到多个promises的场景吧。留待后续研究。

 4.3 自执行函数体代码分析

整个app.bundle.js文件是一个自执行函数,该函数中执行的代码如下:

1     var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
2     var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 复制一个数组的push方法,这个方法的this是jsonpArray
3     jsonpArray.push = webpackJsonpCallback; // TODO: 为什么要复写push,而不是直接增加一个新方法名?
4     jsonpArray = jsonpArray.slice(); // 拷贝一个新数组
5     for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
6     var parentJsonpFunction = oldJsonpFunction;

这段代码主要做了如下几个事情:

1)定义了一个全局变量webpackJsonp,改变量是一个数组,该数组变量的原生push方法被复写为webpackJsonpCallback方法,该方法是懒加载实现的一个核心方法,具体代码会在下边分析。

该全局变量在懒加载文件中被用到。在print.bundle.js中:

1 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([ // 注意:这个push实际是webpackJsonpCallback方法
2     ["print"],
3     {
4         "./src/print.js": (function(module, __webpack_exports__, __webpack_require__) {...})
5     }
6 ]);

2)将数组的原生push方法备份,赋值给parentJsonpFunction变量保存。

注意:该方法的this是全局变量webpackJsonp,也就是说parentJsonpFunction('111')后,全局数组变量webpackJsonp就增加了一个'111'元素。

该方法在webpackJsonpCallback中会用到,是将懒加载文件的内容保存到全局变量webpackJsonp中。

3)上边第一步中复写push的原因?

可能是因为在懒加载文件中,调用了复写后的push,执行了原生push的功能,因此,为了更形象的表达该意思,因此直接复写了push。

但个人认为这个不太好,不易读。直接新增一个_push或者extendPush,这样是不是读起来就很简单了。

4.4 webpackJsonpCallback函数分析

该函数是懒加载的一个比较核心代码。其代码如下:

 1     function webpackJsonpCallback(data) {
 2         var chunkIds = data[0];
 3         var moreModules = data[1];
 4 
 5         // add "moreModules" to the modules object,
 6         // then flag all "chunkIds" as loaded and fire callback
 7         var moduleId, chunkId, i = 0, resolves = [];
 8         for (; i < chunkIds.length; i++) {
 9             chunkId = chunkIds[i];
10             if (installedChunks[chunkId]) {
11                 resolves.push(installedChunks[chunkId][0]);
12             }
13             installedChunks[chunkId] = 0;
14         }
15         for (moduleId in moreModules) {
16             if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
17                 modules[moduleId] = moreModules[moduleId];
18             }
19         }
20         if (parentJsonpFunction) parentJsonpFunction(data);
21 
22         while (resolves.length) {
23             resolves.shift()();
24         }
25     };

参数说明:

参数是一个数组。有两个元素:第一个元素是要懒加载文件中所有模块的chunkId组成的数组;第二个参数是一个对象,对象的属性和值分别是要加载模块的moduleId和模块代码函数。

该函数主要做的事情如下:

1)遍历参数中的chunkId:

判断installedChunks缓存变量中对应chunkId的属性值:如果是真,说明模块正在加载,因为从上边分析中可以知道,installedChunks[chunkId]只有一种情况是真,那就是在对应的模块正在加载时,会将加载模块创建的promise的三个信息搞成一个数组[resolve, reject, proimise]赋值给installedChunks[chunkId]。将resolve存入resolves变量中。

将installedChunks中对应的chunkId置为0,标识该模块已经被加载过了。

2)遍历参数中模块对象所有属性:

将模块代码函数存储到modules中,该modules是入口文件app.bundle.js中自执行函数的参数。

这一步非常关键,因为执行模块加载函数__webpack_require__时,获取模块代码时,就是通过moduleId从modules中查找对应模块代码。

3)调用parentJsonpFunction(原生push方法)将整个懒加载文件的数据存入全局数组变量window.webpackJsonp。

4)遍历resolves,执行所有promise的resolve:

当执行了promise的resolve后,才会走到promise.then的成功回调中,查看源码可以看到:

 1             button.onclick = (
 2                 e => {
 3                     __webpack_require__.e("print")
 4                         .then(__webpack_require__.bind(null, "./src/print.js"))
 5                         .then(
 6                             module => {
 7                                 var print = module.default;
 8                                 print();
 9                             }
10                         )
11                 }
12             );

resolve后,执行了两个then回调:

第一个回调是调用__webpack_require__函数,传入的参数是懒加载文件中的一个模块的moduleId,而这个moduleId就是上边存入到modules变量其中一个。这样就通过__webpack_require__执行了模块的代码。并将模块的返回值,传递给第二个then的回调函数;

第二个回调函数是真正的onclick回调函数的业务代码。

5)重要思考:

从这个函数可以看出:

调用__webpack_require__.e('print')方法,实际只是将对应的print.bundle.js文件加载和创建了一个异步的promise(因为并不知道什么时候这个文件才能执行完,因此需要一个异步promise,而promise的resolve会在对应的文件加载时执行,这样就能实现异步文件加载了),并没有将懒加载文件中保存的模块代码执行。

在加载对应print.bundle.js文件代码时,通过调用webpackJsonpCallback函数,实现触发加载文件时创建的promise的resolve。

resolve触发后,会执行promise的then回调,这个回调通过__webpack_require__函数执行了真正需要模块的代码(注意:如果print.bundle.js中有很多模块,只会执行用到的模块代码,而不是执行所有模块的代码),执行完后将模块的exports返回给promise的下一个then函数,该函数也就是真正的业务代码了。

综上,可以看出,webpack实际是通过promise,巧妙的实现了模块的懒加载功能。

 5 懒加载构建原理图

 

 

posted on 2019-05-29 19:42  ゛墨メ冰ミ  阅读(2019)  评论(1编辑  收藏  举报

导航