不要再依賴 CommonJS 了

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

什麼是 CommonJS?

CommonJS 是 2009 年的標準,爲 JavaScript 模塊建立了約定。它最初打算在 Web 瀏覽器之外的場景中使用,主要用於服務端應用程序。
使用 CommonJS,你可以定義模塊,從中導出功能,並將它們導入其他模塊中。例如,下面的代碼片段定義了一個模塊,其導出五個函數:add,subtract,multiply,divide 和 max:

// utils.js
const { maxBy } = require('lodash-es');
const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

稍後,另一個模塊可以導入和使用這些函數:

// index.js
const { add } = require(‘./utils');
console.log(add(1, 2));

使用 node 調用 index.js 將在控制檯中輸出數字 3。
由於 2010 年代初期瀏覽器中缺乏標準化的模塊系統,CommonJS 也成爲了 JavaScript 客戶端庫的流行模塊格式。

CommonJS 如何影響最終的打包大小?

服務端 JavaScript 應用程序的大小並不像瀏覽器中那樣重要,所以 CommonJS 並沒有在設計時考慮到包大小的控制。與此同時,有分析表明 JavaScript 的包體積仍然是拖慢瀏覽器應用的主要因素之一。

JavaScript 打包器和壓縮器(minifier),例如 webpack 和 terser,會執行多種優化措施以減小應用程序的大小。它們在構建時分析你的應用程序,嘗試儘可能刪掉那些沒用到的源代碼。

例如,在上面的代碼片段中,你的最終打包應該只包括 add 函數,因爲這是你從 utils.js 中導入到 index.js 中的唯一符號。
我們使用以下 webpack 配置來構建這個應用:

const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production',
};

在這裏,我們指定了要使用生產模式優化並將 index.js 用作入口點。調用 webpack 之後,如果我們查看輸出大小,將看到下面這樣的內容:

$ cd dist && ls -lah
625K Apr 13 13:04 out.js

請注意,這個包的大小爲 625KB。看一下輸出,我們將找到來自 utils.js 的所有函數,外加來自 lodash 的很多模塊。儘管我們在 index.js 中不使用 lodash,但它也被加進了輸出,這給我們的生產資產增加了很多額外負擔。
現在我們將模塊格式更改爲 ECMAScript 2015,然後重試。這次,utils.js 將變成如下所示:

export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
import { maxBy } from 'lodash-es';
export const max = arr => maxBy(arr);

並且 index.js 將使用 ES2015 模塊語法從 utils.js 導入:

import { add } from './utils';
console.log(add(1, 2));

使用相同的 webpack 配置,我們可以構建應用程序並打開輸出文件。現在大小隻有 40 字節,輸出如下:

(()=>{"use strict";console.log(1+2)})();

請注意,最後的打包中並沒有包含 utils.js 中我們沒有用到的任何函數,而且也沒有 lodash 的痕跡!更進一步,terser(webpack 使用的 JavaScript 壓縮器)在 console.log 中內聯了 add 函數。

你可能會問一個問題,爲什麼使用 CommonJS 會導致輸出包大了接近 16,000 倍?當然,上面這個應用只是一個簡單的示例,實際應用中的體積差異可能沒那麼大,但 CommonJS 也很有可能給你的生產構建增添了很大的負擔。

一般情況下,CommonJS 模塊難以優化,因爲它們比 ES 模塊動態得多。爲確保打包器和壓縮器可以成功優化應用程序,請避免依賴 CommonJS 模塊,並在整個應用程序中使用 ES2015 模塊語法。

請注意,即使你在 index.js 中使用了 ES2015,但如果你使用的模塊是 CommonJS,應用程序的打包大小也會受到影響。

爲什麼 CommonJS 會讓應用程序體積更大?

爲了回答這個問題,我們將研究 webpack 中 ModuleConcatenationPlugin 的行爲,然後討論靜態可分析性。這個插件將所有模塊合併爲一個閉包,並能讓你的代碼在瀏覽器中執行得更快。我們來看一個例子:

// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from ‘./utils';
const subtract = (a, b) => a - b;
console.log(add(1, 2));

如上所示,我們有一個 ES2015 模塊,然後將其導入 index.js 中。我們還定義了一個 subtract 函數。我們可以使用與上面相同的 webpack 配置來構建項目,但是這次我們將禁用最小化:

const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    minimize: false
  },
  mode: 'production',
};

看一下生成的輸出:

/******/ (() => { // webpackBootstrap
/******/     "use strict";
// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**
/******/ })();

在上面的輸出中,所有函數都在同一個命名空間內。爲了防止衝突,webpack 將 index.js 中的 subtract 函數重命名爲 index_subtract。
如果讓一個壓縮器處理上面的源代碼,它將:

  • 刪除未使用的 subtract 和 index_subtract 函數
  • 刪除所有註釋和多餘的空格
  • 在 console.log 調用中內聯 add 函數的主體
    開發人員通常將這種移除未使用的導入的操作稱爲搖樹優化(tree-shaking)。因爲 webpack 能夠靜態地(在構建時)瞭解我們從 utils.js 導入及導出的符號,所以它才能實現搖樹優化。

ES 模塊默認啓用此行爲,因爲與 CommonJS 相比,它們更容易進行靜態分析。
我們來看完全相同的示例,但是這次將 utils.js 更改爲使用 CommonJS 模塊:

// utils.js
const { maxBy } = require('lodash-es');
const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

這個小小的更新會顯著影響輸出結果。受限於文章篇幅,這裏我只分享其中的一小部分:

...
(() => {
"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));
})();

請注意,最終的打包包含一些 webpack“運行時”:也就是注入的代碼,負責從打包的模塊中導入 / 導出功能。這次,我們不是將 utils.js 和 index.js 中的所有符號放在同一個命名空間下,而是在運行時動態請求使用 webpack_require 的 add 函數。
這是必需的,因爲使用 CommonJS,我們可以從任意表達式中獲取導出名稱。例如,下面的代碼是絕對有效的構造:

module.exports[localStorage.getItem(Math.random())] = () => { … };

打包器無法在構建時知道導出的符號是什麼名稱,因爲這裏需要的信息在用戶瀏覽器的上下文中,而且僅在運行時可用。
這樣壓縮器就無法從 index.js 的依賴項中瞭解它到底使用了哪些內容,因此無法將無用代碼優化掉。我們還能觀察到第三方模塊也有完全相同的行爲。如果我們從 node_modules 導入 CommonJS 模塊,你的構建工具鏈將無法正確優化它。

基於 CommonJS 實現搖樹優化
由於 CommonJS 模塊是動態定義的,因此它們分析起來要困難得多。例如,與 CommonJS 相比,ES 模塊中的導入位置始終是一個字面量(前者則是一個表達式)。

在某些情況下,如果你使用的庫遵循有關 CommonJS 用法的特別約定,則可以在構建時使用這個第三方 webpack 插件刪除未使用的導出。但儘管這個插件增加了對搖樹優化的支持,但並未涵蓋依賴項使用 CommonJS 的所有可能方式。這意味着你無法獲得與 ES 模塊相同的保障。此外,除了默認的 webpack 行爲外,它還會在構建過程中增加額外的成本。

結論
總之,再次強調,爲了確保打包器可以成功優化你的應用程序,請避免依賴 CommonJS 模塊,並在整個應用程序中使用 ES2015 模塊語法。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/zhibo

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-05-20
本文作者:Minko Gechev
本文來自:“InfoQ”,瞭解相關信息可以關注“InfoQ

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章