ES Module,commonjs和Typescript模块系统

首先es和cmj大家都知道不一样,具体区别也就不多说,需要注意的是TS的模块也是自己实现的,不过在ES Module 2015(es6)定稿下来后,TS沿用和支持了ES module,不过TS除了使用ES module的规范,自己也有一套namespace的模块管理,这在某个版本前一直用namespace管理模块,直到后来和ESM保持一致才有改动。例如下面官网说法:

TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

注意babel是典型的ES module写法,babel本身就是将ES6,ES7的高级语法转为浏览器能读取的代码,babel项目迁移到ts,才会出现一些模块写法不兼容的问题。(react+ts项目中代码的转义是靠webpack的babel-loader来转义tsx,ts语法,babel-loader真强大,TS语法,类型的提示和校验则是靠ForkTsCheckerWebpackPlugin,TsconfigPathsPlugin,eslint等插件去完成)

TS中常常用import * as React 导入。

TS 把 CJS 模块作为一个 Namespace 导入,所以,为了解决上面提到的报错,需要这样导入 CJS 模块,以及任何没有 default 导出的模块: import * as React from 'react'

这样子的代码,如果从 babel 迁移到 TS 就需要大幅的改动代码,不过 TS 也注意到了这个问题,添加了一个 compile option 支持 babel 的这种写法 esModuleInterop, PR 在下面 https://github.com/Microsoft/TypeScript/pull/19675

// tsconfig.json

{

"esModuleInterop": true

}

在该选项中,所有 ES Module 文件会导出一个名为 __esModule 的隐藏属性,值为 true。对于导入 ES Module 时,所有行为没有变化。

整体导入其它模块(import * as ns)时,若不是 ES Module,则将导出内容本身作为模块的 default 属性,并将其它所有属性原样拷贝到模块中。

// module.js
module.exports = () => {
  console.log('foo')
}
exports.xixi = 'xixi'

// 使用
import * obj from 'module.js';

即module.export导出的模块,和其他挂载在exports上的属性,都会拷贝到* as obj的obj这个对象寄来,且执行obj.foo === 'foo' 为true,obj.default()则为打印foo,此时和es module的import 就表现一致了,导入的模块对象中default key为es module中default导出的或者cmj module.export导出的,其他属性则为es module 正常export的和cmj中挂载在exports的属性。

而默认导入模块(import name)时,既然普通的module.export导出的内容视为default,那么TS导入就也是import xxx(随便起) from 'xxxx';

但是注意使用babel的项目,比如TS+react,使用的bable-loader,我发现上面的代码obj只会打印出module.export导出的属性,即只打印() => {

console.log('foo')

}这个函数属性,xixi的属性消失,并不是ts这样打印{default:()=>xxx,xixi:"xixi"}。这里是用babel-loader处理cmj模块的表现。并不是ts内置模块系统tsc编译的做法?(待确定研究)

这里只对ts内置的模块转换做研究,后续有时间可以再对bable的多模块系统交互转换做点研究和实践。

TS中相应的源码处理工具函数为:

export function __importStar(mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result.default = mod;
    return result;
}

export function __importDefault(mod) {
    return (mod && mod.__esModule) ? mod : { default: mod };
}

TypeScript 改善了 ES Module 与 CommonJS 的交互支持,但在实际项目还是用的比较少,tsc命令只有在node+ts项目中可能遇到,而且这种项目大家也有用ts-node来运行的,大多数项目,现在的主流ts+react,ts+vue等前端工程化项目,都是在可能的情况下使用 JavaScript 打包工具(webpack等中的一些loader和插件)处理可以避免不必要的运行时开销和不确定行为。

ES6的import函数

es6新增了动态引入模块的import()函数,传参和import关键字一样,只是是在运行时确定,可以再if语句中使用(import关键字不可以,因为在编译时(会被 JavaScript 引擎静态分析)就会先于其他代码执行,所以放在代码块中无意义,只能放在模块的顶层,而来就加入了import()函数,类似node的require函数,都是运行时确定,即什么时候运行到这一句,也会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。(import关键字有时候也叫"连接")。

import()就很像Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

import返回的是promise对象,即也可在await块中使用。