export 和 import
常见的导出方式
// 具名导出 named exports
export function f() {}
export const one = 1
export { foo, b as bar }
// 默认导出 default exports
export default function f() {}
export default 123
// 重新导出 re-exporting
export {install as LineChart} from '../chart/line/install' // echarts
export * from './a.js'
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)
-
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++` 的方式来操作。这样做有两个好处:
- 因为之前共享的变量可以变为导出,所以更易于分离模块
- 这个行为对于支持透明的循环引入是至关重要的
如上图示例: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)
参考
- dynamic imports javascript