ES Module,commonjs和Typescript模块系统

本文探讨TypeScript如何兼容ESModule与CommonJS模块系统,包括TS的模块管理、与ESModule的融合,以及如何通过配置解决babel项目迁移至TS时的模块写法不兼容问题。

首先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块中使用。

在Node.js项目中混合使用ES Module(ECMAScript ModuleCommonJS模块是可行的,但需要根据项目配置Node.js版本进行适当处理。以下是几种实现混合使用的方法: ### 1. 使用Node.js内置支持(Node.js v12+) 从Node.js版本12开始,Node.js原生支持ES Modules,但需要通过文件扩展名`.mjs`来标识ESM文件,而CommonJS则继续使用`.js`扩展名。例如: ```javascript // cjs-file.js (CommonJS) exports.message = 'Hello from CommonJS'; ``` ```javascript // esm-file.mjs (ES Module) export const message = 'Hello from ES Module'; ``` 如果要在CommonJS文件中导入ES Module,则需要使用`import()`函数进行异步加载: ```javascript // cjs-file.js const { message } = await import('./esm-file.mjs'); console.log(message); // 输出: Hello from ES Module ``` 同样地,在ES Module中也可以通过`require`函数导入CommonJS模块,但这要求Node.js运行时环境支持这种互操作性: ```javascript // esm-file.mjs import fs from 'fs'; // 内置模块可以直接导入 const { message } = require('./cjs-file.js'); console.log(message); // 输出: Hello from CommonJS ``` ### 2. 使用构建工具或打包器(如Webpack, Rollup) 构建工具可以将不同类型的模块统一打包成适合部署的单个或多个bundle文件。这些工具通常能够处理混合模块类型,并且提供更灵活的配置选项。例如,在Webpack中,你可以在同一个项目中同时使用`.js`(默认为CommonJS`.mjs`文件,并通过配置来控制输出格式。 ### 3. Babel与TypeScript转换 利用Babel或TypeScript等转译工具,可以将ES6+代码转换为向后兼容的JavaScript版本,包括将ES Modules转换为CommonJS格式。这允许开发者编写现代ESM风格的代码,同时保持与旧版Node.js环境的兼容性。例如,使用Babel配置: ```json { "presets": ["@babel/preset-env"] } ``` 然后,在你的源码中你可以自由地使用`import`/`export`语法,Babel会将其转换为CommonJS形式以便于Node.js执行。 ### 4. 动态导入条件加载 对于某些特定场景,可能需要根据运行时条件动态决定加载哪种类型的模块。这种情况下可以利用`import()`表达式或者`require.resolve()`配合`require()`来实现按需加载。 ### 注意事项 - **Node.js版本**:确保使用的Node.js版本支持所需的ESM特性。 - **性能考量**:异步加载可能会引入额外的延迟,特别是在启动时频繁使用`import()`的情况下。 - **工具链支持**:检查所有依赖项及构建工具是否完全支持ESMCJS的混合使用。 通过以上方法,可以在同一个Node.js项目中有效地混合使用ES ModuleCommonJS模块,从而逐步迁移到ESM或维持现有CJS代码库的同时引入新特性[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值