第四部分:赋予网页健壮的灵魂 —— TypeScript(中)
目录
- 4 类与面向对象:构建复杂的组件
- 4.1 类的定义与成员
- 4.2 继承 (Inheritance)
- 4.3 接口实现 (Implements)
- 4.4 抽象类 (Abstract Class)
- 4.5 静态成员 (Static Members)
- 5 更高级的类型:让类型系统更灵活
- 5.1 联合类型 (`|`)
- 5.2 交叉类型 (`&`)
- 5.3 字面量类型 (Literal Types)
- 5.4 枚举 (`enum`)
- 5.5 `any` vs `unknown`
- 5.6 `void` 和 `never`
- 5.7 类型断言 (`as` 或 `<Type>`)
- 5.8 练习
- 6 模块化与工具链:组织代码和提升效率
- 6.1 模块 (`import` 和 `export`)
- 6.2 编译模块化的 TypeScript 代码
- 6.3 模块打包工具 (Bundlers)
- 6.4 练习
4 类与面向对象:构建复杂的组件
在复杂的智能家居系统中,我们不会把所有功能都写在一起,而是会设计独立的模块,比如照明模块、安防模块、娱乐模块等。每个模块有自己的内部工作原理(属性和方法),对外提供特定的操作接口。面向对象编程 (OOP) 和 TypeScript 的类就是用来实现这种模块化设计的强大工具。
类 (Class) 是创建对象的蓝图或模板。它封装了数据(属性)和操作数据的方法。TypeScript 为 JavaScript 的类添加了类型系统和更强的可见性控制。
4.1 类的定义与成员
// 04.ts// 定义一个表示灯的类
class Light {// 属性 (成员变量)// 默认是 public,表示可以在类的外部访问brightness: number;// private 成员只能在类内部访问,就像灯内部的电路细节不对外暴露private isOn: boolean;// protected 成员可以在类内部和其子类中访问,就像一个家族内部的秘密配方protected maxBrightness: number = 100;// 只读属性,只能在构造函数中初始化readonly id: string;// 构造函数:在创建对象时执行,用于初始化属性constructor(initialBrightness: number = 0, id: string) {this.brightness = initialBrightness;this.isOn = initialBrightness > 0;this.id = id;console.log(`灯 ${this.id} 已创建,初始亮度: ${this.brightness}`);}// 方法 (成员函数)// public 方法,可以在类的外部调用turnOn(): void {if (!this.isOn) {this.isOn = true;console.log(`灯 ${this.id} 打开了.`);this.brightness = 50; // 打开时设置一个默认亮度}}turnOff(): void {if (this.isOn) {this.isOn = false;console.log(`灯 ${this.id} 关闭了.`);this.brightness = 0;}}// private 方法,只能在类内部调用private checkStatus(): void {console.log(`灯 ${this.id} 当前状态: ${this.isOn ? '开' : '关'}`);}// public 方法,内部调用 private 方法reportStatus(): void {this.checkStatus();console.log(`当前亮度: ${this.brightness}`);}// 访问 protected 属性get maxLevel(): number {return this.maxBrightness;}
}// 创建 Light 对象 (实例化)
const livingRoomLight = new Light(0, "LR1");
const kitchenLight = new Light(30, "K1");livingRoomLight.turnOn();
livingRoomLight.reportStatus(); // 调用 public 方法,内部使用了 private 方法// 尝试访问 private 或 protected 成员会报错
// console.log(livingRoomLight.isOn); // 编译时报错:Property 'isOn' is private and only accessible within class 'Light'.
// console.log(livingRoomLight.maxBrightness); // 编译时报错:Property 'maxBrightness' is protected and only accessible within class 'Light' and its subclasses.// 尝试修改 readonly 属性会报错 (在构造函数外)
// livingRoomLight.id = "new-id"; // 编译时报错:Cannot assign to 'id' because it is a read-only property.console.log("客厅灯最大亮度:", livingRoomLight.maxLevel); // 通过 public getter 访问 protected 属性
4.2 继承 (Inheritance)
子类可以继承父类的属性和方法,并可以添加自己的新属性或方法,或者覆盖父类的方法。这就像在现有的基础灯具(父类)上,开发出更高级的智能灯具(子类),它保留了基本功能,但也增加了颜色调节、定时开关等新功能。
// 04.ts (接着上面的代码)// 定义一个继承自 Light 的智能灯类
class SmartLight extends Light {color: string;constructor(initialBrightness: number = 0, id: string, initialColor: string = "白色") {// 调用父类的构造函数,必须是子类构造函数中的第一行super(initialBrightness, id);this.color = initialColor;console.log(`智能灯 ${this.id} 已创建,颜色: ${this.color}`);}// 子类可以添加新的方法setColor(color: string): void {this.color = color;console.log(`灯 ${this.id} 颜色设置为: ${this.color}`);}// 子类可以覆盖父类的方法 (方法重写)reportStatus(): void {// 可以调用父类的同名方法super.reportStatus();console.log(`当前颜色: ${this.color}`);// 子类可以访问父类的 protected 属性console.log(`智能灯最大亮度限制: ${this.maxBrightness}`);}// 尝试访问 private 成员会报错// checkStatus() // 编译时报错:Property 'checkStatus' is private and only accessible within class 'Light'.
}const bedroomSmartLight = new SmartLight(10, "BR1", "蓝色");
bedroomSmartLight.turnOn();
bedroomSmartLight.setColor("红色");
bedroomSmartLight.reportStatus(); // 调用子类重写后的方法
4.3 接口实现 (Implements)
接口不仅可以描述对象的形状,还可以用来约束类的行为。一个类可以声明它“实现”了某个接口,这意味着这个类必须提供接口中定义的所有属性和方法。这就像一个智能设备必须遵循某个行业标准(接口),才能接入到整个智能家居系统中。
// 04.ts (接着上面的代码)// 定义一个接口,描述可以控制电源的设备
interface PowerControllable {powerOn(): void;powerOff(): void;isOn(): boolean;
}// 定义一个表示智能插座的类,实现 PowerControllable 接口
class SmartOutlet implements PowerControllable {private deviceName: string;private status: boolean;constructor(name: string) {this.deviceName = name;this.status = false; // 默认关闭}powerOn(): void {if (!this.status) {this.status = true;console.log(`${this.deviceName} 已通电.`);}}powerOff(): void {if (this.status) {this.status = false;console.log(`${this.deviceName} 已断电.`);}}isOn(): boolean {return this.status;}// 类可以有接口之外的其他成员getDeviceName(): string {return this.deviceName;}
}const deskFanOutlet = new SmartOutlet("台扇插座");
deskFanOutlet.powerOn();
console.log(`${deskFanOutlet.getDeviceName()} 状态: ${deskFanOutlet.isOn() ? '开' : '关'}`);
deskFanOutlet.powerOff();// 一个类可以实现多个接口
interface SettableTimer {setTimer(minutes: number): void;
}// 假设有一个既可以控制电源,又可以设置定时的智能设备
class SmartDevice implements PowerControllable, SettableTimer {// ... 实现 PowerControllable 和 SettableTimer 的所有方法deviceName: string;status: boolean;timerMinutes: number = 0;constructor(name: string) {this.deviceName = name;this.status = false;}powerOn(): void { /* ... */ console.log(`${this.deviceName} 通电`); this.status = true; }powerOff(): void { /* ... */ console.log(`${this.deviceName} 断电`); this.status = false; }isOn(): boolean { return this.status; }setTimer(minutes: number): void {this.timerMinutes = minutes;console.log(`${this.deviceName} 设置定时 ${minutes} 分钟`);}
}
4.4 抽象类 (Abstract Class)
抽象类不能直接实例化,只能作为其他类的父类。它可能包含抽象方法(只有方法签名,没有实现)和具体方法(有实现)。子类必须实现父类中的所有抽象方法。这就像一个“通用电器”的设计概念,它定义了所有电器都应该有的基本操作(比如打开/关闭),但具体如何实现这些操作(比如是灯亮还是风扇转)由具体的子类(灯类、风扇类)来完成。
// 04.ts (接着上面的代码)// 定义一个抽象的智能设备类
abstract class AbstractSmartDevice {abstract deviceType: string; // 抽象属性,子类必须提供protected status: boolean = false;abstract turnOn(): void; // 抽象方法,子类必须实现abstract turnOff(): void; // 抽象方法,子类必须实现// 具体方法getStatus(): boolean {return this.status;}// 构造函数constructor(protected id: string) { // protected 参数属性,会创建一个同名的 protected 属性console.log(`抽象设备 ${this.id} 已创建`);}
}// 不能直接创建抽象类的实例
// const myDevice = new AbstractSmartDevice("abc"); // 编译时报错:Cannot create an instance of an abstract class.// 继承抽象类并实现抽象成员
class ConcreteSmartLight extends AbstractSmartDevice {deviceType: string = "智能灯"; // 实现抽象属性constructor(id: string, private brightness: number = 0) {super(id);}turnOn(): void { // 实现抽象方法this.status = true;this.brightness = 50;console.log(`智能灯 ${this.id} 打开,亮度 ${this.brightness}`);}turnOff(): void { // 实现抽象方法this.status = false;this.brightness = 0;console.log(`智能灯 ${this.id} 关闭`);}setBrightness(level: number): void {if (this.status) {this.brightness = level;console.log(`智能灯 ${this.id} 亮度设置为 ${this.brightness}`);} else {console.log(`智能灯 ${this.id} 已关闭,无法调节亮度`);}}
}const hallwayLight = new ConcreteSmartLight("H1");
hallwayLight.turnOn();
hallwayLight.setBrightness(80);
console.log("走廊灯状态:", hallwayLight.getStatus());
hallwayLight.turnOff();
4.5 静态成员 (Static Members)
类的属性和方法默认是属于类实例的(即创建对象后才能访问)。但有时候,有些属性或方法是属于类本身,而不是任何特定的实例,可以使用 static
关键字。这就像智能家居系统的某个全局配置参数,或者一个用于创建所有设备实例的工厂方法,它们不依赖于某个具体的设备。
// 04.ts (接着上面的代码)class DeviceManager {// 静态属性:所有设备共用的计数器static deviceCount: number = 0;// 静态方法:用于注册新设备static registerDevice(): void {DeviceManager.deviceCount++; // 在静态方法中访问静态属性console.log(`新设备已注册,当前共有 ${DeviceManager.deviceCount} 个设备.`);}constructor() {DeviceManager.registerDevice(); // 在构造函数中调用静态方法 (不太常见,但可行)}
}// 访问静态属性和方法,无需创建类的实例
DeviceManager.registerDevice(); // 输出:新设备已注册,当前共有 1 个设备.
DeviceManager.registerDevice(); // 输出:新设备已注册,当前共有 2 个设备.console.log("总设备数:", DeviceManager.deviceCount); // 输出:总设备数: 2// 创建类实例时也可以触发静态方法的调用 (如果构造函数里调用了)
const device1 = new DeviceManager(); // 输出:新设备已注册,当前共有 3 个设备.
const device2 = new DeviceManager(); // 输出:新设备已注册,当前共有 4 个设备.// 尝试通过实例访问静态成员会报错
// console.log(device1.deviceCount); // 编译时报错:Property 'deviceCount' is a static member of type 'DeviceManager'.
// device1.registerDevice(); // 编译时报错:Property 'registerDevice' is a static member of type 'DeviceManager'.
编译 04.ts
:
tsc --project 04-typescript/tsconfig.json
会生成 04.js
文件。然后在 HTML 中引入 04.js
。
执行后的效果
小结: 类和面向对象编程是构建复杂应用的重要范式。TypeScript 为类添加了强大的类型和可见性控制,支持继承和接口实现,使得代码结构更清晰,更易于管理和扩展,就像有了更标准化的智能家居模块。
练习:
- 定义一个基类
Vehicle
,包含属性speed
(number) 和方法move()
。 - 创建一个子类
Car
继承自Vehicle
,添加属性brand
(string),并重写move()
方法,使其输出汽车正在移动。 - 定义一个接口
Drawable
,包含方法draw(): void
。 - 创建一个类
Circle
,实现Drawable
接口,并在draw
方法中输出绘制圆形的信息。 - 修改
Light
类,使其brightness
属性默认是protected
。创建一个继承自Light
的DimmingLight
类,添加一个setBrightness(level: number)
方法,该方法可以访问父类的brightness
属性来调节亮度。
5 更高级的类型:让类型系统更灵活
TypeScript 的类型系统非常灵活,不仅限于基本类型和固定结构。它提供了多种方式来描述数据可能存在的多种状态或组合,这就像智能家居系统中的一个设备可能有多重模式(联合类型),或者需要同时满足多个功能规范(交叉类型)。
5.1 联合类型 (|
)
联合类型表示一个变量可以持有多种类型中的任意一种。这就像一根电线,有时候用来传输数据信号,有时候用来传输控制信号,但它总归是这两种中的一种。
// 05.ts// 一个变量可以是 string 或 number
let status: string | number;status = "在线"; // 正确
status = 1; // 正确// status = true; // 编译时报错:Type 'boolean' is not assignable to type 'string | number'.// 函数参数也可以是联合类型
function printId(id: number | string): void {console.log(`ID: ${id}`);// 在使用联合类型的变量时,需要进行类型检查 (类型缩小)if (typeof id === 'string') {// 在这个块里,id 被缩小为 string 类型console.log(id.toUpperCase());} else {// 在这个块里,id 被缩小为 number 类型console.log(id.toFixed(2)); // number 类型的方法}
}printId(101);
printId("abc-123");
// printId(true); // 编译时报错
5.2 交叉类型 (&
)
交叉类型表示一个类型是多种类型的组合,它必须同时满足所有类型的要求。这就像一个设备既是照明设备,又是安防设备,它必须同时具备照明和安防的所有功能。
// 05.ts (接着上面的代码)interface Switchable {turnOn(): void;turnOff(): void;
}interface Dimmable {setBrightness(level: number): void;
}// 交叉类型:同时具备 Switchable 和 Dimmable 的能力
type SmartLamp = Switchable & Dimmable;// 创建一个对象,它必须实现 Switchable 和 Dimmable 的所有方法
const mySmartLamp: SmartLamp = {turnOn: () => console.log("灯打开"),turnOff: () => console.log("灯关闭"),setBrightness: (level) => console.log(`亮度设置为 ${level}`)
};mySmartLamp.turnOn();
mySmartLamp.setBrightness(75);
mySmartLamp.turnOff();
5.3 字面量类型 (Literal Types)
字面量类型允许你指定变量的值只能是一个特定的字符串、数字或布尔值。这就像一个开关,它的状态只能是“开”或“关”,不能是其他任何值。
// 05.ts (接着上面的代码)// 变量的值只能是 "success", "error", "loading" 中的一个
let apiStatus: "success" | "error" | "loading";apiStatus = "success"; // 正确
apiStatus = "loading"; // 正确// apiStatus = "done"; // 编译时报错:Type '"done"' is not assignable to type '"success" | "error" | "loading"'.// 函数参数或返回值也可以是字面量类型
function setDirection(direction: "up" | "down" | "left" | "right"): void {console.log(`移动方向: ${direction}`);
}setDirection("up");
// setDirection("forward"); // 编译时报错
5.4 枚举 (enum
)
枚举允许你定义一组命名的常量。这对于表示一组相关的、有限的取值非常有用,比如星期几、交通灯颜色、订单状态等。它们让代码更易读。这就像给智能家居系统的各种工作模式定义了清晰的名称,而不是用数字或字符串的魔术值。
// 05.ts (接着上面的代码)// 数字枚举 (默认从 0 开始)
enum Direction {Up, // 0Down, // 1Left, // 2Right // 3
}let playerDirection: Direction = Direction.Up;
console.log("玩家方向 (数字):", playerDirection); // 输出 0
console.log("玩家方向 (名称):", Direction[playerDirection]); // 输出 "Up"// 可以指定起始值
enum StatusCode {Success = 200,NotFound = 404,InternalError = 500
}let responseStatus: StatusCode = StatusCode.Success;
console.log("响应状态码:", responseStatus); // 输出 200// 字符串枚举 (推荐,可读性更好)
enum ApiStatus {Fetching = "FETCHING",Success = "SUCCESS",Error = "ERROR"
}let currentApiStatus: ApiStatus = ApiStatus.Fetching;
console.log("当前API状态:", currentApiStatus); // 输出 "FETCHING"
5.5 any
vs unknown
any
: 表示可以是任何类型。使用any
会关闭 TypeScript 的类型检查,回到原生 JavaScript 的状态。谨慎使用,它破坏了 TypeScript 提供的安全性。就像一根没有标签的电线,你不知道它是传输什么信号的,使用时非常危险。unknown
: 表示未知类型。与any
不同的是,使用unknown
类型的变量之前,你必须先进行类型检查或类型断言,才能对其进行操作。这就像一根你不知道用途的电线,在使用它之前,你必须先用仪表测试清楚它的类型。
// 05.ts (接着上面的代码)let data: any = 123;
data = "hello";
data = true;
data.toFixed(2); // 不会报错,any 类型不做检查
data.toUpperCase(); // 不会报错,any 类型不做检查let dataUnknown: unknown = "hello world";// 尝试直接对 unknown 类型进行操作会报错
// dataUnknown.toUpperCase(); // 编译时报错:'dataUnknown' is of type 'unknown'.// 必须先进行类型检查或断言
if (typeof dataUnknown === 'string') {console.log(dataUnknown.toUpperCase()); // 在这个块里,dataUnknown 被缩小为 string
}// 或者使用类型断言 (后面会讲)
// console.log((dataUnknown as string).toUpperCase());
5.6 void
和 never
void
: 表示函数没有返回值。这是最常见的,比如只执行某个操作而不返回结果的函数。never
: 表示函数永远不会返回结果。这通常用于会抛出错误或包含无限循环的函数。这就像一个警报系统,一旦触发就进入一个不可停止的状态。
// 05.ts (接着上面的代码)// 没有返回值的函数
function logAction(action: string): void {console.log(`执行动作: ${action}`);
}logAction("初始化系统");// 永远不会返回的函数 (例如,抛出错误)
function throwError(message: string): never {throw new Error(message);
}// 永远不会返回的函数 (例如,无限循环)
// function infiniteLoop(): never {
// while (true) {
// // ...
// }
// }// 调用会抛出错误的函数
// try {
// throwError("出错了!");
// } catch (e) {
// console.error("捕获到错误:", e.message);
// }
5.7 类型断言 (as
或 <Type>
)
有时候,你比 TypeScript 更清楚一个变量的实际类型。类型断言就是告诉编译器,“相信我,这个变量就是这个类型”。这就像你看到一根没有标签的电线,但根据经验你确定它是电源线,你就可以“断言”它是电源线。
重要提示: 类型断言只在编译阶段起作用,不会改变变量的实际运行时类型或执行任何额外的检查。如果断言错误,可能会导致运行时错误。谨慎使用!
有两种语法:
value as Type
(JSX 中推荐使用)<Type>value
(在.tsx
文件中与 JSX 语法冲突,不推荐)
// 05.ts (接着上面的代码)let someValue: unknown = "这是个字符串";// 使用 as 进行类型断言
let strLength1: number = (someValue as string).length;
console.log("字符串长度 (as):", strLength1);// 使用 <Type> 进行类型断言 (在 tsx 文件中可能与 JSX 冲突)
let strLength2: number = (<string>someValue).length;
console.log("字符串长度 (<Type>):", strLength2);// 危险的断言:如果 someValue 实际上不是字符串
let anotherValue: unknown = 123;
// let dangerousLength: number = (anotherValue as string).length; // 编译时不会报错,但运行时会出错!// 获取 DOM 元素时经常用到类型断言
// const element = document.getElementById("my-canvas") as HTMLCanvasElement;
// // 现在 TypeScript 知道 element 是一个 HTMLCanvasElement 类型,可以使用其特有的属性和方法
// const ctx = element.getContext("2d");
编译 05.ts
:
tsc --project 04-typescript/tsconfig.json
会生成 05.js
文件。然后在 HTML 中引入 05.js
。
执行后的效果
小结: 联合类型、交叉类型、字面量类型、枚举等高级类型使得 TypeScript 能够更精确地描述复杂的数据结构和取值范围。理解 any
和 unknown
的区别以及何时使用类型断言是编写安全 TypeScript 代码的关键。
5.8 练习
- 定义一个联合类型
ContactInfo
,可以是string
(邮箱) 或number
(电话号码)。声明一个变量myContact
为此类型,并分别赋值一个邮箱地址和电话号码。 - 定义一个接口
HasArea
,包含方法getArea(): number
。定义一个接口HasPerimeter
,包含方法getPerimeter(): number
。定义一个交叉类型Shape
,它是HasArea
和HasPerimeter
的组合。创建一个对象,实现Shape
类型。 - 定义一个字符串字面量类型
TrafficLightColor
,它的值只能是"Red"
,"Yellow"
,"Green"
中的一个。编写一个函数changeLight(color: TrafficLightColor): void
。 - 定义一个数字枚举
LogLevel
,包含Debug = 0
,Info = 1
,Warning = 2
,Error = 3
。编写一个函数logMessage(level: LogLevel, message: string): void
,根据日志级别输出信息。
6 模块化与工具链:组织代码和提升效率
随着项目代码量的增加,我们不可能把所有代码都放在一个文件里。模块化是将代码分割成独立、可复用的文件的方式,这就像智能家居系统中的各个房间都有独立的电路箱和布线,互不干扰但又能通过主干线连接。TypeScript 完全支持 ES Modules (import
/export
)。
同时,TypeScript 的强大之处在于它的编译器 tsc
和丰富的配置选项,以及与现代前端工具链的集成。
6.1 模块 (import
和 export
)
在 TypeScript 中,每个 .ts
文件默认就是一个模块。你可以使用 export
关键字导出模块中的变量、函数、类、接口等,然后使用 import
关键字在其他模块中引入它们。
创建两个文件:utils.ts
和 main.ts
。
utils.ts
:
// utils.ts - 一个工具函数模块// 导出常量
export const PI = 3.14159;// 导出函数
export function add(x: number, y: number): number {return x + y;
}// 导出接口
export interface Config {apiUrl: string;timeout: number;
}// 导出类
export class Logger {log(message: string): void {console.log(`[日志] ${message}`);}
}// 默认导出 (一个模块只能有一个默认导出)
export default class AppSettings {theme: string = 'light';
}
main.ts
:
// main.ts - 主应用模块// 导入单个或多个导出
import { PI, add, Logger, Config } from './utils'; // 注意文件后缀省略// 导入默认导出 (可以取任意名字)
import Settings from './utils'; // 注意这里没有大括号// 导入模块中的所有导出,并将其放入一个命名空间对象中
import * as Utils from './utils';console.log("PI:", PI);
console.log("2 + 3 =", add(2, 3));const appLogger = new Logger();
appLogger.log("应用启动");const appConfig: Config = {apiUrl: "http://api.example.com",timeout: 5000
};
console.log("应用配置:", appConfig);const settings = new Settings();
console.log("应用设置主题:", settings.theme);// 访问命名空间中的导出
console.log("Utils.PI:", Utils.PI);
6.2 编译模块化的 TypeScript 代码
直接使用 tsc main.ts
编译可能会遇到问题,因为浏览器默认不支持 ES Modules 语法(需要特定的 <script type="module">
标签,并且文件路径和扩展名有要求)。更常见的做法是使用 tsconfig.json
配置文件来管理整个项目的编译设置。
tsconfig.json
文件:
这个文件位于项目的根目录,用来告诉 tsc
编译器如何编译你的 TypeScript 项目。
初始化 tsconfig.json
: 在项目根目录运行命令:
tsc --init
这会生成一个带有大量注释的 tsconfig.json
文件。一些重要的配置项:
"compilerOptions"
: 编译选项的核心配置对象。"target"
: 指定编译后的 JavaScript 版本 (如 “es5”, “es6”, “es2020” 等)。"module"
: 指定生成的模块系统 (如 “commonjs”, “es2015”, “esnext” 等)。对于现代 Web 开发,“es2015” 或 “esnext” 配合模块打包工具更常用。"outDir"
: 指定编译后 JavaScript 文件的输出目录。"rootDir"
: 指定 TypeScript 源文件的根目录。"strict"
: 启用所有严格的类型检查选项(强烈推荐开启!)。"esModuleInterop"
: 允许使用 ES Modules 语法导入 CommonJS 模块。"forceConsistentCasingInFileNames"
: 强制文件名大小写一致。"skipLibCheck"
: 跳过声明文件的类型检查,提高编译速度。
"include"
: 指定需要编译的文件或目录(默认是当前目录及子目录下的所有.ts
文件)。"exclude"
: 指定不需要编译的文件或目录(如node_modules
)。
修改 tsconfig.json
示例:
{"compilerOptions": {"target": "es2018", // 编译到较新版本的 JS,现代浏览器支持"module": "esnext", // 使用最新的 ES Module 语法"outDir": "./dist", // 编译输出到 dist 目录"rootDir": "./src", // TypeScript 源文件在 src 目录"strict": true, // 开启所有严格检查"esModuleInterop": true,"forceConsistentCasingInFileNames": true,"skipLibCheck": true},"include": ["src/**/*.ts" // 包含 src 目录下所有子目录中的 .ts 文件],"exclude": ["node_modules"]
}
现在,将你的 utils.ts
和 main.ts
文件移到 src
目录下。然后在项目根目录运行 tsc
命令(不带文件名):
tsc
tsc
命令会自动查找 tsconfig.json
文件并按照其中的配置编译项目。编译后的 .js
文件会输出到 ./dist
目录。
6.3 模块打包工具 (Bundlers)
虽然 tsc
可以编译 TypeScript 模块,但生成的 ES Module 文件在浏览器中直接使用可能需要 <script type="module">
标签,并且处理复杂的依赖关系(比如导入 node_modules
中的库)并不方便。
现代前端开发通常会使用模块打包工具 (Bundlers),如 Webpack, Parcel, Vite 等。这些工具可以处理模块间的依赖关系,将多个模块打包成一个或少数几个优化过的 JavaScript 文件,并通常内置或容易配置对 TypeScript 的支持。
模块打包工具会读取你的入口文件(例如 dist/main.js
),分析其依赖关系,然后将所有需要的代码打包到一个或多个文件中。
基本打包流程(概念性):
- 你编写
.ts
文件,使用import
/export
。 - 使用
tsc
编译.ts
文件到.js
文件(位于outDir
)。 - 使用打包工具(如 Webpack)读取
outDir
中的入口.js
文件。 - 打包工具分析依赖,将所有相关的
.js
文件整合成最终用于浏览器的文件(通常也包含代码优化、压缩等)。 - 在 HTML 中只引入打包后的文件。
很多现代打包工具(如 Vite)甚至可以直接处理 .ts
文件,无需先用 tsc
编译到单独的 .js
目录,简化了流程。理解 tsconfig.json
仍然非常重要,因为它控制了 TypeScript 的编译行为和类型检查规则。
小结: 模块化是组织大型 TypeScript 项目的关键。ES Modules (import
/export
) 是标准的模块化方案。tsconfig.json
文件是 TypeScript 项目的配置中心,控制着编译器的行为。模块打包工具是现代前端开发中处理模块依赖和优化代码不可或缺的一部分。
6.4 练习
- 创建一个新的项目目录。
- 在目录中运行
npm init -y
初始化一个 Node.js 项目。 - 安装 TypeScript:
npm install typescript --save-dev
(或者全局安装npm install -g typescript
)。 - 运行
tsc --init
生成tsconfig.json
文件。 - 创建
src
目录,并在其中创建calculator.ts
和app.ts
文件。 - 在
calculator.ts
中导出一个函数multiply(a: number, b: number): number
和一个常量E = 2.71828
。 - 在
app.ts
中导入multiply
和E
,使用它们进行计算并在控制台输出结果。 - 修改
tsconfig.json
,设置outDir
为dist
,rootDir
为src
,target
为es2018
,module
为esnext
,并开启strict
。 - 在项目根目录运行
tsc
命令,查看dist
目录下生成的.js
文件。 - 创建一个简单的
index.html
文件,引入dist/app.js
(使用<script type="module" src="dist/app.js"></script>
),在浏览器中查看控制台输出。