Code Splitting是webpack的一个重要特性,他容许你将代码打包生成多个bundle。对多页应用来讲,它是必须的,由于必需要配置多个入口生成多个bundle;对于单页应用来讲,若是只打包成一个bundle可能体积很大,致使没法利用浏览器并行下载的能力,且白屏时间长,也会致使下载不少可能用不到的代码,每次上线用户都得下载所有代码,Code Splitting可以将代码分割,实现按需加载或并行加载多个bundle,可利用并发下载能力,减小首次访问白屏时间,能够只上线必要的文件。
webpack提供了三种方式来切割代码,分别是:javascript
本文将简单介绍多entry方式和公共提取方式,重点介绍的是动态加载。这几种方式能够根据须要组合起来使用。这里是官方文档,中文 英文前端
这种方式就是指定多个打包入口,从入口开始将全部依赖打包进一个bundle,每一个入口打包成一个bundle。此方式特别适合多页应用,咱们能够每一个页面指定一个入口,从而每一个页面生成一个js。此方式的核心配置代码以下:java
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。react
这种方式将公共模块提取出来生成一个bundle,公共模块意味着有可能有不少地方使用,可能致使每一个生成的bundle都包含公共模块打包生成的代码,形成浪费,将公共模块提取出来单独生成一个bundle可有效解决这个问题。这里贴一个官方文档给出的配置示例:webpack
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。git
动态加载的含义就是讲代码打包成多个bundle, 须要用到哪一个bundle时在加载他。这样作的好处是可让用户下载须要用到的代码,避免无用代码下载。肯定是操做体验可能变差,由于操做以后可能还有一个下载代码的过程。关于动态加载,后面详解。github
动态加载就是要实现能够在代码里边去加载其余js,这个太简单了,新建script标签插入dom就能够了,以下:web
function loadScript(url) { const script = document.createElement('script'); script.src = url; document.head.appendChild(script); }
只须要在须要加载某个js时调用便可,例如须要点击按钮时加载js可能就以下边这样。express
btn.onClick = function() { console.log('1'); loadScript('http://abc.com/a.js'); }
看上去很是简单,事实上webpack也是这么作的,可是他的处理更加通用和精细。json
现有一个文件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是如何实现动态加载的。前文大雄给了一个粗陋的动态加载的方法--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打包出的代码的结构和执行过程做了简单分析,介绍了webpack中code splitting的几种方式,重点分析了一下动态加载的流程。分析的不必定彻底正确,你们能够本身使用webpack打包产出代码进行研究,必定会有所收获。大雄看完至少大概知道了原来webpack编出来的代码是那样执行的、Promise原来能够那么灵活的使用。
大雄在学习web开发或在项目中遇到问题时常常须要作一些实验, 在react出了什么新的特性时也经常经过作实验来了解一下. 最开始经常直接在公司的项目作实验, 直接拉个test分支就开搞, 这样作有以下缺点:
基于以上缘由, 特搭建了个基于react,webpack,express的用于web开发相关实验的项目web-test.欢迎使用。