JS模块化之CommonJS、AMD、UMD和ESM规范

3,936 阅读5分钟

在现代化的前端工程中,模块化已经成为习以为常的规范之一。成熟的框架、打包和引用体系已经让我们不需要关注模块的具体格式,随着浏览器对es的支持,开箱即用的 import 和 export 也非常方便。但当我们的项目,需要提供给不同类型的其他项目使用,或者发布到npm平台,就需要对模块规范有一定的了解,以便于做按需的打包和优化。

要了解模块化的规范,首先要了解它的产生背景。最初我们都使用script标签来引入js,但当一个页面引入的js文件越来越多时,就产生了几个难以避免的问题:

  1. 全局变量污染
  2. 变量重名
  3. js之间的依赖关系复杂,无法保证顺序

模块化规范的出现就是为了解决以上问题,同时提高代码的效率和复用性。主流的模块化规范包括CommonJS、AMD、CMD、UMD和ESM这几种,其中CommonJS主要用于服务端NodeJS项目;AMD和CMD提供相似的异步加载模块方式,但对依赖的处理稍有不同;ESM是依赖于ES静态模块结构的一种更为高效的模块规范;UMD则是对以上几种规范的兼容。

可以简单看一下它们的特点:

规范使用场景加载顺序加载方式
CommonJS服务端同步运行时
AMD客户端异步运行时
ESMoudule客户端异步编译时
UMD客户端异步编译时

CommonJS

CommonJS是一种用于非浏览器环境的JavaScript模块化规范,最常见的场景是用于NodeJS。

用法:

 // 引入 
const doSomething = require('./doSomething.js'); 

// 导出
module.exports = function doSomething(n) { 
// do something 

特点:

  • 模块在运行时加载和执行,并且只在首次加载时运行一次,然后将运行结果缓存,以备后续多次加载;
  • 模块加载方式为同步加载,多个模块依次按顺序加载;
  • require 语句可以放在块级作用域或条件语句中

AMD

AMD(Asynchronous Module Definition),即异步模块化定义规范,主要用于浏览器端,其中最具代表性的是require.js。

用法:

 // 引入
require(["./amd.js"], function (m) { 
    console.log(m); 
});

// 导出
define(['dep1', 'dep2'], function (dep1, dep2) { 
    // Define the module value by returning a value. 
    return function () {}; 
});

特点:

  • 异步加载模块
  • 依赖前置,即模块加载前会先加载依赖,加载完成后会执行回调

CMD

CMD 和 AMD 一样,都是用于浏览器端的异步模块定义规范,最常见的实践是sea.js。它和AMD的最大区别是对依赖的处理时机,AMD要求先加载依赖,再执行当前模块逻辑,CMD则是执行到相应依赖时再加载依赖。

用法:

 // 引入 
seajs.use(["./cmd2.js"], function () { 
    ...
});

// 导出
define(function (require, exports, module) { 
    ...
    const num = require("./cmd3.js");
    ...
});

特点:

  • 和AMD规范大体一致

  • 区别是依赖后置,即需要的时候才通过require来加载依赖

UMD

UMD(Universal Module Definition),通用模块定义,顾名思义是对以上几种标准的统一,使每个版本都能兼容运行。

用法:

 (function (root, factory) { 
    if (typeof define === "function" && define.amd) { 
    // 支持 AMD 规范
        define(["jquery", "underscore"], factory);
    } else if (typeof define === 'function' && define.cmd){ 
    // 支持 CMD 规范
        define(function(require, exports, module) { 
            module.exports = factory() 
        })
    } else if (typeof exports === "object") { 
    // 支持 CommonJS
        module.exports = factory(require("jquery"), require("underscore")); 
    } else { 
    // 支持全局引用
        root.Requester = factory(root.$, root._); 
    } 
}(this, function ($, _) { 
    // this is where I defined my module implementation 
    var Requester = { // ... }; 
    return Requester; 
}))

特点:

  • 兼容浏览器和服务端两种场景
  • 兼容 CMD、AMD、CJS 等多种规范,用法也相同

ESM

ESM(ES Module),基于ES的模块标准,也是当前最常使用的模块化标准。

用法:

 // 引入
import {foo, bar} from './myLib'; 

// 导出
export default function() { 
    // your Function 
}; 
export const function1() {...}; 
export const function2() {...}

特点:

  • 支持大多数现代浏览器,但也存在部分浏览器兼容问题 (jakearchibald.com/2017/es-mod…
  • 它既拥有像CJS一样简洁的语法,同时又支持AMD的异步加载
  • 得益于ES6 的静态模块结构(static module structure),ESM规范可以支持打包时的tree-shaking,支持移除不必要的引用
  • 编译时加载
  • 可以在html中引用
 <script type="module"> 
    import {func1} from 'my-lib'; 
    func1(); 
</script

运行时加载与编译时加载

CommonJS是运行时加载,例如下面的代码,是加载了fs模块整体,再读取fs对象中的属性stat等。

 let { stat, exists, readfile } = require('fs')

ESM 是编译时加载,例如下面的代码,就只加载了模块中的两个方法而不是全部。因为es模块设计的思想是静态化,显式地通过export输出,使得编译时就可以进行静态分析。

不同于CommonJS引入的模块是一个对象,ESM引入的是模块本身,并不是一个对象。

 // a.js
export const a = 1;
export const b = 2;
export const c = 3;

// b.js
import { a, b } from 'a.js'

打包配置

了解了不同模块规范的用法,使用打包工具时就可以相应地指定模块规范。

webpack

webpack中,library是指定义一个全局使用的名称变量,libraryTarget是指设置library的暴露方式。配置如下:

module.exports = {
  entry: './index.js',
  output: {
    filename: './dist/testModule.js',
    // export to AMD, CommonJS, or window
    libraryTarget: 'umd',
    // the name exported to window
    library: 'testModule'
  }
}

libraryTarget的可选项如下:

  • libraryTarget: 'var'/'assign'/'assign-properties'
 var MyLibrary = _entry_return_;  // var
MyLibrary = _entry_return_;  // assign
MyLibrary = typeof MyLibrary === 'undefined' ? {} : MyLibrary; // assign-propertie
  • libraryTarget: 'this'/'window'/'global'
 this['MyLibrary'] = _entry_return_;
window['MyLibrary'] = _entry_return_;
global['MyLibrary'] = _entry_return_;
  • libraryTarget: 'commonjs' / 'commonjs2'
exports['MyLibrary'] = _entry_return_; // commonjs
module.exports = _entry_return_; // commonjs2
  • libraryTarget: 'amd' / 'cmd' / 'module' / 'umd'

rollup

除了webpack,还有一些常见的模块打包器,比如rollup。它和webpack相比的特点是拥有更少的功能和更简单的api,适合一些简单的项目。

配置如下:

format支持amd、cjs、es、esm、iife和umd几种格式。

export default { 
  input: "src/main.js", 
  output: { 
    file: "bundle.js", 
    format: "umd", 
    name: "test", 
  }, 
  ...
};

microbundle

microbundle也是一款轻量级的打包器,它也是基于rollup开发的,但甚至可以省略配置文件,通过package.json中的简单配置即可完成打包。

{
  "main": "dist/index.js",
  "source": "src/index.ts"
}

指定source(源文件)和main(打包后的入口文件),运行microbundle,默认会生成多目标格式的文件,也可以通过 --format 参数输出指定格式文件。

ls dist
index.d.ts       index.js.map     index.m.js.map   index.umd.js.map
index.js         index.m.js       index.umd.js

以上就是JS模块化的基本介绍,概括一下就是:

  • ESM是最优的选择,语法简洁,支持异步和tree-shaking
  • UMD是最全面的选择,在不支持ESM时能靠谱兜底
  • CJS支持同步,适用服务器
  • AMD和CMD支持异步,适用浏览器