JS / MJS / TS 文件区别与最佳实践

最后更新: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

分享

官方文档: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 包中。

// 导入
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 的推荐方式。

// 具名导入
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 核心差异对比

// ============ 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 字段决定:

// package.json

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

// type 为 "module":
// .js 文件使用 ESM
{
  "name": "my-package",
  "type": "module"
}
// 当 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

// 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 配置变更而改变文件行为
// 双格式 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

基本类型系统

// 基本类型
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())   // 安全
}

接口与类型别名

// 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

泛型

// 泛型函数
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
}

枚举

// 数字枚举
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,
}

类型断言与类型守卫

// 类型断言(告诉编译器"我知道这是什么类型")
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'
}

实用工具类型

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 核心参数

{
  "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. 编译后运行(生产)

# 安装 TypeScript
npm install -D typescript

# 编译
npx tsc

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

2. ts-node(开发时直接运行 .ts)

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(推荐,更快)

npm install -D tsx

# 运行(支持 ESM,无需额外配置)
npx tsx src/index.ts

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

4. Vite(前端项目)

Vite 原生支持 .ts 文件,无需额外配置,只做转译(不检查类型):

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 服务(现代)

// 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 包(双格式发布)

// 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"
      }
    }
  }
}
# 使用 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(不经过打包工具)

// 必须写完整扩展名
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)

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

tsconfig.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 库提供类型信息。

// 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 库手写声明
# 为没有类型的 JS 包安装类型
npm install -D @types/node
npm install -D @types/lodash

十一、最佳实践总结

1. 新项目统一选 TypeScript

# 前端(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

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true
  }
}
// 不好
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 字段

// 现代项目,明确表态
{
  "type": "module"
}

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

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

5. import 路径保持一致风格

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

// Node.js 原生 ESM:写 .js(指向 .ts 文件)
import { foo } from './utils.js'

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

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

7. 使用 tsup 简化包构建

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

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

1. require 在 ESM 中不可用

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

// 正确
import fs from 'fs'

2. __dirname 和 __filename 在 ESM 中不可用

// 错误: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

// 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 需要配置

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

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

5. ESM 包被 CJS 文件引用

// 纯 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 只做类型检查,不优化代码

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

7. ts-node 默认不支持 ESM

# 错误:默认 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)。tsxts-node --esm 会自动处理,但 tsc 原生输出不会。

// 正确:带 .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.jsonexports 字段取代 main/moduleexports 支持条件导出(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'Fooundefined 或空对象。
原因: 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完全指南
Vue3入门
Vite初级指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog