- 原文地址:How To Make Tree Shakeable Libraries
- 原文做者:François Hendriks
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:darkyzhou
- 校對者:Usualminds、KimYangOfCat
在 Theodo,咱們致力於爲咱們的客戶打造可靠、快速的應用。咱們有一些改進現有應用的性能的項目。在其中一個項目裏,咱們經過對內部庫進行搖樹優化(tree shaking),成功將全部頁面的 gzip 壓縮後的 bundle 體積減小了足足 500KB。前端
在作這件事的時候,咱們意識到搖樹優化並非一種簡單地開啓或關閉就會奏效的技術。有不少因素會影響一個庫的搖樹優化效果。node
本文的目標是爲構建適配搖樹優化的庫提供詳細的指導。其中的步驟的總結以下:android
export
語句。引用 MDN 文檔:webpack
Tree shaking 是一個一般用於描述移除 JavaScript 上下文中的未引用代碼(dead code)行爲的術語。ios
它依賴於 ES2015 中的
import
和export
語句,用來檢測代碼模塊是否被導出、導入,且被 JavaScript 文件使用。git
搖樹優化是一種實現移除未引用代碼(dead code elimination)的方式,實現的方式是檢測哪些導出項(export)在應用代碼裏未被引用。它會被相似 Webpack 和 Rollup 這樣的打包工具執行,最先由 Rollup 實現。github
那麼,爲何它叫搖樹優化? 咱們能夠把應用的導出項(exports)和導入項(imports)想象成一棵樹的樣子。樹上健康的葉子和樹枝表示引用了的導入項。而死亡的葉子表示未引用的代碼,它們和樹的其餘部分是分開的。這時候搖動這棵樹,那麼全部死亡的葉子會被搖下來,即未引用的代碼會被移除。web
爲何搖樹優化很重要? 它可以對你的瀏覽器應用產生巨大的影響。若是應用打包了更多的代碼,那麼瀏覽器將花費更多時間去下載、解壓、轉換和執行它們。所以,對於打造速度最快的應用而言,移除未引用的代碼相當重要。算法
網上有不少解釋搖樹優化和未引用代碼移除的文章和資源。這裏咱們會集中討論那些應用裏中引用的庫。當引用一個庫的應用可以成功地移除這個庫中未被引用的部分時,這個庫才能被認爲作到了搖樹優化(tree shakeable)。typescript
在嘗試讓一個庫變得可被搖樹優化以前,咱們先來看看如何識別一個可搖樹優化的庫。
這乍看上去好像很簡單,可是我注意到不少開發者都會認爲他們的庫是可搖樹優化的,僅僅是由於它們使用了 ES6 模塊(後文會細講),或者是由於它們有對搖樹優化很友好的配置。不幸的是,這並不意味着你的庫其實是可搖樹優化的!
因而,咱們被帶到了這個問題上:如何高效地檢查一個庫是否可搖樹優化?
要作到這件事,咱們須要理解兩件事情:
要檢查咱們的庫是否可搖樹優化,咱們能夠將它放在一個受控的環境下,使用一個引用了它的應用進行測試:
這個策略可以將測試與咱們現有的應用隔離。它可讓咱們隨意地擺弄庫而不破壞任何東西。它還能讓咱們確保出現的問題不是來自於應用打包工具的配置上。
咱們接下來會將這種策略應用到一個叫作 user-library
的庫上,使用一個由 Webpack 進行打包的應用 user-app
進行測試。你也可使用其餘你更喜歡的打包工具。
user-library
的代碼以下所示:
export const getUserName = () => "John Doe";
export const getUserPhoneNumber = () => "***********";
複製代碼
它僅僅是在 index.js
文件裏導出了兩個函數,這兩個函數能夠經過 npm 包來使用。
讓咱們編寫簡單的 user-app
:
package.json
{
"name": "user-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.18.0",
"webpack-cli": "^4.3.1"
},
"dependencies": {
"user-library": "1.0.0"
}
}
複製代碼
注意,咱們使用 user-library
做爲依賴。
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
mode: "development",
optimization: {
usedExports: true,
innerGraph: true,
sideEffects: true,
},
devtool: false,
};
複製代碼
要理解上面的 Webpack 配置,咱們須要先理解 Webpack 是如何進行搖樹優化的。搖樹優化進行的步驟以下所示:
export
語句沒有被其餘模塊所導入。這些步驟僅在 生產模式(production mode) 下才會被執行。
生產模式的問題在於代碼最小化(minification)。它會讓咱們難以分辨搖樹優化是否生效,由於在打包後的代碼裏咱們看不到原來命名的函數。
爲了繞過這個問題,咱們會讓 Webpack 運行在開發模式(development mode)下,但仍然讓它識別哪些代碼未被引用而且會在生產模式下被移除。咱們將配置裏的 optimization
設置成以下所示:
optimization: {
usedExports: true,
sideEffects: true,
innerGraph: true,
}
複製代碼
其中的 usedExports
屬性可以讓 Webpack 識別哪些模塊的導出項沒有被其餘模塊引用。其餘兩個屬性會在後文討論。如今咱們暫且認爲它們可以提升咱們的應用被搖樹優化的效果。
咱們 user-app
的入口文件:src/index.js
import { getUserName } from "user-library";
console.log(getUserName());
複製代碼
打包以後,咱們來分析一下輸出:
/***/ "./node_modules/user-library/dist/index.js":
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ((__unused_webpack_module, exports) => {
var __webpack_unused_export__;
__webpack_unused_export__ = ({ value: true });
const getUserName = () => 'John Doe';
const getUserPhoneNumber = () => '***********';
exports.getUserName = getUserName;
__webpack_unused_export__ = getUserPhoneNumber;
/***/ })
複製代碼
Webpack 將咱們全部的代碼從新組織到了同一個文件裏。請看其中的 getUserPhoneNumer
導出項,注意到 Webpack 將它標記爲了未引用。它會在生產模式下被移除,而 getUserName
則會被保留,由於它被咱們的入口文件 index.js
所使用。
咱們的庫被搖樹優化了!你能夠再寫一些導入項,重複上面的步驟再查看輸出的代碼。咱們的目的是知道 Webpack 會把咱們庫裏沒有被引用的代碼標記爲未引用。
對於咱們這個簡單的 user-library
來講,一切看上去都還不錯。讓咱們將它變得複雜一些,與此同時咱們會關注搖樹優化的一些條件和優化項。
這項要求很是常見,並且不少文檔都有詳細的解釋,但在我看來它們卻有些誤導性。我時常能聽到一些開發者說,咱們應該使用 ES6 模塊來讓咱們的庫可以被搖樹優化。雖然這句話自己是徹底正確的,但 其中包含一種錯誤的觀念,這種觀念覺得僅僅使用 ES6 模塊就足以讓搖樹優化很好地工做。 哎,要是真的這麼簡單,你也毫不會閱讀本文至此!
不過,使用 ES6 模塊確實是搖樹優化的必要條件之一。
JavaScript 代碼的打包格式有不少種:ESM、CJS、UMD、IIFE 等。
爲簡單起見,咱們只考慮兩種格式:ECMA Script 模塊(ESM 或 ES6 模塊)和 CommonJS 模塊(CJS),由於它們在應用庫中受到了最爲普遍的使用。大多數庫會使用 CJS 模塊,由於這樣可以讓它們可以運行在 Node.js 應用裏(不過 Node.js 如今也支持 ESM 了)。在 CJS 誕生好久以後的 2015 年,ES 模塊才伴隨 ECMAScript 2015(也被稱做 ES6)出現,被認爲是 JavaScript 的標準模塊系統。
CJS 格式的例子:
const { userAccount } = require("./userAccount");
const getUserAccount = () => {
return userAccount;
};
module.exports = { getUserAccount };
複製代碼
ESM 格式的例子:
import { userAccount } from "./userAccount";
export const getUserAccount = () => {
return userAccount;
};
複製代碼
這兩種格式有着很大的區別:ESM 的導入是靜態的,而 CJS 的導入是動態的。 這意味着咱們能夠在 CJS 中作到如下的事情,可是在 ESM 中不行:
if (someCondition) {
const { userAccount } = require("./userAccount");
}
複製代碼
雖然這樣看上去更加靈活,但它也意味着打包工具不能在編譯或打包期間構造出一棵有效的模塊樹。someCondition
這個變量只有在運行時才能知道它的值,致使打包工具在編譯期間不管 someCondition
的值是什麼都會把 userAccount
給導入進來。這也致使打包工具沒法檢查這些導入項是否真的被使用了,因而把全部 CJS 格式的導入項打包進 bundle 裏。
讓咱們修改 user-library
的代碼來體現這一點。同時,爲了讓這個庫顯得更貼近現實,它如今有兩個文件:
src/userAccount.js
const userAccount = {
name: "user account",
};
module.exports = { userAccount };
複製代碼
src/index.js
const { userAccount } = require("./userAccount");
const getUserName = () => "John Doe";
const getUserPhoneNumber = () => "***********";
const getUserAccount = () => userAccount;
module.exports = {
getUserName,
getUserPhoneNumber,
getUserAccount,
};
複製代碼
咱們保持 user-app
的入口文件不變,這樣咱們依然不會用到 getUserAccount
函數及其依賴。
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
const { userAccount } = __webpack_require__(/*! ./userAccount */ "./node_modules/user-library/dist/userAccount.js")
const getUserName = () => 'John Doe'
const getUserPhoneNumber = () => '***********'
const getUserAccount = () => userAccount
module.exports = {
getUserName,
getUserPhoneNumber,
getUserAccount
}
/***/ }),
/***/ "./node_modules/user-library/dist/userAccount.js":
/*!*******************************************************!*\ !*** ./node_modules/user-library/dist/userAccount.js ***! \*******************************************************/
/***/ ((module) => {
const userAccount = {
name: 'user account'
}
module.exports = { userAccount }
/***/ })
複製代碼
這三個導出項所有都出如今打包輸出裏,而且沒有被 Webpack 標記爲未引用。對於源文件 userAccount
也是如此。
如今,讓咱們來看看將上面的例子改形成 ESM 以後的結果。咱們作的修改是將 require
和 exports
的語法所有改爲對應的 ESM 的語法。
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "getUserName": () => /* binding */ getUserName
/* harmony export */ });
/* unused harmony exports getUserAccount, getUserPhoneNumber */
/* harmony import */ var _userAccount_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userAccount.js */ "./node_modules/user-library/dist/userAccount.js");
const getUserName = () => 'John Doe';
const getUserPhoneNumber = () => '***********';
const getUserAccount = () => userAccount;
/***/ }),
/***/ "./node_modules/user-library/dist/userAccount.js":
/*!*******************************************************!*\ !*** ./node_modules/user-library/dist/userAccount.js ***! \*******************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* unused harmony export userAccount */
const userAccount = {
name: 'user account'
};
/***/ })
複製代碼
注意,getUserAccount
和 getUserPhoneNumber
都被標記爲了未引用。並且另外一個文件裏的 userAccount
也被標記了。得益於 innerGraph
優化,Webpack 可以將 index.js
文件裏的 getUserAccount
導入項連接到 userAccount
導出項。這讓 Webpack 能夠從入口文件開始,遞歸遍歷它全部的依賴,進而知道每個模塊的哪些導出項未被引用。 由於 Webpack 知道 getUserAccount
沒有被使用,因此它能夠到 userAccount
文件裏對 getUserAccount
的依賴作相同的檢查。
ES 模塊讓咱們能夠在應用代碼裏尋找那些被引用的和未被引用的導出項,這也解釋了爲何這種模塊系統對於搖樹優化是如此的重要。它還解釋了爲何咱們應該使用像 lodash-es
這樣導出了兼容 ESM 的構建產物的依賴。這裏 lodash-es 庫是很受歡迎的 lodash
庫的 ESM 版本。
話雖如此,針對搖樹優化,僅僅使用 ES 模塊仍然不是最佳的方法。在咱們的例子裏,咱們發現 Webpack 會在每一個文件遞歸地檢查導出的代碼是否被引用。對於咱們的例子來講,Webpack 其實能夠直接忽略掉 userAccount
文件,由於它惟一的導出項是未引用的!這就將咱們引入到文章接下來對反作用(side effect)的討論。
本文這一部分的總結以下:
main
和 module
屬性來設置。根據 Webpack 的文檔,搖樹優化能夠被分爲如下兩種優化措施:
爲了闡釋反作用的含義,讓咱們看看以前用到的例子:
import { userAccount } from "./userAccount";
function getUserAccount() {
return userAccount;
}
複製代碼
若是 getUserAccount
沒有被使用,打包工具是否能夠認爲 userAccount
模塊能夠從打包輸出中移除呢?答案是否認的!userAccount
能夠作各類可以影響應用的其餘部分的事情。它能夠向全局可訪問的值裏注入一些變量,好比 DOM。它還能夠是一個 CSS 模塊,會向 document
裏注入樣式。不過我以爲最好的例子是 polyfill。咱們一般會像下面這樣引入它們:
import "myPolyfill";
複製代碼
如今這個模塊必定有反作用了,由於一旦它被導入到其餘模塊,它就會影響到整個應用的代碼。打包工具會將這個模塊視爲可能被刪除的候選者,畢竟咱們沒有使用它的任何導出項。不過移除它會破壞咱們應用的正常運行。
像 Webpack 和 Rollup 這樣的打包工具也所以會默認地將咱們庫中的全部模塊視爲包含反作用。
可是在前面的例子裏,咱們知道咱們的庫不包含任何反作用!所以,咱們能夠告訴打包工具這一點。大多數打包工具能夠讀取 package.json
文件裏的 sideEffects
屬性。這個屬性若是沒有指定,那麼它默認會被設爲 true
(表示這個包裏全部的模塊都含有反作用)。咱們能夠將它設爲 false
(表示全部模塊都不包含反作用)或者也能夠指定一個數組,列舉出含有反作用的源文件。
咱們將這個屬性添加到 user-library
庫的 package.json
中:
{
"name": "user-library",
"version": "1.0.0",
"description": "",
"sideEffects": false,
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
複製代碼
而後從新運行 Webpack 進行打包:
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ getUserName: () => /* binding */ getUserName,
/* harmony export */
});
/* unused harmony exports getUserAccount, getUserPhoneNumber */
const getUserName = () => "John Doe";
const getUserPhoneNumber = () => "***********";
const getUserAccount = () => userAccount;
/***/
};
複製代碼
咱們發現,源文件 userAccount
已經在打包輸出中被移除了。咱們仍然可以看到 getUserAccount
函數在引用 userAccount
,不過此函數已經被 Webpack 標記爲了未引用代碼,它會在代碼最小化的過程當中被移除。
sideEffects
選項對於那些經過一個 index 文件從其餘內部源文件導出 API 的庫尤爲重要。 若是沒有反作用優化,打包工具就必須解析全部包含導出項的源文件。
正如 Webpack 的提示:「sideEffects
很是地高效,由於它可以讓打包工具略過整個的模塊或源文件,以及它的整個子樹」
對於前文介紹的兩種優化措施在介入打包過程上的不一樣之處,簡單來講以下所述:
sideEffects
讓打包工具略過一個被導入的模塊,若是從這個模塊導入的東西所有都沒有被使用的話。usedExports
讓打包工具移除在一個模塊裏未被引用的導出項。那麼,上面兩種措施,一個是「略過文件」,一個是「將導出項標記爲未被使用」。前者影響下的打包輸出又和後者有什麼不一樣之處呢?
大多數狀況下,對一個庫進行搖樹優化,有和沒有反作用優化其實會產生如出一轍的輸出。最終的 bundle 裏包含的代碼量是同樣的。不過在某些狀況下,若是分析未引用的導出項的相關代碼的過程過於複雜,那麼有和沒有反作用優化的結果就不同了。本文接下來的部分將包含這兩種狀況的例子,咱們將看到只有小的模塊和開啓反作用優化的組合產生了最好的打包產物。
這一部分的總結以下:
package.json
文件裏的 sideEffects
屬性告訴打包工具:你的庫不包含任何反作用。side effects
優化中充分獲益你可能注意到咱們在本文先前的例子裏的 user-library
並無被打包到一個單獨的文件裏,而是直接暴露手動加入的 .js
源文件。
一般,一個庫會因爲如下緣由被打包:
import
路徑。像 Webpack、Rollup、Parcel 和 ESBuild 這樣的流行的打包工具被設計爲用來提供一個可以傳輸給瀏覽器使用的 bundle。它們也所以傾向於建立一個單獨的的文件,而後將你的全部代碼從新組合並輸出到這個文件裏,從而只有一個單獨的 .js
文件須要經過網絡進行傳輸。
從搖樹優化的角度來講,這致使了一個問題:反作用優化不復存在了,由於沒有模塊可以被略過。
咱們將列舉兩種狀況來講明:對搖樹優化來講,分割模塊搭配反作用優化是必須的。
爲了演示這個問題,咱們將使用 Rollup 來打包咱們的庫。同時,咱們將讓庫的其中一個模塊導入一個 CJS 格式的依賴:Lodash。
rollup.config.js
export default {
input: "src/index.js",
output: {
file: "dist/index.js",
format: "esm",
},
};
複製代碼
userAccount.js
import { isNil } from "lodash";
export const checkExistance = (variable) => !isNil(variable);
export const userAccount = {
name: "user account",
};
複製代碼
注意,咱們如今將導出 checkExistance
,而後將它導入到咱們庫的 index.js
文件裏。
如下是打包輸出的文件 dist/index.js
:
import { isNil } from "lodash";
const checkExistance = (variable) => !isNil(variable);
const userAccount = {
name: "user account",
};
const getUserAccount = () => {
return userAccount;
};
const getUserPhoneNumber = () => "***********";
const getUserName = () => "John Doe";
export { checkExistance, getUserName, getUserPhoneNumber, getUserAccount };
複製代碼
全部文件都被打包到了一個單獨的文件裏。注意 Lodash 也在此文件的頂部被導入。咱們在 user-app
裏仍然導入和之前同樣的函數,這意味着 checkExistance
函數依然未被引用。然而,在運行 Webpack 打包 user-app
以後,咱們發現即便 checkExistance
函數被標記爲了未引用,整個 Lodash 庫仍然被導入了:
/***/ "./node_modules/user-library/dist/index.js":
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "getUserName": () => (/* binding */ getUserName)
/* harmony export */ });
/* unused harmony exports checkExistance, userAccount, getUserPhoneNumber, getUserAccount */
/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lodash */ "./node_modules/user-library/node_modules/lodash/lodash.js");
/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lodash__WEBPACK_IMPORTED_MODULE_0__);
const checkExistance = (variable) => !isNil(variable);
const userAccount = {
name: "user account",
};
const getUserPhoneNumber = {
number: '***********'
};
const getUserAccount = () => {
return userAccount
};
const getUserName = () => 'John Doe';
/***/ }),
/***/ "./node_modules/user-library/node_modules/lodash/lodash.js":
/*!*****************************************************************!*\ !*** ./node_modules/user-library/node_modules/lodash/lodash.js ***! \*****************************************************************/
/***/ (function(module, exports, __webpack_require__) {
/* module decorator */ module = __webpack_require__.nmd(module);
var __WEBPACK_AMD_DEFINE_RESULT__;/** * @license * Lodash <https://lodash.com/> * Copyright OpenJS Foundation and other contributors <https://openjsf.org/> * Released under MIT license <https://lodash.com/license> * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE> * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */
// ...
複製代碼
Webpack 沒法對 Lodash 進行搖樹優化,由於它的模塊格式是 CJS。這挺讓人失望的,畢竟咱們都很明顯地組織好了咱們的庫,讓 Lodash 僅在 userAccount
模塊裏被導入,並且這個模塊也沒有被咱們的應用所引用。若是模塊結構可以保留,Webpack 就能受益於反作用優化,從而可以檢測到 userAccount
的導出項都未被引用而後直接略過這個模塊,這樣的話 Lodash 就不會被打包了。
在 Rollup 中,咱們能夠使用 preserveModules
選項來保留庫的模塊結構。其餘打包工具也有相似的選項。
export default {
input: "src/index.js",
output: {
dir: "dist",
format: "esm",
preserveModules: true,
},
};
複製代碼
Rollup 如今可以保留本來的文件結構了,咱們再次運行 Webpack,獲得如下的打包輸出:
/***/ "./node_modules/user-library/dist/index.js":
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "getUserName": () => (/* binding */ getUserName)
/* harmony export */ });
/* unused harmony export getUserAccount */
const getUserAccount = () => {
return userAccount
};
const getUserName = () => 'John Doe';
/***/ })
複製代碼
Lodash 如今和 userAccount
模塊一塊兒被略過了。
保留分割後的模塊結構以及開啓反作用優化也有助於 Webpack 的代碼分割。它對於大型應用來講是一個關鍵的優化措施,被普遍應用於那些含有多個頁面的 Web 應用。像 Nuxt 和 Next 這樣的框架都會給各個頁面配置代碼分割。
爲了演示代碼分割帶來的好處,咱們先來看看若是咱們的庫被打包到了一個單獨的文件時會發生什麼。
user-library/src/userAccount.js
export const userAccount = {
name: "user account",
};
複製代碼
user-library/src/userPhoneNumber.js
export const userPhoneNumber = {
number: "***********",
};
複製代碼
user-library/src/index.js
import { userAccount } from "./userAccount";
import { userPhoneNumber } from "./userPhoneNumber";
const getUserName = () => "John Doe";
export { userAccount, getUserName, userPhoneNumber };
複製代碼
爲了對咱們的應用進行代碼分割,咱們會使用 Webpack 的 import
語法。
user-app/src/userService1.js
import { userAccount } from "user-library";
export const logUserAccount = () => {
console.log(userAccount);
};
複製代碼
user-app/src/userService2.js
import { userPhoneNumber } from "user-library";
export const logUserPhoneNumber = () => {
console.log(userPhoneNumber);
};
複製代碼
user-app/src/index.js
const main = async () => {
const { logUserPhoneNumber } = await import("./userService2");
const { logUserAccount } = await import("./userService1");
logUserAccount();
logUserPhoneNumber();
};
main();
複製代碼
打包產生的文件如今有三個:main.js
、src_userService1_js.main.js
和 src_userService2_js.main.js
。仔細查看 src_userService1_js.main.js
的內容,咱們能夠發現整個 user-library
都被打包了:
(self["webpackChunkuser_app"] = self["webpackChunkuser_app"] || []).push([
["src_userService1_js"],
{
/***/ "./node_modules/user-library/dist/index.js":
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ( __unused_webpack_module, __webpack_exports__, __webpack_require__ ) => {
"use strict";
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ userAccount: () => /* binding */ userAccount,
/* harmony export */ userPhoneNumber: () =>
/* binding */ userPhoneNumber,
/* harmony export */
});
/* unused harmony export getUserName */
const userAccount = {
name: "user account",
};
const userPhoneNumber = {
number: "***********",
};
const getUserName = () => "John Doe";
/***/
},
/***/ "./src/userService1.js":
/*!*****************************!*\ !*** ./src/userService1.js ***! \*****************************/
/***/ ( __unused_webpack_module, __webpack_exports__, __webpack_require__ ) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ logUserAccount: () =>
/* binding */ logUserAccount,
/* harmony export */
});
/* harmony import */ var user_library__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! user-library */ "./node_modules/user-library/dist/index.js"
);
const logUserAccount = () => {
console.log(user_library__WEBPACK_IMPORTED_MODULE_0__.userAccount);
};
/***/
},
},
]);
複製代碼
雖然 getUserName
被標記爲了未引用,但 userAccount
並無被標記,即便 userService2
僅僅使用了 userPhoneNumber
。爲何會這樣呢?(譯註:原文中上面的代碼是 userService1
的而不是 userService2
)
咱們須要記住,引用導出(used exports)優化在檢查導出項是否被引用的時候,是在模塊層面上檢查的。只有從這個層面上 Webpack 才能移除那些未被使用的代碼。對於咱們的庫模塊來講,userAccount
和 userPhoneNumber
其實都被使用了。在這個狀況下,Webpack 並不能區分清 userService1
和 userService2
在導入項上的區別,正以下圖所示(你會發現 userAccount
和 userPhoneNumber
都被標註爲綠色):
這意味着 Webpack 在僅依靠引用導出優化的條件下,並不能獨立地針對每一個 chunk 進行搖樹優化。
如今,讓咱們在打包庫時保留模塊結構,這樣反作用優化就能工做:
Webpack 仍然輸出了 3 個文件,不過這一次 src_userService2_js.main.js
僅僅包含了 userPhoneNumber
裏的代碼:
(self["webpackChunkuser_app"] = self["webpackChunkuser_app"] || []).push([
["src_userService2_js"],
{
/***/ "./node_modules/user-library/dist/userPhoneNumber.js":
/*!***********************************************************!*\ !*** ./node_modules/user-library/dist/userPhoneNumber.js ***! \***********************************************************/
/***/ ( __unused_webpack_module, __webpack_exports__, __webpack_require__ ) => {
"use strict";
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ userPhoneNumber: () =>
/* binding */ userPhoneNumber,
/* harmony export */
});
const userPhoneNumber = {
number: "***********",
};
/***/
},
/***/ "./src/userService2.js":
/*!*****************************!*\ !*** ./src/userService2.js ***! \*****************************/
/***/ ( __unused_webpack_module, __webpack_exports__, __webpack_require__ ) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ logUserPhoneNumber: () =>
/* binding */ logUserPhoneNumber,
/* harmony export */
});
/* harmony import */ var user_library__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! user-library */ "./node_modules/user-library/dist/userPhoneNumber.js"
);
const logUserPhoneNumber = () => {
console.log(
user_library__WEBPACK_IMPORTED_MODULE_0__.userPhoneNumber
);
};
/***/
},
},
]);
複製代碼
src_userService1_js.main.js
也和上面相似,僅僅包含了咱們庫裏的 userAccount
模塊。
在上圖中咱們看到,userAccount
和 userPhoneNumber
仍然被識別爲被引用的導出項,畢竟它們都在應用裏被引用了至少一次。不過,這一次反作用優化讓 Webpack 得以略過 userAccount
模塊,由於它從未被 userService2
所導入。一樣的事情也發生在了 userPhoneNumber
和 userService1
之間。
咱們如今理解了:保留庫中原始的模塊結構是很重要的。不過,若是原始模塊結構裏只有一個模塊,好比 index.js
文件,而且其中包含着全部的代碼的話,那麼保留這種模塊結構是毫無用處的。要打造一個對搖樹優化有着良好適配的庫,咱們必須將庫的代碼劃分到若干個小的模塊中,同時每一個模塊負責咱們代碼邏輯的一部分。
若是要使用「樹」的比喻,咱們須要將樹上每片葉子視爲一個模塊。更小、更弱的葉子在樹被搖動的時候更容易掉落!若是樹上的葉子更少、更強,那麼搖樹的結果可能就不同了。
本部分的總結以下:
**打包工具並非惟一可以影響你的庫被搖樹優化的東西。**轉譯工具也會對搖樹優化形成負面影響,由於它們會移除 ES 模塊,或者丟失模塊樹。
轉譯工具的目的之一是讓你的代碼可以在那些不支持 ES 模塊的瀏覽器中工做。不過,咱們也須要記住:咱們的庫並不總會直接地被瀏覽器所加載,而是會被應用所導入。因此,鑑於如下兩條理由,咱們不能針對特定瀏覽器來轉譯咱們的庫代碼:
若是你的庫因爲某些緣由確實須要被轉譯,那麼你須要保證轉譯工具不會移除 ES 模塊的語法,以及不會移除本來的模塊結構,緣由正如前文所述。
據我所知,有兩個轉譯工具會移除掉上述的兩個內容。
Babel 可以使用 Babel preset-env 來讓你的代碼兼容指定的目標瀏覽器(target browsers)。這個插件默認會將庫代碼裏的 ES 模塊移除。爲了不它的發生,咱們須要把 modules
選項設爲 false
:
module.exports = {
env: {
esm: {
presets: [
[
"@babel/preset-env",
{
modules: false,
},
],
],
},
},
};
複製代碼
在編譯你的代碼時,TypeScript 會根據 tsconfig.json
文件裏的 target
和 module
選項來轉換你的模塊。
爲了不它的發生,咱們要將 target
和 module
選項設置到至少 ES2015
或 ES6
。
此部分的總結以下:
JavaScript 的搖樹優化在 Rollup 的帶動下流行了起來。Webpack 自從 v2 以來就支持了搖樹優化。各打包工具都在搖樹優化上作得愈來愈好。
還記得咱們在上文講到的 innerGraph
優化嗎?它可以讓 Webpack 將模塊的導出項和其餘模塊的導入項關聯起來。這項優化是在 Webpack 5 中被引入的。咱們在本文裏雖然一直在使用 Webpack 5,不過仍是有必要認識到這項優化改變了整個業界。它可以讓 Webpack 遞歸地尋找未被使用的導出項!
爲了展現它究竟是怎麼作的,考慮 user-library
中的 index.js
文件:
import { userAccount } from "./userAccount";
const getUserAccount = () => {
return userAccount;
};
const getUserName = () => "John Doe";
export { getUserName, getUserAccount };
複製代碼
咱們的 user-app
僅使用了其中的 getUserName
:
import { getUserName } from "user-library";
console.log(getUserName());
複製代碼
如今,咱們對比一下在有和沒有 innerGraph
優化的狀況下,打包輸出有什麼不一樣。注意,這裏 usedExports
和 sideEffects
優化都是開啓的。
沒有 innerGraph
優化(好比使用 Webpack 4):
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/*! exports provided: userAccount, userPhoneNumber, getUserName, getUserAccount */
/*! exports used: getUserName */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return getUserName; });
/* unused harmony export getUserAccount */
/* harmony import */ var _userAccount_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userAccount.js */ "./node_modules/user-library/dist/userAccount.js");
const getUserAccount = () => {
return _userAccount_js__WEBPACK_IMPORTED_MODULE_0__[/* userAccount */ "a"]
};
const getUserName = () => 'John Doe';
/***/ }),
/***/ "./node_modules/user-library/dist/userAccount.js":
/*!*******************************************************!*\ !*** ./node_modules/user-library/dist/userAccount.js ***! \*******************************************************/
/*! exports provided: userAccount */
/*! exports used: userAccount */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return userAccount; });
const userAccount = {
name: 'user account'
};
/***/ }),
複製代碼
有 innerGraph
優化(好比使用 Webpack 5):
/***/ "./node_modules/user-library/dist/index.js":
/*!*************************************************!*\ !*** ./node_modules/user-library/dist/index.js ***! \*************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "getUserName": () => (/* binding */ getUserName)
/* harmony export */ });
/* unused harmony export getUserAccount */
const getUserAccount = () => {
return userAccount
};
const getUserName = () => 'John Doe';
/***/ })
複製代碼
Webpack 5 可以徹底移除 userAccount
模塊,可是 Webpack 4 不行,即便 getUserAccount
被標記爲了未引用。這是由於 inngerGraph
優化的算法可以讓 Webpack 5 將模塊中未引用的導出項和它對應的導入項連接起來。在咱們的例子裏,userAccount
模塊僅被 getUserAccount
函數所使用,所以能夠被直接略過。
Webpack 4 則沒有這項優化。開發者在使用這個版本的 Webpack 的時候所以應該提升警戒,限制單個源文件裏的導出項數量。 若是一個源文件包含多個導出項,Webpack 會包含全部對應的導入項,即便對於真正被須要的導出項來講有些導入項是多餘的。
總的來講,咱們應該確保老是使用最新版的打包工具,這樣咱們就能從最新的搖樹優化中獲益。
對一個庫進行的搖樹優化,並非在配置文件裏隨便加一行來啓用就能得到很好的效果。它的優化質量取決於多個因素,本文僅僅列出了其中的一小部分。不過,不管咱們遇到的問題是什麼,本文裏作過的如下兩件事情是對任何想要對庫進行搖樹優化的人很重要的:
user-library
和 user-app
的例子作這種事情。我真切地但願本文可以爲你提供幫助,讓你正在進行的構建擁優化程度最高的庫的任務變得可能!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。