news 2026/5/8 15:29:36

TypeScript全栈开发实践:基于Express与React构建类型安全TODO应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript全栈开发实践:基于Express与React构建类型安全TODO应用

1. 项目概述与核心价值

最近在整理自己的个人项目时,翻出了一个几年前用 TypeScript 写的全栈 TODO 应用。这个项目麻雀虽小,五脏俱全,从后端 API 到前端界面,再到数据库操作,完整地走了一遍现代 Web 应用开发的流程。我给它起了个内部代号叫 “kissy24/use-cursor”,听起来有点神秘,其实就是个练手项目。今天把它翻出来,一方面是做个技术复盘,另一方面也是给刚接触全栈开发,特别是想用 TypeScript 统一前后端语言的朋友们一个可以直接参考的“脚手架”。这个项目采用了 Express + TypeORM + SQLite 作为后端技术栈,前端则是经典的 React 配合 Material-UI 组件库。整个项目结构清晰,没有引入过于复杂的状态管理或构建工具,非常适合作为理解全栈开发基础概念和 TypeScript 工程化实践的入门案例。

这个项目解决了什么问题呢?首先,它展示了一个前后端都用 TypeScript 开发的最小可行产品(MVP)是如何组织的。很多教程要么只讲前端,要么只讲后端,真正把两者串联起来,并且共享类型定义、统一开发体验的完整例子并不多。其次,它实践了使用 TypeORM 这类 ORM(对象关系映射)工具来操作数据库,这对于不想写原生 SQL,又想保证类型安全的后端开发来说,是个很舒服的选择。最后,整个项目的搭建和运行非常简单,依赖少,配置清晰,你完全可以把它当作一个模板,在此基础上快速开发自己的小应用。无论你是想学习全栈开发,还是需要一个简单的个人任务管理工具,这个项目都能提供直接的参考价值。

2. 技术栈选型与架构设计思路

2.1 为什么选择这些技术?

当初构思这个项目时,我的核心目标是:用 TypeScript 实现端到端的类型安全,并且让开发体验尽可能简单、一致。基于这个目标,我逐一选择了以下技术栈,并在这里分享一下背后的考量。

后端:Node.js + Express + TypeORM + SQLite

  • Node.js & Express:这是 Node.js 生态里最经典、最轻量的 Web 框架组合。Express 的路由和中间件机制足够清晰,学习成本低,能让我把精力集中在业务逻辑和 TypeScript 集成上,而不是框架本身的各种概念上。对于一个小型 TODO 应用来说,它的性能完全过剩。
  • TypeORM:这是选型中的关键一环。传统的 Node.js 后端操作数据库,要么用mysqlpg这样的驱动直接写 SQL,要么用 Sequelize 这类 ORM。TypeORM 的最大优势在于它对 TypeScript 的原生支持。我可以直接定义一个 TypeScript 的Todo实体类,TypeORM 不仅能根据这个类自动生成数据库表结构,还能在代码的每一处(查询、插入、更新)都提供完整的类型提示和检查。这极大地减少了因字段名拼写错误或类型不匹配导致的运行时错误。
  • SQLite:选择 SQLite 纯粹是为了“简单”。它不需要安装和配置独立的数据库服务,数据就存储在一个本地.sqlite文件中。这对于开发、测试以及像 TODO 这样的轻量级应用部署来说,简直是零负担。TypeORM 可以无缝对接 SQLite,让本地开发环境搭建变得异常快捷。

前端:React + Material-UI + TypeScript

  • React:作为最主流的前端视图库,其组件化思想与 TypeScript 的接口(Interface)定义能完美结合。组件的 Props 和 State 都可以用 TypeScript 严格定义,这在多人协作或项目维护时,能清晰地约束数据流,避免传递错误的数据类型。
  • Material-UI (MUI):我不想在 UI 样式上花费太多时间,但又希望应用看起来足够现代、专业。MUI 提供了一整套遵循 Material Design 设计语言的、高质量的 React 组件。我只需要像搭积木一样组合这些组件,就能快速得到一个美观且响应式的界面,把精力留给业务逻辑与前后端联调。
  • TypeScript (前后端共享):这是整个项目的“灵魂”。我在后端定义了一个Todo实体接口,这个接口可以(通过一些工程化手段)直接共享给前端。这意味着,前端在调用 API、处理返回的 TODO 数据时,使用的类型定义和后端数据库模型、API 响应格式是完全一致的。从根本上杜绝了前后端对数据理解不一致的“扯皮”问题。

2.2 项目整体架构解析

整个项目采用了经典的前后端分离架构,但通过 Monorepo 的形式组织在一起,便于统一管理。

kissy24-use-cursor/ ├── client/ # 前端 React 应用 │ ├── public/ │ ├── src/ │ │ ├── components/ # React 组件 (TodoList, TodoItem等) │ │ ├── types/ # TypeScript 类型定义 (可共享后端) │ │ ├── App.tsx │ │ └── index.tsx │ ├── package.json │ └── tsconfig.json ├── server/ # 后端 Express 应用 │ ├── src/ │ │ ├── entity/ # TypeORM 实体 (Todo.ts) │ │ ├── routes/ # Express 路由 (todos.ts) │ │ └── index.ts # 应用入口 │ ├── package.json │ └── tsconfig.json ├── package.json (根目录) # 统一脚本命令,如同时启动前后端 └── todo.sqlite # SQLite 数据库文件 (运行时生成)

数据流设计

  1. 用户在浏览器中操作前端界面(添加、完成、删除任务)。
  2. 前端 React 组件发起对后端 Express API 的 HTTP 请求(Fetch 或 Axios)。
  3. Express 接收到请求,由对应的路由处理器处理。
  4. 路由处理器调用 TypeORM 的 Repository,对todo.sqlite数据库进行增删改查操作。
  5. TypeORM 将数据库操作结果(已转换为Todo实体对象)返回给路由处理器。
  6. 路由处理器将对象以 JSON 格式响应给前端。
  7. 前端接收到响应,更新 React 组件的状态,并重新渲染 UI,反馈给用户。

这个流程中,从数据库实体到 API 响应,再到前端组件状态,Todo的类型定义贯穿始终,保证了整个数据流的安全与可靠。

注意:在实际项目中,前后端共享类型需要一些配置。一种简单的方式是将后端的实体接口或类型定义文件单独放到一个目录(如shared/),然后通过npm link或直接引用相对路径的方式让前后端项目都依赖它。更工程化的做法是使用 Monorepo 工具(如 Lerna, Nx)或构建工具(将共享代码编译成 npm 包)。在本示例项目中,为了极简,我可能会选择在前后端分别定义相同的接口,但这在大型项目中是不可取的,务必建立可靠的共享机制。

3. 后端核心实现详解

3.1 数据库实体(Entity)定义

这是 TypeORM 的核心,也是我们整个应用的“数据模型”。在server/src/entity/Todo.ts中,我们定义Todo实体。

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity() // 装饰器,表明这是一个 TypeORM 实体,对应数据库中的一张表 export class Todo { @PrimaryGeneratedColumn() // 装饰器,定义自增主键 ID id: number; @Column({ type: 'text' }) // 装饰器,定义文本类型的列 title: string; @Column({ default: false }) // 装饰器,定义布尔列,默认值为 false completed: boolean; @CreateDateColumn() // 装饰器,自动记录创建时间 createdAt: Date; @UpdateDateColumn() // 装饰器,自动记录更新时间 updatedAt: Date; }

代码解读与注意事项

  • 装饰器(Decorators):TypeORM 大量使用了 TypeScript 装饰器来定义元数据。@Entity()@Column()这些就是告诉 TypeORM:“请按照我的描述来创建和管理数据库表”。
  • @PrimaryGeneratedColumn():这通常生成一个名为id的整数主键,并且是自增的。这是每条记录的唯一标识。
  • @Column({ type: 'text' }):明确指定数据库字段类型为text。虽然 TypeORM 通常能从 TypeScript 的string类型推断,但显式声明更稳妥,特别是涉及字符串长度时(如varchar(255))。
  • @Column({ default: false }):为completed字段设置默认值false。这意味着当创建一条新的 TODO 时,如果你不提供completed值,数据库会自动将其设为false(未完成)。
  • @CreateDateColumn@UpdateDateColumn:这两个是 TypeORM 提供的特殊列。它们会自动在插入时设置当前时间,并在更新时(仅@UpdateDateColumn)刷新时间。强烈建议为重要数据模型加上这两个时间戳,对于问题排查和数据审计非常有用。

实操心得:在定义实体时,务必和你的业务需求对齐。比如,如果 TODO 需要优先级,可以加一个priority: number字段;如果需要分类,可以加category: string或者建立另一个Category实体并配置关联关系(@ManyToOne)。TypeORM 支持一对一、一对多、多对多等丰富关系,这是它比直接写 SQL 方便的地方。

3.2 Express 路由与控制器

路由定义了 API 的端点(Endpoint)和如何处理到达这些端点的请求。我们在server/src/routes/todos.ts中实现。

import { Router, Request, Response } from 'express'; import { AppDataSource } from '../data-source'; // 数据库连接源 import { Todo } from '../entity/Todo'; const router = Router(); const todoRepository = AppDataSource.getRepository(Todo); // 获取 Todo 实体的操作仓库 // 1. 获取所有 TODO router.get('/', async (req: Request, res: Response) => { try { const todos = await todoRepository.find({ order: { createdAt: 'DESC' } }); // 按创建时间倒序 res.json(todos); } catch (error) { console.error('获取 TODOs 失败:', error); res.status(500).json({ message: '服务器内部错误' }); } }); // 2. 创建新的 TODO router.post('/', async (req: Request, res: Response) => { try { const { title } = req.body; if (!title || title.trim() === '') { return res.status(400).json({ message: '标题不能为空' }); } const newTodo = todoRepository.create({ title }); // 使用仓库创建实体实例 const savedTodo = await todoRepository.save(newTodo); // 保存到数据库 res.status(201).json(savedTodo); // 返回创建成功的对象,包含生成的 id } catch (error) { console.error('创建 TODO 失败:', error); res.status(500).json({ message: '服务器内部错误' }); } }); // 3. 更新 TODO (标记完成/未完成,或修改标题) router.put('/:id', async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const { title, completed } = req.body; // 先查找是否存在 let todoToUpdate = await todoRepository.findOneBy({ id }); if (!todoToUpdate) { return res.status(404).json({ message: '未找到该 TODO' }); } // 更新字段(这里做简单合并,实际应根据业务需求更精细地控制) if (title !== undefined) todoToUpdate.title = title; if (completed !== undefined) todoToUpdate.completed = completed; const updatedTodo = await todoRepository.save(todoToUpdate); // 保存更新 res.json(updatedTodo); } catch (error) { console.error('更新 TODO 失败:', error); res.status(500).json({ message: '服务器内部错误' }); } }); // 4. 删除 TODO router.delete('/:id', async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const deleteResult = await todoRepository.delete(id); if (deleteResult.affected === 0) { return res.status(404).json({ message: '未找到该 TODO' }); } res.status(204).send(); // 成功删除,无返回内容 } catch (error) { console.error('删除 TODO 失败:', error); res.status(500).json({ message: '服务器内部错误' }); } }); export default router;

关键点解析

  • 错误处理:每个路由都使用了try...catch包裹。这是生产环境的基本要求,防止未处理的 Promise 拒绝导致服务器崩溃。我们向客户端返回了适当的 HTTP 状态码(如 400 请求错误,404 未找到,500 服务器错误)和 JSON 格式的错误信息。
  • 输入验证:在POST /路由中,我们检查了title是否为空。这是一个最基本的验证。对于更复杂的应用,应该使用像class-validator这样的库,配合 TypeORM 的实体装饰器进行声明式验证。
  • TypeORM Repository 模式todoRepository提供了对Todo实体进行增删改查的各种方法(find,findOneBy,create,save,delete)。它的操作返回的都是Todo实体或实体数组,类型安全。
  • HTTP 状态码:正确使用状态码是 RESTful API 设计的好习惯。201 Created用于创建成功,204 No Content用于删除成功,404 Not Found用于资源不存在。

3.3 应用入口与数据库连接

这是后端的起点,server/src/index.ts

import 'reflect-metadata'; // TypeORM 的依赖,必须首先导入 import express from 'express'; import cors from 'cors'; // 处理跨域请求 import { AppDataSource } from './data-source'; import todosRouter from './routes/todos'; const app = express(); const PORT = process.env.PORT || 3000; // 环境变量中获取端口,默认 3000 // 中间件配置 app.use(cors()); // 允许前端跨域访问 app.use(express.json()); // 解析请求体中的 JSON 数据 // 数据库初始化 AppDataSource.initialize() .then(() => { console.log('数据库连接成功!'); // 路由挂载 app.use('/api/todos', todosRouter); // 启动服务器 app.listen(PORT, () => { console.log(`后端服务器运行在 http://localhost:${PORT}`); }); }) .catch((error) => { console.error('数据库连接失败:', error); });

>import { DataSource } from 'typeorm'; import { Todo } from './entity/Todo'; export const AppDataSource = new DataSource({ type: 'sqlite', // 数据库类型 database: 'todo.sqlite', // 数据库文件路径 synchronize: true, // 开发环境:自动同步实体到数据库表结构(生产环境务必关闭!) logging: true, // 开启 SQL 日志,方便调试 entities: [Todo], // 注册实体 // 如果有多个实体,就写成 [Todo, User, ...] });

重要警告

  • synchronize: true:这个配置在开发时非常方便,它会根据你的实体类定义,自动创建或修改数据库表结构。但是,在生产环境中必须将其设置为false。因为自动同步可能导致数据丢失(例如,它可能会删除列或表)。生产环境应该使用 TypeORM 的迁移(Migration)功能来管理数据库结构变更。
  • CORS 中间件:因为前端运行在localhost:3000,后端运行在localhost:3000(或其它端口),浏览器出于安全考虑会阻止这种跨域请求。app.use(cors())简单启用了所有跨域请求,对于开发没问题。在生产环境,你应该配置具体的来源(origin),例如app.use(cors({ origin: 'https://yourfrontend.com' }))

4. 前端核心实现详解

4.1 类型定义与 API 服务层

为了实现类型安全,我们首先定义与后端匹配的Todo类型。可以放在client/src/types/todo.ts

// 这与后端的 Todo 实体结构基本一致 export interface Todo { id: number; title: string; completed: boolean; createdAt: string; // 注意:JSON 序列化后 Date 会变成 string updatedAt: string; } // 创建新的 Todo 时需要的参数(通常不需要 id 和 timestamps) export type CreateTodoDto = Pick<Todo, 'title'>; // 只取 title 属性 // 或者更明确地: // export interface CreateTodoDto { title: string; } // 更新 Todo 时需要的参数 export type UpdateTodoDto = Partial<Pick<Todo, 'title' | 'completed'>>; // title 和 completed 都是可选的

接下来,我们创建一个 API 服务模块来封装所有与后端通信的逻辑,client/src/services/todoApi.ts

import { Todo, CreateTodoDto, UpdateTodoDto } from '../types/todo'; const API_BASE_URL = 'http://localhost:3000/api/todos'; // 后端 API 地址 export const todoApi = { // 获取所有 Todo async getAll(): Promise<Todo[]> { const response = await fetch(API_BASE_URL); if (!response.ok) { throw new Error(`获取列表失败: ${response.statusText}`); } return await response.json(); }, // 创建 Todo async create(todoData: CreateTodoDto): Promise<Todo> { const response = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(todoData), }); if (!response.ok) { throw new Error(`创建失败: ${response.statusText}`); } return await response.json(); }, // 更新 Todo async update(id: number, updateData: UpdateTodoDto): Promise<Todo> { const response = await fetch(`${API_BASE_URL}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData), }); if (!response.ok) { throw new Error(`更新失败: ${response.statusText}`); } return await response.json(); }, // 删除 Todo async delete(id: number): Promise<void> { const response = await fetch(`${API_BASE_URL}/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`删除失败: ${response.statusText}`); } // 204 No Content 没有返回体 }, };

设计思路

  • 职责分离:将数据获取逻辑从 UI 组件中抽离出来,使组件更专注于渲染和用户交互。这使得代码更清晰,也便于未来替换底层 HTTP 库(比如从fetch换成axios)。
  • 类型安全:每个方法都明确了参数和返回值的 TypeScript 类型。当你调用todoApi.create({ title: '学习 TypeScript' })时,如果传参不符合CreateTodoDto类型,编译器会立即报错。
  • 错误处理:这里使用throw new Error将 HTTP 错误转换为 JavaScript 异常,由调用方(通常是组件)通过try...catch.catch()来处理。这是一种常见的模式。

4.2 主组件与状态管理

client/src/App.tsx中,我们构建主要的应用组件。对于这个小应用,我们使用 React 的useStateuseEffectHooks 来管理状态和副作用就足够了,无需引入 Redux 或 Context 等复杂状态管理库。

import React, { useState, useEffect } from 'react'; import { Container, Typography, TextField, Button, List, Paper, CircularProgress, Alert, Box, } from '@mui/material'; import { Todo } from './types/todo'; import { todoApi } from './services/todoApi'; import TodoItem from './components/TodoItem'; // 假设有一个子组件 function App() { // 状态定义 const [todos, setTodos] = useState<Todo[]>([]); const [newTodoTitle, setNewTodoTitle] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); // 初始化:组件挂载时获取 Todo 列表 useEffect(() => { fetchTodos(); }, []); const fetchTodos = async () => { setLoading(true); setError(null); try { const data = await todoApi.getAll(); setTodos(data); } catch (err) { setError(err instanceof Error ? err.message : '获取数据失败'); console.error(err); } finally { setLoading(false); } }; // 处理添加新的 Todo const handleAddTodo = async (e: React.FormEvent) => { e.preventDefault(); if (!newTodoTitle.trim()) return; setError(null); try { const createdTodo = await todoApi.create({ title: newTodoTitle }); // 乐观更新:直接在前端列表中添加,避免重新请求整个列表 setTodos([createdTodo, ...todos]); setNewTodoTitle(''); // 清空输入框 } catch (err) { setError(err instanceof Error ? err.message : '添加失败'); console.error(err); } }; // 处理切换完成状态 const handleToggleComplete = async (id: number, completed: boolean) => { try { const updatedTodo = await todoApi.update(id, { completed: !completed }); // 更新本地状态 setTodos(todos.map(todo => (todo.id === id ? updatedTodo : todo))); } catch (err) { setError(err instanceof Error ? err.message : '更新状态失败'); console.error(err); } }; // 处理删除 const handleDelete = async (id: number) => { if (!window.confirm('确定要删除这个任务吗?')) return; try { await todoApi.delete(id); // 更新本地状态 setTodos(todos.filter(todo => todo.id !== id)); } catch (err) { setError(err instanceof Error ? err.message : '删除失败'); console.error(err); } }; return ( <Container maxWidth="sm" sx={{ mt: 4 }}> <Typography variant="h4" component="h1" gutterBottom align="center"> TypeScript TODO 应用 </Typography> <Paper elevation={3} sx={{ p: 3, mb: 3 }}> <form onSubmit={handleAddTodo}> <Box display="flex" gap={1}> <TextField fullWidth variant="outlined" label="添加新任务..." value={newTodoTitle} onChange={(e) => setNewTodoTitle(e.target.value)} size="small" /> <Button type="submit" variant="contained" disabled={!newTodoTitle.trim()}> 添加 </Button> </Box> </form> </Paper> {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} <Paper elevation={2}> {loading ? ( <Box display="flex" justifyContent="center" p={3}> <CircularProgress /> </Box> ) : ( <List> {todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} onToggleComplete={handleToggleComplete} onDelete={handleDelete} /> ))} {todos.length === 0 && ( <Typography align="center" color="text.secondary" sx={{ py: 4 }}> 暂无任务,添加一个吧! </Typography> )} </List> )} </Paper> </Container> ); } export default App;

状态管理策略

  • 单一数据源:所有 TODO 数据都存储在todos状态中,这保证了 UI 是状态的一个函数,渲染结果可预测。
  • 乐观更新(Optimistic Update):在handleAddTodo中,我们假设 API 调用会成功,在等待响应时就直接更新了本地状态 (setTodos([createdTodo, ...todos]))。这能带来更快的用户体验。如果 API 调用失败,我们需要回滚状态并显示错误。本例中为了简化,采用了等待 API 成功后再更新的“保守”策略。对于“切换完成状态”这种操作,乐观更新体验提升更明显。
  • 副作用管理useEffect用于在组件加载时获取初始数据。注意它的依赖数组是空[],表示只运行一次。

4.3 TodoItem 子组件与 Material-UI 应用

client/src/components/TodoItem.tsx是一个展示单个 TODO 项的展示组件。

import React from 'react'; import { ListItem, ListItemIcon, ListItemText, Checkbox, IconButton, ListItemSecondaryAction, } from '@mui/material'; import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material'; import { Todo } from '../types/todo'; interface TodoItemProps { todo: Todo; onToggleComplete: (id: number, completed: boolean) => void; onDelete: (id: number) => void; } const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggleComplete, onDelete }) => { return ( <ListItem dense button // 使整个列表项可点击 onClick={() => onToggleComplete(todo.id, todo.completed)} sx={{ textDecoration: todo.completed ? 'line-through' : 'none', color: todo.completed ? 'text.disabled' : 'text.primary', bgcolor: todo.completed ? 'action.selected' : 'background.paper', }} > <ListItemIcon> <Checkbox edge="start" checked={todo.completed} tabIndex={-1} // 避免双击选中文本 disableRipple // 阻止事件冒泡,避免与 ListItem 的 onClick 冲突 onClick={(e) => e.stopPropagation()} onChange={() => onToggleComplete(todo.id, todo.completed)} /> </ListItemIcon> <ListItemText primary={todo.title} /> <ListItemSecondaryAction> {/* 可以在这里添加编辑按钮 */} {/* <IconButton edge="end" aria-label="edit" size="small" sx={{ mr: 1 }}> <EditIcon /> </IconButton> */} <IconButton edge="end" aria-label="delete" size="small" onClick={() => onDelete(todo.id)} > <DeleteIcon /> </IconButton> </ListItemSecondaryAction> </ListItem> ); }; export default TodoItem;

Material-UI 使用技巧

  • sx属性:这是 MUI v5 推荐的样式方式,类似于内联样式但功能更强大,支持主题访问。我们用它来根据todo.completed状态动态改变文本装饰、颜色和背景。
  • 事件处理:注意CheckboxonClick中调用了e.stopPropagation()。这是因为Checkbox被包裹在可点击的ListItem内部。如果不阻止事件冒泡,点击复选框会触发两次onToggleComplete
  • ListItemSecondaryAction:这是 MUI 提供的用于在列表项右侧放置操作按钮的容器,排版更美观。

5. 项目配置、启动与调试全流程

5.1 从零开始的环境搭建

假设你从零开始复现这个项目,以下是详细步骤:

  1. 初始化项目根目录

    mkdir kissy24-use-cursor cd kissy24-use-cursor npm init -y # 创建 package.json
  2. 创建后端项目结构

    mkdir server cd server npm init -y

    server目录下安装依赖:

    npm install express cors sqlite3 typeorm reflect-metadata npm install --save-dev typescript ts-node @types/node @types/express @types/cors nodemon

    初始化 TypeScript 配置:

    npx tsc --init

    编辑生成的tsconfig.json,确保包含以下关键配置:

    { "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "experimentalDecorators": true, // TypeORM 需要 "emitDecoratorMetadata": true, // TypeORM 需要 "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }
  3. 创建前端项目: 回到项目根目录,使用 Create React App 并指定 TypeScript 模板:

    npx create-react-app client --template typescript cd client npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
  4. 配置根目录脚本: 在项目根目录的package.json中添加脚本,方便同时启动前后端:

    { "scripts": { "dev:server": "cd server && npm run dev", "dev:client": "cd client && npm start", "dev:full": "concurrently \"npm run dev:server\" \"npm run dev:client\"" }, "devDependencies": { "concurrently": "^8.0.0" } }

    然后运行npm install concurrently安装这个开发依赖。

5.2 开发服务器启动与访问

按照上述配置完成后:

  1. 启动完整开发环境(推荐): 在项目根目录下运行:

    npm run dev:full

    这个命令会利用concurrently同时启动后端和前端开发服务器。

  2. 分别启动

    • 终端1(后端):在项目根目录npm run dev:server。后端服务通常运行在http://localhost:3000(取决于你的server/src/index.ts中的PORT设置)。
    • 终端2(前端):在项目根目录npm run dev:client。Create React App 默认会启动在http://localhost:3000,如果端口冲突,它会提示你切换到另一个端口(如http://localhost:3001)。
  3. 访问应用: 打开浏览器,访问前端开发服务器的地址(通常是http://localhost:3000http://localhost:3001)。你应该能看到 Material-UI 风格的 TODO 界面。尝试添加、完成、删除任务,所有操作都会通过 API 与后端 SQLite 数据库交互。

5.3 数据库文件与迁移

  • 首次运行:当你第一次启动后端服务器时,TypeORM 会根据synchronize: true的设置,自动在项目根目录(或你配置的路径)创建todo.sqlite文件以及todo表。你不需要手动运行任何 SQL 命令。
  • 查看数据:你可以使用 SQLite 命令行工具或图形化工具(如 DB Browser for SQLite)打开todo.sqlite文件,查看todo表中的数据,验证操作是否成功。
  • 生产环境警告再次强调:在将应用部署到生产环境前,必须>问题现象可能原因排查步骤与解决方案后端启动报错,如Cannot find module 'reflect-metadata'依赖未正确安装或 TypeScript 配置问题。1. 在server目录下运行npm install
    2. 检查server/src/index.ts第一行是否导入了import 'reflect-metadata'
    3. 确认tsconfig.jsonexperimentalDecoratorsemitDecoratorMetadata设为true。前端访问后端 API 时报 CORS 错误。浏览器跨域请求被阻止。1. 确认后端app.use(cors())中间件已正确配置。
    2. 检查前端API_BASE_URL的端口是否与后端服务端口一致。
    3. 如果是生产环境,需配置具体的origin。前端能收到数据,但界面不更新。React 状态更新可能未触发重新渲染,或状态更新逻辑有误。1. 检查setTodos等状态更新函数是否被正确调用。
    2. 使用 React 开发者工具检查组件的 Props 和 State。
    3. 确认更新状态时使用了新的数组/对象(遵循不可变原则),例如setTodos([...todos, newTodo])。创建或更新 TODO 后,数据库没变化。后端 API 逻辑错误,或数据库操作失败但未抛出错误。1. 在后端控制台查看 TypeORM 的 SQL 日志(logging: true)。
    2. 在 API 路由中添加更详细的try-catch,打印错误信息。
    3. 使用 SQLite 工具直接查看todo.sqlite文件。修改了后端实体字段,但数据库表未更新。synchronize: true可能在某些复杂变更下不生效,或缓存问题。1.开发时:可以删除todo.sqlite文件,重启服务让 TypeORM 重新建表(注意:会丢失所有数据!)。
    2. 学习并使用 TypeORM 迁移(Migration)来安全地变更表结构。前端编译 TypeScript 报类型错误。前后端类型定义不一致,或 API 返回的数据格式与预期不符。1. 检查client/src/types/todo.ts是否与后端Todo实体匹配。
    2. 在后端 API 响应处,确保返回的是纯粹的实体对象或明确的 DTO,避免循环引用(TypeORM 实体可能有循环引用,需用@Exclude()装饰器或查询时使用select选项)。

    6.2 性能与优化小技巧

    1. 数据库查询优化:当 TODO 数量很多时,todoRepository.find()会返回所有数据。可以考虑分页:

      // 在后端路由中 const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 20; const skip = (page - 1) * limit; const todos = await todoRepository.find({ order: { createdAt: 'DESC' }, skip, take: limit }); const total = await todoRepository.count(); res.json({ data: todos, total, page, limit });
    2. 前端请求防抖:如果“添加任务”的输入框有实时保存的需求,可以使用防抖(debounce)来避免频繁发送 API 请求。

    3. 环境变量管理:将数据库连接字符串、API 端口、密钥等敏感信息放入.env文件,使用dotenv库读取。永远不要将敏感信息硬编码在代码中或提交到版本库。

    6.3 项目扩展思路

    这个基础项目可以作为一个起点,向多个方向扩展:

    • 用户认证:添加User实体,使用 JWT(JSON Web Tokens)实现登录/注册,并将 TODO 与用户关联(@ManyToOne)。
    • 更丰富的功能:为 TODO 添加优先级、截止日期、标签、分类、项目分组等功能。
    • 状态管理升级:当应用复杂后,可以考虑引入 Zustand、Redux Toolkit 或 React Context + useReducer 来管理全局状态。
    • API 文档:使用 Swagger/OpenAPI 自动生成 API 文档。
    • 单元测试与集成测试:为后端路由和服务添加 Jest 测试,为前端组件添加 React Testing Library 测试。
    • 容器化部署:编写Dockerfiledocker-compose.yml,将应用容器化,便于部署到云服务器。
    • 更换数据库:TypeORM 支持多种数据库。只需修改>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 15:29:21

EDA行业逆势增长:从设计工具到产业基石的核心价值解析

1. 行业韧性背后的深层逻辑&#xff1a;为什么EDA能逆势增长&#xff1f; 最近和几个在芯片设计公司工作的老朋友聊天&#xff0c;大家都有一个共同的感受&#xff1a;尽管全球半导体产业链波折不断&#xff0c;但咱们手里吃饭的家伙——那些EDA工具&#xff0c;其市场表现却出…

作者头像 李华
网站建设 2026/5/8 15:28:49

AI应用开发者的MCP服务器精选指南:从原理到实战集成

1. 项目概述&#xff1a;一个为AI应用开发者准备的“瑞士军刀”集合 如果你正在构建基于大型语言模型&#xff08;LLM&#xff09;的智能应用&#xff0c;比如一个能帮你分析数据的聊天机器人&#xff0c;或者一个能自动处理文档的工作流&#xff0c;那你肯定遇到过这样的困境…

作者头像 李华
网站建设 2026/5/8 15:28:47

2026年商城小程序如何设置品牌展示功能?

截至2026年Q1&#xff0c;微信小程序日活用户已突破5.8亿&#xff0c;其中电商类小程序占比超过28%&#xff08;来源&#xff1a;QuestMobile 2026年Q1《中国移动互联网数据报告》&#xff09;。在商品同质化日益严重的商城小程序生态中&#xff0c;品牌展示功能正从"锦上…

作者头像 李华
网站建设 2026/5/8 15:27:44

AI抠图哪个软件好用?2026年最全工具对比与实测分享

最近在做电商产品图处理&#xff0c;我把市面上主流的AI抠图工具都试了一遍&#xff0c;今天就来和大家分享一下我的真实使用体验。说实话&#xff0c;找到一个好用的抠图工具能省下不少时间&#xff0c;尤其是当你需要批量处理证件照、商品图或者去背景的时候。为什么AI抠图成…

作者头像 李华

关于博客

这是一个专注于编程技术分享的极简博客,旨在为开发者提供高质量的技术文章和教程。

订阅更新

输入您的邮箱,获取最新文章更新。

© 2025 极简编程博客. 保留所有权利.