前端模块化-CommonJs, AMD, CMD, UMD, ESM

243 阅读4分钟

在NodeJS之前,由于没有过于复杂的开发场景,前端是不存在模块化的,后端才有模块化。NodeJS诞生之后,它使用CommonJS的模块化规范。从此,js模块化开始快速发展。

模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常来说,一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD、UMD以及ESM的模块系统。

模块化简史

1. 最简单粗暴的方式

function fn1(){
  // ...
}

function fn2(){
  // ...
}

通过 script 标签引入文件,调用相关的函数。这样需要手动去管理依赖顺序,容易造成命名冲突,污染全局,随着项目的复杂度增加维护成本也越来越高。

2. 用对象来模拟命名空间

var output = {
  _count: 0,
  fn1: function(){
    // ...
  }
}

这样可以解决上面的全局污染的问题,有那么点命名空间的意思,但是随着项目复杂度增加需要越来越多的这样的对象需要维护,不说别的,取名字都是个问题。最关键的还是内部的属性还是可以被直接访问和修改。

3. 闭包

var module = (function(){
  var _count = 0;
  var fn1 = function (){
    // ...
  }
  var fn2 = function fn2(){
    // ...
  }
  return {
    fn1: fn1,
    fn2: fn2
  }
})()

module.fn1();
module._count; // undefined

这样就拥有独立的词法作用域,内存中只会存在一份 copy。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,通过 return 暴露出公共接口供外界调用。这其实就是现代模块化实现的基础。

目前实现模块化的规范主要有:

  • CommonJS
  • AMD
  • CMD
  • UMD
  • ESM

CommonJs

Node.js 为主要实践者,require 命令用于输入其他模块提供的功能,module.exports命令用于规范模块的对外接口,输出的是一个值的浅拷贝

CommonJS 采用同步加载模块,而加载的文件资源大多数在本地服务器,所以执行速度或时间没问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

// 模块 a.js
const name = "guoteng";

module.exports = {
  name,
  github: "https://github.com/guoteng",
};
// 模块 b.js
// 引用核心模块或者第三方包模块,不需要写完整路径
const path = require("path");
// 引用自定义模块可以省略.js
const { name, github } = require("./a");

console.log(name, github, path.basename(github));
// 输出 guoteng https://github.com/guoteng guoteng

AMD

RequireJS为主要实践者,define是全局函数,用来定义模块,define(id?, dependencies?, factory)。require 命令用于输入其他模块提供的功能,return 命令用于规范模块的对外接口,define.amd 属性是一个对象,此属性的存在来表明函数遵循 AMD 规范。

AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

// model1.js
define(function() {
  console.log("model1 entry");
  return {
    getHello: function() {
      return "model1";
    },
  };
});
// model2.js
define(function() {
  console.log("model2 entry");
  return {
    getHello: function() {
      return "model2";
    },
  };
});
// main.js
define(function(require) {
  var model1 = require("./model1");
  console.log(model1.getHello());
  var model2 = require("./model2");
  console.log(model2.getHello());
});
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
<script>
  requirejs(["main"]);
</script>
// 输出结果
// model1 entry
// model2 entry
// model1
// model2

CMD

Sea.js为主要实践者,一个文件就是一个模块,可以像 Node.js 一般书写模块代码。主要在浏览器中运行,当然也可以在 Node.js 中运行。

AMD不同:AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。

// model1.js
define(function(require, exports, module) {
  console.log("model1 entry");
  exports.getHello = function() {
    return "model1";
  };
});
// model2.js
define(function(require, exports, module) {
  console.log("model2 entry");
  exports.getHello = function() {
    return "model2";
  };
});
// main.js
define(function(require, exports, module) {
  var model1 = require("./model1"); //在需要时申明
  console.log(model1.getHello());
  var model2 = require("./model2"); //在需要时申明
  console.log(model2.getHello());
});
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
  seajs.use("./main.js");
</script>
// 输出
// model1 entry
// model1
// model2 entry
// model2

UMD

该模式主要用来解决 CommonJS 模式和 AMD 模式代码不能通用的问题,并同时还支持老式的全局变量规范。

  1. 判断define为函数,并且是否存在define.amd,来判断是否为 AMD 规范,
  2. 判断module是否为一个对象,并且是否存在module.exports来判断是否为CommonJS规范
  3. 如果以上两种都没有,设定为原始的代码规范。
// bundle.js
(function(global, factory) {
  typeof exports === "object" && typeof module !== "undefined"
    ? (module.exports = factory())
    : typeof define === "function" && define.amd
    ? define(factory)
    : ((global = global || self), (global.myBundle = factory()));
})(this, function() {
  "use strict";

  var main = () => {
    return "hello world";
  };

  return main;
});
// index.html
<script src="bundle.js"></script>
<script>
  console.log(myBundle());
</script>

ESM

JavaScript官方的标准化模块系统,模块的导入导出,通过importexport来确定。

ESM和CommonJs的不同:

  1. ES modules 输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
  2. ES modules 模块编译时执行,而 CommonJS 模块总是在运行时加载
// index.js
import { name, github } from "./demo.js";

console.log(name(), github());

document.body.innerHTML = `<h1>${name()} ${github()}</h1>`;
export function name() {
  return "guoteng";
}

export function github() {
  return "https://github.com/guoteng";
}
<script src="./index.js" type="module"></script>

总结

  1. CommonJs是同步加载,主要用在nodejs环境。
  2. AMD是异步加载,主要用在浏览器环境。
  3. CMD和AMD类似,用的不多了。
  4. UMD是CommonJs、AMD和全局变量的兼容写法。
  5. ESM是标准,未来支持度会越来越好。