ES Module 简述

257 阅读5分钟

ES Module

浏览器中使用

html 中使用

在 html 中 script 标签添加 type="module",表示可以以 ES Module 的标准执行其中的 JS 代码 ESM 自动采用了严格模式,忽略 'use strict' 每个 ES Module 都是运行在单独的私有作用域中 ESM 可以通过 CORS 的方式请求外部的 JS 模块 ESM 的 script 可以添加 defer ,会延迟执行脚步,即页面加载完成后才会执行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ES Module - 模块的特性</title>
</head>
<body>
<!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
<script type="module">
    console.log('this is es module')
</script>

<!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
<script type="module">
    console.log(this)
</script>

<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
    var foo = 100
    console.log(foo)
</script>
<script type="module">
    console.log(foo)
</script>

<!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->

<!-- 4. ESM 的 script 标签会延迟执行脚本 -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>
</body>
</html>

第二个 module 中 foo 无法获取到第一个 module 的值,表明作用域是分开的 script defer 标签下面有个 p 标签,页面显示了 p 标签的值才弹出对话框,表明脚步延迟执行

image-20220113115624234

js 中导出

  • 通过 export 单个导出属性、函数和类
// module.js
// 导出
export var name = 'foo module'

export function hello() {
  console.log('hello')
}

export class Person {
}

// app.js
// 导入
import {name, hello, Person} from './module.js'

console.log(name, hello, Person)
  • 通过 export 导出一组成员
// module.js
// 导出
var name = 'foo module'

function hello() {
  console.log('hello')
}

class Person {
}

// 这种方式不是对象的方式,只是 export 的语法
export {name, hello, Person}

// app.js
// 导入
import {name, hello, Person} from './module.js'

console.log(name, hello, Person)
  • 通过 export 导出默认值
// module.js
// 导出
var name = 'foo module'
export default name

// app.js
// 导入
// 通过默认值重命名
import {default as fooName} from './module.js'

console.log(fooName)

// 直接修改 default 的命名
import name1 from './module.js'

console.log(name1)
  • 通过 export 导出,并且重命名
// module.js
// 导出
var name = 'foo module'

function hello() {
  console.log('hello')
}

export {
  // 重命名默认导出
  name as default,
  // 更换 hello 的命名
  hello as fooHello
}

// app.js
// 导入
import name1, {fooHello} from './module.js'

console.log(name1, fooHello)

注意

在 CommonJS 中是先将整个模块导入为一个对象,然后从对象中解构出需要的成员 const { name, age } = require('./module.js')

在 ES Module 中 {} 是固定的语法,就是直接提取模块导出成员

import { name, age } from './module.js

在 ES Module 中导入成员并不是复制一个副本,而是直接导入模块成员的引用地址,也就是说 import 得到的变量与 export 导出的变量在内存中是同一块空间。一旦模块中成员修改了,导入的地方也会修改。

import {name, age} from './module.js'

setTimeout(() => {
  console.log(name, age)
})

导入模块变量是只读的

import {name} from './module.js'

name = 'tom' // 报错

如果导入的是一个对象,对象的属性读写不受影响 name.xxx = xxx

export {} 不是一个对象字面量,只是语法上的规则而已

var name = 'jack'
var age = 18
export {name, age}

// 错误用法
// export name
// export 'foo' 

js 中导入

// /test/module.js
var name = 'jack'
var age = 18

export {name, age}

console.log('module action')

export default 'default export'
// /test/utils/index.js
export function lowercase(input) {
  return input.toLowerCase()
}
  • 后缀不能省略
// from 后面需要跟文件的后缀,不能去除
import {name} from './module'

// 正确用法
import {name} from './module.js'

console.log(name)
  • index.js 也不能省略
// 错误导入
import {lowercase} from 'utils'

// 正确导入
import {lowercase} from 'utils/index.js'

console.log(lowercase('HHH'))
  • from 后面导入包内的文件不能省略 ./
// 这样导入会被认为是导入了第三方包,回去 node_modules 中找包
import {name} from 'module.js'

// ./ 不能省略
import {name} from './module.js'

// 或者使用项目为绝对路径的方式导入
import {name} from '/test/module.js'

// 也可以通过网络的方式导入包
import {name} from 'http://localhost:3000/test/module.js'

console.log(name)
  • 只导包,不使用
import {} from './module.js'

import './module.js'
  • 导出包中所有的内容
import * as mod from './module.js'

console.log(mod)
  • 不能通过变量或者条件判断的方式导入包

错误导包

const modulePath = './module.js'
import {name} from 'modulePaht'


if (true) {
  // 错误导入方式
  // import {name} from './module.js'
}
  • 通过 import() 函数动态导入包

import(),函数返回了一个 Promise,可以在 then 中接受到这个模块对象

import('./module.js').then(module => {
  console.log(module)
})
  • 提取包中的值和默认的值
import {name, age, default as title} from './module.js'
import abc, {name, age} from './module.js'

// abc 和 title 都是 default 的值

导入与导出

// components/avatar.js
export var Avatar = 'Avatar Component'
// components/button.js
var Button = 'Button Component'

export default Button

把两个组件合并到一个文件中进行导出

// components/index.js
import Button from './button.js'
import {Avatar} from './avatar.js'

export {Button, Avatar}

或者可以通过 export 直接导出

export {default as Button} from './button.js'
export {Avatar} from './avatar.js'

node 中使用

node 使用 ESM

在 node 中使用 esModule 需要将扩展名由 .js 改为 .mjs
启动的时候需要额外添加 --experimental-modules 参数
可以通过 ESM 的方式导入 node 的内置模块,内置模块兼容了 ESM 的提取成员的方式

import {foo, bar} from './module.mjs'

console.log(foo, bar)

// 可以通过 esm 加载内置模块
import fs from 'fs'

fs.writeFileSync('./foo.txt', 'es module working')

// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员的方式
import {writeFileSync} from 'fs'

writeFileSync('./bar.txt', 'es module working')

// 对于第三方的 NPM 模块也可以通过 ESM 加载
import _ from 'lodash'

console.log(_.camelCase('ES Module'))

// 不支持,第三方模块都是导出默认成员
// import {camelCase} from 'lodash'

// console.log(camelCase('ES Module'))

esModule 中使用 commonJS

// es-module.mjs
import mod from './commonjs.js'

console.log(mod)
// { foo: 'commonjs exports value' }

// 不能从 CommonJS 中直接提取成员,import 不是解构导出对象
// 错误导出方式
import {foo} from './commonjs.js'

console.log(foo)
// commonjs.js
// CommonJS 模块始终只会导出一个默认值
module.exports = {
  foo: 'commonjs exports value'
}

commonJS 中不能使用 esModule

// es-module.mjs
export const foo = 'es module export value'
// commonjs.js

// 报错,不能在 CommonJS 模块中通过 require 载入 ES Module
const mod = require('./es-module.mjs')
console.log(mod)

esModule 和 commonJS 的区别

ESM 中没有模块全局成员,以下变量全部报错

// esm.mjs
// 加载模块函数
console.log(require)

// 模块对象
console.log(module)

// 导出对象别名
console.log(exports)

// 当前文件夹的绝对路径
console.log(__filename)

// 当前文件所在的目录
console.log(__dirname)

ESM 中获取 __filename 和 __dirname

import.meta.url 中为 url 形式的文件路径,需要转为当前的绝对路径

import {fileURLToPath} from 'url'
import {dirname} from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)

定义该项目为 ES Module

将 package.json 的 type 改为 module,这该项目所有的 js 文件都遵循 ES Module,不再需要 .mjs 后缀
CommonJS 的使用这需要改后缀为 .cjs

{
  "type": "module"
}
// module.js
export const foo = 'hello'
export const bar = 'world'
// index.js
import {foo, bar} from './module.js'
import * as mod from './common.cjs'

console.log(mod)
console.log(foo, bar)
// common.cjs
const path = require('path')
console.log(path.join(__dirname, 'foo'))

module.exports = {
  foo: 'hhh'
}