Skip to content
第 106 / 250 章前端⏱ 10 分钟阅读

第 106 章:模块系统(Modules)

学习目标

  • 掌握 ES Module 与 CommonJS 的区别
  • 学会 TS 中的 import/export 语法
  • 理解模块解析策略与路径映射
  • 掌握类型导入导出与模块声明

一、什么是模块

模块是独立的作用域,每个文件就是一个模块。

typescript
// user.ts
const name = 'Tom';
export { name };

// app.ts
import { name } from './user';
console.log(name);

二、模块系统对比

2.1 ES Module(推荐)

typescript
// 导出
export const a = 1;
export function foo() {}
export interface User { name: string }

// 默认导出
export default class UserService {}

// 导入
import UserService, { a, foo, User } from './module';

2.2 CommonJS(Node.js)

typescript
// 导出
module.exports = { a: 1, foo: function() {} };
exports.a = 1;

// 导入
const { a, foo } = require('./module');

2.3 对比

特性ES ModuleCommonJS
语法import / exportrequire / module.exports
加载异步、静态同步、动态
顶层 await✅ 支持❌ 不支持
Tree-shaking✅ 友好❌ 不友好
浏览器✅ 原生❌ 需打包
Node.js✅ 14+✅ 全部

三、export 的各种写法

3.1 命名导出

typescript
// 写法 1:逐个导出
export const a = 1;
export function foo() {}
export interface User { name: string }

// 写法 2:统一导出
const a = 1;
function foo() {}
interface User { name: string }
export { a, foo, User };

// 写法 3:重命名导出
const internal = 'hello';
export { internal as publicName };

3.2 默认导出

typescript
// 一个模块只能有一个默认导出
export default class UserService {
  // ...
}

// 等价于
class UserService { /* ... */ }
export default UserService;

3.3 混合导出

typescript
// 同时有命名导出和默认导出
export const VERSION = '1.0.0';
export default class App {}

3.4 Re-export(聚合)

typescript
// utils/index.ts 聚合多个文件
export * from './string';
export * from './number';
export { default as Date } from './date';

四、import 的各种写法

4.1 基本导入

typescript
// 导入命名导出
import { a, foo } from './module';

// 导入默认导出
import UserService from './module';

// 导入全部
import * as Module from './module';

// 混合
import UserService, { a, foo } from './module';

4.2 重命名导入

typescript
import { a as alias, foo as fn } from './module';

4.3 副作用导入

typescript
// 只执行模块,不导入任何值
import './polyfill';

4.4 动态导入

typescript
// 按需加载
async function loadModule() {
  const module = await import('./heavy-module');
  module.doSomething();
}

// 条件加载
if (condition) {
  import('./module-a');
} else {
  import('./module-b');
}

五、类型导入导出

5.1 单独导入类型

typescript
// ✅ 用 import type
import type { User } from './types';
import { type User, fetchUser } from './api';  // 混合导入

// 优势:
// 1. 编译后被完全移除,不会进入运行时
// 2. 某些场景下必须用(isolatedModules)

5.2 单独导出类型

typescript
// ✅ 用 export type
export type { User, Role };
export { type User, fetchUser };  // 混合导出

5.3 实战:清晰的类型/值分离

typescript
// types.ts
export interface User {
  id: number;
  name: string;
}

export type Status = 'active' | 'inactive';

// api.ts
import type { User, Status } from './types';

export async function fetchUser(id: number): Promise<User> {
  // ...
}

export async function updateStatus(id: number, status: Status): Promise<void> {
  // ...
}

六、模块解析

TS 解析模块路径时,按顺序尝试不同的扩展名和文件。

6.1 相对路径

typescript
import { foo } from './module';      // 当前目录
import { bar } from '../parent/module';  // 上级目录

6.2 非相对路径

typescript
import React from 'react';           // node_modules
import { Button } from '@/components';  // 路径别名

6.3 TS 解析策略

json
// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node"  // 或 "node16" / "bundler" / "classic"
  }
}
策略适用场景
node传统 Node.js 项目
node16Node.js 16+,支持 ESM
bundlerVite / Webpack 5+ 等现代打包器
classicTS 0.6 时代,基本不用

七、路径别名(Path Mapping)

json
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}
typescript
// 使用
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';

提示

路径别名只是 TS 解析用,运行时需要打包器(Vite/Webpack)支持。

八、模块声明(Declaration)

8.1 声明第三方模块

typescript
// 没有类型定义的模块
declare module 'my-lib' {
  export function foo(): void;
  export const bar: number;
}

8.2 声明全局模块

typescript
// 把模块扩展为全局
declare module '*.css' {
  const content: { [className: string]: string };
  export default content;
}

declare module '*.svg' {
  const src: string;
  export default src;
}

// 使用
import logo from './logo.svg';  // 类型: string

8.3 声明全局变量

typescript
// 全局变量
declare const __APP_VERSION__: string;
declare const __DEV__: boolean;

// 使用
console.log(__APP_VERSION__);  // 类型: string

8.4 扩展已有模块

typescript
// 扩展 React 模块
declare module 'react' {
  interface ComponentProps {
    customProp?: string;
  }
}

九、CommonJS 互操作

9.1 导入 CommonJS 模块

typescript
// 第三方 CommonJS 模块
import * as fs from 'fs';
// 或
import fs from 'fs';

// 启用 esModuleInterop 后:
import fs from 'fs';  // 直接 default import
json
// tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,  // 启用互操作
    "allowSyntheticDefaultImports": true
  }
}

9.2 启用严格 ESM

json
// package.json
{
  "type": "module"  // 所有 .js 当作 ESM
}

十、namespace(命名空间)

提示

namespace 是 TS 早期为内部模块设计的,现代项目优先用 ES Module

typescript
// 旧式命名空间
namespace Utils {
  export function formatDate(d: Date) { /* ... */ }
  export class StringHelper { /* ... */ }
}

// 使用
Utils.formatDate(new Date());

// 嵌套
namespace Outer {
  export namespace Inner {
    export const foo = 1;
  }
}

Outer.Inner.foo;

namespace vs module

特性namespacemodule
文件通常单文件一个文件即一个模块
现代项目❌ 不推荐✅ 推荐
适用场景全局类型扩展业务代码组织

十一、实战:项目模块组织

src/
├── api/             # API 请求
│   ├── user.ts
│   └── index.ts     # 聚合导出
├── components/      # 公共组件
├── types/           # 全局类型
│   ├── user.ts
│   └── index.ts
├── utils/           # 工具函数
├── hooks/           # 自定义 hooks
└── main.ts          # 入口
typescript
// types/index.ts
export * from './user';
export * from './common';

// api/index.ts
export * from './user';
typescript
// main.ts
import { User, fetchUser } from './api';
import type { Status } from './types';

十二、tsconfig 模块相关配置

json
{
  "compilerOptions": {
    "module": "ESNext",           // 输出模块格式
    "moduleResolution": "Bundler", // 解析策略
    "esModuleInterop": true,       // 互操作
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,     // 允许导入 JSON
    "isolatedModules": true,       // 强制每个文件独立编译
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

十三、常见错误

13.1 类型被当成值

typescript
// ❌ 错误:把类型当值导入
import { User } from './types';
const u = new User();  // ❌ User 是类型,不是值

// ✅ 解决 1:用 import type
import type { User } from './types';

// ✅ 解决 2:类型可以这样用
const u: User = { id: 1, name: 'Tom' };

13.2 循环依赖

typescript
// a.ts
import { b } from './b';
export const a = b + 1;

// b.ts
import { a } from './a';  // ❌ a 可能是 undefined
export const b = 1;

// 解决:重构代码,或者提取公共部分

13.3 路径别名在测试中失效

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
});

十四、本章小结

要点关键
模块系统ES Module(推荐) / CommonJS
export命名、默认、Re-export
import命名、默认、重命名、动态、副作用
类型导入import type / export type
路径解析moduleResolution + paths
模块声明declare module
互操作esModuleInterop: true
现代项目优先 ES Module,不用 namespace

动手练习

  1. 聚合导出:创建一个 utils/index.ts,聚合 string / number / date 三个工具文件
  2. 路径别名:配置 tsconfig.json,让 @/ 指向 src/
  3. 类型导入:把项目中的类型导入都改成 import type
  4. 模块声明:声明 *.svg*.css 模块的类型

推荐阅读


下一章第 107 章:命名空间(Namespace)

本站基于 VitePress 构建 · 由 Codebook 团队维护