相对于AMD这些社区规范,ES Modules在语言层面实现了模块化规范,所有它更为完善,另外现如今大多数浏览器已经原生支持ES Modules。

1、ES Modules的特性

通过给script添加type=module的属性,就可以以ES Module的标准执行其中的JS代码。另外添加该属性后的script会有以下几个特性:

  • ESM自动采用严格模式,忽略‘use strict’;
    image.png

在非严格模式下,打印出的this是window,而在这里打印的是undefined

  • 每个ES Modules都是运行在单独的私有作用域中;

image.png

  • ESM是通过 CORS 的方式请求外部JS 模块的;
  • ESM的script 标签会延迟执行脚本,等同于script标签的defer属性;

    2、ES Modules的功能

    2.1、导出export

    ESM当中,每一个模块都运行在独立的私有作用域中,所以模块内部的所有成员都不能直接被外部访问,如果需要对外暴露某个成员,需要用到export关键词,如:

    • 在变量的声明前添加export,修饰变量声明
  1. // modules.js
  2. export const name = 'cyy';
  3. export function hello (){
  4. console.log('hello')
  5. }
  6. export class Person{}
  7. // app.js
  8. import { name,hello,Person } from './modules.js'
  9. console.log(name) // foo module
  10. console.log(hello) // ƒ hello () {console.log('hello')}
  11. console.log(Person) // class Person {}
  • 单独使用export,在模块尾部统一导出所有成员
  1. // modules.js
  2. var name = 'foo module'
  3. function hello () {
  4. console.log('hello')
  5. }
  6. class Person {}
  7. export { name, hello, Person }
  8. // app.js
  9. import { name,hello,Person } from './modules.js'
  10. console.log(name) // foo module
  11. console.log(hello) // ƒ hello () {console.log('hello')}
  12. console.log(Person) // class Person {}
  • 通过as对导出的成员重命名
  1. // modules.js
  2. var name = 'foo module'
  3. function hello () {
  4. console.log('hello')
  5. }
  6. class Person {}
  7. export {
  8. name as default,
  9. hello as fooHello
  10. }
  11. // app.js
  12. import { default as firstName,fooHello } from './modules.js'
  13. console.log(firstName) // foo module
  14. console.log(fooHello) // ƒ hello () {console.log('hello')}
  • 2.1.4、export default
  1. // modules.js
  2. var name = 'foo module'
  3. export default name
  4. // app.js
  5. import name from './modules.js'
  6. 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在导出成员的时候,导出的是对这个成员的引用,当在模块中修改该成员,外部引用该模块的成员的值也会发生变化。

  1. // modules.js
  2. var name = 'foo module'
  3. export {name}
  4. setTimeout(() => {
  5. name = 'fooHello'
  6. },1000)
  7. // app.js
  8. import {name} from "./module.js";
  9. console.log(name) //foo module
  10. setTimeout(() => {
  11. console.log(name) //fooHello
  12. }, 2000);

另外export导出的成员是只读的,外部是不能修改的。

  1. // modules.js
  2. var name = 'foo module'
  3. export {name}
  4. // app.js
  5. import {name} from "./module.js";
  6. console.log(name) //foo module
  7. setTimeout(() => {
  8. name = 'fooHello'
  9. }, 2000);
  10. `app.js:5 Uncaught TypeError: Assignment to constant variable.`

2.2、导入import

  • 被导入的模块路径必须使用完整的文件名称,不能省略扩展名
  1. // modules.js
  2. var name = 'foo module'
  3. export {name}
  4. // app.js
  5. import {name} from "./module.js";
  6. import {name} from "./module"; // 错误
  • 不能省略index文件
  1. import {lowercase} from "./utils/index.js";
  2. import {name} from "./utils"; // 错误

如果使用了类似于webpack打包工具,以上都可以省略。

  • 相对路径中不能省略’./‘,也可以使用完整的url或者绝对路径
  1. import {name} from "./module.js";
  2. import {name} from "/myproject/module.js";
  3. import {name} from "http://localhost:3000/myproject/module.js";
  • 只加载模块,并不提取成员
  1. import {} from "./module.js";
  2. //或者
  3. import "./module.js";
  • import as mod from “./module.js”
    当提取的成员很多,可以用将所有成员提取,并放到一个对象当中,使用的时候用*mod[modname]
  1. // modules.js
  2. var name = 'foo module'
  3. function hello () {
  4. console.log('hello')
  5. }
  6. class Person {}
  7. export { name, hello, Person }
  8. //app.js
  9. import * as mod from "./module.js";
  10. mod.hello()
  • 动态导入
    import关键词是一个导入模块的声明,需要在开发阶段明确需要导入的模块路径,但是有时候该文件路径是在运行的时候才知道,我们不能用import from 一个变量:
  1. //app.js
  2. var modulePath = './module.js'
  3. import { name } from modulePath
  4. `Uncaught SyntaxError: Unexpected identifier`

另外有时候需要在条件满足后导入一个模块,import只能出现在最顶层作用域

  1. //app.js
  2. if(true) {
  3. import {name} from './module.js'
  4. }
  5. `Uncaught SyntaxError: Unexpected identifier`

以上情况需要动态导入模块函数,函数执行后返回一个promise,模块的对象可以通过参数拿到

  1. import('./modules.js').then((module)=>{
  2. console.log(module)
  3. })
  • 同时提取默认成员和具名成员
  1. // modules.js
  2. var name = 'foo module'
  3. function hello () {
  4. console.log('hello')
  5. }
  6. class Person {}
  7. export { name, hello }
  8. export default Person
  9. //app.js
  10. import Person, { name, hello } from "./module.js";
  11. //或者
  12. import { name, hello,default as Person } from "./module.js";

2.3、导出导入成员

export可以直接将导入的成员再直接导出,一般会在index文件中用到,将某个目录下散落的模块,通过这种方式组织到一个index文件中,然后统一导出,方便外部使用。

  1. // components/button.js
  2. var button = 'button component'
  3. export default button
  4. // components/avatar.js
  5. export var avatar = 'avatar component'
  6. // /components/index.js
  7. export { default as button } from "./button.js";
  8. export { avatar } from "./avatar.js";
  9. //app.js
  10. 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安装
  1. install es-module-loader --save-dev
  • 2、script标签引入
  1. <script src="dist/babel-browser-build.js"></script>
  2. <script src="dist/browser-es-module-loader.js"></script>
  1. <script src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
  2. <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的浏览器执行。

  1. <script nomodule src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
  2. <script nomodule src="https://www.unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>

polyfill的这种兼容ESM的方式只适合在开发阶段使用,因为其原理是在运行阶段动态的解析脚本,效率低。生产环境中,应该事先将代码编译好,然后放到浏览器执行。

文章内容输出来源:拉勾教育大前端高薪训练营。