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.歡迎使用。