从零到部署:用Gin + Vue 3 + Axios 完整实现一个前后端分离的待办事项应用
从零到部署:用Gin + Vue 3 + Axios 完整实现一个前后端分离的待办事项应用
在当今的Web开发领域,前后端分离架构已成为主流趋势。这种架构模式不仅提高了开发效率,还使得前后端团队能够并行工作。本文将带你从零开始,使用Gin框架构建后端API,配合Vue 3和Axios开发前端界面,最终实现一个完整的待办事项应用并部署到服务器。
1. 项目架构与环境准备
1.1 技术栈选择
我们选择以下技术组合来实现这个项目:
- 后端框架:Gin(Go语言的高性能HTTP框架)
- 前端框架:Vue 3(Composition API)
- HTTP客户端:Axios(处理前端与后端的通信)
- 数据库:SQLite(轻量级,适合演示项目)
这个技术栈组合具有以下优势:
- 高性能:Gin以出色的性能著称,适合构建API服务
- 现代化前端:Vue 3的Composition API提供了更好的代码组织和复用
- 简单易用:SQLite无需额外服务,零配置即可使用
1.2 开发环境配置
首先确保你的开发环境已安装以下工具:
# 检查Go版本 go version # 检查Node.js版本 node -v # 检查npm/yarn版本 npm -v yarn -v安装必要的依赖:
# 后端依赖 go get -u github.com/gin-gonic/gin go get -u github.com/mattn/go-sqlite3 # 前端项目初始化 npm init vue@latest todo-app cd todo-app npm install axios2. 后端API开发
2.1 数据库模型设计
我们首先定义待办事项的数据模型:
// models/todo.go package models import "time" type Todo struct { ID uint `json:"id" gorm:"primaryKey"` Title string `json:"title" binding:"required"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` }2.2 Gin路由与控制器
创建主路由文件:
// main.go package main import ( "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" "todo-app/models" ) func main() { // 初始化数据库 db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{}) if err != nil { panic("failed to connect database") } db.AutoMigrate(&models.Todo{}) // 创建Gin路由 r := gin.Default() // 配置CORS中间件 r.Use(corsMiddleware()) // 路由分组 api := r.Group("/api") { api.GET("/todos", getTodos) api.POST("/todos", createTodo) api.PUT("/todos/:id", updateTodo) api.DELETE("/todos/:id", deleteTodo) } r.Run(":8080") } // CORS中间件 func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } }2.3 实现CRUD操作
下面是各个控制器函数的实现:
// 获取所有待办事项 func getTodos(c *gin.Context) { var todos []models.Todo db := c.MustGet("db").(*gorm.DB) if err := db.Find(&todos).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, todos) } // 创建新待办事项 func createTodo(c *gin.Context) { var todo models.Todo if err := c.ShouldBindJSON(&todo); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } db := c.MustGet("db").(*gorm.DB) if err := db.Create(&todo).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(201, todo) } // 更新待办事项 func updateTodo(c *gin.Context) { id := c.Param("id") var todo models.Todo db := c.MustGet("db").(*gorm.DB) if err := db.First(&todo, id).Error; err != nil { c.JSON(404, gin.H{"error": "Todo not found"}) return } if err := c.ShouldBindJSON(&todo); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if err := db.Save(&todo).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, todo) } // 删除待办事项 func deleteTodo(c *gin.Context) { id := c.Param("id") var todo models.Todo db := c.MustGet("db").(*gorm.DB) if err := db.First(&todo, id).Error; err != nil { c.JSON(404, gin.H{"error": "Todo not found"}) return } if err := db.Delete(&todo).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"message": "Todo deleted successfully"}) }3. 前端开发
3.1 Vue 3组件结构
我们使用Vue 3的Composition API来构建前端界面。项目结构如下:
src/ ├── components/ │ ├── TodoList.vue │ ├── TodoForm.vue │ └── TodoItem.vue ├── stores/ │ └── todoStore.js ├── App.vue └── main.js3.2 状态管理
使用Pinia进行状态管理:
// stores/todoStore.js import { defineStore } from 'pinia' import { ref } from 'vue' import axios from 'axios' export const useTodoStore = defineStore('todo', () => { const todos = ref([]) const loading = ref(false) const error = ref(null) const fetchTodos = async () => { try { loading.value = true const response = await axios.get('http://localhost:8080/api/todos') todos.value = response.data } catch (err) { error.value = err.message } finally { loading.value = false } } const addTodo = async (title) => { try { const response = await axios.post('http://localhost:8080/api/todos', { title, completed: false }) todos.value.push(response.data) } catch (err) { error.value = err.message } } const updateTodo = async (id, updates) => { try { const response = await axios.put(`http://localhost:8080/api/todos/${id}`, updates) const index = todos.value.findIndex(todo => todo.id === id) if (index !== -1) { todos.value[index] = response.data } } catch (err) { error.value = err.message } } const deleteTodo = async (id) => { try { await axios.delete(`http://localhost:8080/api/todos/${id}`) todos.value = todos.value.filter(todo => todo.id !== id) } catch (err) { error.value = err.message } } return { todos, loading, error, fetchTodos, addTodo, updateTodo, deleteTodo } })3.3 组件实现
下面是TodoList组件的实现:
<!-- components/TodoList.vue --> <script setup> import { onMounted } from 'vue' import { useTodoStore } from '../stores/todoStore' import TodoItem from './TodoItem.vue' import TodoForm from './TodoForm.vue' const todoStore = useTodoStore() onMounted(() => { todoStore.fetchTodos() }) </script> <template> <div class="todo-container"> <h1>待办事项</h1> <TodoForm @add-todo="todoStore.addTodo" /> <div v-if="todoStore.loading">加载中...</div> <div v-else-if="todoStore.error" class="error">{{ todoStore.error }}</div> <ul v-else class="todo-list"> <TodoItem v-for="todo in todoStore.todos" :key="todo.id" :todo="todo" @toggle-complete="todoStore.updateTodo(todo.id, { completed: !todo.completed })" @delete="todoStore.deleteTodo(todo.id)" /> </ul> </div> </template> <style scoped> .todo-container { max-width: 600px; margin: 0 auto; padding: 20px; } .todo-list { list-style: none; padding: 0; } .error { color: red; } </style>4. 项目部署
4.1 前端构建
首先构建前端静态文件:
npm run build这会生成一个dist目录,包含所有静态资源。
4.2 后端部署
将后端代码和前端构建结果部署到服务器:
- 编译Go程序:
GOOS=linux GOARCH=amd64 go build -o todo-app- 创建部署目录结构:
deploy/ ├── backend/ │ ├── todo-app │ ├── todo.db │ └── config.yaml └── frontend/ └── dist/- 配置Nginx作为反向代理:
server { listen 80; server_name yourdomain.com; location / { root /path/to/deploy/frontend/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }4.3 使用PM2管理进程
安装PM2并启动后端服务:
npm install -g pm2 pm2 start ./todo-app --name "todo-backend" pm2 save pm2 startup5. 项目优化与扩展
5.1 性能优化
- 后端缓存:使用Redis缓存频繁访问的数据
- 前端懒加载:按需加载组件
- 代码分割:拆分大型JavaScript包
5.2 功能扩展
- 用户认证:添加JWT认证
- 分类标签:为待办事项添加分类
- 搜索过滤:实现按条件筛选功能
5.3 错误处理与日志
改进错误处理和日志记录:
// 自定义错误处理中间件 func errorMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Next() if len(c.Errors) > 0 { c.JSON(c.Writer.Status(), gin.H{ "errors": c.Errors.Errors(), }) } } } // 日志中间件 func loggingMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() latency := time.Since(start) log.Printf("[%s] %s %s %d %v", c.Request.Method, c.Request.RequestURI, c.ClientIP(), c.Writer.Status(), latency, ) } }6. 测试与调试
6.1 单元测试
为后端API编写单元测试:
// main_test.go package main import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestGetTodos(t *testing.T) { r := setupRouter() req, _ := http.NewRequest("GET", "/api/todos", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "[]") }6.2 前端测试
使用Jest进行前端组件测试:
// tests/TodoItem.spec.js import { mount } from '@vue/test-utils' import TodoItem from '../components/TodoItem.vue' describe('TodoItem', () => { it('emits toggle event when checkbox is clicked', async () => { const wrapper = mount(TodoItem, { props: { todo: { id: 1, title: 'Test Todo', completed: false } } }) await wrapper.find('input[type="checkbox"]').trigger('click') expect(wrapper.emitted('toggle-complete')).toBeTruthy() }) })6.3 API调试技巧
使用Postman或cURL测试API端点:
# 获取所有待办事项 curl http://localhost:8080/api/todos # 创建新待办事项 curl -X POST -H "Content-Type: application/json" \ -d '{"title":"New Todo"}' \ http://localhost:8080/api/todos # 更新待办事项 curl -X PUT -H "Content-Type: application/json" \ -d '{"completed":true}' \ http://localhost:8080/api/todos/1 # 删除待办事项 curl -X DELETE http://localhost:8080/api/todos/17. 常见问题解决
7.1 跨域问题
虽然我们已经配置了CORS中间件,但在开发中可能还会遇到跨域问题。解决方案:
- 确保后端CORS配置正确
- 开发环境下可以配置Vite代理:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })7.2 数据库连接问题
SQLite数据库文件权限问题:
# 确保数据库文件可写 chmod 666 todo.db7.3 前端路由问题
部署后刷新页面出现404:
location / { try_files $uri $uri/ /index.html; }8. 项目结构与代码组织
8.1 后端项目结构优化
建议采用以下结构组织后端代码:
backend/ ├── cmd/ │ └── server/ │ └── main.go ├── internal/ │ ├── controllers/ │ ├── models/ │ ├── repositories/ │ ├── services/ │ └── middleware/ ├── pkg/ ├── configs/ ├── migrations/ └── go.mod8.2 前端项目结构优化
对于大型前端项目,可以考虑以下结构:
src/ ├── assets/ ├── components/ │ ├── common/ │ ├── todos/ │ └── auth/ ├── composables/ ├── router/ ├── stores/ ├── views/ ├── utils/ └── App.vue9. 持续集成与部署
9.1 GitHub Actions配置
创建CI/CD流水线:
# .github/workflows/deploy.yml name: Deploy Todo App on: push: branches: [ main ] jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.19 - name: Build backend run: | cd backend go build -o todo-app - name: Build frontend run: | cd frontend npm install npm run build - name: Deploy to server uses: appleboy/scp-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} source: "backend/todo-app,frontend/dist" target: "/opt/todo-app" - name: Restart service uses: appleboy/ssh-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} script: | cd /opt/todo-app pm2 restart todo-app9.2 Docker化部署
创建Dockerfile:
# backend/Dockerfile FROM golang:1.19-alpine AS builder WORKDIR /app COPY . . RUN go build -o todo-app FROM alpine:latest WORKDIR /app COPY --from=builder /app/todo-app . COPY --from=builder /app/todo.db . EXPOSE 8080 CMD ["./todo-app"]使用docker-compose编排服务:
version: '3' services: backend: build: ./backend ports: - "8080:8080" volumes: - ./backend/todo.db:/app/todo.db frontend: image: nginx:alpine ports: - "80:80" volumes: - ./frontend/dist:/usr/share/nginx/html depends_on: - backend10. 安全最佳实践
10.1 后端安全
- 输入验证:对所有API输入进行严格验证
- HTTPS:生产环境必须启用HTTPS
- 速率限制:防止暴力攻击
// 速率限制中间件 func rateLimitMiddleware() gin.HandlerFunc { limiter := rate.NewLimiter(rate.Every(time.Minute), 60) return func(c *gin.Context) { if !limiter.Allow() { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "Too many requests", }) return } c.Next() } }10.2 前端安全
- 环境变量:不要在前端代码中硬编码敏感信息
- CSP:内容安全策略防止XSS攻击
- CSRF保护:使用CSRF令牌
// Axios全局配置 axios.defaults.xsrfCookieName = 'csrftoken' axios.defaults.xsrfHeaderName = 'X-CSRFToken'11. 性能监控与日志
11.1 添加监控中间件
// 监控中间件 func metricsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() duration := time.Since(start).Seconds() status := c.Writer.Status() prometheus.RequestDuration. WithLabelValues(c.Request.Method, c.Request.URL.Path). Observe(duration) prometheus.RequestCount. WithLabelValues(c.Request.Method, c.Request.URL.Path, strconv.Itoa(status)). Inc() } }11.2 日志收集
配置结构化日志:
import "go.uber.org/zap" func main() { logger, _ := zap.NewProduction() defer logger.Sync() r := gin.New() r.Use(ginzap.Ginzap(logger, time.RFC3339, true)) r.Use(ginzap.RecoveryWithZap(logger, true)) // ...其他中间件和路由 }12. 国际化支持
12.1 后端国际化
使用go-i18n包支持多语言API响应:
import "github.com/nicksnyder/go-i18n/v2/i18n" func getTodos(c *gin.Context) { localizer := i18n.NewLocalizer(bundle, c.GetHeader("Accept-Language")) message := localizer.MustLocalize(&i18n.LocalizeConfig{ MessageID: "TodosRetrieved", }) c.JSON(200, gin.H{ "message": message, "data": todos, }) }12.2 前端国际化
使用vue-i18n支持多语言界面:
// src/i18n.js import { createI18n } from 'vue-i18n' const messages = { en: { todo: { title: 'Todo List', add: 'Add Todo', empty: 'No todos yet' } }, zh: { todo: { title: '待办事项', add: '添加待办', empty: '暂无待办事项' } } } export default createI18n({ locale: 'en', fallbackLocale: 'en', messages })13. 移动端适配
13.1 响应式设计
使用CSS媒体查询确保在移动设备上良好显示:
@media (max-width: 768px) { .todo-container { padding: 10px; max-width: 100%; } .todo-form { flex-direction: column; } .todo-form input { margin-bottom: 10px; width: 100%; } }13.2 PWA支持
将应用转换为渐进式Web应用:
// vite.config.js import { VitePWA } from 'vite-plugin-pwa' export default defineConfig({ plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: 'Todo App', short_name: 'Todo', theme_color: '#ffffff', icons: [ { src: '/icon-192.png', sizes: '192x192', type: 'image/png' } ] } }) ] })14. 用户体验优化
14.1 加载状态
添加加载状态提升用户体验:
<template> <button @click="addTodo" :disabled="isAdding"> <span v-if="isAdding">添加中...</span> <span v-else>添加</span> </button> </template> <script setup> const isAdding = ref(false) const addTodo = async () => { isAdding.value = true try { await todoStore.addTodo(newTodo.value) newTodo.value = '' } finally { isAdding.value = false } } </script>14.2 动画效果
添加过渡动画使交互更流畅:
<template> <TransitionGroup name="todo-list" tag="ul"> <TodoItem v-for="todo in filteredTodos" :key="todo.id" :todo="todo" /> </TransitionGroup> </template> <style> .todo-list-move, .todo-list-enter-active, .todo-list-leave-active { transition: all 0.3s ease; } .todo-list-enter-from, .todo-list-leave-to { opacity: 0; transform: translateX(30px); } .todo-list-leave-active { position: absolute; } </style>15. 测试覆盖率提升
15.1 后端测试
使用Go的测试工具提高测试覆盖率:
func TestCreateTodo(t *testing.T) { r := setupTestRouter() todo := `{"title":"Test Todo","completed":false}` req, _ := http.NewRequest("POST", "/api/todos", strings.NewReader(todo)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) assert.Equal(t, "Test Todo", response["title"]) assert.False(t, response["completed"].(bool)) }15.2 前端测试
使用Vue Test Utils和Testing Library提高组件测试覆盖率:
import { render, fireEvent } from '@testing-library/vue' import TodoForm from '../TodoForm.vue' test('emits add-todo event with input value when form is submitted', async () => { const { emitted, getByPlaceholderText, getByText } = render(TodoForm) const input = getByPlaceholderText('输入待办事项') await fireEvent.update(input, 'New Todo') const button = getByText('添加') await fireEvent.click(button) expect(emitted()['add-todo'][0]).toEqual(['New Todo']) })16. 文档与API规范
16.1 Swagger文档
使用swaggo为API生成文档:
// @Summary 获取所有待办事项 // @Description 获取待办事项列表 // @Tags todos // @Accept json // @Produce json // @Success 200 {array} models.Todo // @Router /api/todos [get] func getTodos(c *gin.Context) { // ... }安装swag并生成文档:
go install github.com/swaggo/swag/cmd/swag@latest swag init16.2 前端组件文档
使用Storybook为前端组件创建文档:
// stories/TodoItem.stories.js import TodoItem from '../components/TodoItem.vue' export default { title: 'Components/TodoItem', component: TodoItem } const Template = (args) => ({ components: { TodoItem }, setup() { return { args } }, template: '<TodoItem v-bind="args" />' }) export const Default = Template.bind({}) Default.args = { todo: { id: 1, title: 'Example Todo', completed: false } }17. 错误处理与调试
17.1 全局错误处理
前端全局错误拦截:
// src/utils/axios.js import axios from 'axios' const api = axios.create({ baseURL: import.meta.env.VITE_API_URL }) api.interceptors.response.use( response => response, error => { if (error.response) { switch (error.response.status) { case 401: // 处理未授权 break case 404: // 处理未找到 break case 500: // 处理服务器错误 break } } return Promise.reject(error) } ) export default api17.2 开发工具集成
配置Vue Devtools和Redux DevTools:
// src/main.js import { createApp } from 'vue' import App from './App.vue' import { createPinia } from 'pinia' const pinia = createPinia() const app = createApp(App) app.use(pinia) if (process.env.NODE_ENV === 'development') { const { default: VueDevtools } = await import('@vue/devtools') VueDevtools.connect('http://localhost', 8098) } app.mount('#app')18. 代码质量与规范
18.1 ESLint配置
.eslintrc.js配置示例:
module.exports = { root: true, env: { node: true }, extends: [ 'plugin:vue/vue3-recommended', 'eslint:recommended', '@vue/typescript/recommended' ], rules: { 'vue/multi-word-component-names': 'off', 'vue/component-tags-order': ['error', { order: ['script', 'template', 'style'] }] } }18.2 Go代码规范
使用golangci-lint进行静态分析:
# .golangci.yml linters: enable: - govet - errcheck - staticcheck - gosec - bodyclose - gocritic run: skip-dirs: - vendor tests: false19. 项目维护与迭代
19.1 版本控制策略
采用语义化版本控制:
- 主版本号:重大变更,不兼容API
- 次版本号:向后兼容的功能新增
- 修订号:向后兼容的问题修正
19.2 变更日志
保持规范的变更日志:
# 变更日志 ## [1.1.0] - 2023-06-15 ### 新增 - 添加待办事项分类功能 - 支持多语言界面 ### 修复 - 修复移动端布局问题 - 修正API分页参数处理 ## [1.0.0] - 2023-05-01 ### 初始版本 - 基本CRUD功能 - 前后端分离架构20. 社区与贡献
20.1 贡献指南
创建CONTRIBUTING.md文件:
# 贡献指南 欢迎为项目贡献代码!请遵循以下步骤: 1. Fork仓库并创建分支 2. 提交清晰的提交信息 3. 编写测试覆盖新功能 4. 确保代码通过所有lint检查 5. 创建Pull Request并描述变更 ## 代码风格 - Go代码遵循标准格式 - Vue组件使用Composition API - 提交信息使用约定式提交规范20.2 Issue模板
创建问题模板帮助用户报告问题:
**描述问题** 清晰准确地描述你遇到的问题 **重现步骤** 1. 第一步 2. 第二步 3. 出现的问题 **预期行为** 你期望发生什么 **实际行为** 实际发生了什么 **环境信息** - 操作系统: - 浏览器: - 版本: **附加信息** 任何其他可能有帮助的信息