9
头图

原文地址 What Are CJS, AMD, UMD, ESM, System, and IIFE?

现代 Javascript 项目需要用打包工具来将小段代码编译成库或者应用程序那种更大更复杂的东西。流行的打包器有webpack、Rollup、Parcel、RequireJS 和 Browserify。它们将 JavaScript 代码转换为可以作为一个 bundle 加载的模块。
一个 bundle 可以用不同的格式打包。这篇文章中,我们将展示 CJS, AMD, UMD, ESM, System 和 IIFE 格式的打包示例。

打包工具和格式

下面是一个标准的 HTML 文件,第5行引入了一个样式文件,第6行引入了一个 JS 文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    <link rel="stylesheet" href="style.css" />
    <script src="src/index.js"></script>
  </head>
  <body>
    ... 
  </body>
</html>

简单情况下可以将所有的 JavaScript 代码放到一个文件中。随着项目的增大,我们需要将代码拆分成具有独立命名空间的独立模块。除了更好的结构,模块化还带来了封装的功能、依赖管理和复用性。
这就是打包工具出现的原因。它需要将小块的 JavaScript 代码,样式表和图片编译成更大更复杂的像库或者应用程序一样的东西。
打包工具是如何将打包的代码格式化为输出?有很多的选择, 下面是 Rollup 定义的一些格式:

  • cjs (CommonJS) — 适用于 Node 和其他打包工具(别名:commonjs)。
  • amd (Asynchronous Module Definition,异步模块化定义) — 与 RequireJS 等模块加载工具一起使用。
  • umd (Universal Module Definition,通用模块化定义) — amdcjsiife 包含在一个文件中。
  • es — 将 bundle 保存为 ES 模块文件。适用于其他打包工具,在现代浏览器中用 <script type=module> 标签引入(别名:ems, module)。
  • system — SystemJS 加载器的原生格式 (别名:systemjs)。
  • iife<script> 标签引入的自执行函数。如果你想为你的应用创建一个包,你需要用到的可能就是这种。

在这篇文章中,我们将通过示例解释这些格式。

示例

下面是一个打包四个文件的示例:

  • index.js
  • increase.js
  • decrease.js
  • other.js

入口文件是 index.js

/**
 * This is the main file
 */
import { increase } from './increase';
import { decrease } from './decrease';
import others, { e } from './others';
function multiply(total, value) {
  return total * value;
}

function divide(total, value) {
  return total / value;
}

export function power(total, value) {
  return total ** value;
}

let total = others.a;

total = increase(total, 10);
total = increase(total, 20);
total = decrease(total, 5);
total = multiply(total, e);

console.log(`Total is ${total}`);

4-6 行中,index.js 明确地列出了从 increase.jsdecrease.jsother.js 中引入的内容。

这是引入的 increase.js

/**
 * Increase the current total value
 * @param {number} total The current total value
 * @param {number} value The new value to be added
 * @returns {number} The new total value
 */
export const increase = (total, value) => total + value;

这是引入的 decrease.js

/**
 * Decrease the current total value
 * @param {number} total The current total value
 * @param {number} value The new value to be subtracted
 * @returns {number} The new total value
 */
export const decrease = (total, value) => total - value;

这是引入的 other.js

export default {
  a: 1,
  b: 2,
  c: () => 3,
};

export const d = 4;
export const e = 5;

在这个示例中, other.js 中的 const dindex.js 中的 function divide() 未被使用。index.js 中的 function power() 虽然也未用到但被导出了。

ES2015/ES6 引入了静态 importexport,允许静态分析器在不运行代码的情况下构建完整的依赖关系树。此外,这为 tree shaking(摇树)优化奠定了基础。根据维基百科

Tree shaking 从入口开始,只包含可能被执行的函数,删除包中未使用的函数。

因为这个示例是用 ES2015 编写的,且带有特定的导入(不是 import *),所以摇树过程会删除所有生成格式中的 const dfunction divide()。因为导出的函数可能会被使用,function power() 得以保留。

CommonJS(CJS)

CJS 适用于浏览器之外的 Node 和其他生态系统。它在服务端被广泛使用。CJS 可以通过使用 require() 函数和 module.exports 来识别。require() 是一个可用于从另一个模块导入 symbols 到当前作用域的函数。 module.exports 是当前模块在另一个模块中引入时返回的对象。
CJS 模块的设计考虑到了服务器开发。这个 API 天生是同步的。换言之,在源文件中按 require 的顺序瞬时加载模块。
由于 CJS 是同步的且不能被浏览器识别,CJS 模块不能在浏览器端使用,除非它被转译器打包。像 BabelTraceur 那样的转译器,是一种帮助我们在新版 JavaScript 中编码的工具。如果环境原生不支持新版本的 JavaScript,转译器将它们编译成支持的 JS 版本。
下面是 Rollup 生成的 CJS 格式的文件:

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

/**
 * Increase the current total value
 * @param {number} total The current total value
 * @param {number} value The new value to be added
 * @returns {number} The new total value
 */
const increase = (total, value) => total + value;

/**
 * Decrease the current total value
 * @param {number} total The current total value
 * @param {number} value The new value to be subtracted
 * @returns {number} The new total value
 */
const decrease = (total, value) => total - value;

var others = {
  a: 1,
  b: 2,
  c: () => 3,
};
const e = 5;

/**
 * This is the main file
 */

function multiply(total, value) {
  return total * value;
}

function power(total, value) {
  return total ** value;
}

let total = others.a;

total = increase(total, 10);
total = increase(total, 20);
total = decrease(total, 5);
total = multiply(total, e);

console.log(`Total is ${total}`);

exports.power = power;

我们在浏览器端执行这个文件,它会报错,报错信息是 exports is not defined (第3行)。
这个错误可以通过 index.html 中添加以下代码来修复:

<script>
  const exports = {};
</script>

异步模块定义(AMD)

AMD 脱胎于 CJS,支持异步模块加载。AMD 和 CJS 的主要区别在于它是否支持异步模块加载。RequireJS 使用 AMD 在浏览器端工作。
维基百科

AMD 提供了一些 CJS 相似的特性。它允许在代码中使用类似的 exportsrequire() 接口,尽管它自己的 define() 接口更基础更受欢迎。

下面是 Rollup 生成的 AMD 格式的文件:

define(['exports'], function (exports) { 'use strict';

  /**
   * Increase the current total value
   * @param {number} total The current total value
   * @param {number} value The new value to be added
   * @returns {number} The new total value
   */
  const increase = (total, value) => total + value;

  /**
   * Decrease the current total value
   * @param {number} total The current total value
   * @param {number} value The new value to be subtracted
   * @returns {number} The new total value
   */
  const decrease = (total, value) => total - value;

  var others = {
    a: 1,
    b: 2,
    c: () => 3,
  };
  const e = 5;

  /**
   * This is the main file
   */
                                        
  function multiply(total, value) {
    return total * value;
  }

  function power(total, value) {
    return total ** value;
  }

  let total = others.a;

  total = increase(total, 10);
  total = increase(total, 20);
  total = decrease(total, 5);
  total = multiply(total, e);

  console.log(`Total is ${total}`);

  exports.power = power;

  Object.defineProperty(exports, '__esModule', { value: true });

});

我们在浏览器上执行这个文件,它会报错,报错信息是 define is not a function (第1行)。
这个报错能通过在 index.html 中引入 require.js 修复。

<script src=”https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>

通用模块定义(UMD)

UMD 被设计用于任何地方 — 包括服务端和浏览器端。它试图兼容目前最流行的 script 加载器(如 RequireJS)。在许多情况下,它使用 AMD 作为基础,且兼容 CJS。然而兼容增加了一些复杂度,使得读写变得更加困难。
下面是 Rollup 生成的 UMD 格式的文件:

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.example = {}));
}(this, (function (exports) { 'use strict';

  /**
   * Increase the current total value
   * @param {number} total The current total value
   * @param {number} value The new value to be added
   * @returns {number} The new total value
   */
  const increase = (total, value) => total + value;

  /**
   * Decrease the current total value
   * @param {number} total The current total value
   * @param {number} value The new value to be subtracted
   * @returns {number} The new total value
   */
  const decrease = (total, value) => total - value;

  var others = {
    a: 1,
    b: 2,
    c: () => 3,
  };
  const e = 5;

  /**
   * This is the main file
   */
                             
  function multiply(total, value) {
    return total * value;
  }

  function power(total, value) {
    return total ** value;
  }

  let total = others.a;

  total = increase(total, 10);
total = increase(total, 20);
  total = decrease(total, 5);
  total = multiply(total, e);

  console.log(`Total is ${total}`);

  exports.power = power;

  Object.defineProperty(exports, '__esModule', { value: true });

})));

此代码可以在浏览器中工作。

ES2015 模块(ESM)

静态 import 指令可用于将模块引入当前作用域。与 requiredefine 不同,这个指令只能放在文件的顶部。动态 import() 目前处于 TC39 流程的第4阶段。
另一方面,export 指令可用于显式地将对象公开。
下面是 Rollup 生成的 ESM 格式的文件:

/**
 * Increase the current total value
 * @param {number} total The current total value
 * @param {number} value The new value to be added
 * @returns {number} The new total value
 */
const increase = (total, value) => total + value;

/**
 * Decrease the current total value
 * @param {number} total The current total value
 * @param {number} value The new value to be subtracted
 * @returns {number} The new total value
 */
const decrease = (total, value) => total - value;

var others = {
  a: 1,
  b: 2,
  c: () => 3,
};
const e = 5;

/**
 * This is the main file
 */

function multiply(total, value) {
  return total * value;
}

function power(total, value) {
  return total ** value;
}

let total = others.a;

total = increase(total, 10);
total = increase(total, 20);
total = decrease(total, 5);
total = multiply(total, e);

console.log(`Total is ${total}`);

export { power };

我们在浏览器上运行这个文件,它会报错,报错信息是 Uncaught SyntaxError: Unexpected token 'export'(第45行)。
可以通过设置 script 标签的 typemodule 来修复这个报错:

<script type=”module” src=”dist/bundle.js”></script>

系统模块

SystemJs 是一个通用的模块加载器,支持 CJS,AMD 和 ESM 模块。Rollup 可以将代码打包成 SystemJS 的原生格式。
下面是 Rollup 生成的 System 格式的文件:

System.register([], (function (exports) {
  'use strict';
  return {
    execute: (function () {

      exports('power', power);

      /**
       * Increase the current total value
       * @param {number} total The current total value
       * @param {number} value The new value to be added
       * @returns {number} The new total value
       */
       const increase = (total, value) => total + value;

      /**
       * Decrease the current total value
       * @param {number} total The current total value
       * @param {number} value The new value to be subtracted
       * @returns {number} The new total value
       */
       const decrease = (total, value) => total - value;

      var others = {
        a: 1,
        b: 2,
        c: () => 3,
      };
      const e = 5;

      /**
       * This is the main file
       */
       
       function multiply(total, value) {
         return total * value;
       }
       
       function power(total, value) {
         return total ** value;
       }
       
       let total = others.a;
       
       total = increase(total, 10);
       total = increase(total, 20);
       total = decrease(total, 5);
       total = multiply(total, e);
       
       console.log(`Total is ${total}`);

    })
  };
}));

我们在浏览器运行这个文件,它会报错,报错信息是 System is not defined(第1行)。
安装 system.js

npm install --save-dev systemjs

index.html 中引入 system.js 可以解决这个问题:

<script src="node_modules/systemjs/dist/s.min.js"></script>

立即执行的函数表达式(IIFE)模块

正如模块名所示,IIFE 是一个适合用 <script> 标签引入的自执行函数。我们可以用这种格式为应用创建一个包。它帮助我们将内容放到命名空间中,避免变量冲突并使代码私有。
下面是 Rollup 生成的 IIFE 格式的文件:

var example = (function (exports) {
  'use strict';

  /**
   * Increase the current total value
   * @param {number} total The current total value
   * @param {number} value The new value to be added
   * @returns {number} The new total value
   */
  const increase = (total, value) => total + value;

  /**
   * Decrease the current total value
   * @param {number} total The current total value
   * @param {number} value The new value to be subtracted
   * @returns {number} The new total value
   */
  const decrease = (total, value) => total - value;

  var others = {
    a: 1,
    b: 2,
    c: () => 3,
  };
  const e = 5;

  /**
   * This is the main file
   */

  function multiply(total, value) {
    return total * value;
  }
  
  function power(total, value) {
    return total ** value;
  }

  let total = others.a;

  total = increase(total, 10);
  total = increase(total, 20);
  total = decrease(total, 5);
  total = multiply(total, e);

  console.log(`Total is ${total}`);

  exports.power = power;

  Object.defineProperty(exports, '__esModule', { value: true });

  return exports;

}({}));

此代码可以在浏览器中运行。

生成多种格式

rollup.config.js 是 Rollup 的配置文件。它是可选的,但功能强大,方便,因此推荐使用。
下面是我们用来一次生成多个输出格式的输出配置:

output: [
  {
    file: 'dist/bundle.amd.js',
    format: 'amd',
    sourcemap: false,
  },
  {
    file: 'dist/bundle.cjs.js',
    format: 'cjs',
    sourcemap: false,
  },
  {
    file: 'dist/bundle.umd.js',
    format: 'umd',
    name: 'example',
    sourcemap: false,
  },
  {
    file: 'dist/bundle.es.js',
    format: 'es',
    sourcemap: false,
  },
  {
    file: 'dist/bundle.iife.js',
    format: 'iife',
    name: 'example',
    sourcemap: false,
  },
  {
    file: 'dist/bundle.system.js',
    format: 'system',
    sourcemap: false,
  },
]

结论

今天我们探讨了 JavaScript 模块化。在下一篇文章中,我们将逐步介绍如何用 Rollup 打包 JavaScript 项目。
请持续关注!

注:感谢 Daria Mehra 审阅这篇文章。

文章中的代码参考


MrBigShot
4.8k 声望3.1k 粉丝

菜鸡一个