Vue 3 快速入门:从零搭建前后端 CRUD 应用

Vue 3 快速入门:从零搭建前后端 CRUD 应用

本文面向想要快速上手 Vue 3 并打通前后端的开发者。通过一个完整示例,涵盖 Vue 3 安装、项目创建、组件编写,以及通过 Axios 调用 FastAPI + SQLite 后端接口,实现对数据库的增删改查。

技术栈: Vue 3 · Vite · FastAPI · SQLite · Axios · Composition API


目录

  1. [什么是 Vue 3?](#什么是 Vue 3?)
  2. 安装与环境准备
  3. [创建 Vue 3 项目](#创建 Vue 3 项目)
  4. [Vue 单文件组件基础](#Vue 单文件组件基础)
  5. [后端:FastAPI + SQLite](#后端:FastAPI + SQLite)
  6. [FastAPI CRUD 完整代码](#FastAPI CRUD 完整代码)
  7. [Vue 调用后端接口](#Vue 调用后端接口)
  8. [完整 CRUD 页面示例](#完整 CRUD 页面示例)
  9. 下一步建议

1. 什么是 Vue 3?

Vue 3 是 Vue.js 的最新主要版本,相比 Vue 2 有三项关键提升:

维度 Vue 2 Vue 3
API 风格 Options API(选项式) Composition API(组合式) + <script setup>
状态管理 Vuex Pinia(更轻量、更好 TypeScript 支持)
渲染性能 旧 Virtual DOM 重写后的 Virtual DOM,渲染速度大幅提升

Vue 3 的核心优势:

  • 渐进式:从简单页面到复杂应用,按需引入,无需全栈重构
  • Composition API:逻辑复用性更强、代码组织更清晰、天然支持 TypeScript
  • 性能:Virtual DOM 重写,配合编译器优化,运行速度和打包体积均显著改善
  • 生态成熟:Vue Router 4、Pinia、Vite 组成完整工具链

💡 Vue 2 于 2023 年底正式停止维护,新项目请直接使用 Vue 3。


2. 安装与环境准备

2.1 前提条件

确保已安装 Node.js ≥ 16.0

bash 复制代码
node -v    # v20.x.x 或更高版本均可
npm -v     # npm 8.x+

没有安装?前往 nodejs.org 下载 LTS 版本。

2.2 两种安装方式

方式一:CDN 快速体验(无需构建)

适合写 Demo、验证想法,或者在旧项目中局部引入 Vue 3。创建一个 index.html 即可:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Vue 3 CDN Demo</title>
</head>
<body>
  <div id="app">
    <h1>{{ message }}</h1>
    <button @click="count++">点击次数:{{ count }}</button>
  </div>

  <script type="module">
    import { createApp, ref } from
      'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

    createApp({
      setup() {
        const message = ref('Hello, Vue 3!')
        const count = ref(0)
        return { message, count }
      }
    }).mount('#app')
  </script>
</body>
</html>

直接在浏览器打开即可运行,无需任何构建工具。

方式二:npm + Vite(推荐工程化方式)

CDN 适合尝鲜,但真实项目需要工程化能力(热更新、模块打包、TypeScript 支持等)。

Vite 是 Vue 官方推荐的构建工具,由 Vue 的作者尤雨溪主导开发,相比 Vue CLI 有显著的速度优势:

复制代码
npm create vite@latest my-app -- --template vue
cd my-app && npm install && npm run dev

启动后访问 http://localhost:5173/ 即可看到默认项目页面。


3. 创建 Vue 3 项目

3.1 完整步骤

bash 复制代码
# 1. 创建项目(交互式,会让你选 Vue 或 Vue-ts)
npm create vite@latest my-app -- --template vue

# 2. 进入目录
cd my-app

# 3. 安装依赖
npm install

# 4. 安装 Axios(用于调用后端 API)
npm install axios

# 5. 启动开发服务器
npm run dev
# 输出:Local:   http://localhost:5173/
#       Network: http://192.168.x.x:5173/

3.2 项目结构一览

复制代码
my-app/
├── public/               # 静态资源(原样复制到构建产物)
├── src/
│   ├── assets/           # 样式、图片等资源
│   ├── components/        # ✅ 放你的 .vue 组件
│   ├── App.vue            # ✅ 根组件,入口
│   └── main.js            # ✅ 应用入口文件
├── index.html             # HTML 模板
├── vite.config.js         # Vite 配置
└── package.json

3.3 安装 Python 后端依赖

后端使用 FastAPI,还需要安装 Python 依赖:

bash 复制代码
# 建议用虚拟环境
python3 -m venv venv
source venv/bin/activate   # macOS/Linux
# venv\Scripts\activate    # Windows

pip install fastapi uvicorn

4. Vue 单文件组件基础

Vue 单文件组件(.vue 后缀)将 template(模板)、script(逻辑)、style(样式) 三部分合为一体,是 Vue 项目的标准写法。

4.1 一个最小示例

vue 复制代码
<!-- src/App.vue -->
<template>
  <div class="card">
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
    <button @click="count++">点击次数:{{ count }}</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 响应式状态(Composition API 写法)
const title = ref('Vue3 入门')
const message = ref('你好,世界!')
const count = ref(0)
</script>

<style scoped>
.card {
  padding: 1.5rem;
  border-radius: 12px;
  background: #1e293b;
  color: #f1f5f9;
  max-width: 400px;
  margin: 2rem auto;
}
h1 { color: #42b883; }
button {
  background: #42b883;
  color: #fff;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 1rem;
}
</style>

4.2 关键语法解释

语法 说明
{``{ title }} 双大括号插值,渲染变量值
@click="count++" @clickv-on:click 的简写,绑定事件
ref() 创建响应式变量,修改 .value 自动更新视图
<style scoped> 样式仅对当前组件生效,避免污染全局

4.3 <script setup> 是什么?

<script setup> 是 Vue 3.2 引入的语法糖,与以下写法完全等价:

javascript 复制代码
// 编译后等价于:
import { ref } from 'vue'
export default {
  setup() {
    const title = ref('Vue3 入门')
    const message = ref('你好,世界!')
    const count = ref(0)
    return { title, message, count }
  }
}

使用 <script setup> 代码更简洁,是 Vue 3 的推荐写法。


5. 后端:FastAPI + SQLite

5.1 为什么选 FastAPI?

  • 性能:接近 Node.js 和 Go,可处理高并发请求
  • 自动文档 :运行后访问 http://127.0.0.1:8000/docs,自动生成可交互的 Swagger UI
  • 类型安全:原生支持 Python 类型提示,配合 Pydantic 自动校验请求数据
  • 异步支持 :使用 async def,轻松处理 IO 密集型任务
  • 上手极快:Flask 式的简洁语法,Django 式的功能完整

5.2 SQLite:零配置的嵌入式数据库

SQLite 无须独立服务进程,数据存储在一个 .db 文件中,非常适合中小型应用和本地开发:

python 复制代码
import sqlite3

# 建立连接(文件不存在会自动创建)
conn = sqlite3.connect("data.db")
cursor = conn.cursor()

# 执行 SQL
cursor.execute("""
    CREATE TABLE IF NOT EXISTS items (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        name        TEXT NOT NULL,
        description TEXT
    )
""")
conn.commit()
conn.close()

5.3 最小化后端代码

创建后端文件 backend/main.py

python 复制代码
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import sqlite3

app = FastAPI(title="Vue3 CRUD API")
DB = "backend/data.db"   # SQLite 数据库文件

# ── CORS 配置:允许 Vue 前端跨域访问 ──
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],   # Vite 默认端口
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

def get_conn():
    """获取数据库连接的辅助函数"""
    return sqlite3.connect(DB)

# ── 启动时初始化数据库 ──
@app.on_event("startup")
def init_db():
    with get_conn() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS items (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                name        TEXT NOT NULL,
                description TEXT
            )
        """)
        conn.commit()
    print("✅ 数据库初始化完成")

启动后端:

bash 复制代码
cd backend
uvicorn main:app --reload --port 8000
# 输出:
# INFO:     Uvicorn running on http://127.0.0.1:8000
# ✅ 数据库初始化完成

访问 http://127.0.0.1:8000/docs 可以看到自动生成的交互式 API 文档。


6. FastAPI CRUD 完整代码

下面是与数据库 items 表交互的完整 CRUD 接口。放在 backend/main.pyinit_db() 之后:

python 复制代码
from pydantic import BaseModel

# ── Pydantic 模型:定义请求体结构并自动校验类型 ──
class Item(BaseModel):
    name: str
    description: str | None = None   # 可选字段


# ══════════════════════════════════════════════
#  📖 查询全部  GET /items
# ══════════════════════════════════════════════
@app.get("/items")
def get_items():
    with get_conn() as conn:
        rows = conn.execute("SELECT * FROM items").fetchall()
        return [{"id": r[0], "name": r[1], "description": r[2]} for r in rows]


# ══════════════════════════════════════════════
#  📖 查询单条  GET /items/{id}
# ══════════════════════════════════════════════
@app.get("/items/{id}")
def get_item(id: int):
    from fastapi import HTTPException
    with get_conn() as conn:
        r = conn.execute("SELECT * FROM items WHERE id=?", [id]).fetchone()
        if not r:
            raise HTTPException(status_code=404, detail="Item not found")
        return {"id": r[0], "name": r[1], "description": r[2]}


# ══════════════════════════════════════════════
#  ➕ 新增  POST /items
# ══════════════════════════════════════════════
@app.post("/items")
def create_item(item: Item):
    with get_conn() as conn:
        cur = conn.execute(
            "INSERT INTO items (name, description) VALUES (?, ?)",
            [item.name, item.description]
        )
        conn.commit()
        return {"id": cur.lastrowid, **item.model_dump()}


# ══════════════════════════════════════════════
#  ✏️ 修改  PUT /items/{id}
# ══════════════════════════════════════════════
@app.put("/items/{id}")
def update_item(id: int, item: Item):
    with get_conn() as conn:
        conn.execute(
            "UPDATE items SET name=?, description=? WHERE id=?",
            [item.name, item.description, id]
        )
        conn.commit()
        return {"id": id, **item.model_dump()}


# ══════════════════════════════════════════════
#  🗑️ 删除  DELETE /items/{id}
# ══════════════════════════════════════════════
@app.delete("/items/{id}")
def delete_item(id: int):
    with get_conn() as conn:
        conn.execute("DELETE FROM items WHERE id=?", [id])
        conn.commit()
    return {"ok": True, "deleted": id}

路由方法对照表:

方法 路径 作用 Postman / curl 示例
GET /items 查询全部记录 curl http://localhost:8000/items
GET /items/{id} 查询单条记录 curl http://localhost:8000/items/1
POST /items 新增一条记录 curl -X POST http://localhost:8000/items -H "Content-Type: application/json" -d '{"name":"测试","description":"描述"}'
PUT /items/{id} 修改指定记录 curl -X PUT http://localhost:8000/items/1 -H "Content-Type: application/json" -d '{"name":"修改后","description":"新描述"}'
DELETE /items/{id} 删除指定记录 curl -X DELETE http://localhost:8000/items/1

7. Vue 调用后端接口

7.1 封装 Axios API 模块

为了代码整洁,把所有 API 调用封装到一个文件 src/api/items.js

javascript 复制代码
// src/api/items.js
import axios from 'axios'

// 创建 Axios 实例,配置 baseURL
const api = axios.create({
  baseURL: 'http://localhost:8000',   // FastAPI 后端地址
  headers: { 'Content-Type': 'application/json' }
})

// ── GET /items:查询全部 ──
export const getItems = () => api.get('/items')

// ── GET /items/{id}:查询单条 ──
export const getItem = (id) => api.get(`/items/${id}`)

// ── POST /items:新增 ──
export const createItem = (data) => api.post('/items', data)

// ── PUT /items/{id}:修改 ──
export const updateItem = (id, data) => api.put(`/items/${id}`, data)

// ── DELETE /items/{id}:删除 ──
export const deleteItem = (id) => api.delete(`/items/${id}`)

7.2 在组件中调用

在 Vue 组件中使用 onMounted 钩子在页面加载时获取数据:

vue 复制代码
<!-- src/App.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import { getItems, createItem, updateItem, deleteItem } from './api/items'

const items = ref([])          // 列表数据
const form = ref({             // 表单数据
  name: '',
  description: ''
})
const editingId = ref(null)     // 当前编辑中的 ID(null 表示新增模式)

// ── 页面加载时获取数据 ──
onMounted(async () => {
  try {
    const { data } = await getItems()
    items.value = data
  } catch (err) {
    console.error('加载失败', err)
  }
})

// ── 新增 ──
const handleCreate = async () => {
  const { data } = await createItem(form.value)
  items.value.push(data)
  form.value = { name: '', description: '' }
}

// ── 点击编辑 ──
const handleEdit = (item) => {
  editingId.value = item.id
  form.value = { name: item.name, description: item.description }
}

// ── 更新 ──
const handleUpdate = async () => {
  const { data } = await updateItem(editingId.value, form.value)
  const idx = items.value.findIndex(i => i.id === editingId.value)
  if (idx !== -1) items.value[idx] = data   // 更新列表中的对应项
  editingId.value = null
  form.value = { name: '', description: '' }
}

// ── 删除 ──
const handleDelete = async (id) => {
  await deleteItem(id)
  items.value = items.value.filter(i => i.id !== id)   // 从列表中移除
}
</script>

8. 完整 CRUD 页面示例

下面是一个完整的列表管理页面,包含输入表单和表格,代码可直接复制使用。

创建 src/components/ItemList.vue

vue 复制代码
<!-- src/components/ItemList.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import { getItems, createItem, updateItem, deleteItem } from '../api/items'

const items = ref([])
const form = ref({ name: '', description: '' })
const editingId = ref(null)

// ── 加载列表 ──
onMounted(async () => {
  const { data } = await getItems()
  items.value = data
})

// ── 新增 ──
const handleCreate = async () => {
  if (!form.value.name.trim()) return
  const { data } = await createItem(form.value)
  items.value.push(data)
  form.value = { name: '', description: '' }
}

// ── 编辑 ──
const handleEdit = (item) => {
  editingId.value = item.id
  form.value = { name: item.name, description: item.description }
}

// ── 更新 ──
const handleUpdate = async () => {
  const { data } = await updateItem(editingId.value, form.value)
  const idx = items.value.findIndex(i => i.id === editingId.value)
  if (idx !== -1) items.value[idx] = data
  editingId.value = null
  form.value = { name: '', description: '' }
}

// ── 删除 ──
const handleDelete = async (id) => {
  if (!confirm('确认删除?')) return
  await deleteItem(id)
  items.value = items.value.filter(i => i.id !== id)
}

// ── 取消编辑 ──
const cancelEdit = () => {
  editingId.value = null
  form.value = { name: '', description: '' }
}
</script>

<template>
  <div class="crud-container">
    <h2>📋 项目列表管理</h2>

    <!-- 输入表单 -->
    <div class="form-section">
      <input
        v-model="form.name"
        placeholder="输入名称..."
        @keyup.enter="editingId ? handleUpdate() : handleCreate()"
      />
      <input
        v-model="form.description"
        placeholder="输入描述(可选)..."
        @keyup.enter="editingId ? handleUpdate() : handleCreate()"
      />
      <button v-if="!editingId" @click="handleCreate" class="btn-add">
        ➕ 新增
      </button>
      <template v-else>
        <button @click="handleUpdate" class="btn-update">✓ 更新</button>
        <button @click="cancelEdit" class="btn-cancel">✕ 取消</button>
      </template>
    </div>

    <!-- 列表表格 -->
    <table class="items-table">
      <thead>
        <tr>
          <th>ID</th>
          <th>名称</th>
          <th>描述</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-if="items.length === 0">
          <td colspan="4" class="empty-row">暂无数据</td>
        </tr>
        <tr v-for="item in items" :key="item.id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.description || '---' }}</td>
          <td>
            <button @click="handleEdit(item)" class="btn-edit">✏ 编辑</button>
            <button @click="handleDelete(item.id)" class="btn-del">🗑 删除</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
.crud-container {
  max-width: 800px;
  margin: 2rem auto;
  padding: 0 1rem;
  font-family: system-ui, sans-serif;
}
h2 { color: #42b883; margin-bottom: 1rem; }

.form-section {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
}
input {
  padding: 0.5rem 0.75rem;
  border: 1px solid #334155;
  border-radius: 6px;
  background: #1e293b;
  color: #f1f5f9;
  font-size: 0.95rem;
  flex: 1;
  min-width: 160px;
}
input::placeholder { color: #64748b; }
input:focus { outline: 2px solid #42b883; border-color: transparent; }

button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 500;
  transition: opacity 0.2s;
}
button:hover { opacity: 0.85; }
.btn-add    { background: #22c55e; color: #fff; }
.btn-update { background: #3b82f6; color: #fff; }
.btn-cancel { background: #475569; color: #fff; }
.btn-edit   { background: #f59e0b; color: #fff; margin-right: 0.4rem; }
.btn-del    { background: #ef4444; color: #fff; }

.items-table {
  width: 100%;
  border-collapse: collapse;
  background: #1e293b;
  border-radius: 8px;
  overflow: hidden;
}
th {
  background: #0f172a;
  color: #94a3b8;
  font-weight: 600;
  text-align: left;
  padding: 0.75rem 1rem;
  font-size: 0.85rem;
  letter-spacing: 0.05em;
}
td {
  padding: 0.75rem 1rem;
  border-top: 1px solid #334155;
  color: #f1f5f9;
  font-size: 0.9rem;
}
tr:hover td { background: #1e293b; }
.empty-row { text-align: center; color: #64748b; padding: 2rem; }
</style>

然后在 App.vue 中引入使用:

vue 复制代码
<!-- src/App.vue -->
<script setup>
import ItemList from './components/ItemList.vue'
</script>

<template>
  <main>
    <ItemList />
  </main>
</template>

9. 下一步建议

掌握以上内容后,可以沿着以下方向继续深入:

路由与页面导航

安装 Vue Router,实现多页面切换:

bash 复制代码
npm install vue-router

状态管理

使用 Pinia 在组件间共享复杂状态:

bash 复制代码
npm install pinia

后端进阶

  • SQLAlchemyPeewee ORM 替代手写 SQL
  • 添加 JWT 认证 保护接口
  • 使用 PostgreSQL 替代 SQLite 支持生产部署

部署上线

  • 前端:运行 npm run build,用 Nginx 做反向代理
  • 后端:用 Docker 容器化,或部署到 Railway / Render / 阿里云 ECS

学习资源


完整项目结构

复制代码
project/
├── backend/
│   ├── main.py          # FastAPI 后端(CRUD 接口)
│   └── data.db          # SQLite 数据库(自动生成)
└── my-app/              # Vue 3 前端
    ├── src/
    │   ├── api/
    │   │   └── items.js  # Axios API 封装
    │   ├── components/
    │   │   └── ItemList.vue  # 完整 CRUD 页面
    │   └── App.vue
    └── vite.config.js

启动方式:

bash 复制代码
# 终端 1:后端
cd backend && uvicorn main:app --reload --port 8000

# 终端 2:前端
cd my-app && npm run dev

打开 http://localhost:5173 即可看到完整的增删改查应用。


祝编码愉快!

相关推荐
biubiubiu07061 小时前
Agent 是如何拥有“手脚”的(ReAct 运行流程)
开发语言·前端·javascript
摸鱼的春哥2 小时前
Agent教程21:知识图谱🕸,让AI🤖学会联想
前端·javascript·后端
SuperEugene2 小时前
Vue3 组件拆分实战规范:页面 / 业务 / 基础组件边界清晰化,高内聚低耦合落地指南|Vue 组件与模板规范篇
前端·javascript·vue.js·前端框架
泯泷2 小时前
阶段二:为什么先设计指令集,编译器和运行时才能稳定对齐?
前端·javascript·架构
Dxy12393102162 小时前
HTML常用布局详解:从基础到进阶的网页结构指南
前端·html
ywf12153 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭4 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf10 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特10 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js