Vue3 + Vite2.0 全栈开发实践:从零到一构建通用后台管理系统-下

watch - 监听数据变化

生活类比:监控摄像头

  • 发现有人进入(数据变化)→ 发送通知(执行回调函数)
javascript 复制代码
import { ref, watch } from 'vue'

// 创建搜索关键词的响应式数据
const searchKeyword = ref('')  // 初始值为空字符串

// watch():监听数据变化
// 第一个参数:要监听的数据
// 第二个参数:数据变化时执行的回调函数
watch(searchKeyword, (newValue, oldValue) => {
  // newValue:变化后的新值
  // oldValue:变化前的旧值
  
  console.log(`从 "${oldValue}" 变成 "${newValue}"`)
  // 例如:用户输入 "vue" 时,输出:从 "" 变成 "vue"
  
  // 发送搜索请求到后端
  fetchSearchResults(newValue)  // 用新的关键词搜索
})

// 💡 watch 的使用场景:
// - 数据变化时,需要执行异步操作(如发送请求)
// - 需要知道变化前后的值
// - 需要执行复杂的逻辑(如防抖、节流)

实战场景:搜索框防抖

javascript 复制代码
import { ref, watch } from 'vue'

const searchKeyword = ref('')

// 监听搜索关键词,300ms 后才搜索(防抖)
watch(searchKeyword, (newValue) => {
  setTimeout(() => {
    console.log('搜索:', newValue)
    // 调用搜索 API
  }, 300)
})

watch 工作流程

实战:封装可复用的 Composable
javascript 复制代码
// composables/useUser.js
import { ref, onMounted } from 'vue'
import { getUserInfo } from '@/api/user'

export function useUser() {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const fetchUser = async () => {
    loading.value = true
    try {
      user.value = await getUserInfo()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchUser)

  return {
    user,
    loading,
    error,
    fetchUser
  }
}

在组件中使用:

javascript 复制代码
import { useUser } from '@/composables/useUser'

export default {
  setup() {
    const { user, loading, error } = useUser()
    return { user, loading, error }
  }
}

4.4 Options API vs Composition API 全面对比

对比总览表
对比维度 Options API Composition API 推荐
学习曲线 ⭐⭐ 简单,新手友好 ⭐⭐⭐ 中等,需要理解响应式 Options(新手)
代码组织 按选项类型分散(data/methods/computed) 按功能逻辑聚合 Composition ✅
代码复用 Mixin(容易命名冲突) Composables(清晰、灵活) Composition ✅
TypeScript 支持 ⭐⭐ 一般(this 类型推断困难) ⭐⭐⭐⭐⭐ 优秀(完美支持) Composition ✅
性能 相同 相同 平手
包体积 相同 相同 平手
适用场景 小型组件、简单逻辑 大型组件、复杂逻辑、代码复用 看场景
团队协作 规范统一,易于理解 需要约定 Composable 规范 Options(小团队)

详细对比:同一功能的两种实现

场景:实现一个用户列表页面,包含搜索、分页、删除功能

Options API 实现

vue 复制代码
<template>
  <div>
    <!-- 搜索框 -->
    <input v-model="searchKeyword" placeholder="搜索用户" />
    
    <!-- 用户列表 -->
    <ul>
      <li v-for="user in filteredUsers" :key="user.id">
        {{ user.name }}
        <button @click="deleteUser(user.id)">删除</button>
      </li>
    </ul>
    
    <!-- 分页 -->
    <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
    <span>第 {{ currentPage }} 页</span>
    <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 搜索相关
      searchKeyword: '',
      
      // 用户列表相关
      users: [],
      loading: false,
      
      // 分页相关
      currentPage: 1,
      pageSize: 10,
      total: 0
    }
  },
  
  computed: {
    // 搜索相关:过滤用户
    filteredUsers() {
      return this.users.filter(user => 
        user.name.includes(this.searchKeyword)
      )
    },
    
    // 分页相关:总页数
    totalPages() {
      return Math.ceil(this.total / this.pageSize)
    }
  },
  
  methods: {
    // 用户列表相关:获取用户
    async fetchUsers() {
      this.loading = true
      try {
        const res = await api.getUsers({
          page: this.currentPage,
          pageSize: this.pageSize
        })
        this.users = res.data
        this.total = res.total
      } finally {
        this.loading = false
      }
    },
    
    // 用户列表相关:删除用户
    async deleteUser(id) {
      await api.deleteUser(id)
      this.fetchUsers()
    },
    
    // 分页相关:上一页
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--
        this.fetchUsers()
      }
    },
    
    // 分页相关:下一页
    nextPage() {
      if (this.currentPage < this.totalPages) {
        this.currentPage++
        this.fetchUsers()
      }
    }
  },
  
  mounted() {
    this.fetchUsers()
  }
}
</script>

问题分析
😵 代码分散
😵 代码分散
😵 代码分散
😵 代码分散
😵 代码分散
😵 代码分散
😵 代码分散
😵 代码分散
🔍 搜索功能
📊 data: searchKeyword
🧮 computed: filteredUsers
👥 用户列表功能
📊 data: users, loading
⚙️ methods: fetchUsers, deleteUser
🔄 mounted: fetchUsers
📄 分页功能
📊 data: currentPage, pageSize, total
🧮 computed: totalPages
⚙️ methods: prevPage, nextPage


Composition API 实现

vue 复制代码
<template>
  <div>
    <!-- 搜索框 -->
    <input v-model="searchKeyword" placeholder="搜索用户" />
    
    <!-- 用户列表 -->
    <ul>
      <li v-for="user in filteredUsers" :key="user.id">
        {{ user.name }}
        <button @click="deleteUser(user.id)">删除</button>
      </li>
    </ul>
    
    <!-- 分页 -->
    <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
    <span>第 {{ currentPage }} 页</span>
    <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useUserList } from '@/composables/useUserList'
import { useSearch } from '@/composables/useSearch'
import { usePagination } from '@/composables/usePagination'

// 👇 用户列表功能 - 封装成 Composable
const { users, loading, fetchUsers, deleteUser } = useUserList()

// 👇 搜索功能 - 封装成 Composable
const { searchKeyword, filteredData: filteredUsers } = useSearch(users)

// 👇 分页功能 - 封装成 Composable
const { currentPage, pageSize, totalPages, prevPage, nextPage } = usePagination({
  fetchData: fetchUsers
})
</script>

Composables 封装

javascript 复制代码
// composables/useUserList.js - 用户列表功能
import { ref } from 'vue'
import api from '@/api'

export function useUserList() {
  const users = ref([])
  const loading = ref(false)
  
  const fetchUsers = async (page = 1, pageSize = 10) => {
    loading.value = true
    try {
      const res = await api.getUsers({ page, pageSize })
      users.value = res.data
      return res
    } finally {
      loading.value = false
    }
  }
  
  const deleteUser = async (id) => {
    await api.deleteUser(id)
    await fetchUsers()
  }
  
  return {
    users,
    loading,
    fetchUsers,
    deleteUser
  }
}

// composables/useSearch.js - 搜索功能
import { ref, computed } from 'vue'

export function useSearch(data) {
  const searchKeyword = ref('')
  
  const filteredData = computed(() => {
    if (!searchKeyword.value) return data.value
    return data.value.filter(item => 
      item.name.includes(searchKeyword.value)
    )
  })
  
  return {
    searchKeyword,
    filteredData
  }
}

// composables/usePagination.js - 分页功能
import { ref, computed } from 'vue'

export function usePagination({ fetchData }) {
  const currentPage = ref(1)
  const pageSize = ref(10)
  const total = ref(0)
  
  const totalPages = computed(() => {
    return Math.ceil(total.value / pageSize.value)
  })
  
  const prevPage = async () => {
    if (currentPage.value > 1) {
      currentPage.value--
      await fetchData(currentPage.value, pageSize.value)
    }
  }
  
  const nextPage = async () => {
    if (currentPage.value < totalPages.value) {
      currentPage.value++
      await fetchData(currentPage.value, pageSize.value)
    }
  }
  
  return {
    currentPage,
    pageSize,
    total,
    totalPages,
    prevPage,
    nextPage
  }
}

优势可视化
♻️ 复用
♻️ 复用
♻️ 复用
📱 用户列表页面组件
📦 useUserList
🔍 useSearch
📄 usePagination
👥 用户列表逻辑
🔎 搜索逻辑
📊 分页逻辑
🌟 其他页面


代码复用对比

Options API - 使用 Mixin(不推荐)

javascript 复制代码
// mixins/userMixin.js
export default {
  data() {
    return {
      users: [],
      loading: false
    }
  },
  methods: {
    async fetchUsers() {
      this.loading = true
      // ...
    }
  }
}

// 组件中使用
export default {
  mixins: [userMixin],
  data() {
    return {
      users: []  // ❌ 命名冲突!mixin 里也有 users
    }
  }
}

问题

  • ❌ 命名冲突(不知道数据来自哪里)
  • ❌ 多个 mixin 难以维护
  • ❌ this 指向不明确

Composition API - 使用 Composables(推荐)

javascript 复制代码
// composables/useUser.js
export function useUser() {
  const users = ref([])
  const loading = ref(false)
  // ...
  return { users, loading, fetchUsers }
}

// 组件中使用
import { useUser } from '@/composables/useUser'
import { useArticle } from '@/composables/useArticle'

const { users, loading, fetchUsers } = useUser()
const { articles, loading: articleLoading } = useArticle()  // ✅ 可以重命名

优势

  • ✅ 清晰知道数据来源
  • ✅ 可以重命名避免冲突
  • ✅ 易于测试和维护

大型组件对比

Options API - 代码跳跃

javascript 复制代码
export default {
  data() {
    return {
      // 第 10 行:用户相关数据
      users: [],
      // 第 12 行:文章相关数据
      articles: [],
      // 第 14 行:评论相关数据
      comments: []
    }
  },
  computed: {
    // 第 50 行:用户相关计算
    activeUsers() { /* ... */ },
    // 第 55 行:文章相关计算
    publishedArticles() { /* ... */ }
  },
  methods: {
    // 第 100 行:用户相关方法
    fetchUsers() { /* ... */ },
    // 第 120 行:文章相关方法
    fetchArticles() { /* ... */ },
    // 第 140 行:评论相关方法
    fetchComments() { /* ... */ }
  },
  mounted() {
    // 第 200 行:初始化
    this.fetchUsers()
    this.fetchArticles()
    this.fetchComments()
  }
}
// 总共 250 行,要上下跳跃查看同一功能的代码

Composition API - 代码聚合

javascript 复制代码
export default {
  setup() {
    // 第 10-30 行:用户相关(聚合在一起)
    const users = ref([])
    const activeUsers = computed(() => /* ... */)
    const fetchUsers = () => { /* ... */ }
    
    // 第 32-52 行:文章相关(聚合在一起)
    const articles = ref([])
    const publishedArticles = computed(() => /* ... */)
    const fetchArticles = () => { /* ... */ }
    
    // 第 54-74 行:评论相关(聚合在一起)
    const comments = ref([])
    const fetchComments = () => { /* ... */ }
    
    // 第 76-80 行:初始化
    onMounted(() => {
      fetchUsers()
      fetchArticles()
      fetchComments()
    })
    
    return { /* ... */ }
  }
}
// 总共 100 行,相关代码都在一起

TypeScript 支持对比

Options API - 类型推断困难

typescript 复制代码
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++  // ❌ TypeScript 难以推断 this.count 的类型
    }
  }
}

Composition API - 完美支持

typescript 复制代码
import { ref } from 'vue'

export default {
  setup() {
    const count = ref<number>(0)  // ✅ 明确类型
    
    const increment = () => {
      count.value++  // ✅ TypeScript 完美推断
    }
    
    return { count, increment }
  }
}

何时使用哪种 API?

🏠 小型项目
😊 熟悉 Vue2
🚀 学习新技术
🏢 中大型项目
✅ 是
❌ 否
✅ 是
❌ 否
🤔 选择 API 风格
📊 项目规模?
👥 团队熟悉度?
📦 Options API
⚡ Composition API
♻️ 是否需要代码复用?
⚡ Composition API ✅
📝 是否使用 TypeScript?
🤷 都可以

推荐策略

  • 新项目:优先使用 Composition API
  • 老项目迁移:可以混用,逐步迁移
  • 简单组件:Options API 也可以
  • 复杂组件:必须用 Composition API
  • 代码复用:必须用 Composition API

4.5 Vite:下一代前端构建工具

传统构建工具的痛点

Webpack 等传统构建工具在开发环境需要:

  1. 打包整个应用
  2. 启动开发服务器
  3. 等待几十秒甚至几分钟

问题:项目越大,启动越慢。

Vite 的革命性改进

Vite 基于浏览器原生 ES Modules,实现:

  • 极速冷启动:无需打包,服务器秒启动
  • 按需编译:浏览器请求哪个模块,服务器才编译哪个
  • 热更新速度不随模块增多而变慢
bash 复制代码
# 传统 Webpack 启动
npm run dev
# 等待 30-60 秒...

# Vite 启动
npm run dev
# 1-2 秒即可访问!
Vite 工作原理

开发环境:

复制代码
浏览器请求 /src/App.vue
    ↓
Vite 服务器按需编译 App.vue
    ↓
返回编译后的 ES Module
    ↓
浏览器原生解析 import

生产环境:

  • 使用 Rollup 打包,生成优化后的静态资源

五、全栈开发核心模块

5.1 JWT 认证机制

什么是 JWT?

JWT(JSON Web Token):一种用户身份认证的方式。

生活类比:游乐园手环

  • 你买票进入游乐园 → 工作人员给你戴上手环(Token)
  • 之后玩任何项目,只需要出示手环,不需要重复买票
  • 手环上有你的信息(姓名、票种、有效期)
  • 手环有防伪标记(签名),无法伪造

💾 数据库 🔧 后端 🎨 前端 👤 用户 💾 数据库 🔧 后端 🎨 前端 👤 用户 🔄 之后的每次请求 1️⃣ 输入账号密码 2️⃣ 📡 发送登录请求 3️⃣ 🔍 验证账号密码 4️⃣ ✅ 验证通过 5️⃣ 🎫 生成 JWT Token 6️⃣ 📦 返回 Token 7️⃣ 💾 存储 Token 8️⃣ 📡 请求数据(携带 Token) 9️⃣ 🔐 验证 Token 🔟 ✅ 返回数据

JWT 实战代码

后端:生成 Token

javascript 复制代码
// 后端:生成 Token(相当于制作游乐园手环)
// 引入 jsonwebtoken 库,用于生成和验证 JWT
const jwt = require('jsonwebtoken')

// 生成 Token 的函数
const generateToken = (user) => {
  // jwt.sign():生成 JWT Token
  return jwt.sign(
    // 第一个参数:载荷(Payload),存储用户信息
    { 
      userId: user._id,           // 用户ID(从数据库获取)
      username: user.username,    // 用户名
      role: user.role             // 角色(admin、editor、user 等)
    },
    // 第二个参数:密钥(Secret Key),用于签名
    // 这个密钥只有服务器知道,用来防止 Token 被伪造
    process.env.JWT_SECRET,       // 从环境变量读取密钥(保密!)
    
    // 第三个参数:配置选项
    { expiresIn: '7d' }           // Token 有效期:7 天后过期
  )
}

// 🔐 登录接口
app.post('/api/login', async (ctx) => {
  // 从请求体中获取用户名和密码
  const { username, password } = ctx.request.body
  
  // 💡 第一步:验证账号密码
  // await:等待数据库查询完成
  // User.findOne():在数据库中查找用户名匹配的用户
  const user = await User.findOne({ username })
  
  // 检查用户是否存在 && 密码是否正确
  if (!user || user.password !== password) {
    // 验证失败,返回错误信息
    ctx.body = { success: false, message: '账号或密码错误' }
    return  // 结束函数执行
  }
  
  // 💡 第二步:验证通过,生成 Token
  const token = generateToken(user)  // 调用上面的函数生成 Token
  
  // 💡 第三步:返回 Token 和用户信息给前端
  ctx.body = { 
    success: true,   // 登录成功标志
    token,           // JWT Token(前端要保存这个)
    user: {          // 用户基本信息(不包含密码!)
      username: user.username, 
      role: user.role 
    }
  }
})

// 🎯 整个流程:
// 1. 前端发送账号密码
// 2. 后端验证账号密码
// 3. 验证通过后,生成 Token(包含用户信息)
// 4. 返回 Token 给前端
// 5. 前端保存 Token,之后每次请求都携带这个 Token

前端:存储和使用 Token

javascript 复制代码
// 🔐 前端:登录函数
const login = async (username, password) => {
  // axios.post():发送 POST 请求到后端登录接口
  // await:等待请求完成
  const res = await axios.post('/api/login', { 
    username,   // 用户名
    password    // 密码
  })
  
  // 检查登录是否成功
  if (res.data.success) {
    // 💾 存储 Token 到浏览器本地存储(localStorage)
    // localStorage:浏览器提供的本地存储,关闭浏览器后数据不会丢失
    localStorage.setItem('token', res.data.token)  // 保存 Token
    
    // JSON.stringify():把对象转换成字符串
    // localStorage 只能存储字符串,所以要转换
    localStorage.setItem('user', JSON.stringify(res.data.user))  // 保存用户信息
    
    // 💡 为什么要保存 Token?
    // - 之后每次请求都要携带 Token,证明"我已经登录了"
    // - 就像游乐园手环,进入每个项目都要出示
  }
}

// 🎫 前端:请求拦截器(自动给每个请求加上 Token)
// interceptors.request.use():拦截所有发出的请求
axios.interceptors.request.use(config => {
  // config:请求配置对象,包含 url、method、headers 等
  
  // 从 localStorage 读取 Token
  const token = localStorage.getItem('token')
  
  // 如果 Token 存在,就添加到请求头
  if (token) {
    // Authorization:HTTP 请求头,用于传递身份认证信息
    // Bearer:Token 的类型(标准格式)
    config.headers.Authorization = `Bearer ${token}`
    // 相当于告诉后端:"我带着手环呢,这是我的手环号"
  }
  
  // 返回修改后的配置,继续发送请求
  return config
})

// 🎯 整个流程:
// 1. 用户登录成功后,前端保存 Token
// 2. 之后每次请求(获取用户列表、发布文章等),都自动携带 Token
// 3. 后端收到请求后,验证 Token 是否有效
// 4. Token 有效,返回数据;Token 无效,返回 401 错误

JWT Token 结构
🎫 JWT Token
📋 Header 头部
📦 Payload 载荷
🔐 Signature 签名
⚙️ 算法类型
👤 用户信息
⏰ 过期时间
🛡️ 防伪标记


5.2 RBAC 权限控制

什么是 RBAC?

RBAC(Role-Based Access Control):基于角色的访问控制

生活类比:公司权限系统

  • 普通员工:只能进办公区,不能进机房
  • 技术人员:可以进办公区 + 机房
  • 管理员:可以进所有区域

👤 用户 User
🎭 角色 Role
🔑 权限 Permission
👨 张三
👔 普通员工
👀 查看文章
✏️ 编辑自己的文章
👨‍💼 李四
👑 管理员
👀 查看文章
✏️ 编辑所有文章
🗑️ 删除文章
👥 用户管理

权限模型
javascript 复制代码
// 权限模型
User(用户) → Role(角色) → Permission(权限)

// 示例 1:普通用户
用户:张三
角色:编辑
权限:查看文章、编辑文章、发布文章

// 示例 2:管理员
用户:李四
角色:管理员
权限:用户管理、角色管理、系统设置、所有文章权限

数据库设计
has
has
has
has
USER
string
id
string
username
string
password
USER_ROLE
ROLE
string
id
string
name
string
description
ROLE_PERMISSION
PERMISSION
string
id
string
name
string
code

前端路由权限控制
javascript 复制代码
// router/index.js - 路由守卫(在进入每个页面前检查权限)

// router.beforeEach():全局前置守卫,每次路由跳转前都会执行
router.beforeEach(async (to, from, next) => {
  // to:要去的路由(目标页面)
  // from:来自哪个路由(当前页面)
  // next:放行函数,调用它才能继续跳转
  
  // 从 localStorage 读取 Token
  const token = localStorage.getItem('token')
  
  // 💡 第一步:检查是否登录
  // 如果没有 Token(未登录) && 要去的不是登录页
  if (!token && to.path !== '/login') {
    return next('/login')  // 跳转到登录页
    // return:结束函数执行,不再往下走
  }
  
  // 💡 第二步:检查路由权限
  // store.state.user.permissions:从 Vuex/Pinia 获取用户权限列表
  // 例如:['user:view', 'user:edit', 'article:view']
  const userPermissions = store.state.user.permissions
  
  // to.meta.permission:路由配置中定义的所需权限
  // 例如:访问用户管理页面需要 'user:manage' 权限
  const requiredPermission = to.meta.permission
  
  // 如果这个路由需要权限 && 用户没有这个权限
  if (requiredPermission && !userPermissions.includes(requiredPermission)) {
    return next('/403')  // 跳转到 403 无权限页面
  }
  
  // 💡 第三步:通过所有验证,允许访问
  next()  // 不传参数,表示放行,继续跳转到目标页面
})

// 🎯 执行顺序:
// 用户点击"用户管理"菜单
//   ↓
// beforeEach 拦截
//   ↓
// 检查是否登录(有 Token)
//   ↓
// 检查是否有权限(有 'user:manage' 权限)
//   ↓
// 放行,进入用户管理页面

路由配置示例

javascript 复制代码
const routes = [
  {
    path: '/user',
    component: UserManage,
    meta: { 
      permission: 'user:manage',  // 需要"用户管理"权限
      title: '用户管理'
    }
  },
  {
    path: '/article',
    component: ArticleList,
    meta: { 
      permission: 'article:view',  // 需要"查看文章"权限
      title: '文章列表'
    }
  }
]

权限控制流程
❌ 否
✅ 是
❌ 否
✅ 是
👤 用户访问页面
🔐 是否登录?
🚫 跳转登录页
🔑 是否有权限?
⛔ 跳转 403 页面
🎉 允许访问

5.3 动态菜单

什么是动态菜单?

生活类比:餐厅菜单

  • 普通会员:只能看到普通菜单(家常菜)
  • VIP 会员:可以看到 VIP 菜单(家常菜 + 特色菜)
  • 至尊会员:可以看到所有菜单(家常菜 + 特色菜 + 私房菜)

根据用户权限动态生成菜单,不同角色看到不同的菜单。
👤 普通用户
✏️ 编辑
👑 管理员
🔐 用户登录
🎭 角色判断
📋 显示基础菜单
🏠 首页
👤 个人中心
📋 显示编辑菜单
🏠 首页
📝 文章管理
👤 个人中心
📋 显示全部菜单
🏠 首页
👥 用户管理
📝 文章管理
⚙️ 系统设置
👤 个人中心

动态菜单实现

后端返回用户菜单

javascript 复制代码
// 📋 后端:根据用户角色返回菜单
// GET 请求:/api/user/menus
app.get('/api/user/menus', async (ctx) => {
  // ctx.state.user:从 JWT Token 中解析出的用户信息
  // 这个信息是在前面的中间件中设置的(验证 Token 后)
  const user = ctx.state.user
  
  // 定义菜单数组
  let menus = []
  
  // 💡 根据用户角色返回不同的菜单
  if (user.role === 'admin') {
    // 👑 管理员:可以看到所有菜单
    menus = [
      {
        id: 1,                    // 菜单唯一标识
        name: "用户管理",         // 菜单名称
        path: "/user",            // 菜单路径
        icon: "user",             // 菜单图标
        children: [               // 子菜单
          { 
            id: 2, 
            name: "用户列表", 
            path: "/user/list" 
          },
          { 
            id: 3, 
            name: "角色管理", 
            path: "/user/role" 
          }
        ]
      },
      {
        id: 4,
        name: "内容管理",
        path: "/content",
        icon: "document",
        children: [
          { 
            id: 5, 
            name: "文章列表", 
            path: "/content/article" 
          },
          { 
            id: 6, 
            name: "分类管理", 
            path: "/content/category" 
          }
        ]
      }
    ]
  } else if (user.role === 'editor') {
    // ✏️ 编辑:只能看到内容管理菜单
    menus = [
      {
        id: 4,
        name: "内容管理",
        path: "/content",
        icon: "document",
        children: [
          { 
            id: 5, 
            name: "文章列表", 
            path: "/content/article" 
          }
          // 注意:编辑没有"分类管理"权限
        ]
      }
      // 注意:编辑没有"用户管理"菜单
    ]
  }
  // 如果是普通用户(user),可以继续添加 else if
  
  // 返回菜单数据给前端
  ctx.body = { 
    success: true, 
    menus  // 菜单数组
  }
})

// 🎯 工作流程:
// 1. 前端发送请求(携带 Token)
// 2. 后端验证 Token,获取用户角色
// 3. 根据角色返回对应的菜单
// 4. 前端根据菜单数据渲染侧边栏

前端动态渲染菜单

vue 复制代码
<template>
  <!-- el-menu:ElementPlus 的菜单组件 -->
  <el-menu>
    <!-- 🔄 遍历一级菜单 -->
    <!-- v-for:循环遍历 menus 数组 -->
    <!-- :key:每个元素的唯一标识(必须) -->
    <el-sub-menu 
      v-for="menu in menus" 
      :key="menu.id" 
      :index="menu.path"
    >
      <!-- 菜单标题(一级菜单) -->
      <template #title>
        <!-- 菜单图标 -->
        <el-icon>
          <!-- component :is:动态组件,根据 menu.icon 显示不同图标 -->
          <component :is="menu.icon" />
        </el-icon>
        <!-- 菜单名称,{{ }} 是 Vue 的插值语法 -->
        <span>{{ menu.name }}</span>
      </template>
      
      <!-- 🔄 遍历子菜单(二级菜单) -->
      <el-menu-item 
        v-for="child in menu.children" 
        :key="child.id" 
        :index="child.path"
      >
        <!-- 子菜单名称 -->
        {{ child.name }}
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<script setup>
// 导入 Vue 的响应式 API
import { ref, onMounted } from 'vue'
// 导入 axios,用于发送 HTTP 请求
import axios from 'axios'

// 创建响应式数据:菜单数组
// ref([]):初始值为空数组
const menus = ref([])

// onMounted:组件挂载(显示在页面上)后执行
onMounted(async () => {
  // 💡 发送请求获取菜单数据
  // await:等待请求完成
  const res = await axios.get('/api/user/menus')
  
  // 把后端返回的菜单数据赋值给 menus
  // .value:访问 ref 创建的数据需要用 .value
  menus.value = res.data.menus
  
  // 💡 数据更新后,Vue 会自动重新渲染页面
  // 菜单就会显示出来了
})

// 🎯 整个流程:
// 1. 页面加载完成(onMounted)
// 2. 发送请求获取菜单(axios.get)
// 3. 后端根据角色返回菜单
// 4. 前端保存菜单数据(menus.value = ...)
// 5. Vue 自动渲染菜单(v-for 遍历)
// 6. 用户看到个性化的菜单
</script>

动态菜单流程
💾 数据库 🔧 后端 🎨 前端 👤 用户 💾 数据库 🔧 后端 🎨 前端 👤 用户 1️⃣ ✅ 登录成功 2️⃣ 📡 请求用户菜单 3️⃣ 🎫 从 Token 获取角色 4️⃣ 🔍 查询角色对应菜单 5️⃣ 📦 返回菜单数据 6️⃣ 📋 返回菜单 JSON 7️⃣ 🎨 渲染菜单 8️⃣ 🎉 显示个性化菜单


六、最佳实践与开发技巧

6.1 代码规范

javascript 复制代码
// 使用 ESLint + Prettier
// .eslintrc.js
module.exports = {
  extends: [
    'plugin:vue/vue3-recommended',
    '@vue/typescript/recommended',
    'prettier'
  ]
}

6.2 性能优化

  • 路由懒加载component: () => import('@/views/User.vue')
  • 组件懒加载defineAsyncComponent(() => import('./Heavy.vue'))
  • 虚拟滚动 :大列表使用 vue-virtual-scroller
  • 图片懒加载 :使用 v-lazy 指令

6.3 状态管理

javascript 复制代码
// 使用 Pinia(Vue3 官方推荐)
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    token: ''
  }),
  actions: {
    async login(username, password) {
      const res = await api.login(username, password)
      this.token = res.token
      this.userInfo = res.user
    }
  }
})

七、团队协作与工程化

7.1 团队组成

一个完整的项目团队通常包括:

  • 产品:需求设计、原型设计
  • UED:UI 设计、交互设计
  • 前端:前端开发
  • 后端:接口开发
  • 测试:功能测试、自动化测试
  • 运维:部署、监控
  • 运营:数据分析、用户运营

7.2 前后端协作

javascript 复制代码
// 接口文档示例(使用 Swagger / Apifox)
/**
 * @api {post} /api/user/login 用户登录
 * @apiParam {String} username 用户名
 * @apiParam {String} password 密码
 * @apiSuccess {String} token JWT Token
 * @apiSuccess {Object} user 用户信息
 */

7.3 Git 工作流

bash 复制代码
# 功能分支开发
git checkout -b feature/user-management

# 提交代码
git add .
git commit -m "feat: 添加用户管理模块"

# 合并到主分支
git checkout main
git merge feature/user-management

八、总结

通过 Vue3 + Vite2.0 + ElementPlus + Koa2 + MongoDB 技术栈,我们可以快速构建一个功能完善的通用后台管理系统。

核心要点

  1. Vue3 的 Composition API 和 VDOM 优化带来更好的性能和开发体验
  2. Vite 极速的开发体验让开发效率大幅提升
  3. 规范的开发流程确保项目质量和团队协作
  4. 通用模块(JWT、RBAC、动态菜单)可复用到其他项目
  5. 全栈思维让前端开发者更懂后端,提升综合能力

希望这篇文章能帮助你更好地理解现代前端开发和全栈开发的核心思想。实践出真知,赶快动手搭建自己的通用后台系统吧!


参考资源

标签Vue3 Vite ElementPlus Koa2 MongoDB 全栈开发

相关推荐
切糕师学AI1 小时前
JavaScript 中 == 和 === 的区别
javascript·js语法
i220818 Faiz Ul11 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
暴力袋鼠哥13 小时前
基于 Spring Boot 3 + Vue 3 的农产品在线销售平台设计与实现
vue.js·spring boot·后端
coding随想15 小时前
TypeScript 高级类型全攻略:从“可表达性”到“类型体操”的实践之路
前端·javascript·typescript
WWWWW先生15 小时前
02 登录功能实现
前端·javascript
Lee川16 小时前
深入解析:从内存模型到作用域陷阱——JavaScript变量的前世今生
javascript·算法
豆苗学前端16 小时前
彻底讲透医院移动端手持设备PDA离线同步架构:从"记账本"到"分布式共识",吊打面试官
前端·javascript·后端
paterWang17 小时前
基于SpringBoot+Vue的鞋类商品购物商城系统的设计与实现
vue.js·spring boot·后端