588 模块化开发 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
- 通过插件运行,可以将我们的代码运行在一个本地服务中;
image-20201012153439900
4.2.2. export关键字
export关键字将一个模块中的变量、函数、类等导出;
foo.js文件中默认代码如下:
const name = 'hahaha';
const age = 18;
let message = "my name is why";
function sayHello(name) {
console.log("Hello " + name);
}
我们希望将其他中内容全部导出,它可以有如下的方式:
方式一:在语句声明的前面直接加上export关键字
export const name = 'hahaha';
export const age = 18;
export let message = "my name is why";
export function sayHello(name) {
console.log("Hello " + name);
}
方式二:将所有需要导出的标识符,放到export后面的 {}
中
- 注意:这里的
{}
里面不是ES6的对象字面量的增强写法,{}
也不是表示一个对象; - 所以:
export {name: name}
,是错误的写法;
export const name = 'hahaha';
export const age = 18;
export let message = "my name is why";
export function sayHello(name) {
console.log("Hello " + name);
}
方式三:导出时给标识符
起一个别名
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 { name, age, message, sayHello } from './modules/foo.js';
console.log(name)
console.log(message);
console.log(age);
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 = 'hahaha';
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 = 'hahaha';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);