基础定义
JavaScript 原生后缀(Node.js 层面)
Node.js 提供三种原生 JS 文件后缀,对应不同的模块系统:
| 后缀 | 模块系统 | 对应 TS 后缀 | Node.js 默认行为 |
|---|---|---|---|
.js | 跟随 package.json 的 type 字段配置 | .ts | "type": "module" 时为 ESM,否则为 CJS |
.mjs | 强制 ES Module (ESM) | .mts | 无论 package.json 配置如何,始终作为 ESM 加载 |
.cjs | 强制 CommonJS (CJS) | .cts | 无论 package.json 配置如何,始终作为 CommonJS 加载 |
TypeScript 扩展后缀
TypeScript 在 JS 基础上提供对应的类型文件后缀:
| 后缀 | 模块系统 | 编译输出 | Node.js 默认行为 |
|---|---|---|---|
.ts | 跟随 tsconfig.json 配置的 module 选项 | .js | 根据 package.json 的 type 字段决定 |
.mts | 强制 ES Module (ESM) | .mjs | 始终作为 ES Module 加载 |
.cts | 强制 CommonJS (CJS) | .cjs | 始终作为 CommonJS 加载 |
核心区别与使用场景
JavaScript 后缀详解
1. .js - 默认 JS 后缀
- 适用场景:绝大多数普通 JS 项目、不需要明确区分模块系统的代码
- 行为特点:
- 模块系统由 package.json 的
type字段决定:"type": "module"→ 按 ESM 加载- 无
type字段或"type": "commonjs"→ 按 CJS 加载
- 最灵活,适合大部分开发场景
- 模块系统由 package.json 的
2. .mjs - 强制 ESM 后缀
- 适用场景:
- 需要明确使用 ES Module 的 Node.js 库/工具
- 在 CJS 为主的项目中需要使用 ESM 语法的单个文件
- 双发布包的 ESM 入口
- 行为特点:
- 始终按 ESM 加载,不受 package.json
type字段影响 - 支持
import/export、import.meta、顶层await等 ESM 特性 - 不支持
require()、module.exports、__dirname等 CJS 特性 - 文件必须使用 ESM 语法
- 始终按 ESM 加载,不受 package.json
3. .cjs - 强制 CJS 后缀
- 适用场景:
- 需要明确使用 CommonJS 的 Node.js 库/工具
- 在 ESM 为主的项目中需要使用 CJS 语法的单个文件
- 双发布包的 CJS 入口
- 与旧版 Node.js 项目兼容的代码
- 行为特点:
- 始终按 CJS 加载,不受 package.json
type字段影响 - 支持
require()、module.exports、__dirname、__filename等 CJS 特性 - 不支持顶层
await、import.meta等 ESM 特性 - 文件必须使用 CJS 语法
- 始终按 CJS 加载,不受 package.json
TypeScript 后缀详解
1. .ts - 默认 TS 后缀
- 适用场景:绝大多数普通项目、前端项目、不需要明确区分模块系统的代码
- 行为特点:
- 编译输出格式完全由
tsconfig.json的module和moduleResolution决定 - 在 Node.js 环境中,若 package.json 包含
"type": "module"则按 ESM 加载,否则按 CJS 加载 - 最灵活,适合大部分开发场景,无需刻意指定后缀
- 编译输出格式完全由
2. .mts - 强制 ES Module
- 适用场景:
- 需要明确使用 ES Module 的 Node.js 库/工具
- 同时支持 ESM 和 CJS 的双发布包的 ESM 入口
- 使用
import/export语法且需要和.mjs文件互操作的场景
- 行为特点:
- 编译后固定输出
.mjs文件 - 无论 package.json 配置如何,Node.js 始终按 ESM 加载
- 可以直接使用
import.meta、顶层await等 ESM 专属特性 - 不支持
require()、module.exports等 CommonJS 语法
- 编译后固定输出
3. .cts - 强制 CommonJS
- 适用场景:
- 需要明确使用 CommonJS 的 Node.js 库/工具
- 同时支持 ESM 和 CJS 的双发布包的 CJS 入口
- 与旧版 Node.js 项目兼容的代码
- 行为特点:
- 编译后固定输出
.cjs文件 - 无论 package.json 配置如何,Node.js 始终按 CommonJS 加载
- 支持
require()、module.exports、__dirname、__filename等 CommonJS 专属特性 - 不支持顶层
await
- 编译后固定输出
编译配置说明
tsconfig.json 关键配置
{
"compilerOptions": {
// 输出模块格式,.mts/.cts 会忽略此配置强制对应格式
"module": "NodeNext", // 或 ESNext / CommonJS
"moduleResolution": "NodeNext", // 推荐用于混合模块场景
"target": "ES2022",
// 输出目录
"outDir": "./dist",
// 类型声明输出
"declaration": true,
"declarationMap": true
}
}双模块发布最佳实践
对于需要同时支持 ESM 和 CJS 的 npm 包,推荐目录结构:
src/
├── index.mts # ESM 入口
├── index.cts # CJS 入口
└── core/
├── shared.ts # 通用代码
├── esm-utils.mts # ESM 专属工具
└── cjs-utils.cts # CJS 专属工具
对应的 package.json 配置:
{
"name": "your-package",
"version": "1.0.0",
"type": "module", // 包默认使用 ESM
// 入口配置
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
// 兼容旧版 Node.js
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}常见问题与注意事项
1. 模块导入导出匹配
- 在
.mts文件中只能import其他 ESM 文件(.mts/.ts当配置为 ESM 时) - 在
.cts文件中只能require其他 CommonJS 文件(.cts/.ts当配置为 CJS 时) - 若要在 ESM 中导入 CJS 模块,需要使用
import module from 'cjs-module'语法,且该模块需支持默认导出
2. 类型声明文件
.mts编译后会生成对应的.d.mts类型声明文件.cts编译后会生成对应的.d.cts类型声明文件- TypeScript 会自动处理不同后缀的类型导入导出匹配
3. 工具链兼容性
- 大部分现代打包工具(Vite、Rollup、Webpack 5+)都已支持
.mts/.cts后缀 - Node.js 14.13.0+ 开始原生支持
.mjs/.cjs,对应 TypeScript 4.5+ 开始支持.mts/.cts - 旧版工具可能需要额外配置才能识别新后缀
4. 什么时候不需要用特殊后缀?
- 纯前端项目(浏览器环境)不需要区分,统一用
.ts/.js即可 - 只针对单一模块系统的项目,无需刻意使用
.mts/.cts/.mjs/.cjs - 使用
ts-node、bun、deno等运行时,默认配置下都能正确处理.ts/.js文件的模块系统
相关链接
- tsconfig-json — tsconfig.json 配置详解
- swc — Rust 实现的 TypeScript 编译器,支持所有后缀格式