export 和 import

常见的导出方式

  1. // 具名导出 named exports
  2. export function f() {}
  3. export const one = 1
  4. export { foo, b as bar }
  5. // 默认导出 default exports
  6. export default function f() {}
  7. export default 123
  8. // 重新导出 re-exporting
  9. export {install as LineChart} from '../chart/line/install' // echarts
  10. export * from './a.js'
  11. export * as bar from './b.js'

常见的导入方式

// 具名引入 named import
import { foo, b as bar } from './a.js'
// 命名空间引入 namespace import
import * as customModule from './module.js'
// 默认引入 default import
import module from './module.js'

// 组合
import moduleP1, * as module from './module.js'
import moduleP1, { foo, b as bar } from './module.js'

// 空引入 empty import 含副作用
import './module.js'

在使用组合导入的时候,命名空间引入、默认引入和具名引入只可以同时使用两个,而且默认引入需要放在首位。

常见的模块模式

交付 JS 源码的不同方式

使用方式 运行时 加载模式 扩展名
script 浏览器 async .js
CommonJS node.js sync .js .cjs
AMD 浏览器 async .js
ESM 浏览器和node.js async .js .mjs

在 node.js 中默认使用 commonJS 模块。若想使用 ESM,可以有两种方法实现。第一中方法是修改 package.json 文件中的 type 字段为 module ,如果在这时需要使用 CommonJS 模块,那么可以将对应的模块扩展名改为 .cjs 。第二种方法是直接将文件的后缀名改为 .mjs ,这时 node.js 就会使用 ESM 的方式进行加载。

模块出现之前

最初,浏览器只有 scripts,代码片段都在浏览器的全局环境中执行。

<script src="a.js"></script>
<script src="b.js"></script>
<script src="main.js"></script>

main.js 作为我们的主文件,我们通过使用立即执行函数的方式来模拟实现模块:

var myModule = (function () {
  var importedFun1 = moduleA.fun1
  var importedFun2 = moduleB.fun2

  function internal() {}
  function exportedFun() {
      importedFun1()
    importedFun2()
  }
  return {
      exportedFun: exportedFun
  }
})()

在 ES 6 之前,使用 var 声明的变量没有块级作用域,只有函数作用域。因此我们可以使用 IIFE 的方式来进行模块封装。但使用这种方式存在诸多的问题:

  • 命名冲突
  • 依赖关系不够明确,需要手动实现加载顺序

    ESM 之前的模块

  • CommonJS(Server side)

  • AMD(Client side)

    ESM

    ESM 的特性:

  • 和 CommonJS 一样,ESM 使用了简洁的语法而且支持循环引用

  • 和 AMD 一样,ESM 被设计为异步加载
  • 比 CommonJS 更为简洁的语法
  • 模块拥有静态结构(并且在运行时不可被更改)。这对于静态检查、优化引入的获取(access of imoprts)和减少 dead code 很有帮助
  • 对循环引入的支持非常透明 ```javascript // a.js export let counter = 3

export function incCounter() { counter++ }

// b.js import { counter, incCounter } from ‘./a.mjs’

console.log(‘counter: ‘, counter) // 3 incCounter() console.log(‘counter: ‘, counter) // 4 `` 变量在不同模块间的连接是实时的,我们无法通过直接couter++` 的方式来操作。这样做有两个好处:

  • 因为之前共享的变量可以变为导出,所以更易于分离模块
  • 这个行为对于支持透明的循环引入是至关重要的

ac2a6c966087141ba9f77be190a2a8a63c8fc15f.svg
如上图示例:M 引入了 N 和 O,N 引入来 P 和 Q,P 引入了 M。
解析之后,这些模块有两个设置阶段:

  • 初始化:每一个模块都被访问到了,并且他的引入和导出是相连的。在父级被初始化之前,他的每一个子级都必须被初始化
  • 执行:模块里面的内容被执行的时候,其子级里的内容同样先被执行

这种正确处理循环引入的方式,主要是因为 ESM 的两个特性:

  • 得益于ESM的静态结构,在解析过之后导出的内容就已经知道了。这使得初始化M先于其子级M变得可能:因为 P 已经向上寻找到了 M 的导出
  • 当 P 执行的时候,M 还没有被执行。然而,在 P 中的实体已经提到了来自 M 中的引入,但却不可以使用。

    For example, a function in P can access an import from M. The only limitation is that we must wait until after the evaluation of M, before calling that function.

尽量少用 import * as foo from './foo'import foo from './foo' ,因为这会导致打包工具无法有效的实现 tree shaking.

使用动态引入 import()

  • In the general case, dynamic imports cannot be tree-shaken because we can access the exported symbols with index signature with an expression that contains data only available at runtime (i.e. import('.foo' + '').then(m => m[a]) )
  • Modern bundlers and TypeScript can resolve dynamic imports only when we have specified the module with a string literal (an exception is webpack, which statically performs partial evaluation)

    参考

  • ESM a cartoon deep dive

  • dynamic imports javascript