Docker容器化遗留应用(四):实战案例

Docker容器化遗留应用(四):实战案例

Docker容器化

系列导读

这是《Docker容器化遗留应用》系列的第四篇,前几篇介绍了容器化基础,本篇将通过实战案例演示如何容器化真实应用。


案例1:Apache服务器容器化

场景描述

一个运行了5年的Apache服务器,配置复杂,依赖特定版本的PHP模块。需要在不修改代码的情况下容器化。

步骤一:准备工作

mkdir dockerized-apache && cd dockerized-apache

步骤二:编写Dockerfile

FROM ubuntu:20.04

# 避免交互式提示
ENV DEBIAN_FRONTEND=noninteractive

# 安装Apache和PHP
RUN apt-get update && \
    apt-get install -y apache2 php libapache2-mod-php && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 复制配置文件
COPY ./my-httpd.conf /etc/apache2/apache2.conf
COPY ./html/ /var/www/html/

# 暴露端口
EXPOSE 80

# 启动Apache(前台运行)
CMD ["apachectl", "-D", "FOREGROUND"]

步骤三:构建与运行

# 构建镜像
docker build -t dockerized-apache .

# 运行容器
docker run -d -p 8080:80 --name my-apache dockerized-apache

# 测试访问
curl http://localhost:8080

案例2:数据库容器化

开发环境 vs 生产环境

环境 推荐方案 理由
开发 容器化数据库 快速启停,易于重置
测试 容器化数据库 隔离测试数据
生产 托管数据库服务 自动备份、高可用

数据迁移:平滑过渡

# 从现有数据库导出数据
mysqldump -u root -p mydb > backup.sql

# 启动新的MySQL容器
docker run -d --name my-mysql \
  -e MYSQL_ROOT_PASSWORD=mypassword \
  -v mysql-data:/var/lib/mysql \
  mysql:8.0

# 导入数据
docker exec -i my-mysql mysql -u root -pmypassword < backup.sql

配置灵活性

# 开发环境:使用容器化数据库
docker run -d --name dev-db \
  -e MYSQL_ROOT_PASSWORD=dev mysql:8.0

# 生产环境:连接托管数据库
docker run -d --name prod-app \
  -e DB_HOST=my-rds-instance.amazonaws.com \
  -e DB_PASSWORD=${DB_PASSWORD} \
  my-app

配置与环境变量管理

环境变量注入

# 通过-e参数注入
docker run -d --name my-app \
  -e DB_HOST=database.local \
  -e DB_PASSWORD=${DB_PASSWORD} \
  my-application

使用.env文件

# .env文件
DB_HOST=database.local
DB_PASSWORD=supersecret
API_KEY=abc123
# docker-compose.yml
services:
  app:
    image: my-application
    environment:
      - DB_HOST=${DB_HOST}
      - DB_PASSWORD=${DB_PASSWORD}

配置文件挂载

# 挂载主机配置文件
docker run -d --name my-app \
  -v /path/to/config:/app/config:ro \
  my-application

安全提示:敏感信息不要硬编码在Dockerfile中,使用环境变量或密钥管理服务。


安全加固措施

1. 镜像安全

# 使用Docker Scout扫描漏洞
docker scout cves my-legacy-app

# 使用Clair扫描
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  arminc/clair-local-scan my-legacy-app

2. 最小权限原则

# 创建非root用户
RUN useradd -m myuser

# 切换到非root用户
USER myuser

# 设置工作目录
WORKDIR /home/myuser

3. 能力限制

# 删除所有能力,仅添加必要的
docker run -d --name my-app \
  --cap-drop=all \
  --cap-add=net_bind_service \
  my-application

4. 只读挂载

# 保护配置文件不被篡改
docker run -d --name my-app \
  -v /path/to/config:/app/config:ro \
  my-application

5. 监控与日志

# 查看容器日志
docker logs -f my-app

# 实时监控资源使用
docker stats my-app

下篇预告

下一篇:《Docker容器化遗留应用(五):综合案例》

将详细介绍:

  • 遗留CRM系统容器化全流程
  • 系统架构拆分
  • 测试与部署
  • 效果对比

系列导航

  • (一)为什么要容器化 - 容器化优势与基本概念
  • (二)容器化三步走 - 从零到一的实践指南
  • (三)网络与数据管理 - 容器间通信与持久化
  • (四)实战案例 ← 当前
  • (五)综合案例 - 遗留CRM系统容器化全流程

通过实战案例,你已经掌握了容器化的核心技术!

Views: 6

Docker容器化遗留应用(三):网络与数据管理

Docker容器化遗留应用(三):网络与数据管理

Docker容器化

系列导读

这是《Docker容器化遗留应用》系列的第三篇,前两篇介绍了容器化优势和基本步骤,本篇将深入讲解网络配置和数据持久化。


网络配置:让容器之间"说得上话"

端口映射:打开对外的窗口

# 将主机的8080端口映射到容器的80端口
docker run -d -p 8080:80 --name web-server nginx

# 访问 http://localhost:8080 即可看到Nginx欢迎页

端口映射格式主机端口:容器端口

容器间通信:建立内部专线

默认桥接网络

# 启动数据库容器
docker run -d --name my-database mongo

# 应用容器连接数据库
docker run -d --name my-app --link my-database:db my-legacy-app

自定义桥接网络(推荐)

# 创建自定义网络
docker network create crm-network

# 数据库容器加入网络
docker run -d --network=crm-network \
  --name database --network-alias=db mongo

# 应用容器通过别名访问
docker run -d --network=crm-network \
  --name app -e DB_HOST=db my-legacy-app

优势

  • ✅ 自动DNS解析
  • ✅ 网络隔离
  • ✅ 容器间可通过名称访问

Docker Compose:一键编排多容器

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - DB_HOST=database
    depends_on:
      - database
    networks:
      - crm-network

  database:
    image: mongo:latest
    volumes:
      - mongo-data:/data/db
    networks:
      - crm-network

networks:
  crm-network:
    driver: bridge

volumes:
  mongo-data:

一键启动

docker compose up -d

数据持久化:别让数据随容器消失

容器是短暂的,删除容器后数据也会消失。对于数据库等有状态应用,必须使用数据卷。

三种卷类型对比

类型 特点 适用场景
匿名卷 随机名称,难管理 临时任务
命名卷 显式创建,易管理 生产环境
主机卷 直接挂载主机目录 开发调试

命名卷:生产环境首选

# 创建命名卷
docker volume create my-mongo-data

# 挂载到容器
docker run -d --name my-mongodb \
  -v my-mongo-data:/data/db \
  mongo

主机卷:开发调试利器

# 挂载主机目录到容器
docker run -d --name my-mongodb \
  -v /path/on/host:/data/db \
  mongo

# 主机上的文件修改会实时同步到容器

Docker Compose卷配置

services:
  database:
    image: mongo
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

运行时自动创建和挂载卷。

数据备份:安全第一

# 从容器复制数据到主机
docker cp my-mongodb:/data/db /backup/mongo-$(date +%Y%m%d)

# 或者使用命名卷备份
docker run --rm -v my-mongo-data:/data -v /backup:/backup \
  busybox tar czf /backup/mongo-$(date +%Y%m%d).tar.gz /data

实战:搭建一个Web应用栈

完整的docker-compose.yml

version: '3.8'

services:
  # 反向代理
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    networks:
      - app-network

  # Web应用
  web:
    build: .
    environment:
      - DB_HOST=database
    depends_on:
      - database
    networks:
      - app-network

  # 数据库
  database:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: myapp
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql-data:

一键启动

# 启动所有服务
docker compose up -d

# 查看服务状态
docker compose ps

# 查看日志
docker compose logs -f

# 停止所有服务
docker compose down

下篇预告

下一篇:《Docker容器化遗留应用(四):实战案例》

将详细介绍:

  • Apache服务器容器化
  • 数据库容器化考量
  • 配置与环境变量管理
  • 安全加固措施

系列导航

  • (一)为什么要容器化 - 容器化优势与基本概念
  • (二)容器化三步走 - 从零到一的实践指南
  • (三)网络与数据管理 ← 当前
  • (四)实战案例 - Apache与数据库容器化
  • (五)综合案例 - 遗留CRM系统容器化全流程

掌握了网络和数据管理,你的容器化应用就可以投入生产了!

Views: 11

Docker容器化遗留应用(二):容器化三步走

Docker容器化遗留应用(二):容器化三步走

Docker容器化

系列导读

这是《Docker容器化遗留应用》系列的第二篇,上一篇介绍了容器化的优势,本篇将手把手教你完成容器化的三个基本步骤。


第一步:环境准备(5分钟)

安装Docker

# 一键安装(Ubuntu/Debian)
curl -fsSL https://get.docker.com | bash

# 验证安装
docker --version
# Docker version 24.0.7, build afdd53b

# Docker Compose已内置
docker compose version
# Docker Compose version v2.23.0

国内镜像加速配置(重要!)

国内访问Docker Hub速度较慢,必须配置镜像加速器。

可用的国内镜像源

镜像源 地址 推荐指数
阿里云 https://xxx.mirror.aliyuncs.com ⭐⭐⭐⭐⭐
中科大 https://docker.mirrors.ustc.edu.cn ⭐⭐⭐⭐
网易 https://hub-mirror.c.163.com ⭐⭐⭐

配置方法

# 创建配置文件
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com"
  ]
}
EOF

# 重启Docker
sudo systemctl daemon-reload
sudo systemctl restart docker

# 验证
docker info | grep "Registry Mirrors" -A 1

阿里云专属加速器

  1. 访问 阿里云容器镜像服务
  2. 获取专属加速器地址(格式:https://xxx.mirror.aliyuncs.com
  3. 添加到 daemon.jsonregistry-mirrors 数组

第二步:编写Dockerfile(10分钟)

完整示例(Python Flask)

# 选择轻量级基础镜像
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 先复制依赖文件(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 5000

# 启动命令
CMD ["python", "app.py"]

关键指令

指令 说明 示例
FROM 基础镜像 FROM python:3.11-slim
WORKDIR 工作目录 WORKDIR /app
COPY 复制文件 COPY . .
RUN 执行命令 RUN pip install flask
EXPOSE 暴露端口 EXPOSE 5000
CMD 启动命令 CMD ["python", "app.py"]

优化技巧:利用Docker缓存

错误做法(每次都重新安装依赖):

COPY . .
RUN pip install -r requirements.txt

正确做法(代码修改不重新安装):

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

第三步:构建与运行(1分钟)

构建镜像

# 构建镜像
docker build -t my-legacy-app:v1.0 .

# 查看镜像
docker images | grep my-legacy-app

运行容器

# 运行容器(-d 后台,-p 端口映射)
docker run -d -p 5000:5000 --name my-app my-legacy-app:v1.0

# 查看状态
docker ps

# 查看日志
docker logs -f my-app

测试访问

curl http://localhost:5000

常用命令速查

操作 命令
构建镜像 docker build -t 镜像名:标签 .
运行容器 docker run -d -p 端口:端口 --name 容器名 镜像名
查看容器 docker ps
查看日志 docker logs -f 容器名
停止容器 docker stop 容器名
进入容器 docker exec -it 容器名 /bin/bash

下篇预告

下一篇:《Docker容器化遗留应用(三):网络与数据管理》

将介绍:

  • 端口映射与容器间通信
  • Docker Compose一键编排
  • 数据持久化与卷管理

系列导航

  • (一)为什么要容器化 - 容器化优势与基本概念
  • (二)容器化三步走 ← 当前
  • (三)网络与数据管理 - 容器间通信与持久化
  • (四)实战案例 - Apache与数据库容器化
  • (五)综合案例 - 遗留CRM系统容器化全流程

三步完成,你的第一个容器化应用已经运行起来了!

Views: 11

Docker容器化遗留应用(一):为什么要容器化

Docker容器化遗留应用(一):为什么要容器化

Docker容器化

系列导读

这是《Docker容器化遗留应用》系列的第一篇,整个系列包括:

  1. (一)为什么要容器化 - 容器化优势与基本概念
  2. (二)容器化三步走 - 从零到一的实践指南
  3. (三)网络与数据管理 - 容器间通信与持久化
  4. (四)实战案例 - Apache与数据库容器化
  5. (五)综合案例 - 遗留CRM系统容器化全流程

你的遗留应用还在裸奔吗?

还记得那些跑在物理机上的老系统吗?每次部署都像拆盲盒——"在我机器上能跑"成了开发者的经典借口。环境配置不一致、依赖冲突、扩展困难,这些问题像幽灵一样缠绕着运维团队。

容器化不是什么黑科技,它就是给你的应用穿上统一的"防护服"。


容器化带来的三大改变

1. 环境一致性:告别"在我机器上能跑"

容器将应用及其所有依赖打包在一起,确保开发、测试、生产环境完全一致。

传统部署

  • 开发环境:Python 3.11 + Flask 2.0
  • 测试环境:Python 3.9 + Flask 1.1
  • 生产环境:Python 3.10 + Flask 2.1
  • 结果:各种奇怪的bug,浪费大量调试时间

容器化部署

  • 所有环境:同一个镜像,完全一致
  • 结果:开发环境能跑,生产环境也能跑

2. 水平扩展能力:应对流量高峰

通过增加或减少容器数量实现应用水平扩展,结合Kubernetes等编排工具能优化资源分配。

扩展速度对比

方式 扩展时间 操作复杂度
传统虚拟机 30-60分钟 需要配置新机器
容器 5-10秒 一条命令

真实场景

  • 双11流量高峰:几秒钟启动100个新容器
  • 流量回落后:自动销毁多余容器,节省资源

3. 部署与回滚效率:快速迭代

容器化部署速度快,出现问题时回滚也迅速。

部署效率对比

操作 传统部署 容器化部署
部署时间 2小时 2分钟
回滚时间 1小时 10秒
失败率 15% 2%

效果对比:容器化前后的世界

指标 容器化前 容器化后 提升幅度
部署时间 2小时 2分钟 98% ↓
回滚时间 1小时 10秒 99.9% ↓
资源利用率 15% 70% 366% ↑
扩展速度 30分钟/台 5秒/容器 99.7% ↓
环境差异导致的bug 每月3-5个 0 100% ↓

容器化适合哪些遗留应用?

✅ 适合容器化的场景

  • Web应用:Apache、Nginx、Tomcat等
  • API服务:RESTful API、微服务
  • 批处理任务:定时任务、数据处理
  • 数据库:MySQL、PostgreSQL、MongoDB(需注意持久化)

⚠️ 需要谨慎的场景

  • 有状态应用:数据库需要特殊处理
  • 高性能计算:可能需要直接访问硬件
  • Windows应用:需要Windows容器支持

容器化不是万能药

虽然容器化有很多优势,但也需要注意:

优点

  • ✅ 环境一致性
  • ✅ 快速部署和回滚
  • ✅ 高效资源利用
  • ✅ 便于扩展

挑战

  • ⚠️ 需要学习新技术栈
  • ⚠️ 有状态应用需要特殊处理
  • ⚠️ 监控和日志需要重新设计
  • ⚠️ 安全配置不能忽视

下篇预告

下一篇:《Docker容器化遗留应用(二):容器化三步走》

将详细介绍:

  • 环境准备(5分钟)
  • 国内镜像加速配置(重要!)
  • 编写Dockerfile(10分钟)
  • 构建与运行容器(1分钟)

系列导航

  • (一)为什么要容器化 ← 当前
  • (二)容器化三步走 - 从零到一的实践指南
  • (三)网络与数据管理 - 容器间通信与持久化
  • (四)实战案例 - Apache与数据库容器化
  • (五)综合案例 - 遗留CRM系统容器化全流程

你的遗留应用还在等什么?开始容器化之旅吧!

Views: 14

08. TypeScript进阶:接口、泛型与类型体操

TypeScript 进阶:接口、泛型与类型体操

TypeScript Advanced

上一期我们学习了 TypeScript 的基础类型。这一期,我们深入接口、泛型和高级类型系统——这些是 TypeScript 真正强大的地方。

接口(Interface)

接口定义了对象的形状。

基本用法

interface User {
  id: number
  name: string
  email: string
}

function printUser(user: User) {
  console.log(`${user.name} `)
}

printUser({
  id: 1,
  name: "Alice",
  email: "alice@example.com"
})

可选属性

interface User {
  id: number
  name: string
  email?: string  // 可选
}

let user1: User = { id: 1, name: "Alice" }
let user2: User = { id: 2, name: "Bob", email: "bob@example.com" }

只读属性

interface User {
  readonly id: number
  name: string
}

let user: User = { id: 1, name: "Alice" }
user.id = 2  // Error: Cannot assign to 'id' because it is read-only

函数类型

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc = (source, sub) => {
  return source.search(sub) !== -1
}

可索引类型

interface StringArray {
  [index: number]: string
}

let myArray: StringArray = ["Bob", "Fred"]
let myStr: string = myArray[0]

类类型

interface ClockInterface {
  currentTime: Date
  setTime(d: Date): void
}

class Clock implements ClockInterface {
  currentTime: Date = new Date()
  
  setTime(d: Date) {
    this.currentTime = d
  }
  
  constructor(h: number, m: number) {}
}

继承接口

interface Shape {
  color: string
}

interface Square extends Shape {
  sideLength: number
}

let square: Square = {
  color: "blue",
  sideLength: 10
}

多继承:

interface Shape {
  color: string
}

interface PenStroke {
  penWidth: number
}

interface Square extends Shape, PenStroke {
  sideLength: number
}

泛型(Generics)

泛型让我们可以编写可重用的组件。

基本泛型

function identity(arg: T): T {
  return arg
}

let output1 = identity("myString")  // 类型为 string
let output2 = identity(123)  // 类型推断为 number

泛型变量

function loggingIdentity(arg: T[]): T[] {
  console.log(arg.length)
  return arg
}

泛型类型

let myIdentity: (arg: T) => T = identity

// 对象字面量形式
let myIdentity2: { (arg: T): T } = identity

泛型接口

interface GenericIdentityFn {
  (arg: T): T
}

function identity(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

把泛型参数移到接口:

interface GenericIdentityFn {
  (arg: T): T
}

function identity(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

泛型类

class GenericNumber {
  zeroValue: T
  add: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber()
myGenericNumber.zeroValue = 0
myGenericNumber.add = (x, y) => x + y

泛型约束

interface Lengthwise {
  length: number
}

function loggingIdentity(arg: T): T {
  console.log(arg.length)  // 现在可以访问 .length
  return arg
}

loggingIdentity("hello")  // OK
loggingIdentity([1, 2, 3])  // OK
loggingIdentity(123)  // Error

在泛型中使用类类型

function create(c: { new(): T }): T {
  return new c()
}

class BeeKeeper {
  hasMask: boolean = true
}

class ZooKeeper {
  nametag: string = "Mikle"
}

class Animal {
  numLegs: number = 4
}

class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper()
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper()
}

function createInstance<A>(c: new () => A): A {
  return new c()
}

createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask

高级类型

交叉类型

将多个类型合并为一个。

interface Person {
  name: string
}

interface Loggable {
  log(): void
}

type PersonLoggable = Person & Loggable

const person: PersonLoggable = {
  name: "Alice",
  log() {
    console.log(this.name)
  }
}

联合类型

type StringOrNumber = string | number

function add(a: StringOrNumber, b: StringOrNumber) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString()
  }
  return a + b
}

类型保护

interface Fish {
  swim(): void
}

interface Bird {
  fly(): void
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim()
  } else {
    pet.fly()
  }
}

typeof 类型保护

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value
  }
  if (typeof padding === "string") {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

instanceof 类型保护

class Dog {
  bark() {}
}

class Cat {
  meow() {}
}

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark()
  } else {
    pet.meow()
  }
}

null 检查

function f(str: string | null) {
  if (str === null) {
    return "default"
  }
  return str.toUpperCase()
}

可选链:

let x = foo?.bar.baz()
// 等价于
let x = foo === null || foo === undefined ? undefined : foo.bar.baz()

空值合并:

let x = foo ?? "default"
// 等价于
let x = foo !== null && foo !== undefined ? foo : "default"

类型推断

基础推断

let x = 3  // 推断为 number
let y = [0, 1, null]  // 推断为 (number | null)[]

上下文推断

window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.button)  // 推断为 MouseEvent
}

类型推断方向

// 从右向左
let x = 3

// 从左向右(上下文类型)
window.onmousedown = function(mouseEvent) {
  return mouseEvent.button
}

映射类型

从旧类型创建新类型。

Partial

所有属性变为可选。

type Partial = {
  [P in keyof T]?: T[P]
}

interface User {
  id: number
  name: string
}

type PartialUser = Partial
// { id?: number; name?: string; }

Required

所有属性变为必选。

type Required = {
  [P in keyof T]-?: T[P]
}

Readonly

所有属性变为只读。

type Readonly = {
  readonly [P in keyof T]: T[P]
}

Pick

选取部分属性。

type Pick = {
  [P in K]: T[P]
}

interface User {
  id: number
  name: string
  email: string
}

type UserPreview = Pick
// { id: number; name: string; }

Omit

排除部分属性。

type Omit = Pick<T, Exclude>

type UserWithoutEmail = Omit
// { id: number; name: string; }

Record

创建对象类型。

type Record = {
  [P in K]: T
}

type UserMap = Record

条件类型

根据条件选择类型。

type ReturnType = T extends (...args: any[]) => infer R ? R : any

function foo(): number {
  return 1
}

type R = ReturnType  // number

实战示例

API 响应类型

interface ApiResponse {
  code: number
  message: string
  data: T
}

interface User {
  id: number
  name: string
}

async function fetchUser(id: number): Promise<ApiResponse> {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// 使用
const result = await fetchUser(1)
console.log(result.data.name)

事件处理

interface EventHandler {
  (event: T): void
}

interface ClickEvent {
  type: "click"
  x: number
  y: number
}

const handler: EventHandler = (event) => {
  console.log(`Clicked at ${event.x}, ${event.y}`)
}

状态管理

type State = {
  user: User | null
  loading: boolean
  error: string | null
}

type Action =
  | { type: "FETCH_START" }
  | { type: "FETCH_SUCCESS"; payload: User }
  | { type: "FETCH_ERROR"; payload: string }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "FETCH_START":
      return { ...state, loading: true }
    case "FETCH_SUCCESS":
      return { ...state, loading: false, user: action.payload }
    case "FETCH_ERROR":
      return { ...state, loading: false, error: action.payload }
  }
}

总结

TypeScript 的高级特性:

  • 接口:定义对象形状
  • 泛型:创建可重用组件
  • 类型保护:缩小类型范围
  • 映射类型:从旧类型创建新类型
  • 条件类型:根据条件选择类型

下一期,我们学习 TypeScript 的项目配置和最佳实践。


进阶资源

Views: 24