说说你对 TypeScript 中模块的理解?
问题解析
模块是现代 TypeScript/JavaScript 开发的核心概念,考察候选人对 ES Module、CommonJS、模块解析策略以及模块声明的理解。
核心概念
- 模块(Module):具有独立作用域的代码单元
- ES Module:ES6 标准的模块系统(
import/export) - CommonJS:Node.js 的模块系统(
require/module.exports) - AMD/UMD:浏览器端模块规范
- 模块解析:编译器如何查找模块
- 声明文件:为无类型模块提供类型定义
详细解答
1. 模块是什么
TypeScript 与 ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。
相反地,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的。
// 1.ts - 没有 export,全局作用域
const a = 1;
// 2.ts - 会报错:重复声明 "a"
const a = 1;
解决方式:添加 export 使其成为模块
// 1.ts
export const a = 1;
// 2.ts
export const a = 1; // ok,模块作用域隔离
2. 模块的导出
2.1 命名导出
// math.ts
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export class Calculator {
multiply(a: number, b: number): number {
return a * b;
}
}
// 或者统一导出
const PI = 3.14159;
function add(a: number, b: number): number {
return a + b;
}
export { PI, add };
2.2 默认导出
// utils.ts
export default function greet(name: string): string {
return `Hello, ${name}!`;
}
// 一个模块只能有一个默认导出
const config = { apiUrl: 'https://api.example.com' };
export default config;
2.3 类型导出
// types.ts
export type User = {
id: number;
name: string;
};
export interface Product {
sku: string;
price: number;
}
// 重新导出
export { User as UserType } from './user';
3. 模块的导入
3.1 命名导入
import { PI, add, Calculator } from './math';
console.log(PI);
console.log(add(1, 2));
const calc = new Calculator();
3.2 默认导入
import greet from './utils';
// 或者
import greetFn from './utils';
console.log(greet('World'));
3.3 命名空间导入
import * as math from './math';
console.log(math.PI);
console.log(math.add(1, 2));
3.4 混合导入
import React, { useState, useEffect } from 'react';
3.5 仅类型导入(TypeScript 3.8+)
import type { User, Product } from './types';
// 或
import { type User, type Product } from './types';
仅类型导入在编译后会被完全移除,不会产生运行时依赖。
4. 模块解析策略
TypeScript 提供了两种模块解析策略:
4.1 Classic(经典)
TypeScript 原始的解析策略,现在很少使用。
4.2 Node(Node.js)
与 Node.js 的模块解析行为一致,是默认策略。
import { a } from './module';
// 查找顺序:
// 1. ./module.ts
// 2. ./module.tsx
// 3. ./module.d.ts
// 4. ./module/package.json("types" 字段)
// 5. ./module/index.ts
import { b } from 'lodash';
// 查找顺序:
// 1. node_modules/lodash.ts
// 2. node_modules/lodash.tsx
// 3. node_modules/lodash.d.ts
// 4. node_modules/lodash/package.json("types" 字段)
// 5. node_modules/@types/lodash.d.ts
// 6. node_modules/lodash/index.ts
5. 模块格式与编译目标
TypeScript 可以编译为不同的模块格式:
{
"compilerOptions": {
"module": "ESNext", // ES Module
// "module": "CommonJS", // CommonJS
// "module": "AMD", // AMD
// "module": "UMD", // UMD
// "module": "System", // SystemJS
"target": "ES2020"
}
}
6. 声明文件(.d.ts)
为无类型的 JavaScript 库提供类型定义:
// lodash.d.ts
declare module 'lodash' {
export function chunk<T>(array: T[], size?: number): T[][];
export function compact<T>(array: (T | null | undefined | false | 0 | "")[]): T[];
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait?: number,
options?: { leading?: boolean; trailing?: boolean }
): T;
}
7. 全局声明
扩展全局对象或模块:
// 扩展全局 Window 对象
declare global {
interface Window {
myLib: any;
API_BASE_URL: string;
}
}
// 扩展现有模块
declare module 'vue' {
export interface ComponentCustomProperties {
$http: AxiosInstance;
}
}
// 声明非代码模块
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}
深入理解
ES Module vs CommonJS
| 特性 | ES Module | CommonJS |
|---|---|---|
| 语法 | import/export |
require/module.exports |
| 加载时机 | 编译时静态分析 | 运行时动态加载 |
| 循环依赖 | 支持 | 部分支持 |
| 浏览器支持 | 原生支持 | 需要打包工具 |
| Tree Shaking | 支持 | 不支持 |
动态导入
// 静态导入(编译时确定)
import { add } from './math';
// 动态导入(运行时确定)
async function loadModule() {
const math = await import('./math');
console.log(math.add(1, 2));
}
// 条件导入
if (condition) {
const { feature } = await import('./feature');
feature.init();
}
路径别名
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';
最佳实践
- 优先使用 ES Module:现代浏览器和 Node.js 都支持
- 显式类型导入:使用
import type区分类型和值的导入 - 避免循环依赖:重构代码结构,使用接口或事件解耦
- 路径别名:简化模块导入路径
- ** barrels(索引文件)**:组织模块导出
// components/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Select } from './Select';
// 使用
import { Button, Input, Select } from './components';
面试要点
- 模块定义:包含顶级
import/export的文件 - 导出方式:命名导出、默认导出、类型导出
- 导入方式:命名导入、默认导入、命名空间导入、仅类型导入
- 模块解析:Node 策略,查找顺序
- 声明文件:
.d.ts为 JS 库提供类型 - 全局扩展:
declare global、declare module - ESM vs CJS:静态 vs 动态、编译时 vs 运行时
- 动态导入:
import()函数,支持代码分割