ES Modules

7/13/2021 模块化开发

# 基本特性

目前,绝大多数浏览器都已经直接支持ES Modules,因此可以直接通过如下方式使用(为script标签添加**type="module"**即可)

<script type="module">
    console.info("hello, ES Modules")
</script>
1
2
3

上述代码可以正常执行,但它相对于普通的script标签会有一些新的特性

  1. ESM会自动采用严格模式,忽略'use strict'
    • 例如:我们再内部打印this,会发现它是undefined,因为在严格模式全局的this,是undefined,而非严格模式下指向window
<script type="module">
    console.info(this) // undefined
</script>
1
2
3
  1. 每个ES Module 都是运行在单独的私有作用域
    • 每个ES Module之间的变量不会互相影响,这样就不会造成全局污染
<script type="module">
    var foo = 100
    console.info(foo) // 100
</script>
<script type="module">
    console.info(foo) // 报错,foo is not defined
</script>
1
2
3
4
5
6
7
  1. ESM是通过CORS的方式请求外部JS模块的,如果所在的服务端不支持CORS就会报跨域错误
<!-- 该地址不支持CORS就会报错 -->
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<!-- 该地址支持CORS,就会去请求 -->
<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
1
2
3
4
  1. ESM不支持文件访问,必须使用http server的方式让其工作
  2. ESM的script标签会延迟执行脚本,等同于添加了一个defer属性
    • 普通的script标签在HTML中会采用立即执行的机制,也就是包装一层立即执行函数(一个就形成了所谓的调用栈),因此网页会等待script标签加载

<body>
    <!-- 同步,alert弹窗不关闭,就不会渲染下面的p标签 -->
    <script>
        alert("弹窗")
    </script>
    <p>要显示的内容</p>

    <!-- 延迟,alert会在body渲染完后执行 -->
    <script defer>
        alert("弹窗")
        <p>要显示的内容</p>
    </script>
    <!-- 延迟,alert会在body渲染完后执行 -->
    <script type="module">
        alert("弹窗")
        <p>要显示的内容</p>
    </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 导入和导出

export 用于导出成员,import用于导入成员

# 导出语法

  1. export 可以修饰变量、函数、类等的导出
export const name = "foo module"
export function hello() {
    console.info("hh")
}
export class Person {}
1
2
3
4
5
  1. 更为常见的是通过在后面单独统一导出,这样可以更加直观的表示导出的值有哪些
const name = "foo module"
function hello() {
    console.info("hh")
}
class Person {}

// 单独导出
export { name, hello, Person }
1
2
3
4
5
6
7
8
  1. 可以将要导出的值重命名,导入时就需要使用重命名后的名字导入

注意,如果重命名为default,那导入时必须重命名,因为default是关键字

// 将name重命名为fooName
export {
    name as fooName
    hello as default
}
1
2
3
4
5
  1. 直接通过default导出默认成员,导入时可以任意命名
export default name
1

# 导入语法

# import 部分问题

  1. 在import其他模块时文件路径必须加上.js的扩展名,不能省略(原生ES Modules)
import { name, hello, Person } from "./xx.js"
1
  1. index文件引入的时候必须是全路径的,在CommonJS中是可以省略index的
// Es module中
import { name } from "./utils/index.js"
1
2

当然上述两个问题,在打包工具中都已经帮助我们解决,因此在使用时可以不添加扩展名,也可以不加index.js 3. 引入的问题,相对路径的./不能省略,省略了会认为是在引入第三方模块 4. 可以使用/表示网站根路径来引入

// / 表示网站根路径
import { name } from "/04/1.js"
1
2
  1. 引入完整的url(直接使用cdn)
import { name } from "https://XXX"
1
  1. 可以直接使用空{}引入,就相当于直接执行这个模块,就不会去提取成员
import {} from "./1.js"
// 或者
import "./1.js"
1
2
3

这在我们导入一些不需要外界控制的子功能模块时非常有用 7. 如果一个功能模块在导出时特别多,而且在导入时都会用到,就可以使用*将模块中所有成员全部提取出来,再使用as将所有成员全部放在对象中

import * as mod from "./module.js"
1

# 动态导入

  1. import关键词不能from一个变量(比如说运行阶段才知道要导入的路径)
  2. import关键词只能出现在最顶层

所有为了解决这个问题,我们可以使用使用import函数:

import("路径").then(module => {
    // import 函数返回值是一个promise
    console.info(module); // 回调中获取的值就是模块下所有成员组成的对象
})
1
2
3
4

# 同时导入default和其他命名成员

import { name, age, default as title } from "./module.js"
// 也可以使用逗号分开default和其他命名成员
// 逗号左边就是default成员,逗号右边就是其他命名成员
import title, { name, age } from "./module.js"
1
2
3
4

# 将导入的结果作为导出成员

直接将import修改为export

export { name, age } from "./1.js"

// 当然在这个文件中,也不能访问上述成员,因为没有导入进来
1
2
3

这个特性一般在index文件中用到,在这个文件中集中导出该类模块下所有需要导出的成员,例如components或者actions等

# 导入导出注意事项

  1. 在使用export导出的{}里的内容并不是对象字面量,引入的时候也不是对这个对象的解构
const name = "Rain"
const age = 18
export { name, age }
1
2
3

很多人会联想到ES6的对象字面量的用法,但这二者不是同一个东西

export后跟的{}是一个固定的用法,如果要在后面导出一个对象成员,可以使用 export default,此时后面的内容才是一个对象。

const name = "Rain"
const age = 18
export default { name, age }
1
2
3

导入时会理解成对象解构,此时就会报错(找不到name和age两个成员),因此可以看出import后面的{}不是解构,只是一种语法。

同时如果将export {}{} 理解成对象,那export 123就会被认为是正确的,但其实是错误

  1. 在ESM中导出一个成员,导出的是这个成员的引用
// module A
const name = "zs"
export { name }

// module B
import { name } from "./1.js"
1
2
3
4
5
6

在模块A和模块B中的name都是指向相同的地址,导出时只是导出了name的地址,访问的name始终指向的是模块A中定义的name的空间

  1. 导出的是只读的,即一个常量

# 解决浏览器环境兼容性(Polyfill兼容方案)

ES Modules在2014年被提出,早期浏览器不支持,而且截止到目前为止,很多浏览器都还是不支持,例如:ie,baidu Browser等

当然,借助编译工具是可以解决兼容性的,例如:Browser ES Module Loader。它只需要引入到HTML中,就可以让浏览器支持ES Modules

  1. 安装
yarn add browser-es-module-loader
1
  1. 通过模块引入到页面中,或者去npm的cdn (opens new window)去找cdn文件,直接引入
<body>
    <script src="https://unpkg.com/promise-polyfill@8.2.0/dist/polyfill.min.js" nomodule></script>
    <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js" nomodule></script>
    <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js" nomodule></script>
</body>
1
2
3
4
5

并且在ie中还需要引入promise的Polyfill,因为ie不支持promise

其实其工作原理就是将不识别的语法交给babel去转换,将不支持的文件通过请求拿过来再转换一次

当然,在支持的ES Modules浏览器中会重复执行,可以使用nomodule属性解决,这个属性会让脚步只在不支持的ES Modules的浏览器中工作

当然这种方式只能在开发阶段用,不能在生成环境使用,因为它是运行阶段动态解析脚本,性能会非常差,真正的生产环境还是要预先编译成ES5

Last Updated: 1/21/2025, 10:16:53 AM