TypeScript 任务管理系统实战:从前端到后端(二十八)

在现代 Web 开发中,TypeScript 已成为提升代码质量和开发效率的重要工具。本文将通过一个完整的任务管理系统项目,详细介绍如何在实际开发中应用 TypeScript。从项目结构设计到具体的功能实现,我们将逐步展示如何构建一个类型安全、高效且易于维护的应用程序。

为什么需要综合项目实战

学习 TypeScript 语法只是第一步,真正掌握这门语言需要通过实际项目来巩固知识。本教程将带你从零开始,构建一个完整的任务管理系统。这个项目不仅涵盖了前端(React + TypeScript)和后端(Node.js + Express + TypeScript)的开发,还展示了如何进行类型定义和状态管理。通过这个项目,你将能够:

  • 掌握 TypeScript 在实际项目中的最佳实践
  • 学会如何组织大型项目的文件结构
  • 理解前后端类型定义的一致性
  • 实现类型安全的 API 和状态管理

项目结构

为了更好地管理和维护代码,我们采用 Monorepo 风格来组织项目结构。Monorepo 允许我们将多个相关模块放在同一个仓库中,便于协同开发和版本控制。

task-manager/
├── client/        # 前端项目
│   ├── public/
│   ├── src/
│   └── vite.config.ts
├── server/        # 后端项目
│   ├── src/
│   └── tsconfig.json
└── package.json

目录结构

每个子项目内部的目录结构按照功能进行划分,确保代码的高内聚低耦合。

client/
├── components/    # React 组件
├── hooks/         # 自定义 Hook
├── services/      # 服务层
├── types/         # 类型定义
├── App.tsx
└── index.tsx

server/
├── controllers/   # 控制器
├── models/        # 数据模型
├── routes/        # 路由
├── services/      # 服务层
├── types/         # 类型定义
└── app.ts

类型定义

在 TypeScript 项目中,类型定义是确保代码质量的关键。我们首先定义项目的核心类型,包括任务的状态、优先级、任务接口等。

src/types/task.ts

// 任务状态枚举
export type TaskStatus = 'pending' | 'in-progress' | 'completed';

// 任务优先级枚举
export type TaskPriority = 'low' | 'medium' | 'high';

// 任务接口定义
export interface Task {
  id: string; // 任务ID
  title: string; // 任务标题
  description?: string; // 任务描述(可选)
  status: TaskStatus; // 任务状态
  priority: TaskPriority; // 任务优先级
  createdAt: string; // 创建时间
  updatedAt: string; // 更新时间
  dueDate?: string; // 截止日期(可选)
  tags?: string[]; // 标签(可选)
}

// 创建任务的输入类型
export interface CreateTaskInput {
  title: string;
  description?: string;
  priority: TaskPriority;
  dueDate?: string;
  tags?: string[];
}

// 更新任务的输入类型
export interface UpdateTaskInput {
  title?: string;
  description?: string;
  status?: TaskStatus;
  priority?: TaskPriority;
  dueDate?: string;
  tags?: string[];
}

// 任务过滤选项
export interface TaskFilter {
  status?: TaskStatus;
  priority?: TaskPriority;
  search?: string;
}

类型分层

将输入类型、输出类型和过滤类型分开定义,有助于提高代码的可维护性和可读性。例如,CreateTaskInput 和 UpdateTaskInput 分别用于创建和更新任务时的数据验证。

API 类型定义

为了确保前后端的一致性和类型安全,我们需要定义 API 相关的类型。

src/types/api.ts

// 通用 API 响应类型
export interface ApiResponse
<T> {
  success: boolean;
  data?: T;
  error?: string;
  message?: string;
}

// 分页元数据
export interface PaginationMeta {
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
}

// 分页响应类型
export interface PaginatedResponse
<T> {
  items: T[];
  meta: PaginationMeta;
}

// 请求错误类型
export interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string>;
}

// HTTP 方法类型
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

// 任务相关的 API 端点
export interface TaskEndpoints {
  getAll: '/api/tasks';
  getById: '/api/tasks/:id';
  create: '/api/tasks';
  update: '/api/tasks/:id';
  delete: '/api/tasks/:id';
}

API 类型

通过定义统一的响应格式和错误处理类型,我们可以简化前后端的对接过程,减少调试时间。

任务服务层

任务服务层负责实现任务管理的业务逻辑。我们将这些逻辑封装在一个服务类中,以便于测试和维护。

src/services/taskService.ts

// 导入类型定义
import { Task, CreateTaskInput, UpdateTaskInput, TaskFilter, TaskStatus, TaskPriority } from '../types/task';

// 生成唯一ID
function generateId(): string {
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

// 模拟数据库(内存存储)
let tasks: Task[] = [
  {
    id: '1',
    title: '学习 TypeScript',
    description: '掌握 TypeScript 基础和高级特性',
    status: 'completed',
    priority: 'high',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    tags: ['学习', 'TypeScript']
  },
  {
    id: '2',
    title: '开发任务管理系统',
    description: '使用 React + TypeScript 开发',
    status: 'in-progress',
    priority: 'high',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    tags: ['项目', '实战']
  }
];

// 任务服务类
class TaskService {
  // 获取所有任务
  getAll(filter?: TaskFilter): Task[] {
    let result = [...tasks];

    if (filter) {
      if (filter.status) {
        result = result.filter(t => t.status === filter.status);
      }

      if (filter.priority) {
        result = result.filter(t => t.priority === filter.priority);
      }

      if (filter.search) {
        const search = filter.search.toLowerCase();
        result = result.filter(t =>
          t.title.toLowerCase().includes(search) ||
          t.description?.toLowerCase().includes(search)
        );
      }
    }

    return result;
  }

  // 根据ID获取任务
  getById(id: string): Task | undefined {
    return tasks.find(t => t.id === id);
  }

  // 创建任务
  create(input: CreateTaskInput): Task {
    const now = new Date().toISOString();
    const task: Task = {
      id: generateId(),
      title: input.title,
      description: input.description,
      status: 'pending',
      priority: input.priority,
      createdAt: now,
      updatedAt: now,
      dueDate: input.dueDate,
      tags: input.tags
    };

    tasks.push(task);
    return task;
  }

  // 更新任务
  update(id: string, input: UpdateTaskInput): Task | null {
    const index = tasks.findIndex(t => t.id === id);

    if (index === -1) return null;

    const task = tasks[index];
    const updated: Task = {
      ...task,
      ...input,
      updatedAt: new Date().toISOString()
    };

    tasks[index] = updated;
    return updated;
  }

  // 删除任务
  delete(id: string): boolean {
    const index = tasks.findIndex(t => t.id === id);

    if (index === -1) return false;

    tasks.splice(index, 1);
    return true;
  }

  // 更新任务状态
  updateStatus(id: string, status: TaskStatus): Task | null {
    return this.update(id, { status });
  }
}

// 导出服务实例
export const taskService = new TaskService();

服务层

将业务逻辑集中在服务层,不仅可以提高代码的可测试性,还可以方便地进行维护和扩展。

自定义 Hook

在 React 应用中,使用自定义 Hook 可以将状态管理和业务逻辑封装起来,使组件更加简洁和易于复用。

src/hooks/useTasks.ts

// 导入 React Hooks 和类型

import { useState, useEffect, useCallback } from 'react';
import { Task, CreateTaskInput, UpdateTaskInput, TaskFilter, TaskStatus, TaskPriority } from '../types/task';
import { taskService } from '../services/taskService';

// Hook 返回的状态类型
interface UseTasksReturn {
  tasks: Task[];
  loading: boolean;
  error: string | null;
  filter: TaskFilter;
  createTask: (input: CreateTaskInput) => Promise
<void>;
  updateTask: (id: string, input: UpdateTaskInput) => Promise
<void>;
  deleteTask: (id: string) => Promise
<void>;
  updateStatus: (id: string, status: TaskStatus) => Promise
<void>;
  setFilter: (filter: TaskFilter) => void;
  refresh: () => void;
}

// 初始化默认过滤器
const defaultFilter: TaskFilter = {};

export function useTasks(): UseTasksReturn {
  // 任务列表状态
  const [tasks, setTasks] = useState<Task[]>([]);
  // 加载状态
  const [loading, setLoading] = useState(true);
  // 错误状态
  const [error, setError] = useState<string | null>(null);
  // 过滤条件
  const [filter, setFilter] = useState
<TaskFilter>(defaultFilter);

  // 加载任务列表
  const loadTasks = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const data = await taskService.getAll(filter);
      setTasks(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : '加载失败');
    } finally {
      setLoading(false);
    }
  }, [filter]);

  // 初始加载和过滤器变化时重新加载
  useEffect(() => {
    loadTasks();
  }, [loadTasks]);

  // 创建任务
  const createTask = useCallback(async (input: CreateTaskInput) => {
    try {
      await taskService.create(input);
      await loadTasks();
    } catch (err) {
      setError(err instanceof Error ? err.message : '创建失败');
    }
  }, [loadTasks]);

  // 更新任务
  const updateTask = useCallback(async (id: string, input: UpdateTaskInput) => {
    try {
      await taskService.update(id, input);
      await loadTasks();
    } catch (err) {
      setError(err instanceof Error ? err.message : '更新失败');
    }
  }, [loadTasks]);

  // 删除任务
  const deleteTask = useCallback(async (id: string) => {
    try {
      await taskService.delete(id);
      await loadTasks();
    } catch (err) {
      setError(err instanceof Error ? err.message : '删除失败');
    }
  }, [loadTasks]);

  // 更新任务状态
  const updateStatus = useCallback(async (id: string, status: TaskStatus) => {
    try {
      await taskService.updateStatus(id, status);
      await loadTasks();
    } catch (err) {
      setError(err instanceof Error ? err.message : '状态更新失败');
    }
  }, [loadTasks]);

  // 刷新任务列表
  const refresh = useCallback(() => {
    loadTasks();
  }, [loadTasks]);

  return {
    tasks,
    loading,
    error,
    filter,
    createTask,
    updateTask,
    deleteTask,
    updateStatus,
    setFilter,
    refresh
  };
}

自定义 Hook

通过将状态管理和业务逻辑封装在自定义 Hook 中,我们可以使组件更加简洁和易于维护。这种方式不仅提高了代码的可复用性,还减少了重复代码。

React 组件

在前端部分,我们将使用 React 组件来构建用户界面。每个组件都将利用 useTasks Hook 来管理任务状态和操作。

示例组件:src/components/TaskList.tsx

import React from 'react';

import { useTasks } from '../hooks/useTasks';
import { Task } from '../types/task';

const TaskList: React.FC = () => {
  const { tasks, loading, error, createTask, updateTask, deleteTask, updateStatus, setFilter, refresh } = useTasks();

  const handleCreateTask = async () => {
    const input: CreateTaskInput = {
      title: '新任务',
      priority: 'medium'
    };
    await createTask(input);
  };

  const handleUpdateTask = async (id: string) => {
    const input: UpdateTaskInput = {
      title: '更新后的任务',
      priority: 'high'
    };
    await updateTask(id, input);
  };

  const handleDeleteTask = async (id: string) => {
    await deleteTask(id);
  };

  const handleUpdateStatus = async (id: string) => {
    await updateStatus(id, 'completed');
  };

  if (loading) return 
<div>加载中...</div>;
  if (error) return 
<div>{error}</div>;

  return (

<div>
      <button onClick={handleCreateTask}>创建任务</button>

<ul>
        {tasks.map(task => (
          <li key={task.id}>
            <span>{task.title}</span>
            <button onClick={() => handleUpdateTask(task.id)}>更新</button>
            <button onClick={() => handleDeleteTask(task.id)}>删除</button>
            <button onClick={() => handleUpdateStatus(task.id)}>完成</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TaskList;

组件说明

在这个示例组件中,我们展示了如何使用 useTasks Hook 来管理任务列表的状态和操作。通过简单的按钮点击事件,我们可以创建、更新、删除和更改任务状态。

总结

通过本教程,我们详细介绍了如何使用 TypeScript 构建一个任务管理系统。从项目结构设计到具体的类型定义、服务层实现以及前端组件开发,每一个环节都遵循了最佳实践。希望这篇文章能帮助你在实际项目中更好地应用 TypeScript,提升代码质量和开发效率。如果你有任何问题或建议,欢迎在评论区留言交流。