前端库是如何判断运行环境并导出模块的?

316 阅读5分钟

前言

最近翻看 underscore v1.13.6 的源码,开头就有一段很有意思的代码,这里对这段代码稍微分析一下,顺便查缺补漏,记录一下。

正文

代码片段

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define('underscore', factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () {
    var current = global._;
    var exports = global._ = factory();
    exports.noConflict = function () { global._ = current; return exports; };
  }()));
}(this, (function () { 
    ... 
})));

分析过程

首先,这个片段的大概结构是一个立即执行函数:

(function (global, factory)){}(this, factoryFunc))

这里的 this 对应的是形参 global,在浏览器侧对应 window 对象,在 node 环境对应 global 对象;factoryFunc = (function(){}), 是一个工厂函数,对应形参 factory。

然后,这段嵌套的三元表达式判断究竟是什么意思? 简化来看,这个嵌套的三元表达式是这样的:

if (typeof exports === "object" && typeof module !== "undefined") {
  module.exports = factory();
} else if (typeof define === "function" && define.amd) {
  define("underscore", factory);
} else {
  (global = typeof globalThis !== "undefined" ? globalThis : global || self),
    (function () {
      var current = global._;
      var exports = (global._ = factory());
      exports.noConflict = function () {
        global._ = current;
        return exports;
      };
    })();
}

直观来看

  1. 先判断 exports 是否是个对象,且module 不为 undefined,那么就将工厂函数以 commonjs 的形式导出;
  2. 再判断是否存在 define 方法,存在,则默认当前为浏览器环境,以 amd 形式导出模块;
  3. 最后,那就当作是其他环境情况下,如果 globalThis 不为 undefined,就给全局属性 global 赋值为 globalThis,否则使用 global 以及兜底 self 赋值;接着执行一个立即执行函数,函数体内容主要是重置 global._ 并导出,同时导出了一个 noConflict 方法,用于让渡变量 _ 的控制权,用于解决命名冲突的问题。补充一句:noConflict 方法在许多库中都有这样类似的实现,例如:jQuerybackbonelodash等等。

总结

这个代码块实际上就是针对不同模块规范下,处理了导出的方式。之后开发模块时就可以用上这样的方式,对代码进行导出处理。

模块化的知识点

既然提到了不同模块的规范导出方式不同,那究竟是怎样的不同,这里再稍微记录一下。

JavaScript 模块化的发展历程如下:
无模块化 --> CommonJS规范 --> AMD规范 --> CMD规范 --> ES6模块化。

一、无模块化时期

早期无模块化时,使用 script 标签引入 js 文件,互相之间如果有依赖关系,引入的顺序还需要特别注意,例如 file2.js 依赖 file1.js,那就需要将 file1.js 在 file2.js 之前引入,如下:

<script src="main.js"></script>   
<script src="file1.js"></script>   
<script src="file2.js"></script>   
<script src="file3.js"></script>

缺点很多,这里不多做深入:

  • 污染全局作用域
  • 维护成本高
  • 依赖关系不明显

二、CommonJS 规范

node.js 的模块系统,就是参照 CommonJS 规范实现的。 node 对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。

基本用法
用 module.exports 定义当前模块对外输出的接口(不推荐直接用 exports),用 require 加载模块,注意:这里的加载模块是同步的方式

这也就可以理解 underscore 判断 commonjs 规范的是通过 module 和 exports 关键字来处理的了。

三、AMD 规范

AMD(Asynchronous module definition),意为“异步的模块定义” ,不同于 CommonJS 规范的同步加载,AMD 正如其名所有模块默认都是异步加载,这也是早期为了满⾜ web 开发的需要,因为如果在 web 端也使⽤同步加载,那么⻚⾯在解析脚本⽂件的过程中可能使⻚⾯暂停响应。

基本用法

AMD 规范中,定义了下面三个API:

define(id, [depends], callback)
require([module], callback)
require.config()

通过 define 来定义一个模块,然后使用 require 来加载一个模块, 使用require.config() 指定引用路径。

四、CMD 规范

CMD(Common Module Definition) ,意为“通用模块定义”,它提供了模块定义和按需加载执行模块。该规范明确了模块的基本书写格式和基本的交互规则。

CMD 和 AMD 都是针对浏览器端的。AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

五、ES6模块化(ESM)

基本用法

  • 使用 import 关键字引入模块,通过 export 关键字导出模块,功能较之于前几个方案更为强大
  • ES6 目前在许多浏览器中已经开始支持了,在不支持的浏览器中还是需要通过 babel 将不被支持的 import 编译为当前受到广泛支持的 require[]

六、UMD 模块

UMD(Universal Module Definition),意为“通用模块定义规范”。随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJS、CMD 甚至是 AMD 的项目中运行。

这也就是开头看到的那段代码的用意,实际上就是 amd + commonjs + 全局变量 这三种风格的结合来对当前运行环境的判断,如果是 Node 环境 就是使用 CommonJS 规范, 如果不是就判断是否为 AMD 环境, 最后导出全局变量:

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global.libName = factory());
}(this, (function () { 'use strict';})));

有了 UMD 后我们的代码可同时运行在 Node 和 浏览器上所以现在前端大多数的库最后打包都使用的是 UMD 规范。

相关链接

[1] AMD规范定义: github.com/amdjs/amdjs…

[2] CMD 规范定义: github.com/cmdjs/speci…

[3] AMD和CMD的区别:github.com/seajs/seajs…

[6] CommonJS:www.commonjs.org/

[4] CommonJS Modules/1.1: wiki.commonjs.org/wiki/Module…

[5] UMD规范定义:github.com/umdjs/umd