# ES Module
es module的功能主要由两个命令完成, 一个是export
, 一个是import
, export
用于规定模块对外的接口, import
用于输入其他模块的功能。
# 1、export
其实主要就是要知道各种export
export输出变量,可以输出三种变量声明中的任何一种
export var value1 = 'hello1';
export let value2 = 'hello2';
export const value3 = 'hello3';
还有一种统一输出的办法,和上面的效果是等价的。 推荐使用下面的办法, 这样子可以清楚的知道自己输出了哪些变量。
var value1 = 'hello1';
let value2 = 'hello2';
const value3 = 'hello3';
export { value1, value2, value3 };
也可以输出函数和类等
export function log(...args) {
console.log.apply(null, args);
}
export class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
print() {
console.log(this.name, this.age);
}
}
使用as
关键字给输出的变量重命名
const name = 'hello, world';
export { name as username };
下面的几种写法都是错误的,因为export命令规定的是对外的接口, 必须与模块内部的变量建立一对一的关系。
// 错误
export 1;
// 错误
var m = 1;
export m;
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
// export.js
export var foo = 'foo1';
setTimeout(() => (foo = 'foo2'), 1000);
// import.js
import { foo } from './1.js';
console.log(foo); // foo1
setTimeout(() => {
console.log(foo); // foo2
}, 2000);
最后,export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
# 2、import
# 2.1 import基础知识
通过import加载外部的模块。导入的模块名称必须和导出的接口对应。
import { log, Person } from 'xx.js'
同样可以使用as
关键字给重命名输入的变量
import { foo as func } from 'xx.js';
WARNING
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
但是如果导入的是一个对象, 那么改写他的属性是被允许的
import { obj } from './1.js';
obj.x = 'x';
上面代码中,obj的属性可以成功改写, 同时也会影响其他导入的模块,非常不建议这种操作。
注意,import
命令具有提升效果,会提升到整个模块的头部,首先执行。下面的代码不会报错
console.log(obj);
obj.x = 'x';
import { obj } from './1.js';
import
语句会执行加载的模块, 因此可以下面这么写
import 'lodash'
上面的代码仅仅执行 lodash
模块, 但是不输入任何的值。
# 2.2 模块的整体加载
可以使用*
加载所有的输出的输出到一个对象上面。 不需要使用大括号
import * as module1 from './1.js';
console.log(module1.obj);
# 3、export default 命令
export default
用于输出一个默认接口。输入的时候也可以自己定义变量名称。一个模块只能输出一个默认接口。
// default.js
export default function() {
console.log('log');
}
// import.js
import log from './default.js';
log();
本质上,export default
就是输出一个叫做default
的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。
// export.js
function log() {
console.log('log');
}
export { log as default, foo };
// 等同于
// export default log
// import.js
import {default as log} from "export.js"
// 等同于
// import log from "export.js"
所以我们可以认为export default
只是输出一个叫做default
的变量, 所以下面这种写法是会报错的。
export default var a = 1;
同时导入默认接口和其他接口
import defaultInterface, { otherInterface } from "export.js"
# 4、export 和 import 的复合写法
先导入一个模块,再导出, 两个语句就可以写在一起
// log.js
export function log(...args) {
console.log.apply(null, args);
}
// export.js
export { log } from './log.js';
// 等同于
// import { log } from './log.js';
// export { log }
// import.js
import { log } from "export.js"
但需要注意的是,写成一行以后,log
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用log
。
模块的接口改名和整体输出,也可以采用这种写法。
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出--使用的时候和从my_module导入的用法一致
export * from 'my_module';
# 5、import()
import()
函数主要用于动态加载模块, 返回一个Promise。
import('./1.js').then(module => {
// console.log(module);
console.log(module.x);
module.foo();
console.log(module.default); // 获取默认的导出接口
});
主要作用有几个
- 按需加载
- 条件加载
- 动态的模块路径
# 6、import.meta
开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。import 命令的元属性import.meta
,返回当前模块的元信息。
1、(1)import.meta.url
// 0.js
const url = import.meta.url
console.log(url); // http://127.0.0.1:8080/0.js
2、(2)import.meta.scriptElement
import.meta.scriptElement
是浏览器特有的元属性,返回加载模块的那个<script>
元素,相当于document.currentScript
属性。
这个我目前没有在我的Chrome看到, 不过一般也不用到
# 7、Module的加载实现
# 7.1 浏览器加载
主要是script
标签需要使用type="module"
属性。
<!-- index.html -->
<script type="module" src="./index.js"></script>
// index.js
import('./1.js').then(module => {
// console.log(module);
console.log(module.x);
module.foo();
console.log(module.default);
});
# 7.2 Node.js的模块加载办法
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。Node.js 遇到.mjs
文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。
一旦设置了以后,该项目的 JS 脚本,就被解释成 ES6 模块。
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs
。如果没有type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS 模块。
总结为一句话:.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import
命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。
# 7.3 package.json的main字段
package.json
文件有两个字段可以指定模块的入口文件:main
和exports
。比较简单的模块,可以只使用main
字段,指定模块加载的入口文件。
// ./node_modules/es-module-package/package.json
{
"type": "module",
"main": "./src/index.js"
}
上面代码指定项目的入口脚本为./src/index.js
,它的格式为 ES6 模块。如果没有type
字段,index.js
就会被解释为 CommonJS 模块。
然后,import
命令就可以加载这个模块。
// ./my-app.mjs
import { something } from 'es-module-package';
// 实际加载的是 ./node_modules/es-module-package/src/index.js
上面代码中,运行该脚本以后,Node.js 就会到./node_modules
目录下面,寻找es-module-package
模块,然后根据该模块package.json
的main
字段去执行入口文件。
这时,如果用 CommonJS 模块的require()
命令去加载es-module-package
模块会报错,因为 CommonJS 模块不能处理export
命令。
# 7.4 package.json的exports字段
exports
字段的优先级高于main
字段。它有多种用法。
子目录别名
package.json
文件的exports
字段可以指定脚本或子目录的别名。// ./node_modules/es-module-package/package.json { "exports": { "./submodule": "./src/submodule.js" } }
上面的代码指定
src/submodule.js
别名为submodule
,然后就可以从别名加载这个文件。import submodule from 'es-module-package/submodule'; // 加载 ./node_modules/es-module-package/src/submodule.js
main 的别名
exports
字段的别名如果是.
,就代表模块的主入口,优先级高于main
字段,并且可以直接简写成exports
字段的值。{ "exports": { ".": "./main.js" } } // 等同于 { "exports": "./main.js" }