各类模块规范以及AMD/UMD原理

几种模块化规范

  • CommonJS:Nodejs,运行在服务端环境

  • CMD(Common Module Definition - 通用模块定义):主要通过 sea.js 流行起来,是国内大佬的作品。

    写法模仿CommonJS规范,不过是运行在浏览器环境

  • AMD(Asynchronous Module Definition - 异步模块定义):采用异步加载,运行在浏览器环境,使用广泛。常用的库是 require.js

  • UMD(Universal Module Definition - 通用模块定义):该模式主要用来解决CommonJS模式和AMD模式代码不能通用的问题。
    在开发中,产物模块规范变成umd后,可以在commonjs、AMD、CMD等多种环境下使用,应用广泛,功能强大,是线上产物常用规范。

  • ESM(ES Modules):ES6 规范,目前也可以运行在高版本的浏览器和Nodejs中

参考文章

bookmark

IIFE

CMD、AMD、UMD的实现,本质上就是利用函数立即执行表达式(immediately invoked function expression),在不同环境下的全局对象上,挂入模块。

Sea.js 实现原理(不推荐使用)

魔法在哪里?

1
2
3
define('a', function (require,exports,modules){
var b = require('b')
})

require.js 显式声明依赖不一样,写法类似 CommonJS 规范。

由于没有去提前声明/配置(回调函数只有定义,没有运行),那么如何在「加载期」知道依赖哪个模块呢?

实现原理

核心是利用 Function.toString 方法,拿到「函数的字符串定义」。

「加载期」:

  • 通过回调函数的Function.toString函数,使用正则表达式来捕捉内部的require字段,找到require(‘jquery’)内部依赖的模块jquery
  • 根据配置文件,找到jquery的js文件的实际路径;如果有多个依赖,也会根据依赖树确定先后顺序
  • 在dom中插入script标签,载入模块指定的js,绑定加载完成的事件,使得加载完成后将js文件绑定到require模块指定的id(这里就是jquery这个字符串)上

「运行期」:

  • 回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数
  • 当回调函数调用require(‘jquery’),即执行绑定在’jquery’这个id上的js文件,即刻执行,并将返回值传给var b

参考文章

bookmark

bookmark

UMD

魔法在哪里?

支持AMD、CMD规范,也支持原生的直接在window上定义的做法,还支持CommonJS。

实现原理

利用 IIFE 的写法,内部区别处理即可。

下面是考虑到「有前置依赖」的UMD的实现思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
console.log('是commonjs模块规范,nodejs环境')
var depModule = require('./umd-module-depended')
module.exports = factory(depModule);
} else if (typeof define === 'function' && define.amd) {
console.log('是AMD模块规范,如require.js')
define(['depModule'], factory)
} else if (typeof define === 'function' && define.cmd) {
console.log('是CMD模块规范,如sea.js')
define(function(require, exports, module) {
var depModule = require('depModule')
module.exports = factory(depModule)
})
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory(root.depModule);
}
}(this, function(depModule) {
console.log('我调用了依赖模块', depModule)
// ...省略了一些代码,去代码仓库看吧
return {
name: '我自己是一个umd模块'
}
}))

在线案例演示了在sea.js上下文中,引入了一个UMD规范的文件,其可以被正常加载:

bookmark

参考文档

bookmark

配套工具

NPM 入口

在monorepo / SDK开发中,经常会对不同的运行环境,输出不同的产物。这些产物的「入口文件」在 package.json 中可以声明,如下所示:

Untitled.png

目前支持3种字段:

  • browser:在 browser 下的入口文件
  • module:在 browser & node 下的入口文件,符合 ESM 规范
  • main:在 browser & node 下的入口文件

同时声明这3个字段时,在不同的环境下,解析优先级不一样:

  • 在非browser下:module > main
  • 在browser下:browser > module > main

Webpack

  • 环境:可以通过 target 字段来声明产物运行环境。 每个环境,解析依赖包的入口文件时,优先级都不同。
  • 解析顺序:通过 mainFields 字段可以自定义解析顺序,比如 [main, module]

参考文档

bookmark

用法示例

下面文章都挺好的,没必要重复拷贝,直接看原文吧。

AMD & require.js

bookmark

bookmark

bookmark

bookmark

NodeJS

bookmark