webpack 代碼分割一點事

webpack 儼然已經成爲前端最主流的構建工具,其功能多種多樣,咱們今天就來分析下關於代碼分割這部分的一點事,並在最後講述如何實如今webpack編譯出的代碼裏手動添加一個異步chunk。javascript

什麼是chunkId與moduleId?

每一個chunkId對應的是一個js文件,每一個moduleId對應的是一個個js文件的內容的模塊(一個js文件裏面能夠require多個資源,每一個資源分配一個moduleId),因此它兩的關係就是一個chunkId可能由不少個moduleId組成。前端

在webpack 編譯出來的代碼有定義了一個名稱爲__webpack_require__的函數,這個函數就是用來加載模塊的,因此它的參數天然就是moduleId,以下:java

/******/ 	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了,以下:react

/******/ 	__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讀取下是否已經有緩存了,若是有緩存直接使用即可。webpack

一個Demo

幹說原理,都抽象啊,仍是從一個demo來分析,你輕鬆我也輕鬆😉,廢話少說,先亮出demo地址:https://github.com/canfoo/webpack-code-splitgit

這個demo是一個react項目,既然要講代碼分割,那麼確定要實現動態路由加載了,以下:github

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),打開瀏覽器,從控制檯咱們也看到咱們想要的結果:web

 

這時候咱們打開編輯器看看webpack編譯的代碼app.js,直接搜索「about-chunk」,咱們就定位到上文所說的requireEnsure的函數裏有一段這樣的代碼:promise

/******/         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文件了(這個效果其實在上圖效果有體現了,這裏就不貼出了)

相關文章
相關標籤/搜索