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_




    
        My Sample Project
        
    
    
        

My Sample Project

// 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。


特点

  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?

你可能感兴趣的:(CJS / AMD / CMD / UMD / ESM 一本全)