了解 CommonJS 如何使捆绑包变得更大

发布时间:2020-11-19 17:41:29 阅读:8
在这篇文章中,我们将研究什么是 CommonJS,以及为什么它会使你的 JavaScript 包超出必需的范围。

简介:为了确保捆绑程序能够成功优化你的应用程序,请避免依赖 CommonJS 模块,并在整个应用程序中使用 ECMAScript 模块语法。

什么是 CommonJS?

CommonJS 是 2009 年以来的一项标准,为 JavaScript 模块建立了约定。它最初打算在 Web 浏览器之外使用,主要用于服务器端应用程序。

使用 CommonJS,你可以定义模块,从中导出功能,以及将其导入其他模块中。例如,下面定义了片段其中出口 5 个功能的模块:addsubtractmultiplydivide,和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.jsconst { add } = require(‘./utils');console.log(add(1, 2)); 

调用index.jsnode将输出数量3在控制台中。

由于 2010 年代初期浏览器中缺乏标准化的模块系统,CommonJS 也成为 JavaScript 客户端库的流行模块格式。

CommonJS 如何影响捆绑包大小?

服务器端 JavaScript 应用程序的大小并不像浏览器中那样重要,这就是为什么 CommonJS 并非在设计时就考虑到了减小生产包的大小。同时,分析表明,JavaScript 软件包的大小仍然是使浏览器应用程序变慢的第一原因。

JavaScript 捆绑程序和压缩程序(例如webpackterser)执行不同的优化以减小应用程序的大小。他们在构建时分析你的应用程序,他们尝试从你不使用的源代码中尽可能多地删除。

例如,在上面的代码段中,你的最终捆绑包应仅包含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 模块,然后再试一次。这一次,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 会使用 ECMAScript 模块语法从 utils.js 中导入:

import { add } from './utils';

console.log(add(1, 2)); 

使用相同的webpack配置,我们可以构建应用程序并打开输出文件。它现在是40个字节,输出如下:

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

请注意,最终的包没有包含来自 util.js 中我们不使用的任何函数,并且没有来自 lodash 的跟踪!更进一步,terser (webpack 使用的 JavaScript 缩小器) 将 add 函数内联到 console.log 中。

你可能会问一个合理的问题,为什么使用 CommonJS 会导致输出包几乎增大 16000 倍?当然,这是一个玩具样例,实际上,大小的差异可能没有那么大,但是 CommonJS 很有可能为你的生产构建增加了很大的权重。

在一般情况下,CommonJS 模块更难优化,因为它们比 ES 模块更动态。为了确保你的 bundler 和 minifier 能够成功地优化你的应用程序,请避免依赖于 CommonJS 模块,并在整个应用程序中使用 ECMAScript 模块语法。

请注意,即使你在中使用 ECMAScript 模块,如果你使用 index.js 的模块是 CommonJS 模块,应用程序的分发包大小也会受到影响。

为什么 CommonJS 会让你的应用变大?

为了回答这个问题,我们将研究webpackModuleConcatenationPlugin 的行为,然后讨论静态可分析性。这个插件将你所有模块的范围连接到一个闭包中,让你的代码在浏览器中有更快的执行时间。让我们来看一个例子:

// 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)); 

上面,我们有一个 ECMAScript 模块,我们在 index.js 中导入它。我们还定义了一个减法函数。我们可以使用与上面相同的 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));**

/******/ })(); 

在上面的输出中,所有函数都在同一个名称空间中。为了防止冲突,webpackindex.js 中的减法函数重命名 为 index_subtract

如果使用压缩程序处理上面的源代码,它会:

  • 删除未使用的函数 subtractindex_subtract
  • 删除所有注释和多余的空格
  • console.log 调用中内联 add 函数的主体

通常,开发人员将这种未使用导入的移除称为“摇树”。是因为 webpack 能够静态地(在构建时)理解我们从 util .js 导入哪些符号,导出哪些符号。

CommonJS 相比,ES模块在默认情况下启用了这种行为,因为它们更容易静态分析。

让我们看看完全相同的例子,但这次改变 util .js 使用 CommonJS 而不是ES模块:

// 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.jsindex.js 放在同一个命名空间下,我们需要动态地在运行时,该add功能使用__webpack_require__

这是必要的,因为使用 CommonJS,我们可以从任意表达式中获得导出名称。例如,下面的代码是一个绝对有效的构造:

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

在构建时,绑定者无法知道导出符号的名称,因为只有在运行时才能在用户浏览器上下文中使用的信息。

这样,minifier 无法从依赖关系中理解 index.js 到底使用了什么,所以它无法将其树式删除。我们还将观察到第三方模块的完全相同的行为。如果我们从 node_modules 导入一个 CommonJS 模块,则你的构建工具链将不能正确地优化它。

CommonJS 中的 Tree-shaking

由于 CommonJS 模块是动态定义的,因此要分析它们要困难得多。例如,与作为表达式的 CommonJS 相比,ES 模块中的导入位置始终是字符串文字。

在某些情况下,如果你使用的库遵循了使用 CommonJS 的特定约定,那么可以使用第三方 webpack 插件在构建时删除未使用的导出。尽管这个插件增加了对树晃动的支持,但它并没有涵盖你的依赖项使用 CommonJS 的所有不同方式。这意味着你不会获得与 ES 模块相同的保证。此外,它还增加了构建过程中在默认 webpack 行为之上的额外成本。

结论

为确保捆绑程序可以成功优化应用程序,请避免依赖 CommonJS 模块,并在整个应用程序中使用 ECMAScript 模块语法。

以下是一些可行的技巧,可用来验证你是否处于最佳状态:

  • 使用 Rollup.js 的 node-resolve 插件并设置modulesOnly标志以指定你仅希望依赖 ECMAScript 模块。
  • 使用该软件包 is-esm 来验证 npm 软件包是否使用 ECMAScript 模块。
  • 如果你使用的是 Angular,则默认情况下,如果你依赖不可摇树的模块,则会收到警告。

我们在微信上24小时期待你的声音
解答:网站优化,网站建设,搜索引擎优化,APP 开发,小程序开发

非常感谢您有耐心的读完这篇文章:"了解 CommonJS 如何使捆绑包变得更大",此文章仅为提供更多信息供用户参考使用或为学习交流的方便。如果对您有帮助,请收藏我们的网址:https://www.91webs.cn


18617670560