CJS / AMD / CMD / UMD / ESM 一本全

慈母手中线,游子身上衣。尽管这是个不恰当的比喻,但大脑里的很多知识对我来说就像手里的毛线团一样,是杂乱零散的,需要一张网才可以把他们编织成型,so 特以此篇文章来记录一下。

前情提要

代码模块化早已是基操(基础操作)了,众所周知的有 CommonJs、AMD、CMD、UMD、ES Module 这五种解决规范,该文章是对自己学习的记录,如有错误欢迎大家批评指正。

CommonJs / CJS

CommonJs是一种规范。

概念

我将维基百科(CommonJS - wiki - 链接)上提到的几个重点,压缩为以下的内容:

针对环境:web浏览器外的(非web浏览器环境的)JavaScript项目
项目目标:JavaScript生态的模块化解决方案
主要示例:尤其是使用nodeJs用于服务端JavaScript编程
浏览器 :浏览器不能直接执行CommonJs代码,需要通过编译转化
如何识别:我们可以通过是否使用 require()function和module.exports来识别是否使用了CommonJs

CommonJs并没有成为ECMA组织发布的模块化标准(ES Module),但有很多ECMA成员参与其中。

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 模块按照在代码中的顺序,依次同步加载
  3. 模块会在运行时加载且执行,执行得到对象A,后续通过require获取的都是对对象A值的拷贝(换句话说,模块可以多次加载,在第一次加载时执行并缓存其结果,后续加载会直接返回该结果),要想模块再次运行,必须清除缓存。

如果你想要多次执行一个模块,可以导出一个函数,然后调用函数。

NodeJs的模块化

  1. 在执行模块代码之前,NodeJs会使用如下的函数封装器将其封装;
  • 通过闭包的形式避免了变量污染;
  • 提供了看似全局,实际上是模块特定的变量;
(function(exports, require, module, __filename, __dirname) {
    // 模块的代码实际上在这里
})
  1. 可以通过 module.exports 导出模块内容;
  2. 变量 exports 是对 module.exports 的引用,所以不能对exports有赋值操作;
  • exports = module.exports;
  • exports变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给module.exports
  • 因此module.exports.f = ...可以更简洁地写成exports.f = ...
// 错误用法,exports被重新赋值,此function并未被导出
exports = function(x) {console.log(x)}; 

// 正确用法
exports.a = function (x){ console.log(x);};

/*
    错误写法二
  
  这是由于module.exports 被改写,导致exports也被重新改写
  
  这意味着,如果一个模块的对外接口,就是一个单一的值,
  最好不要使用exports输出,最好使用module.exports输出。
*/

exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';

  1. 通过 require(id) 引入模块、JSON、或本地文件;
  1. require.cache 被引入的模块将被缓存到这个对象中,如果删除该对象的某个模块会导致下次require的时候重新加载该模块。

AMD(Asynchronous Module Definition)

JavaScript的异步模块化定义方案。

概念

针对环境:web浏览器
项目目标:JavaScript生态的模块化解决方案
主要示例:require.js
如何识别:我们可以通过是否使用 define(id?, dependencies?, factory);function来识别是否使用了AMD规范。

// 其中对于"require", "exports", "beta" 这几个依赖可不填
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
  exports.verb = function() {
    return beta.verb();
    //Or:
    return require("beta").verb();
  }
});

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 模块会被异步加载
  3. 依赖模块加载完成后,会立即执行其回调函数(即factory函数);
  4. 主模块会等所有的依赖模块加载完成后,再调用其对应的回调函数(依赖前置);

require.js 的模块加载原理

简单使用

首先简单介绍一下 require.js 的使用:

  1. 在html文件内,需要有一个script标签引入require.js以及项目的入口文件main.js
  2. 文件 main.js 里的就是项目的主逻辑了。

项目结构如下:

_project-directory/_

- _project.html_
- _scripts/_
   - _main.js_
   - _helper/_
      - _util.js_
<!-- project.html -->

<!DOCTYPE html>
<html>
    <head>
        <title>My Sample Project</title>
        <script data-main="scripts/main" src="scripts/require.js"></script>
    </head>
    <body>
        <h1>My Sample Project</h1>
    </body>
</html>
// main.js

requirejs(["helper/util"], function(util) {
    // you can do everything you want
});

原理介绍

不过 RequireJS 从2.0开始,也改成了可以延迟执行(暂不讨论)

目的:

  • 理解require使用script标签
  • 关于script标签是否加载成功可以通过onload事件来判断,具体实现细节并不讨论
  1. 引入require.js时,我们会通过data-main引入入口文件;
  2. require.js获取到入口文件后,将文件以及对应的依赖通过script标签append到html上;
  3. 依赖是依次、同步append到html,但是script标签的加载却是异步的;
  4. 依赖加载完成后,会立即调用其回调执行函数;
  5. 入口文件监听到所有的依赖都加载完成后,再调用其回调函数(即回调函数factory)。

CMD(Common Modules Definition)

CMD 是 sea.js 在推广过程中对模块定义的规范化产出。和 AMD 很像,这里只简单讨论他们的异同。

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 模块会被异步加载
  3. 模块加载完成后,不会执行其回调函数,而是等到主函数运行且需要的执行依赖的时候才运行依赖函数(依赖后置、按需加载);

UMD(Universal Module Definition)

UMD 提供了支持多种风格的“通用”模式,在兼容 CommonJS 和 AMD 规范的同时,还兼容全局引用的方式。

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["jquery", "underscore"], factory);
    } else if (typeof exports === "object") {
        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;
}));

原理

实现原理很简单。
  1. 判断是否支持AMD,若存在则使用 AMD 方式加载模块,否则继续步骤2;
  2. 判断是否支持 CommonJs ,若存在则使用 Node.js 的模块格式,否则继续步骤3;
  3. 将模块公开到全局(window 或 global)

ES Module

ES Module 是用于处理模块的ECMAScript标准。现代浏览器(高版本)以基本支持 ES Module。

<script type="module" src="index.js"></script>

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 在编译时输出模块;
  3. 输出的模块内容为只读,不可修改;
  4. 不会缓存模块结果,每次都会动态执行模块内容;

ES6 的 import & export

ES6也是基操了,必须会的。
这篇文章写得非常好 require和import的区别 - 链接

  1. import 语句会被提升;
  2. import 的变量都是只读的;
  3. import 是静态执行,所以不能用表达式;
  4. import 语句支持 Singleton 模式(如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。);
  5. export 需要输出一个对象 或 变量声明语句;
  6. export default 相当于输出了一个名叫default的变量/对象;
  7. export与import连用;
    1. 其中foo和bar其实并没有导入到当前文件,相当于通过该入口文件转发了出去,通常可用作utils/index.js的转接。其他地方可以通过**import {foo} from 'util'**引用该文件
export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

Q&A 模块的运行时加载 和 编译时加载

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为编译。“分词/词法分析” -> “解析/语法分析” -> “代码生成”。

ES6中的import语句,就是在编译过程中生成的,引入的是模块的地址,所以在执行的时候会动态拿到地址去执行相应的内容。
requireJs内require('foo')一个模块的时候,拿到的是这个模块运行后得到的值的(浅)拷贝,是一个“死”的内容,如果需要重新运行需要手动清理 require.cache对应的模块。


结束

文章到此就全部结束了,内容以“理论”为主,我会将自己了解的的内容都输出为视频和文章的形式,考验自己基础的同时希望可以给迷惑的小伙伴“解解惑”。

感谢观看,下次再见!


其他推荐

参考文章列表:

  1. https://www.jianshu.com/p/eb5948a70294 JavaScript模块化 之( Commonjs、AMD、CMD、ES6 modules)演变史
  2. https://www.jianshu.com/p/d7fdcc89fbee CommonJS,AMD,CMD,ES6 Module
  3. https://www.jianshu.com/p/929b56dcfbbf 模块化开发
  4. https://segmentfault.com/a/1190000021911869 require和import的区别
  5. https://juejin.cn/post/6844903759009595405#heading-9 模块化之AMD与CMD原理(附源码)
  6. https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm What are CJS, AMD, UMD, and ESM in Javascript?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352