webpack 代碼分割一點事
webpack 儼然已經成為前端最主流的構建工具,其功能多種多樣,我們今天就來分析下關于代碼分割這部分的一點事,并在最后講述如何實現在webpack編譯出的代碼里手動添加一個異步chunk。
什么是chunkId與moduleId?
每個chunkId對應的是一個js文件,每個moduleId對應的是一個個js文件的內容的模塊(一個js文件里面可以require多個資源,每個資源分配一個moduleId),所以它兩的關系就是一個chunkId可能由很多個moduleId組成。
在webpack 編譯出來的代碼有定義了一個名稱為__webpack_require__的函數,這個函數就是用來加載模塊的,所以它的參數自然就是moduleId,如下:
/******/ 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;
/******/ }
這里我們要講代碼分割,那自然也要看看webpack編譯出來的代碼是通過什么方法進行異步加載js文件的,從代碼中我們可以找到一個名為requireEnsure的函數,這個函數便是來做這個事情的,那自然而然它的參數就是chunkId了,如下:
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData === 0) {
/******/ return new Promise(function(resolve) { resolve(); });
/******/ }
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ return installedChunkData[2];
/******/ }
/******/
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ installedChunkData[2] = promise;
/******/
/******/ // start chunk loading
/******/ var head = document.getElementsByTagName('head')[0];
/******/ var script = document.createElement('script');
/******/ script.type = 'text/javascript';
/******/ script.charset = 'utf-8';
/******/ script.async = true;
/******/ script.timeout = 120000;
/******/
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = __webpack_require__.p + "" + ({"0":"home-chunk","1":"users-chunk","2":"about-chunk"}[chunkId]||chunkId) + ".js";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var chunk = installedChunks[chunkId];
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ head.appendChild(script);
/******/
/******/ return promise;
/******/ };
從上面代碼我們可以看出requireEnsure其實就是通過動態創建script標簽來加載js文件的,但是這里不是每次訪問這個js文件,都進行創建script請求的,在創建script前,requireEnsure會先通過installedChunks讀取下是否已經有緩存了,如果有緩存直接使用便可。
一個Demo
干說原理,都抽象啊,還是從一個demo來分析,你輕松我也輕松??,廢話少說,先亮出demo地址:https://github.com/canfoo/webpack-code-split。
這個demo是一個react項目,既然要講代碼分割,那么肯定要實現動態路由加載了,如下:
export default {
path: '/',
component: Core,
indexRoute: {
getComponent(location, cb) {
System.import(/* webpackChunkName: "home-chunk" */ './components/Home')
.then(loadRoute(cb))
.catch(errorLoading);
},
},
childRoutes: [
{
path: 'about',
getComponent(location, cb) {
System.import(/* webpackChunkName: "about-chunk" */ './components/About')
.then(loadRoute(cb))
.catch(errorLoading);
},
},
{
path: 'users',
getComponent(location, cb) {
System.import(/* webpackChunkName: "users-chunk" */ './components/Users')
.then(loadRoute(cb))
.catch(errorLoading);
},
},
{
path: '*',
getComponent(location, cb) {
System.import(/* webpackChunkName: "home-chunk" */ './components/Home')
.then(loadRoute(cb))
.catch(errorLoading);
},
},
],
};
如上,demo總共實現三個頁面,三個頁面都是通過分割的形式進行加載,名稱分別為:home-chunk、about-chunk、users-chunk,build下,啟動服務(anywhere),打開瀏覽器,從控制臺我們也看到我們想要的結果:

這時候我們打開編輯器看看webpack編譯的代碼app.js,直接搜索“about-chunk”,我們就定位到上文所說的requireEnsure的函數里有一段這樣的代碼:
/******/ script.src = __webpack_require__.p + "" + ({"0":"home-chunk","1":"users-chunk","2":"about-chunk"}[chunkId]||chunkId) + ".js";
從這段代碼,可以看到每個chunk對應key都是一個數字,這個數字正是chunkId,所以這也是為什么上文說requireEnsure函數的參數是chunkId的原因,因為這個函數正是通過chunkId來異步加載對應的js文件的。
接下來我們來看看代碼是在何時調用了這個方法的,全局搜索__webpack_require__.e,我們便知道了:
exports.default = {
path: '/',
component: _Core2.default,
indexRoute: {
getComponent: function getComponent(location, cb) {
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 227)).then(loadRoute(cb)).catch(errorLoading);
}
},
childRoutes: [{
path: 'about',
getComponent: function getComponent(location, cb) {
__webpack_require__.e/* import() */(2).then(__webpack_require__.bind(null, 268)).then(loadRoute(cb)).catch(errorLoading);
}
}, {
path: 'users',
getComponent: function getComponent(location, cb) {
__webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 269)).then(loadRoute(cb)).catch(errorLoading);
}
},
{
path: '*',
getComponent: function getComponent(location, cb) {
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 227)).then(loadRoute(cb)).catch(errorLoading);
}
}]
};
從上面代碼就可以看到,正是路由訪問適合,調取了這個函數的。我們直接分析其中一個路由就可以了,比如about路由,由上面代碼可以看到,當路徑訪問about時候,就調取對應的getComponent函數,這個函數里面首先執行__webpack_require__.e方法,成功再通過then執行__webpack_require__方法,即先去加載chunk文件,然后再去加載當前chunk文件里的模塊,因此我們可以從這里推斷出,上面方法中由兩個數字 1 和 268 ,這兩個數字肯定就是chunkId和modleId了,很顯然,1 就是chunkId,而 268 就是moduleId。到了這里,我們是不是也可以推論出 about-chunk.js 文件中是不是也會存在 1 和 268 這兩個數字呢?答案是肯定的,我們打開about-chunk.js文件:
webpackJsonp([2],{
/***/ 268:
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _react = __webpack_require__(13);
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var About = function About() {
return _react2.default.createElement(
'div',
null,
'About'
);
};
exports.default = About;
/***/ })
});
如上所示,這個文件里直接調用了webpackJsonp方法,而這個方法第一個參數就是chunkIds 列表,而第二個參數就是一個moduleId與模塊的對象,而這里正出現我們上文出現兩個數字 1 和 268,那為什么需要這兩個數字呢。這時候我們還得繼續看代碼,看webpackJsonp是何方神圣,直接貼代碼:
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [], result;
/******/ 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(chunkIds, moreModules, executeModules);
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ };
從上面代碼可以看出,webpackJsonpCallback主要做的是就是將每個chunk存入到resolves中,并最后依次執行,另外還行chunk里模塊緩存到modules變量。
仿照一個異步chunk?
至此,我們就粗略分析完了代碼分割事情,那么我們是不是可以直接在編譯出的代碼里添加一些代碼就可以生成一個新的異步chunk了?當然可以了!
假設我們要添加一個新的chunk,名稱為test-chunk
第一步,首先到頁面添加個路由,這個比較簡單,就不貼代碼了。
第二步,就在路由代碼中添加一個新的路徑,這里添加路徑就需要手動生成chunkId和moduleId,我們就取以最大的chunkId和moduleId分別加1,即令chunkId=3,moduleId=270:
{ path: 'test', getComponent: function getComponent(location, cb) { // 3 為 chunkId ,270 為moduleId __webpack_require__.e/* import() */(3).then(__webpack_require__.bind(null, 270)).then(loadRoute(cb)).catch(errorLoading); } }
第三步,手動生成一個新的test-chunk.js(copy about-chunk.js文件內容改改部門內容即可),這里要注意的是需要將 chunkId=3,moduleId=270 正確的填入到參數里:
webpackJsonp([3],{ // chunkId
/***/ 270: // moduleId
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _react = __webpack_require__(13);
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var Test = function Test() {
return _react2.default.createElement(
'div',
null,
'Test'
);
};
exports.default = Test;
/***/ })
});
到此就添加完畢了,這時候刷新頁面,并點擊"Test"鏈接,就可以看到瀏覽器正確加載出js文件了(這個效果其實在上圖效果有體現了,這里就不貼出了)
浙公網安備 33010602011771號