从零到部署用Gin Vue 3 Axios 完整实现一个前后端分离的待办事项应用在当今的Web开发领域前后端分离架构已成为主流趋势。这种架构模式不仅提高了开发效率还使得前后端团队能够并行工作。本文将带你从零开始使用Gin框架构建后端API配合Vue 3和Axios开发前端界面最终实现一个完整的待办事项应用并部署到服务器。1. 项目架构与环境准备1.1 技术栈选择我们选择以下技术组合来实现这个项目后端框架GinGo语言的高性能HTTP框架前端框架Vue 3Composition APIHTTP客户端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 vuelatest 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 classtodo-container h1待办事项/h1 TodoForm add-todotodoStore.addTodo / div v-iftodoStore.loading加载中.../div div v-else-iftodoStore.error classerror{{ todoStore.error }}/div ul v-else classtodo-list TodoItem v-fortodo in todoStore.todos :keytodo.id :todotodo toggle-completetodoStore.updateTodo(todo.id, { completed: !todo.completed }) deletetodoStore.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; } /style4. 项目部署4.1 前端构建首先构建前端静态文件npm run build这会生成一个dist目录包含所有静态资源。4.2 后端部署将后端代码和前端构建结果部署到服务器编译Go程序GOOSlinux GOARCHamd64 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[typecheckbox]).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 前端路由问题部署后刷新页面出现404location / { 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/checkoutv2 - name: Set up Go uses: actions/setup-gov2 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-actionmaster 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-actionmaster 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 --frombuilder /app/todo-app . COPY --frombuilder /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-CSRFToken11. 性能监控与日志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 clickaddTodo :disabledisAdding span v-ifisAdding添加中.../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 } } /script14.2 动画效果添加过渡动画使交互更流畅template TransitionGroup nametodo-list tagul TodoItem v-fortodo in filteredTodos :keytodo.id :todotodo / /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; } /style15. 测试覆盖率提升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/swaglatest 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-bindargs / }) 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. 出现的问题 **预期行为** 你期望发生什么 **实际行为** 实际发生了什么 **环境信息** - 操作系统: - 浏览器: - 版本: **附加信息** 任何其他可能有帮助的信息