V3 Admin Vite 4.x 中文文档

148,496 阅读11分钟

该文档针对 4.x 版本编写

✨ 开头随意点

本文本身也是充当该开源模板的中文文档,如果只是想简单了解一下该模板,可以直接看这两篇简短的软文:

  1. 一个基于 Vue3 + TS + Element-Plus + Pinia 的低成本后台管理模板

  2. 一个项目让你熟悉 Vue3 以及相关生态

如果你是新手,那你可以选择阅读针对新手编写的专栏:V3 Admin Vite 手摸手教程

⚡ 简介

V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术.

❓ 伴随着 vite 2.9.x 版本的发布(现已更新到 5.x),我也决定将基于 vue-cli 5.x 的 v3-admin 迁移到 vite,所以说 v3-admin-vite,并不算一个全新的模板项目,而是有一定用户基础,并可以用上生产环境的!

1️⃣ 特性

  • Vue3:采用 Vue3 + script setup 最新的 Vue3 组合式 API
  • Element Plus:Element UI 的 Vue3 版本
  • Pinia: 传说中的 Vuex5
  • Vite:真的很快
  • Vue Router:路由路由
  • TypeScript:JavaScript 语言的超集
  • PNPM:更快速的,节省磁盘空间的包管理工具
  • Scss:和 Element Plus 保持一致
  • CSS 变量:主要控制项目的布局和颜色
  • ESlint:代码校验
  • Prettier:代码格式化
  • Axios:发送网络请求(已封装好)
  • UnoCSS:具有高性能且极具灵活性的即时原子化 CSS 引擎
  • 注释:各个配置项都写有尽可能详细的注释
  • 兼容移动端: 布局兼容移动端页面分辨率

2️⃣ 功能

  • 用户管理:登录、登出演示
  • 权限管理:内置页面权限(动态路由)、指令权限、权限函数、路由守卫
  • 多环境:开发环境(development)、预发布环境(staging)、正式环境(production)
  • 多主题:内置普通、黑暗、深蓝三种主题模式
  • 多布局:内置左侧、顶部、混合三种布局模式
  • 错误页面: 403、404
  • Dashboard:根据不同用户显示不同的 Dashboard 页面
  • 其他内置功能:SVG、动态侧边栏、动态面包屑、标签页快捷导航、Screenfull 全屏、自适应收缩侧边栏、Hook(Composables)

3️⃣ 目录

# v3-admin-vite
├─ .husky                # 用户提交代码时格式化代码
├─ .vscode               # 本项目推荐的 vscode 配置和拓展
├─ public
│  ├─ favicon.ico
│  ├─ app-loading.css    # 首屏加载 loading
├─ src
│  ├─ api                # api 接口
│  ├─ assets             # 静态资源
│  ├─ components         # 全局组件
│  ├─ config             # 全局配置
│  ├─ constants           # 常量/枚举
│  ├─ directives         # 全局指令
│  ├─ hooks              # 全局 hook
│  ├─ icons              # svg icon
│  ├─ layouts             # 布局
│  ├─ plugins            # 全局插件
│  ├─ router             # 路由
│  ├─ store              # pinia store
│  ├─ styles             # 全局样式
│  ├─ utils              # 全局公共方法
│  └─ views              # 所有页面
│  ├─ App.vue            # 入口页面
│  └─ main.ts            # 入口文件
├─ tests                 # 单元测试
├─ types                 # ts 声明
├─ .env.development      # 开发环境
├─ .env.production       # 正式环境
├─ .env.staging          # 预发布环境
├─ .eslintrc.js          # eslint 配置
├─ .prettier.config.js   # prettier 配置
├─ tsconfig.json         # ts 编译配置
├─ unocss.config.ts      # unocss 配置
└─ vite.config.ts        # vite 配置

4️⃣ 开发

# 配置
1. 一键安装 .vscode 中推荐的插件
2. node 版本 18.x 或 20+
3. pnpm 版本 8.x 或最新版

# 克隆项目
git clone https://github.com/un-pany/v3-admin-vite.git

# 进入项目目录
cd v3-admin-vite

# 安装依赖
pnpm i

# 启动项目
pnpm dev

📚 基础

1️⃣ 路由

配置项

// 设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: 'noRedirect'

// 动态路由:必须设定路由的名字,一定要填写不然重置路由可能会出问题
// 如果要在 tags-view 中展示,也必须填 name
name: 'router-name'

meta: {
  // 设置该路由在侧边栏和面包屑中展示的名字
  title: 'title'
  
  // 设置该路由的图标,记得将 svg 导入 @/icons/svg
  svgIcon: 'svg name'
  
  // 设置该路由的图标,直接使用 Element Plus 的 Icon(与 svgIcon 同时设置时,svgIcon 将优先生效)
  elIcon: 'element-plus icon name'
  
  // 默认 false,设置 true 的时候该路由不会在侧边栏出现
  hidden: true
  
  // 设置该路由进入的权限,支持多个权限叠加(动态路由才需要设置)
  roles: ['admin', 'editor']
  
  // 默认 true,如果设置为 false,则不会在面包屑中显示
  breadcrumb: false
  
  // 默认 false,如果设置为 true,它则会固定在 tags-view 中
  affix: true
  
  // 当一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式
  // 只有一个时,会将那个子路由当做根路由显示在侧边栏
  // 若想不管路由下面的 children 声明的个数都显示你的根路由
  // 可以设置 alwaysShow: true,这样就会忽略之前定义的规则,一直显示根路由
  alwaysShow: true

  // 示例: activeMenu: "/xxx/xxx"
  // 当设置了该属性进入路由时,则会高亮 activeMenu 属性对应的侧边栏
  // 该属性适合使用在有 hidden: true 属性的路由上
  activeMenu: '/dashboard'
  

  // 是否缓存该路由页面
  // 默认为 false,为 true 时代表需要缓存,此时该路由和该页面都需要设置一致的 Name
  keepAlive: false
}

动态路由

constantRoutes 把不需要判断权限的路由放置在常驻路由里面,如 /login/dashboard

dynamicRoutes 放置需要动态判断权限并通过 addRoute 动态添加的路由。

注意:动态路由必须配置 name 属性,不然重置路由时,会漏掉没有该属性的动态路由,可能会导致业务 BUG

2️⃣ 侧边栏和面包屑

侧边栏

image.png image.png

侧边栏 @/layout/components/Sidebar 是通过读取路由并结合权限判断而动态生成的(换句话说就是常驻路由 + 有权限的动态路由)

侧边栏外链

可以在侧边栏中配置一个外链,只要你在 path 中填写了合法的 url 路径,当你点击侧边栏的时候就会帮你新开这个页面

{
    path: "/link",
    component: Layout,
    children: [
      {
        path: "https://github.com/un-pany/v3-admin-vite",
        component: () => {},
        name: "Link",
        meta: {
          title: "外链",
          icon: "link"
        }
      }
    ]
  }

面包屑

breadcrumb.png

面包屑 @/layout/components/BreadCrumb 也是根据路由动态生成的,为路由设置 breadcrumb: false 时该路由将不会出现在面包屑中,设置 redirect: 'noRedirect' 时该路由在面包屑中不能被点击

3️⃣ 权限

登录时通过获取当前用户的权限(角色)去比对路由表,生成当前用户具有的权限可访问的路由表,通过 addRoute 动态挂载到 router 上

页面权限

控制代码都在路由守卫 @/router/permission.ts 中,这里可根据具体的业务做相应的修改:

import router from "@/router"
import { useUserStoreHook } from "@/store/modules/user"
import { usePermissionStoreHook } from "@/store/modules/permission"
import { ElMessage } from "element-plus"
import { setRouteChange } from "@/hooks/useRouteListener"
import { useTitle } from "@/hooks/useTitle"
import { getToken } from "@/utils/cache/cookies"
import routeSettings from "@/config/route"
import isWhiteList from "@/config/white-list"
import NProgress from "nprogress"
import "nprogress/nprogress.css"

const { setTitle } = useTitle()
NProgress.configure({ showSpinner: false })

router.beforeEach(async (to, _from, next) => {
  NProgress.start()
  const userStore = useUserStoreHook()
  const permissionStore = usePermissionStoreHook()
  const token = getToken()

  // 判断该用户是否已经登录
  if (!token) {
    // 如果在免登录的白名单中,则直接进入
    if (isWhiteList(to)) return next()
    // 其他没有访问权限的页面将被重定向到登录页面
    return next("/login")
  }

  // 如果已经登录,并准备进入 Login 页面,则重定向到主页
  if (to.path === "/login") {
    return next({ path: "/" })
  }

  // 如果用户已经获得其权限角色
  if (userStore.roles.length !== 0) return next()

  // 否则要重新获取权限角色
  try {
    await userStore.getInfo()
    // 注意:角色必须是一个数组! 例如: ["admin"] 或 ["developer", "editor"]
    const roles = userStore.roles
    // 生成可访问的 Routes
    routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes()
    // 将 "有访问权限的动态路由" 添加到 Router 中
    permissionStore.addRoutes.forEach((route) => router.addRoute(route))
    // 确保添加路由已完成
    // 设置 replace: true, 因此导航将不会留下历史记录
    next({ ...to, replace: true })
  } catch (err: any) {
    // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
    userStore.resetToken()
    ElMessage.error(err.message || "路由守卫过程发生错误")
    next("/login")
  }
})

router.afterEach((to) => {
  setRouteChange(to)
  setTitle(to.meta.title)
  NProgress.done()
})

取消页面权限

假如你的业务场景中没有 动态路由 的概念,那么在 @/config/route 里可以关闭该功能,关闭后系统将启用默认角色,每个登录的用户都可见所有路由

/** 路由配置 */
interface RouteSettings {
  /**
   * 是否开启动态路由功能?
   * 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
   * 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false
   */
  dynamic: boolean
  /** 当动态路由功能关闭时:
   * 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的)
   * 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
   */
  defaultRoles: Array<string>
  /**
   * 是否开启三级及其以上路由缓存功能?
   * 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由)
   * 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效
   */
  thirdLevelRouteCache: boolean
}

const routeSettings: RouteSettings = {
  dynamic: true,
  defaultRoles: ["DEFAULT_ROLE"],
  thirdLevelRouteCache: false
}

export default routeSettings

指令权限

简单快速的实现按钮级别的权限判断(已注册到全局,可直接使用):

<el-tag v-permission="['admin']">admin可见</el-tag>
<el-tag v-permission="['editor']">editor可见</el-tag>
<el-tag v-permission="['admin','editor']">admin和editor都可见</el-tag>

但在某些情况下,不适合使用 v-permission。例如:element-plus 的 el-tab 或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。

这时候可以使用权限判断函数

import { checkPermission } from '@/utils/permission'
<el-tab-pane v-if="checkPermission(['admin'])" label="Admin">admin可见</el-tab-pane>
<el-tab-pane v-if="checkPermission(['editor'])" label="Editor">editor可见</el-tab-pane>
<el-tab-pane v-if="checkPermission(['admin','editor'])" label="AdminEditor">admin和editor都可见</el-tab-pane>

4️⃣ 发送HTTP请求

大致的流程如下:

image.png

统一管理的 API

@/api/login.ts

import { request } from "@/utils/service"
import type * as Login from "./types/login"

/** 获取登录验证码 */
export function getLoginCodeApi() {
  return request<Login.LoginCodeResponseData>({
    url: "login/code",
    method: "get"
  })
}

/** 登录并返回 Token */
export function loginApi(data: Login.ILoginRequestData) {
  return request<Login.LoginResponseData>({
    url: "users/login",
    method: "post",
    data
  })
}

/** 获取用户详情 */
export function getUserInfoApi() {
  return request<Login.UserInfoResponseData>({
    url: "users/info",
    method: "get"
  })
}

封装的 service.ts

@/utils/service.ts 是基于 axios 的封装,封装了全局 request 拦截器、response 拦截器、统一的错误处理、统一做了超时处理、baseURL 设置等。

5️⃣ 多环境

构建

项目开发完成,打包代码时,内置两种环境:

# 打包预发布环境
pnpm build:stage

# 打包正式环境
pnpm build:prod

变量

.env.production.env.xxx 文件中,配置了该环境对应的一些环境变量,例如:

# 当前环境对应接口的 baseURL
VITE_BASE_API = '/api/v1'

获取方式:

console.log(import.meta.env.VITE_BASE_API)

✈️ 进阶

1️⃣ ESLint

规范代码很重要!

  • 配置项:在 .eslintrc.js 文件中
  • 推荐 VSCode 的 ESlint 插件,它可在写代码时,将不符合规范的代码标红,并且在你保存代码是自动修复一些简单的标红的代码
  • 手动校验:pnpm lint(提交代码前可以执行该命令)

2️⃣ Git 提交校验

模板采用 husky + lint-staged 的方式,在提交代码的时候,进行 ESlint 代码校验和 Prettier 代码格式化。

会自动初始化,如果发现没有初始化 husky,也可以通过命令 pnpm prepare 初始化 husky!

3️⃣ 跨域

vite.config 里就有 proxy 进行反向代理。

与之对应的生产环境,则可以使用 nginx 来做反向代理。

反向代理

proxy: {
  "/api/v1": {
    target: "https://xxxxxx/api/v1",
    ws: true,
    changeOrigin: true,
    rewrite: (path) => path.replace("/api/v1", "")
  }
}

CORS

这种方案对于前端来说没有什么工作量,和正常发送请求写法上没有任何区别,工作量基本都在后端这里。

实现 CORS 之后,不管是开发环境还是生产环境,都能方便的调用接口。

4️⃣ SVG

使用全局 svg-icon 组件

有全局 @/components/svg-icon 组件,把下载好的图标存放在 @/icons/svg 即可。

无需在页面中引入组件,可直接使用

<!-- name 为 svg 文件名 -->
<!-- class 可修改默认样式 -->
<svg-icon name="user" font-size="20px" class="icon" />

这种方式一般用来处理将 svg 当做 icon 的场景,比如左侧导航菜单

将 svg 图片直接转化为 vue 组件

vite-svg-loader 插件提供的的功能:

<script lang="ts" setup>
import Svg404 from "@/assets/error-page/404.svg?component"
</script>

<template>
  <Svg404 />
</template>

这种方式一般用来处理将 svg 当做图片展示的场景,比如 404 页面的大图

下载 svg icon 网址

推荐 iconfont(如果是为左侧导航菜单下载 icon,建议选择单色图标库下载,如果发现下载后的 svg,自带颜色,可以手动删除 svg 图片源码中的 fill 属性试试)

5️⃣ 新增主题(黑暗主题为例)

新增主题样式文件

  • src/styles/theme/dark/index.scss
  • src/styles/theme/dark/variables.scss

注册新的主题

  • src/styles/theme/register.scss
  • src/hooks/useTheme.ts

❓ 常见问题

1️⃣ 所有的报错

  • Google 一下可以解决 99% 的报错
  • 尝试删除 node_modules 和 .lock 文件后再次依赖

2️⃣ 依赖失败/依赖慢

  • 不要使用 cnpm
  • 推荐用 pnpm
  • 国内用户可以设置最新的淘宝源加快依赖速度
  • 检查 node 版本和 pnpm 版本是否符合要求

3️⃣ 热更新失效

  • 检查配置路由时填写的路径是否正确(特别是字母大小写问题)

4️⃣ 页面出现空白

控制台出现警告:Component inside <Transition> renders non-element root node that cannot be animated

解决办法:页面只保留一个根元素节点(注意:根元素外的注释也要删)

详情可见:juejin.cn/post/707444…

5️⃣ 交流群

在 GitHub 仓库上可见(掘金不能放二维码,会被误认为引流)