继 CJS、AMD 之后又来一个 UMD,究竟是什么?有什么用?

187 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

最近研究了一下 VueReact 编译出来的版本文件,我发现 Vue 提供了 CommonJS|ES Module|ES Module browser|UMD 四种格式,而 React 提供 CommonJS|UMD 两个版本,对于 CommonJS|ES Module 对应的八股可以说是很多了,而里面夹杂的 UMD 又是什么东西呢?

UMD

UMD (Universal Module Definition),希望提供一个前后端跨平台的解决方案(支持AMDCommonJS 模块方式)。

好家伙,弄半天原来是 AMDCommonJS 的集成!

那为啥要用 UMD 呢?

CJS 和 AMD

CJS(CommonJS)AMD(Asynchronous Module Definition)JS 的两种模块标准,众所周知,前端入门通常时 HTML/CSS/JavaScript 三板斧一把梭,最早的网站开发其实也这样,但是后面随着浏览器(或者说网站)功能越来越多,JS 的模块化成为了一种必然

早期的模块方案是 IIFE(立即执行函数),在加载 HTML 的时候给根对象(也就是 window)去挂载一个属性,这种方案的著名例子就是 Jquery

<script src="http://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script> 
  // window.$
  $
</script>

像这种模块使用,结构如下

<head>
  <script src="assets/js/jquery.js"></script>
  <script src="assets/js/util.js"></script>
</head>
<body></body>
<script>
  // your script
</script>

通常此类模块化只是用来抽取一些普通公共函数或者类似 Jquery 等操作 DOM 的框架,因为这个时候前端还是处于前后端分离前,例如 JSP 的编写方式

至于啥时候前后端分离还是幻想时间

202212210153775.png

2010facebook 工程师提出的 bigpipe,其中就提到了将模板引擎放到前端,也就是前后端分离

到了 2009Node.js 的诞生,同时 Node.js 作者和模块管理方案(也就是 npm)作者的结盟为,服务端 JS 带来了模块化标准 CommonJS(CJS)(终于到本段落的主角了!),Node.js 提高了 JS 的上限,npm 丰富了 Node.js 的生态,但这个时候 JS 的模块化仍然只有后端的模块化也就是 CommonJS,但是随着前后端分离的提出,而且 Node.js 提供了许多构建工具,属于前端的模块化也要来了!

然后就有了一些专门为浏览器使用的库,模块化使用的就是 CommonJS,但是在浏览器使用就编译成 IIFE,但 CommonJS 的模块标准是同步引用,这样会阻塞我们 HTML 的渲染,因此就引发了讨论,也就是后面 AMD 出现的原因,AMD 就是基于 CommonJS 的异步的模块导入,是专门给浏览器使用的,像 RequireJS 就是基于 AMD 标准实现的一个模块加载器

UMD 出现原因

但是如果说我的模块或者 npm 包是需要在浏览器使用,也需要在 node 环境使用,要如何解决?

解决方案就是针对每个不同的环境各打一个包,比如 react-script.js<script> 标签引入,react-amd.jsAMDreact-common.jsCJS(node)

对于模块的开发者来说,我觉得每次发版都打个包太麻烦了,我想来个大一统,一个打包文件就能够覆盖当前的所有模块化标准,即 AMD/CJS/IIFE,因此 UMD 应运而生,案例很多,比如最著名的就是 ReactReact 它提供的打包文件就有 umd 格式的

它的解决思路就是修改模块导出定义

因为模块本身最终要导出一个对象,函数,或者变量,不同的模块规范,关于模块导出这部分的定义是完全不一样的

比如 AMD 标准下定义符合其标准的模块

define("math", ["exports"], function (exports) {
  /**
   * 计算 x 的平方
   * @param {number} x
   */
  exports.square = function (x) {
    return x * x;
  };
});

比如 CJS 标准下定义符合其标准的模块

/**
 * 计算 x 的平方
 * @param {number} x 
 */
const square = (x) => {
  return x * x;
};

module.exports = {
  square,
};

比如 IIFE 标准下定义符合其标准的模块

var math = (function () {
  /**
   * 计算 x 的平方
   * @param {number} x
   */
  const square = (x) => {
    return x * x;
  };

  return {
    square,
  };
})();

如果能保证模块具有单一出口,也就是唯一 module.exportsdefine,就可以在兼容两个导出关键字的情况下实现一个打包文件就能够覆盖当前的所有模块化标准,而这就是 UMD 的思路

逐步实现 UMD

您可以在线查看源代码

模块最终要导出一个对象,函数,或者变量,所以我们抛开所谓的模块标准,从模块本身的 JS 逻辑开始,从 0 到 1 实现符合 UMD 的模块

从上面的 math/square 出发,如果我的项目需要用到 math/square 这个模块,它应该怎么写?

// math/square
/**
 * 计算 x 的平方
 * @param {number} x
 */
const square = (x) => {
  return x * x;
};

const math = {
  square,
}
// 使用例子
// math.square(2);

如果在浏览器我需要使用这个模块,可以使用 IIFE 标准的模块定义和导出

IIFE

window.math = function () {
  /**
   * 计算 x 的平方
   * @param {number} x
   */
  const square = (x) => {
    return x * x;
  };
  return {
    square,
  };
};

挂载在全局对象上,任何地方都可以访问

但如果说我又要兼容 CJS 呢?

CJS

根据当前的 this 去判断是浏览器环境(Browser)还是 Node.js 环境

// 使用匿名函数避免模块命名冲突
(function (global, factory) {
  // global 是全局对象,JS 环境的上下文
  // factory 是模块出口,用于导出一个对象,函数,或者变量
  if (typeof exports === "object" && typeof module !== "undefined") {
    // Node.js
    module.exports = factory();
  } else {
    // Browser
    global.math = factory();
  }
})(this, function () {
  /**
   * 计算 x 的平方
   * @param {number} x
   */
  const square = (x) => {
    return x * x;
  };

  return {
    square,
  };
});

如果我要兼容 AMD 也是同样的套路

AMD

(function (global, factory) {
  if (typeof exports === "object" && typeof module !== "undefined") {
    // CJS + Node.js
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    // AMD + Require.js
    define("math", ["exports"], factory);
  } else {
    // Browser
    global.math = factory();
  }
})(this, function () {
  /**
   * 计算 x 的平方
   * @param {number} x
   */
  const square = (x) => {
    return x * x;
  };

  return {
    square,
  };
});

Webpack

如果上面的 factory 是用的 CJS 写的呢?比如

/**
 * 计算 x 的平方
 * @param {number} x 
 */
const square = (x) => {
  return x * x;
};

module.exports = {
  square,
};

原理同上,Webpack 的编译结果如下

!(function (e, t) {
  "object" == typeof exports && "object" == typeof module
    ? (module.exports = t())
    : "function" == typeof define && define.amd
    ? define([], t)
    : "object" == typeof exports
    ? (exports.math = t())
    : (e.math = t());
})(this, () => {
  return (
    (e = {
      10: (e) => {
        e.exports = { square: (e) => e * e };
      },
    }),
    (t = {}),
    (function o(r) {
      var p = t[r];
      if (void 0 !== p) return p.exports;
      var n = (t[r] = { exports: {} });
      return e[r](n, n.exports, o), n.exports;
    })(10)
  );
  var e, t;
});

Rollup

如果上面的 factory 是用的 ESM 写的呢?比如

/**
 * 计算 x 的平方
 * @param {number} x 
 */
const square = (x) => {
  return x * x;
};

export {
  square,
}

这个时候又会有所不同,rollup 编译 umd 的原理是保留 ESM 模块定义关键字,将不同环境的模块定义关键字替代 ESM 模块定义关键字

(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.math = {})));
})(this, function (exports) {
  "use strict";

  /**
   * 计算 x 的平方
   * @param {number} x
   */
  const square = (x) => {
    return x * x;
  };

  exports.square = square;
});

原理如下

参考资料

  1. 前后端分离历史 - 水无垠ZZU - CSDN
  2. 软件架构之前后端分离与前端模块化发展史 - xiangzhihong - segmentfault
  3. 可能是最详细的UMD模块入门指南 - Tusi - 掘金
  4. umdjs/umd - github