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

10. Vue3 + TypeScript:完整集成指南

Vue3 + TypeScript:完整集成指南

Vue3 TypeScript

恭喜你走到最后一期!前面我们学习了 ES6、Vite、Vue3 基础、组合式 API 和 TypeScript。现在,让我们把 TypeScript 完整集成到 Vue3 项目中。

项目创建

Vite + Vue + TypeScript

npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install

项目结构

src/
├── main.ts
├── App.vue
├── vite-env.d.ts
├── components/
│   └── HelloWorld.vue
├── views/
├── router/
│   └── index.ts
├── stores/
│   └── user.ts
├── types/
│   └── index.ts
├── api/
│   └── user.ts
├── composables/
│   └── useUser.ts
└── utils/
    └── format.ts

类型定义

全局类型

// types/index.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
}

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

export type Theme = 'light' | 'dark'

export interface AppState {
  user: User | null
  theme: Theme
  loading: boolean
}

组件 Props 类型


interface Props {
  title: string
  count?: number
  users: User[]
}

const props = withDefaults(defineProps(), {
  count: 0
})

组件 Emits 类型


interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'change', event: Event): void
}

const emit = defineEmits()

function handleUpdate() {
  emit('update', 'new value')
}

组合式 API + TypeScript

ref

import { ref } from 'vue'

// 类型推断
const count = ref(0)
const name = ref('Alice')

// 显式类型
const user = ref(null)
const items = ref([])

reactive

import { reactive } from 'vue'

interface State {
  count: number
  name: string
  items: string[]
}

const state = reactive({
  count: 0,
  name: '',
  items: []
})

computed

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 自动推断返回类型
const fullName = computed(() => <code>${firstName.value} ${lastName.value})

// 显式类型
const userCount = computed(() => users.value.length)

组合式函数

// composables/useUser.ts
import { ref, computed } from 'vue'
import type { User } from '@/types'

export function useUser() {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const isLoggedIn = computed(() => !!user.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  async function fetchUser(id: number) {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(<code>/api/users/${id})
      user.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
  }

  return {
    user,
    loading,
    error,
    isLoggedIn,
    userName,
    fetchUser,
    logout
  }
}

Pinia + TypeScript

Store 类型定义

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    const data = await response.json()
    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isLoggedIn,
    userName,
    login,
    logout
  }
}, {
  persist: true
})

使用 Store


import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

// Actions
const { login, logout } = userStore

// 表单
const email = ref('')
const password = ref('')

async function handleLogin() {
  await login(email.value, password.value)
}

  <div>
    <p>Welcome, {{ userName }}</p>
    <button>Logout</button>
  </div>

    <button type="submit">Login</button>

Vue Router + TypeScript

路由类型

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true,
    meta: {
      requiresAuth: true
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

路由元信息类型

// types/router.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    roles?: string[]
  }
}

使用路由


import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 获取参数(类型安全)
const userId = computed(() => Number(route.params.id))

// 编程式导航
function goToUser(id: number) {
  router.push({ name: 'User', params: { id } })
}

API 集成

类型安全的 API 客户端

// api/client.ts
import type { ApiResponse } from '@/types'

class ApiClient {
  private baseUrl: string

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }

  private async request(
    path: string,
    options?: RequestInit
  ): Promise {
    const response = await fetch(<code>${this.baseUrl}${path}, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers
      }
    })

    if (!response.ok) {
      throw new Error(HTTP error! status: ${response.status})
    }

    return response.json()
  }

  get(path: string): Promise {
    return this.request(path)
  }

  post(path: string, data: unknown): Promise {
    return this.request(path, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  put(path: string, data: unknown): Promise {
    return this.request(path, {
      method: 'PUT',
      body: JSON.stringify(data)
    })
  }

  delete(path: string): Promise {
    return this.request(path, {
      method: 'DELETE'
    })
  }
}

export const api = new ApiClient('/api')

API 模块

// api/user.ts
import { api } from './client'
import type { User, ApiResponse } from '@/types'

export const userApi = {
  getAll(): Promise {
    return api.get<ApiResponse>('/users').then(r => r.data)
  },

  getById(id: number): Promise {
    return api.get<ApiResponse>(<code>/users/${id}).then(r => r.data)
  },

  create(data: Omit): Promise {
    return api.post<ApiResponse>('/users', data).then(r => r.data)
  },

  update(id: number, data: Partial): Promise {
    return api.put<ApiResponse>(/users/${id}, data).then(r => r.data)
  },

  delete(id: number): Promise {
    return api.delete(/users/${id})
  }
}

使用 API


import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import type { User } from '@/types'

const users = ref([])
const loading = ref(false)
const error = ref(null)

async function fetchUsers() {
  loading.value = true
  error.value = null

  try {
    users.value = await userApi.getAll()
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Unknown error'
  } finally {
    loading.value = false
  }
}

onMounted(fetchUsers)

  <div>Loading...</div>
  <div>{{ error }}</div>
  <ul>
    <li>
      {{ user.name }}
    </li>
  </ul>

表单处理

类型安全的表单


import { ref, reactive } from 'vue'

interface FormData {
  name: string
  email: string
  age: number
  subscribe: boolean
}

interface FormErrors {
  name?: string
  email?: string
  age?: string
}

const form = reactive({
  name: '',
  email: '',
  age: 0,
  subscribe: false
})

const errors = reactive({})

function validate(): boolean {
  let isValid = true

  if (!form.name.trim()) {
    errors.name = 'Name is required'
    isValid = false
  }

  if (!form.email.includes('@')) {
    errors.email = 'Invalid email'
    isValid = false
  }

  if (form.age  150) {
    errors.age = 'Invalid age'
    isValid = false
  }

  return isValid
}

async function handleSubmit() {
  if (!validate()) return

  // 提交表单
  await submitForm(form)
}

    <div>

      <span>{{ errors.name }}</span>
    </div>

    <div>

      <span>{{ errors.email }}</span>
    </div>

    <div>

      <span>{{ errors.age }}</span>
    </div>

    <div>
      <label>

        Subscribe to newsletter
      </label>
    </div>

    <button type="submit">Submit</button>

使用 vee-validate

npm install vee-validate zod @vee-validate/zod

import { useForm, useField } from 'vee-validate'
import { z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'

const schema = toTypedSchema(
  z.object({
    name: z.string().min(1, 'Name is required'),
    email: z.string().email('Invalid email'),
    age: z.number().min(0).max(150)
  })
)

const { handleSubmit, errors } = useForm({
  validationSchema: schema
})

const { value: name } = useField('name')
const { value: email } = useField('email')
const { value: age } = useField('age')

const onSubmit = handleSubmit((values) => {
  console.log(values)
})

工具类型

组件实例类型

import type { ComponentInstance } from 'vue'

// 获取组件实例类型
type MyComponentInstance = ComponentInstance

提取 Props 类型

import type { ExtractPropTypes } from 'vue'

const propsDefinition = {
  title: String,
  count: {
    type: Number,
    default: 0
  }
} as const

type Props = ExtractPropTypes
// { title: string; count: number }

组件自定义类型

// 自定义组件类型
declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    MyButton: typeof import('@/components/MyButton.vue')['default']
    MyInput: typeof import('@/components/MyInput.vue')['default']
  }
}

测试

Vitest 配置

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true
  }
})

组件测试

// components/__tests__/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../Button.vue'

describe('Button', () => {
  it('renders with text', () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })
    expect(wrapper.text()).toBe('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

最佳实践

1. 使用 script setup


// 推荐

2. 类型优先

// 先定义类型,再实现
interface User {
  id: number
  name: string
}

const user = ref(null)

3. 使用组合式函数

// 提取可复用逻辑
export function useUser() {
  // ...
}

4. 严格模式

// tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

5. 类型安全的 Props


interface Props {
  title: string
  count?: number
}

const props = withDefaults(defineProps(), {
  count: 0
})

系列总结

恭喜你完成了从 ES6 到 Vue3 + TypeScript 的完整学习之旅!

第一期:TypeScript 基础类型系统
第二期:接口、泛型与类型体操
第三期:项目配置与最佳实践
第四期:TypeScript 与前端框架
第五期:Vue3 + TypeScript 完整集成

你现在掌握了:

  • TypeScript 类型系统
  • Vue3 组合式 API
  • 类型安全的组件开发
  • Pinia 状态管理
  • Vue Router 类型安全
  • API 集成最佳实践

下一步

  • 学习 Nuxt 3(Vue3 全栈框架)
  • 探索 Vite 插件开发
  • 学习服务端渲染
  • 阅读官方文档

进阶资源


恭喜!你已经掌握了 Vue3 + TypeScript 的核心技能。去构建你的下一个项目吧!

Views: 12

09. TypeScript与前端框架:React、Node.js

TypeScript 与前端框架:React、Node.js

TypeScript Frameworks

TypeScript 与现代前端框架是天生一对。这一期,我们学习 TypeScript 在 React 和 Node.js 中的应用。

React + TypeScript

创建项目

# Vite(推荐)
npm create vite@latest my-app -- --template react-ts

# Create React App
npx create-react-app my-app --template typescript

# Next.js
npx create-next-app@latest my-app --typescript

组件类型

函数组件

// 简单组件
const Button: React.FC = () => {
  return <button>Click me</button>
}

// 带 Props 的组件
interface ButtonProps {
  text: string
  onClick: () => void
  disabled?: boolean
}

const Button: React.FC = ({ 
  text, 
  onClick, 
  disabled = false 
}) => {
  return (
    <button disabled="{disabled}">
      {text}
    </button>
  )
}

内联 Props

type UserCardProps = {
  name: string
  email: string
  avatar?: string
}

function UserCard({ name, email, avatar }: UserCardProps) {
  return (
    <div>
      {avatar && <img src="{avatar}" alt="{name}" />}
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

Hooks 类型

useState

// 类型推断
const [count, setCount] = useState(0)

// 显式类型
const [user, setUser] = useState(null)

// 复杂类型
const [items, setItems] = useState([])

useEffect

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数
  }
}, [dependency])

useRef

// DOM 引用
const inputRef = useRef(null)

// 任意值引用
const timerRef = useRef(null)

useContext

interface ThemeContextType {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}

const ThemeContext = createContext(null)

function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

useReducer

type State = {
  count: number
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return { count: action.payload }
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 })

事件类型

function Form() {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
  }

  const handleChange = (e: React.ChangeEvent) => {
    console.log(e.target.value)
  }

  const handleClick = (e: React.MouseEvent) => {
    console.log('clicked')
  }

  return (

      <button>Submit</button>

  )
}

泛型组件

interface ListProps {
  items: T[]
  renderItem: (item: T) => React.ReactNode
}

function List({ items, renderItem }: ListProps) {
  return (
    <ul>
      {items.map((item, index) => (
        <li>{renderItem(item)}</li>
      ))}
    </ul>
  )
}

// 使用
 <span>{user.name}</span>}
/>

高阶组件

interface WithLoadingProps {
  loading?: boolean
}

function withLoading<P>(
  Component: React.ComponentType<P>
): React.FC<P> {
  return ({ loading, ...props }) => {
    if (loading) {
      return <div>Loading...</div>
    }
    return 
  }
}

Node.js + TypeScript

项目设置

mkdir my-project
cd my-project
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Express 示例

npm install express
npm install @types/express --save-dev
import express, { Request, Response, NextFunction } from 'express'

const app = express()
app.use(express.json())

// 类型定义
interface User {
  id: number
  name: string
  email: string
}

// 内存数据库
const users: User[] = []

// 路由
app.get('/users', (req: Request, res: Response) => {
  res.json(users)
})

app.get('/users/:id', (req: Request, res: Response) => {
  const user = users.find(u => u.id === parseInt(req.params.id))
  if (!user) {
    res.status(404).send('User not found')
    return
  }
  res.json(user)
})

app.post('/users', (req: Request<{}, User, Omit>, res: Response) => {
  const newUser: User = {
    id: users.length + 1,
    ...req.body
  }
  users.push(newUser)
  res.status(201).json(newUser)
})

// 错误处理
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack)
  res.status(500).json({ error: err.message })
})

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
})

中间件类型

interface AuthRequest extends Request {
  user?: {
    id: number
    role: string
  }
}

function authMiddleware(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  const token = req.headers.authorization
  if (!token) {
    res.status(401).json({ error: 'Unauthorized' })
    return
  }

  // 验证 token...
  req.user = { id: 1, role: 'admin' }
  next()
}

app.get('/protected', authMiddleware, (req: AuthRequest, res: Response) => {
  res.json({ user: req.user })
})

数据库集成

// Prisma 示例
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function getUsers() {
  const users = await prisma.user.findMany({
    include: { posts: true }
  })
  return users
}

async function createUser(data: { name: string; email: string }) {
  const user = await prisma.user.create({
    data
  })
  return user
}

工具函数

API 客户端

interface HttpClient {
  get(url: string): Promise
  post(url: string, data: unknown): Promise
  put(url: string, data: unknown): Promise
  delete(url: string): Promise
}

class FetchHttpClient implements HttpClient {
  private baseUrl: string

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }

  async get(url: string): Promise {
    const response = await fetch(<code>${this.baseUrl}${url})
    if (!response.ok) {
      throw new Error(HTTP error! status: ${response.status})
    }
    return response.json()
  }

  async post(url: string, data: unknown): Promise {
    const response = await fetch(${this.baseUrl}${url}, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    return response.json()
  }

  // ... 其他方法
}

状态管理

// 简单的状态管理器
type Listener = (state: T) => void

class Store {
  private state: T
  private listeners: Listener[] = []

  constructor(initialState: T) {
    this.state = initialState
  }

  getState(): T {
    return this.state
  }

  setState(newState: Partial) {
    this.state = { ...this.state, ...newState }
    this.listeners.forEach(listener => listener(this.state))
  }

  subscribe(listener: Listener) {
    this.listeners.push(listener)
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener)
    }
  }
}

// 使用
interface AppState {
  user: User | null
  loading: boolean
}

const store = new Store({
  user: null,
  loading: false
})

测试

Jest 配置

npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init

单元测试

// math.ts
export function add(a: number, b: number): number {
  return a + b
}

// math.test.ts
import { add } from './math'

describe('add', () => {
  it('should add two numbers', () => {
    expect(add(1, 2)).toBe(3)
  })
})

React 组件测试

// Button.tsx
interface ButtonProps {
  text: string
  onClick: () => void
}

export const Button: React.FC = ({ text, onClick }) => {
  return <button>{text}</button>
}

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

describe('Button', () => {
  it('should render with text', () => {
    render(<Button> {}} />)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('should call onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button />)
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

实战技巧

类型安全的 API 路由

// 路由定义
type Route = {
  path: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  handler: (req: Request, res: Response) => void | Promise
}

const routes: Route[] = [
  {
    path: '/users',
    method: 'GET',
    handler: getUsers
  },
  {
    path: '/users',
    method: 'POST',
    handler: createUser
  }
]

// 自动注册路由
routes.forEach(route => {
  app[route.method.toLowerCase()](route.path, route.handler)
})

类型安全的环境变量

// env.ts
import { z } from 'zod'

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.string().transform(Number),
  DATABASE_URL: z.string(),
  JWT_SECRET: z.string()
})

export const env = envSchema.parse(process.env)

总结

TypeScript 与框架结合要点:

  • React:组件类型、Hooks 类型、事件类型
  • Node.js:Express 类型、中间件、数据库
  • 测试:Jest + ts-jest
  • 最佳实践:类型安全的 API、环境变量

下一期,我们学习 Vue3 + TypeScript 的完整集成指南。


框架文档

Views: 5