Javascript模块化简介--简述CommonJS、AMD、CMD、ES Module等规范

999 阅读9分钟

一、 模块化规范及其发展历史

什么是模块化?为什么需要模块化?

  • 模块化无论在哪个编程领域都是相当常见的事情,模块化存在的意义就是为了增加可复用性,以尽可能少的代码实现需求;

  • 开发是将程序划分成一个个小的块/文件(提高可维护性),这个块中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的块(避免命名冲突);

  • 这个块可以将自己希望暴露的变量、函数、对象等导出给其他块使用,也可以通过某种方式,导入另外块中的变量、函数、对象等(按需导入、提高复用性);

  • 随着AJAX、SPA、前端路由等技术的发展,前端项目的越来越庞大,模块化已经是必须的要求;

  • 尽管早期 JavaScript 语言规范不支持模块化,但这并没有阻止 JavaScript 的发展,在官方没有模块化标准之前,社区里就有CommonJS、AMD、CMD等规范出现;

模块化的进化过程

  1. 自定义对象限制命名空间

作用: 减少了全局变量,解决命名冲突 问题: 数据不安全(外部可以直接修改模块内部的数据)

图片1.png

  1. 匿名函数自调用方式

作用:数据是私有的, 外部只能通过暴露的方法操作

图片2.png

  1. 以上两种方式存在的问题

会引入多个<script>标签,请求过多; 模块之间的依赖关系模糊,我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错; 很难维护,可能出现修改一处会影响其他地方,牵一发而动全身导致项目出现严重的问题。

这些问题的解决,需要我们制定一定的规范来约束每个人都按照这个规范去编写模块化的代码; 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性 主要的规范有CommonJS\AMD\CMD\ES Module等;


二、CommonJS规范及其代表性实现NodeJS

在 2009 年 1 月,Mozilla 的工程师 Kevin Dangoor 发起了 CommonJS 的提案,呼吁 JavaScript 爱好者联合起来,编写 JavaScript 运行在服务端的相关规范,一周之后,就有了 224 个参与者。

  • CommonJS 标准囊括了 JavaScript 需要在服务端运行所必备的基础能力,比如:模块化、IO 操作、二进制字符串、进程管理、Web网关接口 (JSGI) 。但是影响最深远的还是 CommonJS 的模块化方案,CommonJS 的模块化方案是JavaScript社区第一次在模块系统上取得的成果,不仅支持依赖管理,而且还支持作用域隔离和模块标识。再后来 NodeJS 问世,它直接采用了 CommonJS 的模块化规范,同时还带来了npm。

  • CommonJS 在服务端表现良好,很多人就想将 CommonJS 移植到客户端 (也就是我们说的浏览器) 进行实现。由于- CommonJS 的模块加载是同步的,而服务端直接从磁盘或内存中读取,耗时基本可忽略,但是在浏览器端如果还是同步加载,对用户体验极其不友好,模块加载过程中势必会向服务器请求其他模块代码,网络请求过程中会造成长时间白屏。

  • Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

基本语法

  • CommonJS 规范规定,每个模块内部有两个变量可以使用,require 和 module;
  • module变量代表当前模块,这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports。
  • require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的 exports对象,如果没有发现指定模块,会报错。
  • CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

个人感觉规范最弄巧成拙的一个知识点,一度给我造成极大的理解困难😜

为了方便,Node.js 在实现 CommonJS 规范时,为每个模块提供一个 exports的私有变量,指向 module.exports。你可以理解为 Node.js 在每个模块开始的地方,添加了如下这行代码。

有一点要尤其注意,exports 是模块内的私有局部变量,它只是指向了 module.exports,所以直接对 exports 赋值是无效的,这样只是让 exports 不再指向 module.exports了而已;

所以你可以这样写:

3.png

但是你不能这样写:

4.png

CommonJS规范模块加载机制

  • 模块在被第一次引入时,模块中的js代码会被运行一次
  • 模块被多次引入时,会缓存,最终只加载(运行)一次,每个模块对象module都有一个属性:loaded module.loaded为false表示还没有加载,为true表示已经加载;
  • 如果有循环引入,那么加载顺序会按深度优先搜索

三、AMD、CMD规范及代表性实现RequireJS、SeaJS

CommonJS 的规范加载模块是同步的,require 命令读入并执行一个 js 文件,然后返回该模块的 exports 对象,这在服务端是可行的,因为服务端加载并执行一个文件的时间是可以忽略的。

但是这种规范天生就不适用于浏览器,可想而知,浏览器端每加载一个文件,都要发网络请求去取,如果网速慢,就非常耗时,因为模块同步加载,浏览器就要一直等 require 返回,就会一直卡在那里,阻塞后面代码的执行,从而阻塞页面渲染,使得页面出现假死状态。

所以,在浏览器端,我们一般使用webpack等工具把CommonJS规范代码转化成可在浏览器直接执行的代码。或者在早期ES Module 规范没有出现以前,通常我们会采用AMD、CMD规范。

RequireJS 是 AMD 规范的代表之作,它之所以能代表 AMD 规范,是因为 RequireJS 的作者 (James Burke) 就是 AMD 规范的提出者。同时作者还开发了 amdefine,一个让你在 node 中也可以使用 AMD 规范的库。AMD 规范由 CommonJS 的 Modules/Transport/C 提案发展而来,毫无疑问,Modules/Transport/C 提案的发起者就是 James Burke。

和 AMD 类似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。所以CMD追求像 CommonJS 那样的简单、自然的书写形式。

不过,现在AMD、CMD 规范用到很少了,推荐使用后面要介绍的ES Module 标准规范。

RequireJS

5.png

四、 Javascript 语言层面实现的模块化规范ES Module

以前的JavaScript在语言层面没有模块化一直是它的痛点,前面介绍的CommonJS、AMD、CMD等都是社区规范,所以在ES6开始推出自己的模块化系统时,大家也是兴奋异常。

前面提到的 CommonJS 是服务于服务端的,而 AMD、CMD 是服务于浏览器端的,但它们都有一个共同点:都在代码运行后才能确定导出的内容;ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

从ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代 CommonJS 和 CMD、AMD 规范,成为浏览器和服务器通用的模块解决方案。

任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块功能主要由两个命令构成:export 和 import。export 命令用于导出模块的对外接口,import 命令用于导入其他模块导出的内容。采用ES Module将自动采用严格模式:use strict。

导入import

import关键字负责从另外一个模块中导入内容

6.png

导出export

export关键字负责向其他模块导出内容

7.png

五、 ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 CommonJS 模块是运行时加载的,并且是同步的;ES6 模块是编译时加载的,并且是异步的。 import 命令具有提升效果,会提升到整个模块的头部,首先执行。 ES6 异步的加载意味着JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行,也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性,如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行。 高版本的NodeJS是支持ES6 Module的,这也表明ES6 Module是以后的主流标准,会逐渐提代CommonJS。

第一个差异表明ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。