news 2026/7/6 11:45:21

半小时构建全栈Todo应用:基于Spec Coding与AI辅助编程的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
半小时构建全栈Todo应用:基于Spec Coding与AI辅助编程的实战指南

🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度

在实际前端和全栈开发中,一个常见的痛点是从零开始构建一个具备基础增删改查(CRUD)和用户交互的完整应用模块,这个过程往往需要前后端联调、数据库设计、接口编写和页面渲染,耗时数天甚至数周。随着 AI 辅助编程工具的成熟,开发者开始探索如何利用这些工具将重复性、模式化的编码工作自动化,从而将精力聚焦于核心业务逻辑和架构设计。本文将以一个具体的“待办事项(Todo List)”全栈应用为例,演示如何结合 AI 工具(以 Codex 类模型为例)和 Spec Coding(规格化编码)思想,将原本可能需要数天工期的开发流程,压缩到半小时内完成核心功能的搭建。

我们将构建一个典型的全栈应用:前端使用 Vue 3 + TypeScript + Vite,后端使用 Node.js + Express + TypeScript,数据库使用 SQLite。整个流程的核心在于,我们不再手动编写每一行代码,而是通过清晰、结构化的自然语言描述(Spec)来驱动 AI 生成可运行、可集成的代码块,然后进行最小化的手动调整和集成。

1. 理解 Spec Coding 与 AI 辅助编程的工作流

在传统开发中,我们通常先设计数据库,再写后端 API,最后开发前端页面。Spec Coding 是一种思维转变:它将开发重心从“编写代码”转移到“定义规格”。你首先需要清晰地描述你想要的功能、数据结构、API 接口和用户交互,这份描述就是“规格说明书(Spec)”。

AI 辅助编程工具(如基于 Codex 的 Cursor、GitHub Copilot 等)能够理解这份规格,并生成对应的代码。关键在于,你的规格描述必须足够精确、无歧义,并且符合目标技术栈的常见模式。

一个有效的工作流如下:

  1. 环境准备:初始化项目,安装必要的依赖。
  2. 规格定义:为每个模块(数据库模型、API 接口、前端组件)编写清晰的文本描述。
  3. AI 生成:将规格描述输入 AI 工具,生成初始代码。
  4. 手动集成与微调:将生成的代码放入正确的位置,修复可能的导入错误、类型错误或逻辑偏差。
  5. 运行与验证:启动服务,测试功能是否按预期工作。

这个流程的核心挑战在于,你需要具备足够的全栈知识来评估和修正 AI 生成的代码,并设计出能被 AI 高效理解的规格。下面我们进入实战。

2. 环境准备与项目初始化

首先,确保你的本地开发环境已经就绪。你需要 Node.js(建议 LTS 版本,如 18.x 或 20.x)和 npm/yarn/pnpm 包管理器。同时,你需要一个支持 AI 代码补全的编辑器,例如 Cursor 或安装了 GitHub Copilot 的 VS Code。

我们将创建一个 monorepo 风格的项目,前后端代码放在同一个仓库的不同目录下,便于管理。

2.1 创建项目根目录与后端服务

打开终端,执行以下命令:

# 创建项目根目录 mkdir ai-todo-fullstack && cd ai-todo-fullstack # 初始化后端项目(Node.js + Express + TypeScript) mkdir backend && cd backend npm init -y

接下来,安装后端所需依赖。我们可以直接让 AI 生成一个完整的package.json依赖列表,但更可控的方式是自己安装。在backend目录下执行:

npm install express cors sqlite3 npm install -D typescript @types/node @types/express @types/cors ts-node-dev

然后,初始化 TypeScript 配置:

npx tsc --init

我们需要修改生成的tsconfig.json,使其更适合 Node.js 开发。一个基础的配置如下:

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

创建后端源代码目录和入口文件:

mkdir src touch src/index.ts

2.2 创建前端项目

回到项目根目录,初始化前端项目。我们使用 Vite 官方模板快速搭建 Vue + TypeScript 项目。

cd .. npm create vite@latest frontend -- --template vue-ts cd frontend npm install

为了简化 HTTP 请求,我们安装axios

npm install axios

至此,基础的项目结构已经搭建完成:

ai-todo-fullstack/ ├── backend/ # 后端 Node.js 服务 │ ├── src/ │ ├── package.json │ └── tsconfig.json └── frontend/ # 前端 Vue 应用 ├── src/ ├── package.json └── vite.config.ts

3. 使用 Spec Coding 驱动后端开发

后端开发的核心是数据模型和 RESTful API。我们将使用 SQLite 作为数据库,因为它无需额外安装服务,适合快速原型开发。

3.1 定义数据库模型与操作规格

首先,在backend/src目录下创建db.ts文件。现在,我们不是自己写代码,而是向 AI 描述需求。

规格描述(用于 AI 提示):“请创建一个 SQLite 数据库工具文件。使用sqlite3包,以 Promise 风格封装数据库操作。需要创建一个todos表,包含以下字段:id(整数,主键,自增),title(文本,非空),description(文本,可为空),completed(布尔值,默认 false),createdAt(时间戳,默认当前时间)。提供四个函数:initializeDb()用于初始化数据库和创建表;getAllTodos()获取所有待办事项;createTodo(title: string, description?: string)创建新待办;updateTodo(id: number, updates: { title?: string, description?: string, completed?: boolean })更新待办;deleteTodo(id: number)删除待办。请使用 TypeScript 编写。”

将这段描述输入你的 AI 编程工具(例如在 Cursor 中新建db.ts文件,然后使用Cmd+K输入上述提示)。AI 可能会生成类似下面的代码:

// backend/src/db.ts import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; let db: Database | null = null; export async function initializeDb(): Promise<void> { db = await open({ filename: './todos.db', driver: sqlite3.Database }); await db.exec(` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, completed BOOLEAN DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP ) `); } export interface Todo { id: number; title: string; description: string | null; completed: boolean; createdAt: string; } export async function getAllTodos(): Promise<Todo[]> { if (!db) throw new Error('Database not initialized'); return db.all('SELECT * FROM todos ORDER BY createdAt DESC'); } export async function createTodo(title: string, description?: string): Promise<{ id: number }> { if (!db) throw new Error('Database not initialized'); const result = await db.run( 'INSERT INTO todos (title, description) VALUES (?, ?)', title, description || null ); return { id: result.lastID! }; } export async function updateTodo( id: number, updates: { title?: string; description?: string; completed?: boolean } ): Promise<void> { if (!db) throw new Error('Database not initialized'); const setClause: string[] = []; const values: any[] = []; if (updates.title !== undefined) { setClause.push('title = ?'); values.push(updates.title); } if (updates.description !== undefined) { setClause.push('description = ?'); values.push(updates.description); } if (updates.completed !== undefined) { setClause.push('completed = ?'); values.push(updates.completed ? 1 : 0); } if (setClause.length === 0) { return; } values.push(id); await db.run(`UPDATE todos SET ${setClause.join(', ')} WHERE id = ?`, values); } export async function deleteTodo(id: number): Promise<void> { if (!db) throw new Error('Database not initialized'); await db.run('DELETE FROM todos WHERE id = ?', id); }

手动检查与调整:

  1. 检查导入语句是否正确。sqlite3是默认导出,而open是从sqlite包中导入的命名导出,上述代码正确。
  2. SQLite 的BOOLEAN类型实际存储为 0 或 1,所以插入和查询时需要转换。AI 在updateTodo中已经处理了(updates.completed ? 1 : 0),但在getAllTodos返回的Todo接口中,completedboolean类型,SQLite 驱动通常会帮我们转换,但为了保险,我们可以让 AI 再生成一个查询时转换的版本,或者信任驱动。这里我们选择信任驱动。
  3. 确保错误处理存在(Database not initialized)。

3.2 定义 API 路由规格

接下来,创建backend/src/routes/todos.ts文件。向 AI 描述路由规格。

规格描述:“请创建一个 Express 路由文件,用于处理待办事项的 RESTful API。路径前缀为/api/todos。需要实现以下端点:

  1. GET /:获取所有待办事项列表,返回 JSON 数组。
  2. POST /:创建新的待办事项。请求体为 JSON,包含title(字符串,必需)和description(字符串,可选)。成功时返回{ id: number }
  3. PUT /:id:更新指定 ID 的待办事项。请求体为 JSON,可包含title,description,completed。成功返回 204 状态码。
  4. DELETE /:id:删除指定 ID 的待办事项。成功返回 204 状态码。 请使用之前定义的db.ts中的函数。添加必要的错误处理,例如 ID 不存在时返回 404。使用express.json()中间件解析请求体。”

AI 生成的代码可能如下:

// backend/src/routes/todos.ts import { Router } from 'express'; import * as db from '../db'; const router = Router(); router.get('/', async (req, res) => { try { const todos = await db.getAllTodos(); res.json(todos); } catch (error) { console.error('Failed to fetch todos:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.post('/', async (req, res) => { const { title, description } = req.body; if (!title || typeof title !== 'string') { return res.status(400).json({ error: 'Title is required and must be a string' }); } try { const result = await db.createTodo(title, description); res.status(201).json(result); } catch (error) { console.error('Failed to create todo:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.put('/:id', async (req, res) => { const id = parseInt(req.params.id, 10); if (isNaN(id)) { return res.status(400).json({ error: 'Invalid todo ID' }); } const { title, description, completed } = req.body; const updates: any = {}; if (title !== undefined) updates.title = title; if (description !== undefined) updates.description = description; if (completed !== undefined) updates.completed = completed; if (Object.keys(updates).length === 0) { return res.status(400).json({ error: 'No update fields provided' }); } try { // 可选:先检查是否存在 await db.updateTodo(id, updates); res.sendStatus(204); } catch (error) { console.error(`Failed to update todo ${id}:`, error); res.status(500).json({ error: 'Internal server error' }); } }); router.delete('/:id', async (req, res) => { const id = parseInt(req.params.id, 10); if (isNaN(id)) { return res.status(400).json({ error: 'Invalid todo ID' }); } try { await db.deleteTodo(id); res.sendStatus(204); } catch (error) { console.error(`Failed to delete todo ${id}:`, error); res.status(500).json({ error: 'Internal server error' }); } }); export default router;

手动检查与调整:

  1. 检查导入路径../db是否正确。
  2. updateTodo前可以添加一个存在性检查,但为了简化,我们假设更新不存在的 ID 只是无操作。生产环境需要更严谨。
  3. 注意completed字段在请求体中应该是布尔值,AI 生成的代码直接使用了req.body.completed,这符合预期。

3.3 定义主应用入口规格

最后,创建backend/src/index.ts文件。向 AI 描述启动服务器的规格。

规格描述:“请创建一个 Express 服务器入口文件。需要做以下事情:

  1. 导入express,cors
  2. 导入之前创建的todos路由。
  3. 调用initializeDb()初始化数据库。
  4. 使用express.json()中间件解析 JSON 请求体。
  5. 使用cors()中间件允许前端跨域请求。
  6. /api/todos路径映射到todos路由。
  7. 在 3001 端口启动服务器,并打印启动日志。”

AI 生成的代码:

// backend/src/index.ts import express from 'express'; import cors from 'cors'; import { initializeDb } from './db'; import todosRouter from './routes/todos'; const app = express(); const PORT = process.env.PORT || 3001; app.use(cors()); app.use(express.json()); app.use('/api/todos', todosRouter); async function startServer() { try { await initializeDb(); console.log('Database initialized'); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } startServer();

手动检查:

  1. 确保所有导入路径正确。
  2. 端口号(3001)不要与前端开发服务器(通常是 5173)冲突。

现在,后端代码已经生成完毕。在backend目录下,修改package.jsonscripts字段,添加启动命令:

{ "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts", "build": "tsc", "start": "node dist/index.js" } }

运行npm run dev启动后端服务器。如果一切顺利,你将看到数据库初始化和服务器启动的日志。此时,你可以用 API 测试工具(如 curl 或 Postman)测试GET http://localhost:3001/api/todos,应该返回空数组[]

4. 使用 Spec Coding 驱动前端开发

前端开发的核心是组件、状态管理和 API 调用。我们将创建一个简单的待办事项管理界面。

4.1 定义主视图组件规格

首先,修改frontend/src/App.vue。向 AI 描述一个完整的 Todo 应用界面。

规格描述:“请创建一个 Vue 3 单文件组件,使用 Composition API 和 TypeScript。这个组件是一个待办事项管理器。需要包含:

  1. 一个输入框和按钮用于添加新待办(标题必填,描述可选)。
  2. 一个列表展示所有待办事项,每条显示标题、描述、完成状态和创建时间。
  3. 每条待办事项旁边有复选框可以切换完成状态,有编辑按钮和删除按钮。
  4. 点击编辑按钮可以进入编辑模式,内联修改标题和描述。
  5. 使用axios调用后端 API(假设后端运行在http://localhost:3001)。
  6. 使用refreactive管理状态(待办列表、新增待办的标题和描述、编辑状态等)。
  7. onMounted生命周期中获取初始待办列表。
  8. 实现添加、切换完成状态、更新、删除的函数,并调用对应的 API。
  9. 使用简单的样式,例如 Flexbox 布局,让界面清晰即可。”

将这段描述输入 AI,它可能会生成一个较长的App.vue文件。由于篇幅,这里展示关键部分和需要手动调整的地方。

AI 生成的<script setup>部分可能如下:

<!-- frontend/src/App.vue --> <script setup lang="ts"> import { ref, onMounted, reactive } from 'vue'; import axios from 'axios'; const API_BASE = 'http://localhost:3001/api/todos'; interface Todo { id: number; title: string; description: string | null; completed: boolean; createdAt: string; } const todos = ref<Todo[]>([]); const newTodo = reactive({ title: '', description: '' }); const editingTodoId = ref<number | null>(null); const editingTodo = reactive({ title: '', description: '' }); const fetchTodos = async () => { try { const response = await axios.get<Todo[]>(API_BASE); todos.value = response.data; } catch (error) { console.error('Failed to fetch todos:', error); alert('获取待办列表失败'); } }; const addTodo = async () => { if (!newTodo.title.trim()) { alert('标题不能为空'); return; } try { const response = await axios.post<{ id: number }>(API_BASE, { title: newTodo.title, description: newTodo.description || undefined }); // 重新获取列表以显示最新数据(或直接在前端添加) await fetchTodos(); newTodo.title = ''; newTodo.description = ''; } catch (error) { console.error('Failed to add todo:', error); alert('添加待办失败'); } }; const toggleTodo = async (todo: Todo) => { try { await axios.put(`${API_BASE}/${todo.id}`, { completed: !todo.completed }); // 更新本地状态 todo.completed = !todo.completed; } catch (error) { console.error('Failed to toggle todo:', error); alert('更新状态失败'); } }; const startEdit = (todo: Todo) => { editingTodoId.value = todo.id; editingTodo.title = todo.title; editingTodo.description = todo.description || ''; }; const saveEdit = async () => { if (!editingTodoId.value) return; try { await axios.put(`${API_BASE}/${editingTodoId.value}`, { title: editingTodo.title, description: editingTodo.description || undefined }); // 更新本地列表 const index = todos.value.findIndex(t => t.id === editingTodoId.value); if (index !== -1) { todos.value[index].title = editingTodo.title; todos.value[index].description = editingTodo.description; } cancelEdit(); } catch (error) { console.error('Failed to update todo:', error); alert('更新待办失败'); } }; const cancelEdit = () => { editingTodoId.value = null; editingTodo.title = ''; editingTodo.description = ''; }; const deleteTodo = async (id: number) => { if (!confirm('确定删除吗?')) return; try { await axios.delete(`${API_BASE}/${id}`); // 从本地列表移除 todos.value = todos.value.filter(todo => todo.id !== id); } catch (error) { console.error('Failed to delete todo:', error); alert('删除待办失败'); } }; onMounted(() => { fetchTodos(); }); </script>

AI 生成的<template>部分可能如下:

<template> <div class="app"> <h1>AI 全栈待办事项</h1> <div class="add-todo"> <input v-model="newTodo.title" placeholder="输入待办标题" /> <input v-model="newTodo.description" placeholder="输入描述(可选)" /> <button @click="addTodo">添加</button> </div> <ul class="todo-list"> <li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.completed }"> <div v-if="editingTodoId !== todo.id"> <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" /> <div class="todo-content"> <strong>{{ todo.title }}</strong> <p v-if="todo.description">{{ todo.description }}</p> <small>{{ new Date(todo.createdAt).toLocaleString() }}</small> </div> <div class="actions"> <button @click="startEdit(todo)">编辑</button> <button @click="deleteTodo(todo.id)">删除</button> </div> </div> <div v-else class="edit-mode"> <input v-model="editingTodo.title" /> <input v-model="editingTodo.description" /> <button @click="saveEdit">保存</button> <button @click="cancelEdit">取消</button> </div> </li> </ul> </div> </template>

AI 生成的<style>部分可能如下:

<style scoped> .app { max-width: 800px; margin: 0 auto; padding: 20px; font-family: sans-serif; } .add-todo { display: flex; gap: 10px; margin-bottom: 20px; } .add-todo input { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } .todo-list { list-style: none; padding: 0; } .todo-list li { display: flex; align-items: center; padding: 15px; border: 1px solid #eee; margin-bottom: 10px; border-radius: 8px; } .todo-list li.completed { opacity: 0.6; background-color: #f9f9f9; } .todo-list li.completed .todo-content strong { text-decoration: line-through; } .todo-content { flex: 1; margin-left: 10px; } .todo-content p { margin: 5px 0; color: #666; } .todo-content small { color: #999; } .actions { display: flex; gap: 5px; } button { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; background-color: #007bff; color: white; } button:hover { background-color: #0056b3; } .edit-mode { display: flex; gap: 10px; width: 100%; } .edit-mode input { flex: 1; padding: 6px; } </style>

手动检查与调整:

  1. API 基础路径:确认API_BASE与后端服务器地址和端口一致。
  2. 类型定义:检查Todo接口是否与后端返回的数据结构匹配。
  3. 状态更新策略:在addTodotoggleTodosaveEditdeleteTodo函数中,AI 混合使用了“重新获取整个列表”和“直接更新本地状态”两种策略。为了减少不必要的网络请求并提升用户体验,我们通常优先使用直接更新本地状态。但直接更新需要确保与后端操作完全同步,逻辑更复杂。对于快速原型,重新获取列表更稳妥。本文示例中,addTodo使用了重新获取,而toggleTododeleteTodo使用了直接更新,这可能导致状态不一致。为了简单和一致,我们可以将所有操作都改为成功后重新获取列表,或者都改为直接更新。这里我们选择直接更新,但需要更仔细地处理addTodosaveEdit
  4. 编辑模式:AI 生成的编辑模式 UI 是内联的,这没问题。但注意editingTodo.description的初始值处理。

我们可以对addTodosaveEdit进行优化,改为直接更新本地状态:

// 修改 addTodo 函数 const addTodo = async () => { if (!newTodo.title.trim()) { alert('标题不能为空'); return; } try { const response = await axios.post<{ id: number }>(API_BASE, { title: newTodo.title, description: newTodo.description || undefined }); // 直接在前端添加新待办,而不是重新获取 const newTodoItem: Todo = { id: response.data.id, title: newTodo.title, description: newTodo.description || null, completed: false, createdAt: new Date().toISOString() // 注意:这里使用了本地时间,与服务器时间可能不一致。更严谨的做法是让后端返回完整的 Todo 对象。 }; todos.value.unshift(newTodoItem); // 添加到列表前面 newTodo.title = ''; newTodo.description = ''; } catch (error) { console.error('Failed to add todo:', error); alert('添加待办失败'); } }; // 修改 saveEdit 函数 const saveEdit = async () => { if (!editingTodoId.value) return; try { await axios.put(`${API_BASE}/${editingTodoId.value}`, { title: editingTodo.title, description: editingTodo.description || undefined }); // 直接更新本地列表 const index = todos.value.findIndex(t => t.id === editingTodoId.value); if (index !== -1) { todos.value[index].title = editingTodo.title; todos.value[index].description = editingTodo.description; } cancelEdit(); } catch (error) { console.error('Failed to update todo:', error); alert('更新待办失败'); } };

注意:addTodo中我们手动构造了newTodoItem,但createdAt使用了本地时间。在生产环境中,更好的做法是让 POST 接口返回完整的、由服务器生成的待办对象,然后直接将其加入列表。这需要修改后端createTodo的响应。为了保持示例简单,我们暂时这样处理。

4.2 配置前端代理(可选)

为了避免跨域问题,我们可以在前端开发服务器中配置代理。修改frontend/vite.config.ts

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3001', changeOrigin: true, }, }, }, })

配置后,前端代码中的API_BASE可以改为相对路径'/api/todos',Vite 开发服务器会将/api开头的请求代理到后端服务器。

5. 运行验证与功能测试

现在,让我们启动整个应用并进行测试。

5.1 启动后端服务

backend目录下,确保终端正在运行:

npm run dev

你应该看到Database initializedServer is running on http://localhost:3001的日志。

5.2 启动前端服务

在另一个终端,进入frontend目录,运行:

npm run dev

Vite 会启动开发服务器,通常运行在http://localhost:5173

5.3 功能测试

  1. 打开前端页面:在浏览器中访问http://localhost:5173
  2. 添加待办:在输入框中输入标题和描述,点击“添加”按钮。页面列表应立即显示新添加的待办事项。
  3. 切换完成状态:点击某条待办事项前的复选框,其样式应发生变化(如文字中间出现删除线),并且刷新页面后状态应被保持。
  4. 编辑待办:点击某条待办事项的“编辑”按钮,该条目应变为内联编辑模式。修改标题或描述后点击“保存”,修改应生效。点击“取消”则放弃修改。
  5. 删除待办:点击“删除”按钮,确认后该条目应从列表中消失。
  6. 数据持久化:刷新浏览器页面,所有待办事项应重新从后端加载并显示。

如果以上步骤都成功,说明这个由 AI 生成、经我们微调的全栈待办应用已经可以正常运行。从项目初始化到功能完成,核心开发时间可以控制在半小时以内。

6. 常见问题排查与优化建议

在实际操作中,你可能会遇到一些问题。以下是常见问题的排查路径和优化建议。

6.1 后端服务无法启动或 API 调用失败

问题现象可能原因检查方式处理建议
npm run dev报错Cannot find module依赖未安装或 TypeScript 编译问题1. 检查backend/package.jsonnode_modules
2. 检查tsconfig.json中的rootDiroutDir
1. 在backend目录运行npm install
2. 确保src/index.ts文件存在且路径正确。
服务器启动后,访问GET /api/todos返回 404路由未正确注册或路径错误1. 检查index.ts中是否app.use(‘/api/todos’, todosRouter)
2. 检查todos.ts路由文件是否导出router
1. 确认路由前缀和导入路径。
2. 重启服务器。
POST 请求返回 400,提示Title is required前端未发送正确的请求体或未设置Content-Type1. 使用浏览器开发者工具的“网络”选项卡查看请求详情。
2. 检查请求头Content-Type: application/json
3. 检查请求体 JSON 格式。
1. 确保axios.post的第二个参数是对象。
2.axios默认会设置Content-Typeapplication/json
SQLite 数据库文件未创建或表未创建initializeDb函数未执行或路径错误1. 检查服务器启动日志是否有Database initialized
2. 检查项目根目录下是否生成了todos.db文件。
1. 确保startServer函数中调用了await initializeDb()
2. 检查db.ts中数据库文件路径。
跨域(CORS)错误后端未启用 CORS 或配置不正确1. 浏览器控制台查看错误信息。
2. 检查后端index.ts是否使用了app.use(cors())
1. 确保cors中间件在路由之前使用。
2. 可以配置更具体的 CORS 选项,如origin: ‘http://localhost:5173’

6.2 前端页面显示或交互异常

问题现象可能原因检查方式处理建议
页面空白,控制台有 Vue 警告或错误Vue 组件编译错误或运行时错误1. 检查浏览器控制台(Console)和开发者工具。
2. 检查App.vue中的语法,特别是模板和脚本部分。
1. 根据控制台错误信息修复代码。
2. 确保v-model绑定的是refreactive对象属性。
列表不显示数据,但网络请求成功数据绑定问题或响应数据结构不匹配1. 在fetchTodos函数中打印response.data
2. 检查todos.value赋值是否正确。
3. 检查Todo接口定义是否与后端返回的数据类型一致。
1. 确保todosref<Todo[]>类型。
2. 检查后端返回的 JSON 字段名(如createdAt)是否与前端接口定义一致。
编辑或删除后,列表状态未更新本地状态更新逻辑错误1. 在操作函数中打印todos.value查看变化。
2. 检查是直接更新了todos.value还是修改了它的副本。
1. 确保使用响应式方法更新数组(如filter,map,unshift等)。
2. 对于对象属性修改,确保目标对象本身是响应式的。
样式混乱或未生效<style scoped>作用域问题或 CSS 错误1. 检查浏览器 Elements 面板,查看样式是否被正确应用。
2. 检查 CSS 选择器是否正确。
1. 确认<style scoped>中的选择器能匹配模板中的元素。
2. 可以使用更简单的全局样式先测试。

6.3 生产环境部署前的优化建议

当前项目仅为快速原型,若要用于生产环境,还需考虑以下方面:

  1. 数据库连接与池化:示例中使用的是简单的 SQLite 连接,生产环境应考虑连接池(对于 SQLite 可能不是必须的,但如果是 PostgreSQL/MySQL 则是必需的)和更完善的错误重试机制。
  2. 环境变量管理:数据库连接字符串、API 密钥、端口号等应通过环境变量(如dotenv)管理,而不是硬编码在代码中。
  3. 输入验证与清理:后端仅做了最基本的验证(如title必填)。生产环境应使用如JoiZodclass-validator进行严格的输入验证,并对数据库查询参数进行清理以防止 SQL 注入(虽然本例中使用参数化查询已能防范)。
  4. 错误处理中间件:应创建全局错误处理中间件,统一处理异常和返回错误响应格式,而不是在每个路由中写try-catch
  5. 日志记录:使用winstonpino等日志库替代console.log,并记录到文件或日志服务。
  6. 前端 API 服务封装:将axios调用封装成独立的服务层,便于统一处理请求头、错误拦截、加载状态等。
  7. 状态管理:对于更复杂的前端应用,应考虑使用Pinia进行集中式状态管理。
  8. 构建与部署:分别构建前端和后端,将前端静态文件托管在 Nginx 或 CDN 上,后端部署在 Node.js 运行环境(如 PM2 管理)。

7. Spec Coding 与 AI 辅助编程的最佳实践

通过本次实战,我们可以总结出一些高效利用 AI 进行全栈开发的最佳实践:

  1. 规格描述要精确、结构化:AI 生成代码的质量很大程度上取决于你的提示词。像写技术需求文档一样描述功能,包括输入、输出、边界条件、错误处理和技术栈。
  2. 分模块生成,逐步集成:不要试图让 AI 一次性生成整个项目。按模块(数据库层、API 层、前端组件)分别生成,然后手动集成和测试。这比调试一个庞大且可能包含隐藏错误的整体代码要容易得多。
  3. 你必须是代码的最终负责人:AI 生成的代码是起点,不是终点。你必须理解每一行代码的作用,检查其正确性、安全性和性能。特别是数据库操作、用户输入处理和 API 设计等关键部分。
  4. 利用 AI 进行代码重构和解释:当你不理解某段 AI 生成的代码时,可以直接询问 AI(例如在 Cursor 中使用Cmd+L选中代码并提问)。你也可以让 AI 为代码添加注释,或者将冗长的函数重构得更清晰。
  5. 建立可复用的规格模板:对于常见的 CRUD 操作、表单验证、API 客户端等模式,可以总结出自己的规格描述模板。下次开发类似功能时,只需替换实体名称和字段即可快速生成代码。
  6. 版本控制至关重要:即使 AI 能快速生成代码,也要频繁提交 Git。这样当 AI 生成的方向出现偏差时,你可以轻松回退到上一个可工作的版本。

AI 辅助编程不会取代开发者,但它正在改变开发者的工作方式。从“逐行编写”转向“定义规格、审查生成、集成调试”,要求开发者具备更强的系统设计、代码审查和问题诊断能力。掌握 Spec Coding 思维,善用 AI 工具,能让你在保证代码质量的前提下,极大提升全栈开发的效率,将宝贵的时间投入到更核心的架构设计和业务逻辑中。

🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 11:44:01

Hu矩 vs SIFT vs ORB:3种特征提取算法在图像匹配中的性能对比

Hu矩 vs SIFT vs ORB&#xff1a;3种特征提取算法在图像匹配中的性能对比1. 特征提取算法概述在计算机视觉领域&#xff0c;特征提取是图像处理的基础环节&#xff0c;它决定了后续匹配、识别等任务的性能上限。Hu矩、SIFT和ORB作为三种经典算法&#xff0c;各自有着独特的优势…

作者头像 李华
网站建设 2026/7/6 11:42:45

Spring Boot+Vue高校固定资产管理系统毕业设计实战指南

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 在实际的计算机专业毕业设计项目中&#xff0c;选择一个既能体现技术栈综合运用&#xff0c;又能解决实际业务问题的课题至关重要。高…

作者头像 李华
网站建设 2026/7/6 11:41:42

Excel 2021 + BAT 批处理:3步实现千级文件批量重命名与格式转换

Excel 2021 BAT 批处理&#xff1a;3步实现千级文件批量重命名与格式转换当你的硬盘里堆积着数千张照片、文档或音频文件&#xff0c;文件名杂乱无章得像被猫抓过的毛线球时&#xff0c;手动重命名无疑是场噩梦。我曾接手过一个项目&#xff0c;需要整理5年积累的12000份客户资…

作者头像 李华