相对于AMD这些社区规范,ES Modules在语言层面实现了模块化规范,所有它更为完善,另外现如今大多数浏览器已经原生支持ES Modules。
1、ES Modules的特性
通过给script添加type=module的属性,就可以以ES Module的标准执行其中的JS代码。另外添加该属性后的script会有以下几个特性:
- ESM自动采用严格模式,忽略‘use strict’;
在非严格模式下,打印出的this是window,而在这里打印的是undefined
- 每个ES Modules都是运行在单独的私有作用域中;
- ESM是通过 CORS 的方式请求外部JS 模块的;
ESM的script 标签会延迟执行脚本,等同于script标签的defer属性;
2、ES Modules的功能
2.1、导出export
ESM当中,每一个模块都运行在独立的私有作用域中,所以模块内部的所有成员都不能直接被外部访问,如果需要对外暴露某个成员,需要用到export关键词,如:
- 在变量的声明前添加export,修饰变量声明
// modules.js
export const name = 'cyy';
export function hello (){
console.log('hello')
}
export class Person{}
// app.js
import { name,hello,Person } from './modules.js'
console.log(name) // foo module
console.log(hello) // ƒ hello () {console.log('hello')}
console.log(Person) // class Person {}
- 单独使用export,在模块尾部统一导出所有成员
// modules.js
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name, hello, Person }
// app.js
import { name,hello,Person } from './modules.js'
console.log(name) // foo module
console.log(hello) // ƒ hello () {console.log('hello')}
console.log(Person) // class Person {}
- 通过as对导出的成员重命名
// modules.js
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export {
name as default,
hello as fooHello
}
// app.js
import { default as firstName,fooHello } from './modules.js'
console.log(firstName) // foo module
console.log(fooHello) // ƒ hello () {console.log('hello')}
- 2.1.4、export default
// modules.js
var name = 'foo module'
export default name
// app.js
import name from './modules.js'
console.log(name) // foo module
注意:当export导出的成员被重命名为default,那么在导入的时候不能直接导入default,因为default是关键词,不能被直接当做变量被使用。
注意事项:
export,import后面的{}是固定的语法,并不是导出的是一个字面量对象,另外import后面的{}也不是对这个对象的解构。
export后面不可以直接加一个变量或值,如export 123,和export ‘cyy’ 都是错误的,正确的写法是export {name}。如果想导出一个对象,用export default {name,Person} ,这时候不能用import {name} from ‘./modules.js’。
export在导出成员的时候,导出的是对这个成员的引用,当在模块中修改该成员,外部引用该模块的成员的值也会发生变化。
// modules.js
var name = 'foo module'
export {name}
setTimeout(() => {
name = 'fooHello'
},1000)
// app.js
import {name} from "./module.js";
console.log(name) //foo module
setTimeout(() => {
console.log(name) //fooHello
}, 2000);
另外export导出的成员是只读的,外部是不能修改的。
// modules.js
var name = 'foo module'
export {name}
// app.js
import {name} from "./module.js";
console.log(name) //foo module
setTimeout(() => {
name = 'fooHello'
}, 2000);
`app.js:5 Uncaught TypeError: Assignment to constant variable.`
2.2、导入import
- 被导入的模块路径必须使用完整的文件名称,不能省略扩展名
// modules.js
var name = 'foo module'
export {name}
// app.js
import {name} from "./module.js";
import {name} from "./module"; // 错误
- 不能省略index文件
import {lowercase} from "./utils/index.js";
import {name} from "./utils"; // 错误
如果使用了类似于webpack打包工具,以上都可以省略。
- 相对路径中不能省略’./‘,也可以使用完整的url或者绝对路径
import {name} from "./module.js";
import {name} from "/myproject/module.js";
import {name} from "http://localhost:3000/myproject/module.js";
- 只加载模块,并不提取成员
import {} from "./module.js";
//或者
import "./module.js";
- import as mod from “./module.js”
当提取的成员很多,可以用将所有成员提取,并放到一个对象当中,使用的时候用*mod[modname]
// modules.js
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name, hello, Person }
//app.js
import * as mod from "./module.js";
mod.hello()
- 动态导入
import关键词是一个导入模块的声明,需要在开发阶段明确需要导入的模块路径,但是有时候该文件路径是在运行的时候才知道,我们不能用import from 一个变量:
//app.js
var modulePath = './module.js'
import { name } from modulePath
`Uncaught SyntaxError: Unexpected identifier`
另外有时候需要在条件满足后导入一个模块,import只能出现在最顶层作用域
//app.js
if(true) {
import {name} from './module.js'
}
`Uncaught SyntaxError: Unexpected identifier`
以上情况需要动态导入模块函数,函数执行后返回一个promise,模块的对象可以通过参数拿到
import('./modules.js').then((module)=>{
console.log(module)
})
- 同时提取默认成员和具名成员
// modules.js
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name, hello }
export default Person
//app.js
import Person, { name, hello } from "./module.js";
//或者
import { name, hello,default as Person } from "./module.js";
2.3、导出导入成员
export可以直接将导入的成员再直接导出,一般会在index文件中用到,将某个目录下散落的模块,通过这种方式组织到一个index文件中,然后统一导出,方便外部使用。
// components/button.js
var button = 'button component'
export default button
// components/avatar.js
export var avatar = 'avatar component'
// /components/index.js
export { default as button } from "./button.js";
export { avatar } from "./avatar.js";
//app.js
import {button,avatar} from './components/index.js'
3、ES Modules in Browser 的兼容性解决方案Polyfill
ES Module在2014年才被提出,也就意味着早期的浏览器不支持该特性,截至到目前,还有IE还有部分国产浏览器仍然不支持,所以我们在使用ESM需要考虑兼容性问题。
- polyfill工具—browser-es-module-loader
该模块可以让浏览器直接支持ESM绝大部分特性,使用方法: - 1、npm安装
install es-module-loader --save-dev
- 2、script标签引入
<script src="dist/babel-browser-build.js"></script>
<script src="dist/browser-es-module-loader.js"></script>
- unpkg网站输入https://www.unpkg.com/browser-es-module-loader 获取cdn文件,
一共有两个文件:
工作原理就是通过browser-es-module-loader将代码读取,并交给babel转换为浏览器支持的文件。
<script src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
引入了polyfill工具后,如果浏览器原生支持ESM,则会执行两次export、import代码,可以借助script标签的新属性nomodule解决该问题,添加nomodule属性的script脚本只会在不支持ESM的浏览器执行。
<script nomodule src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
polyfill的这种兼容ESM的方式只适合在开发阶段使用,因为其原理是在运行阶段动态的解析脚本,效率低。生产环境中,应该事先将代码编译好,然后放到浏览器执行。
文章内容输出来源:拉勾教育大前端高薪训练营。