> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# JS / MJS / TS 文件区别与最佳实践
- URL: https://blog.vercanti.com/js-mjs-ts-wen-jian-qu-bie-yu-zui-jia-shi-jian/
- Published: 2026-08-28T14:35:15.000Z
- Updated: 2026-08-28T14:58:24.000Z
- Description: 最后更新：2026-03-05 JavaScript 生态中有三种主要文件扩展名，它们的核心区别体现在两个维度：模块系统和类型系统。 Node.js 最初的模块系统（2009年），同步加载，至今仍大量存在于旧项目和 npm 包中。 CJS 特点 ES2015（ES6）引入的官方标准模块系统，现为前端和 Node.js 的推荐方式。 ESM 特点 .js 的模块系统由 package.json 中的 type 字段决定： 同一项目中混用两种格式 .mjs 无论 package.json 如何配置，始终强制使用 ESM。 何时需要 .mjs 1. 在 "ty
- Author: yellowdog
- Tags: 前端开发

> 官方文档：<https://tc39.es/ecma262/#sec-modules>  
> 适用版本：Node.js 22 LTS / TypeScript 5.x（2026-05-08 核实）

最后更新：2026-03-05

---

## 一、文件扩展名本质

JavaScript 生态中有三种主要文件扩展名，它们的核心区别体现在两个维度：**模块系统**和**类型系统**。

| 扩展名  | 模块系统             | 类型系统  | 需要编译 | 适用场景          |
| ---- | ---------------- | ----- | ---- | ------------- |
| .js  | CJS 或 ESM（由配置决定） | 无     | 否    | 通用 JavaScript |
| .mjs | 强制 ESM           | 无     | 否    | 明确标记 ESM 的文件  |
| .cjs | 强制 CJS           | 无     | 否    | 明确标记 CJS 的文件  |
| .ts  | CJS 或 ESM（编译后决定） | 有静态类型 | 是    | 需要类型安全的项目     |
| .mts | 强制 ESM（编译为 .mjs） | 有静态类型 | 是    | TS + 强制 ESM   |
| .cts | 强制 CJS（编译为 .cjs） | 有静态类型 | 是    | TS + 强制 CJS   |

---

## 二、模块系统详解

### CommonJS（CJS）

Node.js 最初的模块系统（2009年），同步加载，至今仍大量存在于旧项目和 npm 包中。

```js
// 导入
const fs = require('fs')
const { join } = require('path')
const myModule = require('./myModule')

// 导出
module.exports = { foo, bar }       // 导出对象
module.exports = function() {}       // 导出函数
exports.foo = foo                    // 单个导出（等价于 module.exports.foo = foo）

```

**CJS 特点**

| 特点           | 说明                                |
| ------------ | --------------------------------- |
| 加载时机         | 同步，运行时（require() 执行到哪一行才加载）       |
| 缓存           | 首次 require 后结果被缓存，重复 require 返回缓存 |
| 循环依赖         | 可能得到不完整的导出对象                      |
| 动态导入         | 支持（require() 可在 if/函数内使用）         |
| Tree-shaking | 不支持（打包工具难以静态分析）                   |
| 顶层 await     | 不支持                               |
| 浏览器原生        | 不支持，需打包工具处理                       |

### ES Modules（ESM）

ES2015（ES6）引入的官方标准模块系统，现为前端和 Node.js 的推荐方式。

```js
// 具名导入
import { readFile, writeFile } from 'fs/promises'

// 默认导入
import express from 'express'

// 命名空间导入
import * as path from 'path'

// 同时导入默认和具名
import React, { useState, useEffect } from 'react'

// 动态导入（异步，返回 Promise）
const module = await import('./heavy-module.js')

// 具名导出
export function hello() {}
export const PI = 3.14
export class MyClass {}

// 默认导出
export default function main() {}

// 重新导出
export { foo } from './other.js'
export * from './other.js'
export { foo as bar } from './other.js'

```

**ESM 特点**

| 特点           | 说明                            |
| ------------ | ----------------------------- |
| 加载时机         | 异步，解析阶段（import 语句在代码执行前就被处理）  |
| 静态结构         | import/export 必须在顶层，不能在条件语句中  |
| 动态导入         | 支持（通过 import() 函数，返回 Promise） |
| Tree-shaking | 支持（打包工具可分析哪些导出被使用）            |
| 顶层 await     | 支持（可在模块顶层直接使用 await）          |
| 浏览器原生        | 支持（<script type="module">）    |
| 严格模式         | 自动启用                          |

### CJS vs ESM 核心差异对比

```js
// ============ CJS ============
// 运行时加载，可以在函数/条件中使用
function loadModule(name) {
    const mod = require(name)   // 合法
    return mod
}

// 可以修改导入的值
const obj = require('./obj')
obj.count = 100   // 可以（因为是值的拷贝）

// ============ ESM ============
// 静态导入必须在顶层
import { count } from './counter.js'   // 只能在顶层

// 绑定是实时的（live binding），不能直接重新赋值
count = 100   // TypeError: Assignment to constant variable

// 动态导入（ESM 版的 require）
async function loadModule(name) {
    const mod = await import(name)   // 合法，返回 Promise
    return mod
}

```

---

## 三、.js 文件

`.js` 的模块系统由 `package.json` 中的 `type` 字段决定：

```json
// package.json

// 未设置 type 或 type 为 "commonjs"（默认）：
// .js 文件使用 CommonJS
{
  "name": "my-package"
}

// type 为 "module"：
// .js 文件使用 ESM
{
  "name": "my-package",
  "type": "module"
}

```

```js
// 当 package.json "type": "commonjs"（或不设置）时，这是 CJS
const path = require('path')
module.exports = { path }

// 当 package.json "type": "module" 时，这是 ESM
import path from 'path'
export { path }

```

**同一项目中混用两种格式**

```
project/
├── package.json          ("type": "module")  → .js 默认 ESM
├── src/
│   ├── app.js            → ESM（遵循 package.json）
│   └── legacy.cjs        → 强制 CJS（无视 package.json）
└── config/
    └── webpack.config.cjs → 强制 CJS（webpack 配置通常需要 CJS）

```

---

## 四、.mjs 文件

`.mjs` 无论 `package.json` 如何配置，**始终强制使用 ESM**。

```js
// config.mjs —— 即使 package.json 没有 "type": "module"，这里也是 ESM
import { readFileSync } from 'fs'
export const config = JSON.parse(readFileSync('./config.json', 'utf-8'))

```

**何时需要 .mjs**

1. 在 `"type": "commonjs"` 的旧项目中，需要某个文件使用 ESM
2. 发布双格式 npm 包时（同时提供 ESM 和 CJS 版本）
3. 明确意图，避免因 `package.json` 配置变更而改变文件行为

```js
// 双格式 npm 包的典型结构
// package.json
{
  "main": "./dist/index.cjs",       // CJS 入口
  "module": "./dist/index.mjs",     // ESM 入口（打包工具识别）
  "exports": {
    ".": {
      "import": "./dist/index.mjs", // ESM（Node.js import 用）
      "require": "./dist/index.cjs" // CJS（Node.js require 用）
    }
  }
}

```

---

## 五、.ts 文件（TypeScript）

TypeScript 是 JavaScript 的超集，添加了静态类型系统。`.ts` 文件**不能直接运行**，需先编译为 `.js`。

### 基本类型系统

```ts
// 基本类型
let name: string = 'Alice'
let age: number = 25
let active: boolean = true
let data: null = null
let value: undefined = undefined

// 数组
let nums: number[] = [1, 2, 3]
let strs: Array<string> = ['a', 'b']

// 元组（固定长度和类型）
let pair: [string, number] = ['Alice', 25]

// 联合类型
let id: string | number = 'abc'

// 字面量类型
let direction: 'left' | 'right' | 'up' | 'down' = 'left'

// 任意类型（尽量避免）
let anything: any = 'hello'

// 未知类型（比 any 安全，使用前需类型收窄）
let unknown: unknown = getData()
if (typeof unknown === 'string') {
    console.log(unknown.toUpperCase())   // 安全
}

```

### 接口与类型别名

```ts
// interface：定义对象结构（可扩展）
interface User {
    id: number
    name: string
    email?: string          // 可选属性
    readonly createdAt: Date // 只读属性
}

// 接口继承
interface Admin extends User {
    role: 'super' | 'normal'
    permissions: string[]
}

// type：类型别名（更灵活，支持联合、交叉等）
type ID = string | number

type Point = {
    x: number
    y: number
}

// 交叉类型（合并多个类型）
type AdminUser = User & { role: string }

```

**interface vs type 选择**

| 场景            | 推荐                 |
| ------------- | ------------------ |
| 定义对象/类结构      | interface（可继承、可扩展） |
| 联合类型、元组       | type               |
| 交叉类型          | type               |
| 需要多次被 extends | interface          |
| 简单类型别名        | type               |

### 泛型

```ts
// 泛型函数
function identity<T>(arg: T): T {
    return arg
}

const result = identity<string>('hello')   // 显式指定
const result2 = identity(42)               // 类型推断为 number

// 泛型接口
interface ApiResponse<T> {
    data: T
    status: number
    message: string
}

// 使用
const userRes: ApiResponse<User> = {
    data: { id: 1, name: 'Alice' },
    status: 200,
    message: 'ok'
}

// 泛型约束
function getLength<T extends { length: number }>(arg: T): number {
    return arg.length
}

```

### 枚举

```ts
// 数字枚举
enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right,   // 3
}

// 字符串枚举（更推荐，可读性好）
enum Status {
    Active = 'ACTIVE',
    Inactive = 'INACTIVE',
    Pending = 'PENDING',
}

// const enum（编译后内联，无运行时对象）
const enum Flags {
    Read = 1,
    Write = 2,
    Execute = 4,
}

```

### 类型断言与类型守卫

```ts
// 类型断言（告诉编译器"我知道这是什么类型"）
const input = document.getElementById('username') as HTMLInputElement
const value = (input as HTMLInputElement).value

// 类型守卫（运行时收窄类型）
function process(value: string | number) {
    if (typeof value === 'string') {
        return value.toUpperCase()   // 这里 value 是 string
    }
    return value.toFixed(2)          // 这里 value 是 number
}

// instanceof 守卫
function handle(err: Error | string) {
    if (err instanceof Error) {
        console.log(err.message)
    } else {
        console.log(err)
    }
}

// 自定义类型守卫
function isUser(obj: any): obj is User {
    return typeof obj.id === 'number' && typeof obj.name === 'string'
}

```

### 实用工具类型

```ts
interface User {
    id: number
    name: string
    email: string
    age: number
}

// Partial<T>：所有属性变可选
type UpdateUser = Partial<User>
// { id?: number; name?: string; email?: string; age?: number }

// Required<T>：所有属性变必填
type StrictUser = Required<Partial<User>>

// Readonly<T>：所有属性只读
type FrozenUser = Readonly<User>

// Pick<T, K>：选取指定属性
type UserPreview = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// Omit<T, K>：排除指定属性
type PublicUser = Omit<User, 'email'>
// { id: number; name: string; age: number }

// Record<K, V>：键值映射类型
type UserMap = Record<string, User>

// Exclude<T, U>：从联合类型中排除
type OnlyString = Exclude<string | number | boolean, number | boolean>
// string

// Extract<T, U>：从联合类型中提取
type Numbers = Extract<string | number | boolean, number | boolean>
// number | boolean

// NonNullable<T>：排除 null 和 undefined
type SafeValue = NonNullable<string | null | undefined>
// string

// ReturnType<T>：获取函数返回值类型
function fetchUser(): Promise<User> { ... }
type FetchResult = ReturnType<typeof fetchUser>
// Promise<User>

// Parameters<T>：获取函数参数类型
type FetchParams = Parameters<typeof fetchUser>
// []

```

---

## 六、TypeScript 编译配置

### tsconfig.json 核心参数

```json
{
  "compilerOptions": {
    // 编译目标
    "target": "ES2022",         // 编译输出的 JS 版本
    "lib": ["ES2022", "DOM"],   // 包含的类型声明库

    // 模块系统
    "module": "ESNext",         // 输出的模块格式
    "moduleResolution": "bundler", // 模块解析策略（现代项目推荐）

    // 路径
    "rootDir": "./src",         // 源码根目录
    "outDir": "./dist",         // 编译输出目录
    "baseUrl": ".",             // 路径别名的基准目录
    "paths": {                  // 路径别名
      "@/*": ["src/*"]
    },

    // 严格模式（强烈推荐全部开启）
    "strict": true,             // 开启所有严格检查（等价于下面全开）
    "strictNullChecks": true,   // null/undefined 不能赋给其他类型
    "noImplicitAny": true,      // 禁止隐式 any
    "strictFunctionTypes": true,// 严格函数类型检查

    // 辅助检查
    "noUnusedLocals": true,     // 不允许未使用的局部变量
    "noUnusedParameters": true, // 不允许未使用的参数
    "noImplicitReturns": true,  // 函数所有分支必须有返回值
    "noFallthroughCasesInSwitch": true, // switch 不允许 fallthrough

    // 输出控制
    "declaration": true,        // 生成 .d.ts 类型声明文件
    "sourceMap": true,          // 生成 .js.map 调试映射
    "removeComments": false,    // 是否移除注释

    // 互操作
    "esModuleInterop": true,    // 允许 import CJS 模块（如 import fs from 'fs'）
    "allowSyntheticDefaultImports": true, // 允许无默认导出的模块使用默认导入
    "resolveJsonModule": true,  // 允许导入 .json 文件
    "skipLibCheck": true        // 跳过 .d.ts 文件的类型检查（加快编译）
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

```

**tsconfig.json 核心参数说明表**

| 参数               | 推荐值       | 说明                         |
| ---------------- | --------- | -------------------------- |
| target           | "ES2022"  | 编译产物的 JS 版本，越新特性越多但兼容性越低   |
| module           | "ESNext"  | 输出模块格式，Vite/打包工具项目用 ESNext |
| moduleResolution | "bundler" | 模块查找策略，现代打包工具项目用 bundler   |
| strict           | true      | 开启所有严格检查，新项目必开             |
| esModuleInterop  | true      | 兼容 CJS 默认导入，几乎必开           |
| skipLibCheck     | true      | 跳过第三方库类型检查，加快速度            |
| declaration      | true      | 发布 npm 包时需要生成 .d.ts        |
| sourceMap        | true      | 开发和调试时需要                   |

### module 选项详解

| 值          | 说明                                | 适用场景             |
| ---------- | --------------------------------- | ---------------- |
| "CommonJS" | 输出 CJS（require/module.exports）    | Node.js 旧项目      |
| "ESNext"   | 输出 ESM（import/export），保留动态 import | Vite/现代打包工具      |
| "NodeNext" | Node.js 原生 ESM，严格要求扩展名            | Node.js 项目不用打包工具 |
| "Preserve" | 保留输入格式不变（TS 5.4+）                 | 最新项目             |

### moduleResolution 选项详解

| 值          | 说明                        | 适用场景           |
| ---------- | ------------------------- | -------------- |
| "node"     | Node.js CJS 解析规则          | 旧项目兼容          |
| "bundler"  | 模拟打包工具（Vite/webpack）的解析行为 | 现代前端项目（推荐）     |
| "NodeNext" | Node.js ESM 解析，要求精确扩展名    | Node.js 原生 ESM |

---

## 七、运行 TypeScript 的方式

### 1\. 编译后运行（生产）

```bash
# 安装 TypeScript
npm install -D typescript

# 编译
npx tsc

# 运行编译产物
node dist/index.js

```

### 2\. ts-node（开发时直接运行 .ts）

```bash
npm install -D ts-node

# 运行
npx ts-node src/index.ts

# 带 ESM 支持
npx ts-node --esm src/index.ts

```

**ts-node 常用参数表**

| 参数                       | 说明             |
| ------------------------ | -------------- |
| \--esm                   | 启用 ESM 模式      |
| \--transpile-only        | 只转译不类型检查（更快）   |
| \--project tsconfig.json | 指定 tsconfig 路径 |
| \--skip-project          | 忽略 tsconfig    |

### 3\. tsx（推荐，更快）

```bash
npm install -D tsx

# 运行（支持 ESM，无需额外配置）
npx tsx src/index.ts

# 监听模式
npx tsx watch src/index.ts

```

### 4\. Vite（前端项目）

Vite 原生支持 `.ts` 文件，无需额外配置，只做转译（不检查类型）：

```bash
npm create vite@latest my-app -- --template vue-ts
cd my-app && npm install && npm run dev

```

---

## 八、文件扩展名决策指南

### 选择流程

```
你在写什么项目？
│
├── 前端项目（Vue/React）
│   └── 用 Vite/webpack 打包 → 统一用 .ts（或 .tsx/.vue）
│       package.json 的 type 字段无关紧要，打包工具处理一切
│
├── Node.js 项目（不打包）
│   ├── 新项目（Node 18+）
│   │   ├── 需要类型安全 → .ts + tsx 运行，或编译后运行
│   │   └── 不需要类型 → .js + package.json "type":"module"
│   │
│   └── 旧项目（维护中）
│       ├── 已有 "type":"commonjs" 或无 type
│       │   ├── 需要某文件用 ESM → 改名为 .mjs
│       │   └── 整体迁移 ESM → 加 "type":"module"，CJS 文件改 .cjs
│       └── 发布 npm 包（双格式）→ 构建产物用 .mjs 和 .cjs
│
└── 发布 npm 包
    └── 用 tsup/unbuild 构建，自动生成 .mjs + .cjs + .d.ts

```

### 不同项目类型的推荐配置

#### 前端（Vue 3 + Vite）

```
// package.json（type 字段不影响，Vite 接管所有）
{
  "type": "module"
}

// 文件全部用 .ts / .vue
src/
├── main.ts
├── App.vue
├── types/
│   └── index.ts
└── utils/
    └── http.ts

```

#### Node.js 服务（现代）

```json
// package.json
{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

```

```
src/
├── index.ts
├── routes/
│   └── user.ts
└── utils/
    └── db.ts

```

#### npm 包（双格式发布）

```json
// package.json
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    }
  }
}

```

```bash
# 使用 tsup 一键构建双格式
npm install -D tsup

# tsup.config.ts
import { defineConfig } from 'tsup'
export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  clean: true,
})

```

---

## 九、import 扩展名规则

这是从 CJS 迁移到 ESM 最容易踩坑的地方。

### Node.js 原生 ESM（不经过打包工具）

```js
// 必须写完整扩展名
import { foo } from './utils.js'      // 正确
import { foo } from './utils'          // 错误！Node.js 不会自动补全

// TypeScript 写 .js，编译器能找到对应的 .ts 文件
import { foo } from './utils.js'      // TS 文件里这样写，指向 utils.ts

```

### 通过打包工具（Vite/webpack/esbuild）

```js
// 打包工具会自动处理扩展名，可以省略
import { foo } from './utils'          // 正确（打包工具解析）
import { foo } from './utils.ts'       // 也正确
import { foo } from './utils.js'       // 也正确

```

### tsconfig.json 中的扩展名处理

```json
// 用于打包工具的 tsconfig
{
  "compilerOptions": {
    "moduleResolution": "bundler",     // 允许省略扩展名
    "allowImportingTsExtensions": true // 允许 import './foo.ts'（不输出 .js 时）
  }
}

// 用于 Node.js 原生 ESM 的 tsconfig
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext"     // 强制要求完整扩展名
  }
}

```

---

## 十、类型声明文件（.d.ts）

`.d.ts` 是纯类型声明文件，不包含任何运行时代码，用于为 JavaScript 库提供类型信息。

```ts
// mylib.d.ts —— 为 JS 库提供类型（不包含实现）
export declare function add(a: number, b: number): number

export declare interface Config {
    host: string
    port: number
    debug?: boolean
}

export declare class Client {
    constructor(config: Config)
    connect(): Promise<void>
    disconnect(): void
}

```

**常见 .d.ts 来源**

| 来源                               | 说明                                 |
| -------------------------------- | ---------------------------------- |
| TypeScript 编译（declaration: true） | 自动生成，与源码对应                         |
| @types/xxx 包                     | DefinitelyTyped 社区提供，如 @types/node |
| 库自带                              | 现代库通常随包附带 \*.d.ts                  |
| 手动编写                             | 给无类型的 JS 库手写声明                     |

```bash
# 为没有类型的 JS 包安装类型
npm install -D @types/node
npm install -D @types/lodash

```

---

## 十一、最佳实践总结

### 1\. 新项目统一选 TypeScript

```bash
# 前端（Vue）
npm create vite@latest my-app -- --template vue-ts

# 前端（React）
npm create vite@latest my-app -- --template react-ts

# Node.js
npm init -y
npm install -D typescript tsx @types/node
npx tsc --init

```

### 2\. 开启严格模式，不用 any

```json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true
  }
}

```

```ts
// 不好
function process(data: any) { ... }

// 好
function process(data: unknown) {
    if (typeof data === 'string') { ... }
}

// 或用泛型
function process<T extends Record<string, unknown>>(data: T) { ... }

```

### 3\. package.json 明确声明 type 字段

```json
// 现代项目，明确表态
{
  "type": "module"
}

```

### 4\. 避免在同一项目中混用 CJS 和 ESM

需要混用时，用扩展名明确区分（`.mjs`/`.cjs`），而不是依赖 `package.json` 的隐式行为。

### 5\. import 路径保持一致风格

```ts
// 打包工具项目：省略扩展名
import { foo } from './utils'

// Node.js 原生 ESM：写 .js（指向 .ts 文件）
import { foo } from './utils.js'

```

### 6\. 发布 npm 包提供完整的 exports 字段

```json
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  }
}

```

### 7\. 使用 tsup 简化包构建

```bash
# 一条命令构建 CJS + ESM + 类型声明
npx tsup src/index.ts --format cjs,esm --dts

```

---

## 十二、常见陷阱与注意事项

### 1\. require 在 ESM 中不可用

```js
// 错误：.mjs 文件或 "type":"module" 的 .js 中
const fs = require('fs')   // ReferenceError: require is not defined

// 正确
import fs from 'fs'

```

### 2\. \_\_dirname 和 \_\_filename 在 ESM 中不可用

```js
// 错误：ESM 中无 __dirname
console.log(__dirname)   // ReferenceError

// 正确：用 import.meta.url 替代
import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

```

### 3\. 动态 require 无法直接迁移到 ESM

```js
// CJS 中可以动态加载
const lang = 'zh'
const messages = require(`./locales/${lang}.json`)

// ESM 中需用动态 import（异步）
const lang = 'zh'
const messages = await import(`./locales/${lang}.json`, {
    assert: { type: 'json' }
})

```

### 4\. TypeScript 中导入 JSON 需要配置

```ts
// tsconfig.json 需要
{
  "compilerOptions": {
    "resolveJsonModule": true
  }
}

// 然后才能导入
import config from './config.json'
console.log(config.port)

```

### 5\. ESM 包被 CJS 文件引用

```js
// 纯 ESM 包（如 node-fetch v3、chalk v5）无法被 CJS require
const fetch = require('node-fetch')   // ERR_REQUIRE_ESM

// 解决方案一：用旧版本（提供 CJS）
// "node-fetch": "2.x"

// 解决方案二：动态 import（在 async 函数中）
const { default: fetch } = await import('node-fetch')

// 解决方案三：将自己的项目迁移到 ESM

```

### 6\. tsc 只做类型检查，不优化代码

```bash
# 如果需要打包/压缩，tsc 单独不够
# 配合 esbuild/rollup/webpack 使用，或直接用 tsup/Vite

```

### 7\. ts-node 默认不支持 ESM

```bash
# 错误：默认 ts-node 遇到 ESM 会报错
ts-node src/index.ts   # SyntaxError: Cannot use import statement

# 正确方案一：用 --esm 标志
ts-node --esm src/index.ts

# 正确方案二：改用 tsx（推荐）
tsx src/index.ts

```

---

## 最佳实践

**新项目全面使用 ESM，不混用 CJS**：在 `package.json` 声明 `"type": "module"` 后，所有 `.js` 文件均按 ESM 解析；只在必须兼容老依赖时用 `.cjs` 扩展名隔离 CJS 代码，避免两套模块系统并存导致的运行时错误。

**TypeScript 项目的导入路径带 `.js` 后缀**：`tsc` 输出 ESM 时不会改写扩展名，源码中写 `import { foo } from './foo.js'` 才能在 Node.js ESM 模式下正常解析（即使源文件是 `.ts`）。`tsx` 和 `ts-node --esm` 会自动处理，但 tsc 原生输出不会。

```ts
// 正确：带 .js 后缀（tsc 输出后 Node 能找到）
import { parse } from './parser.js'

// 错误：不带后缀，Node ESM 下找不到模块
import { parse } from './parser'

```

**动态 `import()` 做代码分割而非条件加载**：条件加载（`if (flag) require('x')`）在 ESM 中无效且破坏静态分析。用 `const mod = await import('./heavy')` 配合顶层 `await` 或异步函数，bundler 才能识别为分割点并生成独立 chunk。

**`package.json` 的 `exports` 字段取代 `main`/`module`**：`exports` 支持条件导出（`import`/`require`/`browser`/`node`），是发布双模式（ESM+CJS）库的现代标准。旧的 `main`/`module` 字段继续兼容旧版 bundler，但新项目应优先用 `exports`。

**避免在模块顶层执行副作用**：模块只要被 `import`（即使只用到其中一个导出），顶层代码就全部执行。数据库连接、`console.log`、全局状态初始化放在导出函数内部，让调用方显式触发，方便 tree-shaking 和测试隔离。

---

## 常见陷阱

### 陷阱：`require is not defined` — CJS 代码在 ESM 模块中使用

**现象：** 给项目加了 `"type": "module"` 后，某些老代码或第三方库报 `ReferenceError: require is not defined`。  
**原因：** `"type": "module"` 让所有 `.js` 文件按 ESM 解析，而 `require`/`module.exports` 是 CJS 专属，ESM 中不存在。  
**解决：** 把需要用 `require` 的文件改为 `.cjs` 扩展名（Node.js 始终将其解析为 CJS）；或用 `import()` 动态导入替换 `require()`；对于无法修改的第三方库，用 `createRequire(import.meta.url)` 创建 CJS-compatible `require`。

### 陷阱：命名导出与默认导出混用导致 `undefined`

**现象：** `import Foo from './foo'` 后 `Foo` 是 `undefined` 或空对象。  
**原因：** `foo.ts` 只有命名导出（`export const Foo = ...`）没有默认导出，但调用方用了默认导入语法。  
**解决：** 命名导出用 `import { Foo } from './foo'`；或在 `foo.ts` 中补 `export default Foo`。库的 CJS 互操作（`esModuleInterop: true`）会将 `module.exports` 整体作为默认导出，但不影响 ESM 原生模块。

### 陷阱：循环依赖导致导入值为 `undefined`

**现象：** 模块 A 导入模块 B 的值，B 也导入 A 的值；运行时其中一个值是 `undefined`。  
**原因：** ESM 的循环依赖会创建"活绑定"（live binding），但若被引用的绑定在初始化时尚未赋值（函数声明提升但 `const`/`let` 不提升），读取到的就是 `undefined`。  
**解决：** 重构代码消除循环依赖；把共享的常量/类型提取到第三个文件；如果必须循环，确保被引用的值是函数声明（会提升）而非 `const`/`let`。

---

## 参见

[TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)  
[Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)  
[Vite初级指南](https://blog.vercanti.com/vite-chu-ji-zhi-nan/)