🚀 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 等)能够理解这份规格,并生成对应的代码。关键在于,你的规格描述必须足够精确、无歧义,并且符合目标技术栈的常见模式。
一个有效的工作流如下:
- 环境准备:初始化项目,安装必要的依赖。
- 规格定义:为每个模块(数据库模型、API 接口、前端组件)编写清晰的文本描述。
- AI 生成:将规格描述输入 AI 工具,生成初始代码。
- 手动集成与微调:将生成的代码放入正确的位置,修复可能的导入错误、类型错误或逻辑偏差。
- 运行与验证:启动服务,测试功能是否按预期工作。
这个流程的核心挑战在于,你需要具备足够的全栈知识来评估和修正 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.ts2.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.ts3. 使用 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); }手动检查与调整:
- 检查导入语句是否正确。
sqlite3是默认导出,而open是从sqlite包中导入的命名导出,上述代码正确。 - SQLite 的
BOOLEAN类型实际存储为 0 或 1,所以插入和查询时需要转换。AI 在updateTodo中已经处理了(updates.completed ? 1 : 0),但在getAllTodos返回的Todo接口中,completed是boolean类型,SQLite 驱动通常会帮我们转换,但为了保险,我们可以让 AI 再生成一个查询时转换的版本,或者信任驱动。这里我们选择信任驱动。 - 确保错误处理存在(
Database not initialized)。
3.2 定义 API 路由规格
接下来,创建backend/src/routes/todos.ts文件。向 AI 描述路由规格。
规格描述:“请创建一个 Express 路由文件,用于处理待办事项的 RESTful API。路径前缀为/api/todos。需要实现以下端点:
GET /:获取所有待办事项列表,返回 JSON 数组。POST /:创建新的待办事项。请求体为 JSON,包含title(字符串,必需)和description(字符串,可选)。成功时返回{ id: number }。PUT /:id:更新指定 ID 的待办事项。请求体为 JSON,可包含title,description,completed。成功返回 204 状态码。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;手动检查与调整:
- 检查导入路径
../db是否正确。 updateTodo前可以添加一个存在性检查,但为了简化,我们假设更新不存在的 ID 只是无操作。生产环境需要更严谨。- 注意
completed字段在请求体中应该是布尔值,AI 生成的代码直接使用了req.body.completed,这符合预期。
3.3 定义主应用入口规格
最后,创建backend/src/index.ts文件。向 AI 描述启动服务器的规格。
规格描述:“请创建一个 Express 服务器入口文件。需要做以下事情:
- 导入
express,cors。 - 导入之前创建的
todos路由。 - 调用
initializeDb()初始化数据库。 - 使用
express.json()中间件解析 JSON 请求体。 - 使用
cors()中间件允许前端跨域请求。 - 将
/api/todos路径映射到todos路由。 - 在 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();手动检查:
- 确保所有导入路径正确。
- 端口号(3001)不要与前端开发服务器(通常是 5173)冲突。
现在,后端代码已经生成完毕。在backend目录下,修改package.json的scripts字段,添加启动命令:
{ "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。这个组件是一个待办事项管理器。需要包含:
- 一个输入框和按钮用于添加新待办(标题必填,描述可选)。
- 一个列表展示所有待办事项,每条显示标题、描述、完成状态和创建时间。
- 每条待办事项旁边有复选框可以切换完成状态,有编辑按钮和删除按钮。
- 点击编辑按钮可以进入编辑模式,内联修改标题和描述。
- 使用
axios调用后端 API(假设后端运行在http://localhost:3001)。 - 使用
ref和reactive管理状态(待办列表、新增待办的标题和描述、编辑状态等)。 - 在
onMounted生命周期中获取初始待办列表。 - 实现添加、切换完成状态、更新、删除的函数,并调用对应的 API。
- 使用简单的样式,例如 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>手动检查与调整:
- API 基础路径:确认
API_BASE与后端服务器地址和端口一致。 - 类型定义:检查
Todo接口是否与后端返回的数据结构匹配。 - 状态更新策略:在
addTodo、toggleTodo、saveEdit、deleteTodo函数中,AI 混合使用了“重新获取整个列表”和“直接更新本地状态”两种策略。为了减少不必要的网络请求并提升用户体验,我们通常优先使用直接更新本地状态。但直接更新需要确保与后端操作完全同步,逻辑更复杂。对于快速原型,重新获取列表更稳妥。本文示例中,addTodo使用了重新获取,而toggleTodo和deleteTodo使用了直接更新,这可能导致状态不一致。为了简单和一致,我们可以将所有操作都改为成功后重新获取列表,或者都改为直接更新。这里我们选择直接更新,但需要更仔细地处理addTodo和saveEdit。 - 编辑模式:AI 生成的编辑模式 UI 是内联的,这没问题。但注意
editingTodo.description的初始值处理。
我们可以对addTodo和saveEdit进行优化,改为直接更新本地状态:
// 修改 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 initialized和Server is running on http://localhost:3001的日志。
5.2 启动前端服务
在另一个终端,进入frontend目录,运行:
npm run devVite 会启动开发服务器,通常运行在http://localhost:5173。
5.3 功能测试
- 打开前端页面:在浏览器中访问
http://localhost:5173。 - 添加待办:在输入框中输入标题和描述,点击“添加”按钮。页面列表应立即显示新添加的待办事项。
- 切换完成状态:点击某条待办事项前的复选框,其样式应发生变化(如文字中间出现删除线),并且刷新页面后状态应被保持。
- 编辑待办:点击某条待办事项的“编辑”按钮,该条目应变为内联编辑模式。修改标题或描述后点击“保存”,修改应生效。点击“取消”则放弃修改。
- 删除待办:点击“删除”按钮,确认后该条目应从列表中消失。
- 数据持久化:刷新浏览器页面,所有待办事项应重新从后端加载并显示。
如果以上步骤都成功,说明这个由 AI 生成、经我们微调的全栈待办应用已经可以正常运行。从项目初始化到功能完成,核心开发时间可以控制在半小时以内。
6. 常见问题排查与优化建议
在实际操作中,你可能会遇到一些问题。以下是常见问题的排查路径和优化建议。
6.1 后端服务无法启动或 API 调用失败
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
npm run dev报错Cannot find module | 依赖未安装或 TypeScript 编译问题 | 1. 检查backend/package.json和node_modules。2. 检查 tsconfig.json中的rootDir和outDir。 | 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-Type | 1. 使用浏览器开发者工具的“网络”选项卡查看请求详情。 2. 检查请求头 Content-Type: application/json。3. 检查请求体 JSON 格式。 | 1. 确保axios.post的第二个参数是对象。2. axios默认会设置Content-Type为application/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绑定的是ref或reactive对象属性。 |
| 列表不显示数据,但网络请求成功 | 数据绑定问题或响应数据结构不匹配 | 1. 在fetchTodos函数中打印response.data。2. 检查 todos.value赋值是否正确。3. 检查 Todo接口定义是否与后端返回的数据类型一致。 | 1. 确保todos是ref<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 生产环境部署前的优化建议
当前项目仅为快速原型,若要用于生产环境,还需考虑以下方面:
- 数据库连接与池化:示例中使用的是简单的 SQLite 连接,生产环境应考虑连接池(对于 SQLite 可能不是必须的,但如果是 PostgreSQL/MySQL 则是必需的)和更完善的错误重试机制。
- 环境变量管理:数据库连接字符串、API 密钥、端口号等应通过环境变量(如
dotenv)管理,而不是硬编码在代码中。 - 输入验证与清理:后端仅做了最基本的验证(如
title必填)。生产环境应使用如Joi、Zod或class-validator进行严格的输入验证,并对数据库查询参数进行清理以防止 SQL 注入(虽然本例中使用参数化查询已能防范)。 - 错误处理中间件:应创建全局错误处理中间件,统一处理异常和返回错误响应格式,而不是在每个路由中写
try-catch。 - 日志记录:使用
winston或pino等日志库替代console.log,并记录到文件或日志服务。 - 前端 API 服务封装:将
axios调用封装成独立的服务层,便于统一处理请求头、错误拦截、加载状态等。 - 状态管理:对于更复杂的前端应用,应考虑使用
Pinia进行集中式状态管理。 - 构建与部署:分别构建前端和后端,将前端静态文件托管在 Nginx 或 CDN 上,后端部署在 Node.js 运行环境(如 PM2 管理)。
7. Spec Coding 与 AI 辅助编程的最佳实践
通过本次实战,我们可以总结出一些高效利用 AI 进行全栈开发的最佳实践:
- 规格描述要精确、结构化:AI 生成代码的质量很大程度上取决于你的提示词。像写技术需求文档一样描述功能,包括输入、输出、边界条件、错误处理和技术栈。
- 分模块生成,逐步集成:不要试图让 AI 一次性生成整个项目。按模块(数据库层、API 层、前端组件)分别生成,然后手动集成和测试。这比调试一个庞大且可能包含隐藏错误的整体代码要容易得多。
- 你必须是代码的最终负责人:AI 生成的代码是起点,不是终点。你必须理解每一行代码的作用,检查其正确性、安全性和性能。特别是数据库操作、用户输入处理和 API 设计等关键部分。
- 利用 AI 进行代码重构和解释:当你不理解某段 AI 生成的代码时,可以直接询问 AI(例如在 Cursor 中使用
Cmd+L选中代码并提问)。你也可以让 AI 为代码添加注释,或者将冗长的函数重构得更清晰。 - 建立可复用的规格模板:对于常见的 CRUD 操作、表单验证、API 客户端等模式,可以总结出自己的规格描述模板。下次开发类似功能时,只需替换实体名称和字段即可快速生成代码。
- 版本控制至关重要:即使 AI 能快速生成代码,也要频繁提交 Git。这样当 AI 生成的方向出现偏差时,你可以轻松回退到上一个可工作的版本。
AI 辅助编程不会取代开发者,但它正在改变开发者的工作方式。从“逐行编写”转向“定义规格、审查生成、集成调试”,要求开发者具备更强的系统设计、代码审查和问题诊断能力。掌握 Spec Coding 思维,善用 AI 工具,能让你在保证代码质量的前提下,极大提升全栈开发的效率,将宝贵的时间投入到更核心的架构设计和业务逻辑中。
🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度