ES Modules 是 2015 年推出的,语言层面的模块化规范,与运行环境无关,服务器和浏览器中都能使用。
在 html 中,通过给 script 添加 type = module 的属性,就可以用 ES Module 的标准执行其中的 JS 代码
特性
ESM 自动采用严格模式,忽略 'use strict',比如在全局范围不能使用
this
。每个 ES Module 都是运行在单独的私有作用域中,不会有全局作用域污染的问题。
ESM 通过 CORS【跨源资源共享(Cross-Origin Resource Sharing)】的方式请求外部 JS 模块,所以在跨域请求时,要求服务端支持 CORS。
ESM 的 script 标签会延迟执行脚本,等同于 script 标签的 defer 属性 【<script> 详情】。一般情况下,script 脚本在网页加载中默认立即执行 ,页面渲染会等待脚本加载完成之后才继续渲染,而 defer 这种延迟执行的机制可以使得脚本在网页渲染之后才进行执行。
导入和导出
ESM 分别通过 import 和 export 进行模块的导入和导出。
导出
export 可以导出模块内的变量成员、函数成员和类成员,其余未进行导出的成员将以私有成员的方式存在于模块中,外部不可调用。
导出写法有两种:
- 声明修饰,在导出成员声明前添加 export 关键字进行修饰。
示例代码 // module.js // 变量成员 export var name = 'foo module' // 函数成员 export function hello () { console.log('hello') } // 类成员 export class Person {}
- 集中导出,在模块尾部通过 export 关键字导出对外成员组成的集合,注意,。还可以通过 as 关键字对导出成员进行重命名,在导入时则使用重命名之后的成员名。
示例代码 // module.js // 变量成员 var name = 'foo module' // 函数成员 function hello () { console.log('hello') } // 类成员 class Person {} // 尾部集中导出 export { name, hello, Person } // as 重命名 export { name as fooName, hello as fooHello, Person as FooPerson }
注意:
- 将导出成员设置为 default 的则作为模块的默认导出成员。如果以
export { name as default }
这种形式默认导出,则在导入的时候必须重命名import { default as name } from './module.js'
才能使用。或者通过export default name
形式导出,则可以在 import 时设置任意成员名import fooName from './module.js'
。 -
{}
并不是对象字面量,只是 ESM 导出语法的一种固定用法,如果通过export default { name }
这种形式{}
内才为对象字面量。 - export 所导出的成员,并不是复制,而且原成员的引用,也就是导出的成员名中存储的是原成员的地址,并不是原成员的值。
导入
import 在模块中负责引入其他模块的导出成员。
示例代码
// app.js
import { name, hello, Person } from './module.js'
在
import name from './module.js'
的导出语法中,from 关键字后紧跟完整的模块路径,且不能省略模块的后缀名,即.js
,否则没法加载模块。-
在加载自定义模块时,不能省略
./
的相对路径标识,或者/
的绝对路径标识,否则会认为是加载第三方模块。也可以直接填写完整的 http 路径访问 CDN 的模块。示例代码 import { name } from 'module.js' import { name } from './module.js' import { name } from '/04-import/module.js' import { name } from 'http://localhost:3000/04-import/module.js'
如果
{}
内为空,则只会执行导入的模块,而不会提取导出成员,可以进行简写,比如:import './module.js'
。假如模块内导出的成员比较多,可以通过
*
进行全部导入,但是需要as
命名一个对象字面量进行接收,比如:import * as mod from './module.js'
,mod 就是导出的成员对象字面量。-
import 只能是在模块的顶层使用,不能嵌套在其他作用域内,并且没法通过变量声明导入路径。如果想动态导入模块,可以使用 import() 函数在当前模块任意位置导入模块。import() 函数返回的是一个Promise 对象,当模块执行完成之后会自动执行 then 中的回调函数,因此可以通过 then 的回调函数的参数拿到模块的导出成员对象。
示例代码 import('./module.js').then(function (module) { console.log(module) // 输出模块导出成员对象 })
-
如果导入的模块内除了命名的导出成员还有默认的导出成员,那么默认的导出成员需要通过 as 进行重命名,或者通过逗号分隔,逗号左边为默认成员(可以任意命名),右边为命名成员。
示例代码 // as 方式导入默认成员 import { name, age, default as title } from './module.js' // 逗号重命名方式导入默认成员 import abc, { name, age } from './module.js'
导入与导出协同
可以将导入的 import 改为 export ,这种写法表示将引入模块的成员直接导出,可以在模块包内设置一个 index 模块,在 index 统一用 export 导出模块包中每个模块的导出成员,这样在其他模块导入模块包时就不需要多次导入。
例如上图的 components 模块包中的 index.js ,统一导出了模块包内的模块导出成员,在外部的 app.js 只需要导入 index.js 就可以了。
// index.js
export { Button } from './button.js'
export { Avatar } from './avatar.js'
// app.js
import { Button, Avatar } from './components/index.js'
Polyfill 兼容方案
因为 ESM 是 2015年推出的,这意味着之前版本的浏览器并不兼容 ESM ,这种兼容问题可以通过在 html 中用 script 标签加载 Polyfill 的兼容方案进行解决。可以在 upkg 上获取对应的资源库。
<body>
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
</body>
该方案的原理是通过获取 ES Modules 的模块代码,经过 label 兼容转换之后交給 Polyfill 进行执行。 因此在已经支持 ES Modules 的浏览器上同样的代码会执行两次,可以通过 script 新增的 nomodule 属性判断是否支持 ES Modules,不支持则加载。
这种形式的兼容方案只适用于开发阶段的调试,因为其原理是动态的解析脚本,运行效率并不高,因此在生产阶段应直接将代码进行兼容转换,让其直接在浏览器上工作。
ES Modules in Node.js
Node.js 在 8.5 版本之后已经以实验特性的方式支持 ES modules ,因此可以通过原生方式在 Node.js 中编写 ES module 相关代码,但由于 ES modules 与 CommonJS
之间的差距比较大,因此该特性目前仍处于过渡阶段。
在 Node.js 中使用 ES Modules 需要将 JS 文件的扩展名改为 mjs
,在命令行启动时需要加上 --experimental-modules
。
ES Modules 导入模块
Node.js 的内置模块是可以照常通过 import 进行导入的,比如:import fs from 'fs'
。但是需要注意的是,第三方模块都是默认导出成员,所以不能通过对象结构的形式导入第三方模块的成员,比如:import { camelCase } from 'lodash'
是不成功的。而内置模块则因为兼容了 ESM 的提取成员方式,所以可以通过对象解构的方式导入,比如:import { writeFileSync } from 'fs'
是成功的。
ES Modules 与 CommonJS 模块交互
ES Modules 模块导入 CommonJS 模块时 CommonJS 模块始终只会导出一个默认成员,即 ES Modules ,模块只能以默认成员的方式导入 CommonJS 模块,同时因为 import 不是解构出对象,所以不能直接提前成员,比如 import { foo } from './commonjs.js'
是不成功的。
注意:Node.js 原生环境中不能在 CommonJS 模块中通过 require 载入 ES Modules
ES Modules 与 CommonJS 差异
ES Modules 中没有 CommonJS 中的那些模块全局成员,比如:require
、module
、export
、__filename
、__dirname
。
-
module
和export
在 ESM 可以通过 import 和 export 代替。 -
__filename
可以通过 import 对象下的 meta 属性内的 url 拿到当前文件 URL,再通过 url 模块下的 fileURLToPath 方法将文件 URL 转换成文件路径获得。 -
__dirname
可以将上述的__filename
接着使用 path 模块下的 dirname 方法提取目录部分获得。
Node.js 新版本对 ESM 的支持
Node.js 在最新的版本中可以在 package.json 中添加 "type":"module"
使得当前项目中的 js 文件全部以 ES Modules 的方式执行文件,不再需要将 js 文件扩展名改为 mjs
,如果此时还想使用 CommonJS 的话需要将 js 的扩展名改为 cjs
。