不要再依靠 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”