模块系统

常用模块系统分为ADM、CommonJS、UMD。ES6也实现了模块的导入导出。

导入导出

ES6

在项目中引入es6/c.ts,即可在浏览器的console中查看输出数据。
es6/a.ts

  1. export let a = 1
  2. let b = 2
  3. let c = 3
  4. export { b, c }
  5. export interface P {
  6. x: number
  7. y: number
  8. }
  9. export function f() {
  10. console.log('func')
  11. }
  12. function g() {}
  13. export { g as G }
  14. export default function () {
  15. console.log('default')
  16. }
  17. export { str as hello } from './b'

es6/b.ts

  1. export let str = 'hello'

es6/c.ts

  1. import { a } from './a'
  2. import { b, c } from './a'
  3. import { P } from './a'
  4. import { f } from './a'
  5. import DefaultFunc from './a'
  6. import * as All from './a'
  7. console.log(a)
  8. console.log(b, c)
  9. let p: P = {
  10. x: 1,
  11. y: 1,
  12. }
  13. f()
  14. DefaultFunc()
  15. console.log(All)

CommonJS

node无法直接执行,需要安装一个ts-node然后通过命令行执行 ts-node node/c.ts查看结果
node/a.ts

  1. let a = {
  2. x: 1,
  3. y: 2,
  4. }
  5. module.exports = a

node/b.ts

  1. exports.b = 2
  2. exports.c = 3
  3. // module.exports = {}; // 将会覆盖前面两个导出,只允许一个顶级导出 多个次级导出

node/c.ts

  1. let c1 = require('./a')
  2. let c2 = require('./b')
  3. console.log(c1) // { x: 1, y: 2 }
  4. console.log(c2) // { b: 2, c: 3 }

差异总结

ES6可以有default,也可以有多个单独导出;CommonJS只允许一个顶级导出 或 多个次级导出

配置项目

在tsconfig.json中有三个参数和模块处理相关:targetmoduleesModuleInterop

target和module

这里先介绍target和module两个属性:

  • target:ts代码被编译成什么版本,在项目中默认是es5,在命令行中默认是es3
  • module: 编译成什么样的模块系统,默认是commonjs

注:tsc命令如果如果指定了具体文件,将不会读tsconfig.json,需要单独在命令行指定参数

下面例子分别测试了 target 和 module 模块的编译输出,可以非常直观看到差别

  1. $ tsc ./es6/a.ts -t es3
  2. $ tsc ./es6/a.ts -t es5
  3. $ tsc ./es6/a.ts -t es
  4. $ tsc ./es6/a.ts -m 'amd'
  5. $ tsc ./es6/a.ts -m 'umd'

模块兼容问题

我们的代码是用ES6书写的,最终编译成了CommonJS。根据前面的介绍,ES6是可以有顶级导出(export default)和顶级导入的,而CommonJS是没有的,编译器是如何抹平这种差异的呢。

通过阅读编译后的代码,可以看到顶级导出成了exports的default属性,同样在使用的地方也通过default来调用:

  1. // 导出模块编译后代码
  2. function default_1() {
  3. console.log('default');
  4. }
  5. exports["default"] = default_1;
  6. // 导入模块编译后代码
  7. var a_4 = require("./a");
  8. a_4["default"]();

如果单纯都是用ES6自然没问题,如果要在代码中混用ES6 和 CommonJS,则会出现问题,例如:

node/c.ts

  1. let c3 = require('../es6/a')
  2. console.log(c3)
  3. c3.default()

我们发现,期望默认导入的是函数,结果输出是一个对象,要通过 c3.default() 才可以达到效果,自然不方便。

混用模块的建议

为此提供了建议
1)不要混用两类模块系统
2)使用TS提供的export =语法,可以被ES6模块识别

es6/d.ts

  1. export = function () {
  2. console.log('func')
  3. }

node/c.ts

  1. import c4 = require('../es6/d')
  2. c4(); // func

围绕兼容性的话题,在简单提供几个例子,有src/test.ts文件,假定我们使用 tsc src/test.ts -m commonjs 编译它,文件内容 和 编译产生的结果在注释中:

src/test.ts

  1. let li: string = 'hello';
  2. module.exports = li
  3. // 编译结果:module.exports = li
  4. export = li;
  5. // 编译结果:module.exports = li;
  6. export defaul li
  7. // exports["default"] = li;

可见,module.exportsexport = 编译结果完全一样。

但是如果我们尝试ES6模块中,使用impor方式导入上述三种声明的导出,第一个是导出会无法被识别。因此:CommonJS模块代码为了更好的兼容性,建议使用 export =的语法。

src/main.ts

  1. import li from './test.ts' // 'src/test.ts' is not a module
  2. import li from './test.ts' // ok
  3. import li from './test.ts' // ok

esModuleInterop

上述例子中,我们通过 export =语法 和 import = require语法实现了在CommonJS中使用ES6顶级导出。如果想在导入的时候,更ES6的风格,即采用如下方式,必须要讲tsconfig.json中 的 esModuleInterop设置为true。

  1. import c4 from '../es6/d';
  2. c4(); // func