三. AMD和CMD规范
3.1. CommonJS规范缺点
CommonJS加载模块是同步的:
- 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
- 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
如果将它应用于浏览器呢?
- 浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;
- 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:
- 当然在webpack中使用CommonJS是另外一回事;
- 因为它会将我们的代码转成浏览器可以直接执行的代码;
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
- 但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换;
- AMD和CMD已经使用非常少了,所以这里我们进行简单的演练;
3.2. AMD规范
AMD主要是应用于浏览器的一种模块化规范:
- AMD是Asynchronous Module Definition(异步模块定义)的缩写;
- 它采用的是异步加载模块;
- 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;
我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:
- AMD实现的比较常用的库是require.js和curl.js;
这里我们以require.js为例讲解:
第一步:下载require.js
- 下载地址:https://github.com/requirejs/requirejs
- 找到其中的require.js文件;
第二步:定义HTML的script标签引入require.js和定义入口文件:
- data-main属性的作用是在加载完src的文件后会加载执行该文件
<script src="./lib/require.js" data-main="./index.js"></script>
第三步:编写如下目录和代码
├── index.html ├── index.js ├── lib │ └── require.js └── modules ├── bar.js └── foo.js
index.js
(function() { require.config({ baseUrl: '', paths: { foo: './modules/foo', bar: './modules/bar' } }) // 开始加载执行foo模块的代码 require(['foo'], function(foo) { }) })();
modules/bar.js
- 如果一个模块不依赖其他,那么直接使用define(function)即可
define(function() { const name = "coderwhy"; const age = 18; const sayHello = function(name) { console.log("Hello " + name); } return { name, age, sayHello } })
modules/foo.js
define(['bar'], function(bar) { console.log(bar.name); console.log(bar.age); bar.sayHello('kobe'); })
3.3. CMD规范
CMD规范也是应用于浏览器的一种模块化规范:
- CMD 是Common Module Definition(通用模块定义)的缩写;
- 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
- 但是目前CMD使用也非常少了;
CMD也有自己比较优秀的实现方案:
- SeaJS
我们一起看一下SeaJS如何使用:
第一步:下载SeaJS
- 下载地址:https://github.com/seajs/seajs
- 找到dist文件夹下的sea.js
第二步:引入sea.js和使用主入口文件
seajs
是指定主入口文件的
<script src="./lib/sea.js"></script> <script> seajs.use('./index.js'); </script>
第三步:编写如下目录和代码
├── index.html ├── index.js ├── lib │ └── sea.js └── modules ├── bar.js └── foo.js
index.js
define(function(require, exports, module) { const foo = require('./modules/foo'); })
bar.js
define(function(require, exports, module) { const name = 'lilei'; const age = 20; const sayHello = function(name) { console.log("你好 " + name); } module.exports = { name, age, sayHello } })
foo.js
define(function(require, exports, module) { const bar = require('./bar'); console.log(bar.name); console.log(bar.age); bar.sayHello("韩梅梅"); })
四. ES Module
4.1. 认识ES Module
JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等,所以在ES推出自己的模块化系统时,大家也是兴奋异常。
ES Module和CommonJS的模块化有一些不同之处:
- 一方面它使用了import和export关键字;
- 另一方面它采用编译期静态类型检测,并且动态引用的方式;
ES Module模块采用export和import关键字来实现模块化:
- export负责将模块内的内容导出;
- import负责从其他模块导入内容;
了解:采用ES Module将自动采用严格模式:use strict
- 如果你不熟悉严格模式可以简单看一下MDN上的解析;
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
4.2. ES Module的使用
4.2.1. 代码结构组件
这里我在浏览器中演示ES6的模块化开发:
代码结构如下:
├── index.html ├── main.js └── modules └── foo.js
index.html中引入两个js文件作为模块:
<script src="./modules/foo.js" type="module"></script> <script src="main.js" type="module"></script>
如果直接在浏览器中运行代码,会报如下错误:
模块化运行
这个在MDN上面有给出解释:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
- 你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个
file://
路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。 - 你需要通过一个服务器来测试。
我这里使用的VSCode,VSCode中有一个插件:Live Server
- 通过插件运行,可以将我们的代码运行在一个本地服务中;
4.2.2. export关键字
export关键字将一个模块中的变量、函数、类等导出;
foo.js文件中默认代码如下:
const name = 'coderwhy'; const age = 18; let message = "my name is why"; function sayHello(name) { console.log("Hello " + name); }
我们希望将其他中内容全部导出,它可以有如下的方式:
方式一:在语句声明的前面直接加上export关键字
export const name = 'coderwhy'; export const age = 18; export let message = "my name is why"; export function sayHello(name) { console.log("Hello " + name); }
方式二:将所有需要导出的标识符,放到export后面的 {}
中
- 注意:这里的
{}
里面不是ES6的对象字面量的增强写法,{}
也不是表示一个对象的; - 所以:
export {name: name}
,是错误的写法;
const name = 'coderwhy'; const age = 18; let message = "my name is why"; function sayHello(name) { console.log("Hello " + name); } export { name, age, message, sayHello }
方式三:导出时给标识符
起一个别名
export { name as fName, age as fAge, message as fMessage, sayHello as fSayHello }
4.2.3. import关键字
import关键字负责从另外一个模块中导入内容
导入内容的方式也有多种:
方式一:import {标识符列表} from '模块'
;
- 注意:这里的
{}
也不是一个对象,里面只是存放导入的标识符列表内容;
import { name, age, message, sayHello } from './modules/foo.js'; console.log(name) console.log(message); console.log(age); sayHello("Kobe");
方式二:导入时给标识符起别名
import { name as wName, age as wAge, message as wMessage, sayHello as wSayHello } from './modules/foo.js';
方式三:将模块功能放到一个模块功能对象(a module object)上
import * as foo from './modules/foo.js'; console.log(foo.name); console.log(foo.message); console.log(foo.age); foo.sayHello("Kobe");
4.2.4. export和import结合
如果从一个模块中导入的内容,我们希望再直接导出出去,这个时候可以直接使用export来导出。
bar.js中导出一个sum函数:
export const sum = function(num1, num2) { return num1 + num2; }
foo.js中导入,但是只是做一个中转:
export { sum } from './bar.js';
main.js直接从foo中导入:
import { sum } from './modules/foo.js'; console.log(sum(20, 30));
甚至在foo.js中导出时,我们可以变化它的名字
export { sum as barSum } from './bar.js';
为什么要这样做呢?
- 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
- 这样方便指定统一的接口规范,也方便阅读;
- 这个时候,我们就可以使用export和import结合使用;
4.2.4. default用法
前面我们学习的导出功能都是有名字的导出(named exports):
- 在导出export时指定了名字;
- 在导入import时需要知道具体的名字;
还有一种导出叫做默认导出(default export)
- 默认导出export时可以不需要指定名字;
- 在导入时不需要使用
{}
,并且可以自己来指定名字; - 它也方便我们和现有的CommonJS等规范相互操作;
导出格式如下:
export default function sub(num1, num2) { return num1 - num2; }
导入格式如下:
import sub from './modules/foo.js'; console.log(sub(20, 30));
注意:在一个模块中,只能有一个默认导出(default export);
4.2.5. import()
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
if (true) { import sub from './modules/foo.js'; }
为什么会出现这个情况呢?
- 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
- 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
- 甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值;
const path = './modules/foo.js'; import sub from path;
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
- 如果根据不懂的条件,动态来选择加载模块的路径;
- 这个时候我们需要使用
import()
函数来动态加载;
aaa.js模块:
export function aaa() { console.log("aaa被打印"); }
bbb.js模块:
export function bbb() { console.log("bbb被执行"); }
main.js模块:
let flag = true; if (flag) { import('./modules/aaa.js').then(aaa => { aaa.aaa(); }) } else { import('./modules/bbb.js').then(bbb => { bbb.bbb(); }) }
4.3. ES Module的原理
4.3.1. ES Module和CommonJS的区别
CommonJS模块加载js文件的过程是运行时加载的,并且是同步的:
- 运行时加载意味着是js引擎在执行js代码的过程中加载 模块;
- 同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行;
console.log("main代码执行"); const flag = true; if (flag) { // 同步加载foo文件,并且执行一次内部的代码 const foo = require('./foo'); console.log("if语句继续执行"); }
CommonJS通过module.exports导出的是一个对象:
- 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量;
- 但是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改;
ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的:
- 编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用:
- 比如from后面的路径需要动态获取;
- 比如不能将import放到if等语句的代码块中;
- 所以我们有时候也称ES Module是静态解析的,而不是动态或者运行时解析的;
- 异步的意味着:JS引擎在遇到
import
时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行;
- 也就是说设置了
type=module
的代码,相当于在script标签上也加上了async
属性; - 如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行;
<script src="main.js" type="module"></script> <!-- 这个js文件的代码不会被阻塞执行 --> <script src="index.js"></script>
ES Module通过export导出的是变量本身的引用:
- export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录(module environment record);
- 模块环境记录会和变量进行
绑定
(binding),并且这个绑定是实时的; - 而在导入的地方,我们是可以实时的获取到绑定的最新值的;
export和import绑定的过程
所以我们下面的代码是成立的:
bar.js文件中修改
let name = 'coderwhy'; setTimeout(() => { name = "湖人总冠军"; }, 1000); setTimeout(() => { console.log(name); }, 2000); export { name }
main.js文件中获取
import { name } from './modules/bar.js'; console.log(name); // bar中修改, main中验证 setTimeout(() => { console.log(name); }, 2000);
但是,下面的代码是不成立的:main.js中修改
import { name } from './modules/bar.js'; console.log(name); // main中修改, bar中验证 setTimeout(() => { name = 'kobe'; }, 1000);
导入的变量不可以被修改
思考:如果bar.js中导出的是一个对象,那么main.js中是否可以修改对象中的属性呢?
- 答案是可以的,因为他们指向同一块内存空间;(自己编写代码验证,这里不再给出)
4.3.2. Node中支持 ES Module
在Current版本中
在最新的Current版本(v14.13.1)中,支持es module我们需要进行如下操作:
- 方式一:在package.json中配置
type: module
(后续再学习,我们现在还没有讲到package.json文件的作用) - 方式二:文件以
.mjs
结尾,表示使用的是ES Module;
这里我们暂时选择以 .mjs
结尾的方式来演练:
bar.mjs
const name = 'coderwhy'; export { name }
main.mjs
import { name } from './modules/bar.mjs'; console.log(name);
在LTS版本中
在最新的LST版本(v12.19.0)中,我们也是可以正常运行的,但是会报一个警告:
lts版本的警告
4.3.3. ES Module和CommonJS的交互
CommonJS加载ES Module
结论:通常情况下,CommonJS不能加载ES Module
- 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码;
- 但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持;
- Node当中是不支持的;
ES Module加载CommonJS
结论:多数情况下,ES Module可以加载CommonJS
- ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用;
- 这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的;
- 但是在最新的LTS版本中就不支持;
foo.js
const address = 'foo的address'; module.exports = { address }
main.js
import foo from './modules/foo.js'; console.log(foo.address);