欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

用Vue3.3、Vite和Element-Plus构建企业级后端管理系统的全栈教程(源代码公开)

最编程 2024-02-02 15:28:54
...

我正在参加「掘金·启航计划」

vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 + Element Plus 版本的后台管理前端解决方案,技术栈为 Vue3 + Vite4 + TypeScript + Element Plus + Pinia + Vue Router 等当前主流框架。

相较于其他管理前端框架,vue3-element-admin 的优势在于一有一无 (配套后端、复杂封装):

  • 配套完整 Java 后端 权限管理接口,开箱即用,提供 OpenAPI 文档 搭配 Apifox 生成 Node、Python、Go等其他服务端代码;

  • 完全基于 vue-element-admin 升级的 Vue3 版本,没有对框架(Element Plus)的组件再封装,上手成本低和扩展性高。

前言

本篇是 vue3-element-admin v2.x 版本从 0 到 1,相较于 v1.x 版本 主要增加了对原子CSS(UnoCSS)、按需自动导入、暗黑模式的支持。

项目源码

前后端源码 Gitee Github
前端 vue3-element-admin vue3-element-admin
后端 youlai-boot youlai-boot

项目预览

在线预览

vue3.youlai.tech/

首页控制台

明亮模式

接口文档

接口文档

权限管理系统

用户管理 角色管理
菜单管理 字典管理

扩展生态

youlai-mall 有来开源商城:Spring Cloud微服务+ vue3-element-admin+uni-app

youlai-mall 商品管理 mall-app 移动端

项目指南

  • 功能清单

  • 技术栈
技术栈 描述 官网
Vue3 渐进式 JavaScript 框架 cn.vuejs.org/
Element Plus 基于 Vue 3,面向设计师和开发者的组件库 element-plus.gitee.io/zh-CN/
Vite 前端开发与构建工具 cn.vitejs.dev/
TypeScript 微软新推出的一种语言,是 JavaScript 的超集 www.tslang.cn/
Pinia 新一代状态管理工具 pinia.vuejs.org/
Vue Router Vue.js 的官方路由 router.vuejs.org/zh/
wangEditor Typescript 开发的 Web 富文本编辑器 www.wangeditor.com/
Echarts 一个基于 JavaScript 的开源可视化图表库 echarts.apache.org/zh/
vue-i18n Vue 国际化多语言插件 vue-i18n.intlify.dev/
VueUse 基于Vue组合式API的实用工具集(类比HuTool工具) www.vueusejs.com/
  • 接口文档

    • 接口调用地址:vapi.youlai.tech
    • 接口文档地址:在线接口文档
    • OpenAPI 3.0 文档地址:vapi.youlai.tech/v3/api-docs

环境准备

名称 备注
开发工具 VSCode 下载 -
运行环境 Node 16+ 下载 image-20230224222640120
VSCode插件(必装) 插件市场搜索 Vue Language Features (Volar) TypeScript Vue Plugin (Volar) 安装,且禁用 Vetur image-20230224222541797

vite 项目初始化

按照 ????Vite 官方文档 - 搭建第一个 Vite 项目 说明,执行以下命令完成 vuetypescirpt 模板项目的初始化

 npm init vite@latest vue3-element-admin --template vue-ts
  • vue3-element-admin: 自定义的项目名称

  • vue-tsvue + typescript 模板的标识,查看 create-vite 以获取每个模板的更多细节:vue,vue-ts,react,react-ts

初始化完成项目位于 D:\project\demo\vue3-element-admin , 使用 VSCode 导入,执行以下命令启动:

npm install
npm run dev

浏览器访问 localhost:5173 预览

src 路径别名配置

相对路径别名配置,使用 @ 代替 src

Vite 配置

配置 vite.config.ts ,截图展示关键代码,完整代码移步:vite.config.ts

安装@types/node

import path from 'path'编译器报错:TS2307: Cannot find module 'path' or its corresponding type declarations.

本地安装 Node 的 TypeScript 类型描述文件即可解决编译器报错

npm install @types/node --save-dev

TypeScript 编译配置

同样还是import path from 'path' 编译报错: TS1259: Module '"path"' can only be default-imported using the 'allowSyntheticDefaultImports' flag

因为 typescript 特殊的 import 方式 , 需要配置允许默认导入的方式,还有路径别名的配置

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"] 
    },
    "allowSyntheticDefaultImports": true // 允许默认导入
  }
}

路径别名使用

// src/App.vue
import HelloWorld from '/src/components/HelloWorld.vue'import HelloWorld from '@/components/HelloWorld.vue'

unplugin 自动导入

Element Plus 官方文档中推荐 按需自动导入 的方式,而此需要使用额外的插件 unplugin-auto-importunplugin-vue-components 来导入要使用的组件。所以在整合 Element Plus 之前先了解下自动导入的概念和作用

概念

为了避免在多个页面重复引入 API组件,由此而产生的自动导入插件来节省重复代码和提高开发效率。

插件 概念 自动导入对象
unplugin-auto-import 按需自动导入API ref,reactive,watch,computed 等API
unplugin-vue-components 按需自动导入组件 Element Plus 等三方库和指定目录下的自定义组件

看下自动导入插件未使用和使用的区别:

插件名 未使用自动导入 使用自动导入
unplugin-auto-import
unplugin-vue-components

安装插件依赖

npm install -D unplugin-auto-import unplugin-vue-components 

vite.config.ts - 自动导入配置

先创建好 /src/types 目录用于存放自动导入函数和组件的TS类型声明文件,再进行自动导入配置

下面只贴关键配置代码,完整代码移步:vite.config.ts

// vite.config.ts
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";

import path from "path";

const pathSrc = path.resolve(__dirname, "src");

plugins: [
  AutoImport({
    // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
    imports: ["vue"],
    eslintrc: {
      enabled: true, // 是否自动生成 eslint 规则,建议生成之后设置 false 
      filepath: "./.eslintrc-auto-import.json", // 指定自动导入函数 eslint 规则的文件
    },
    dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // 指定自动导入函数TS类型声明文件路径
  }),
  Components({
    dts: path.resolve(pathSrc, "types", "components.d.ts"), // 指定自动导入组件TS类型声明文件路径
  }),
]

.eslintrc.cjs - 自动导入函数 eslint 规则引入

"extends": [
    "./.eslintrc-auto-import.json"
],

tsconfig.json - 自动导入TS类型声明文件引入

{
  "include": ["src/**/*.d.ts"]
}

自动导入效果

运行项目 npm run dev 自动

整合 Element Plus

参考: element plus 按需自动导入

需要完成上面一节的 自动导入 的安装和配置

安装 Element Plus

npm install element-plus

安装自动导入 Icon 依赖

npm i -D unplugin-icons

vite.config.ts 配置

参考: element-plus-best-practices - vite.config.ts

// vite.config.ts
import vue from "@vitejs/plugin-vue";
import { UserConfig, ConfigEnv, loadEnv, defineConfig } from "vite";

import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";

export default ({ mode }: ConfigEnv): UserConfig => {

  return {
    plugins: [
      // ...
      AutoImport({
        // ...  
        resolvers: [
          // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
          ElementPlusResolver(),
          // 自动导入图标组件
          IconsResolver({}),
        ]
        vueTemplate: true, // 是否在 vue 模板中自动导入
        dts: path.resolve(pathSrc, 'types', 'auto-imports.d.ts') // 自动导入组件类型声明文件位置,默认根目录
          
      }),
      Components({ 
        resolvers: [
          // 自动导入 Element Plus 组件
          ElementPlusResolver(),
          // 自动注册图标组件
          IconsResolver({
            enabledCollections: ["ep"] // element-plus图标库,其他图标库 https://icon-sets.iconify.design/
          }),
        ],
        dts: path.resolve(pathSrc, "types", "components.d.ts"), //  自动导入组件类型声明文件位置,默认根目录
      }),
      Icons({
        // 自动安装图标库
        autoInstall: true,
      }),
    ],
  };
};

示例代码

<!-- src/components/HelloWorld.vue -->
<div>
  <el-button type="success"><i-ep-SuccessFilled />Success</el-button>
  <el-button type="info"><i-ep-InfoFilled />Info</el-button>
  <el-button type="warning"><i-ep-WarningFilled />Warning</el-button>
  <el-button type="danger"><i-ep-WarnTriangleFilled />Danger</el-button>
</div>

效果预览

整合 SVG 图标

通过 vite-plugin-svg-icons 插件整合 Iconfont 第三方图标库实现本地图标

参考: vite-plugin-svg-icons 安装文档

安装依赖

npm install -D fast-glob@3.2.11 
npm install -D vite-plugin-svg-icons@2.0.1 

创建 src/assets/icons 目录 , 放入从 Iconfont 复制的 svg 图标

main.ts 引入注册脚本

// src/main.ts
import 'virtual:svg-icons-register';

vite.config.ts 配置插件

// vite.config.ts
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

export default ({command, mode}: ConfigEnv): UserConfig => {
 return (
     {
         plugins: [
             createSvgIconsPlugin({
                 // 指定需要缓存的图标文件夹
                 iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
                 // 指定symbolId格式
                 symbolId: 'icon-[dir]-[name]',
             })
         ]
     }
 )
}

SVG 组件封装

<!-- src/components/SvgIcon/index.vue -->
<script setup lang="ts">
const props = defineProps({
  prefix: {
    type: String,
    default: "icon",
  },
  iconClass: {
    type: String,
    required: false,
  },
  color: {
    type: String,
  },
  size: {
    type: String,
    default: "1em",
  },
});

const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
</script>

<template>
  <svg
    aria-hidden="true"
    class="svg-icon"
    :style="'width:' + size + ';height:' + size"
  >
    <use :xlink:href="symbolId" :fill="color" />
  </svg>
</template>

<style scoped>
.svg-icon {
  display: inline-block;
  outline: none;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
  fill: currentColor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
  overflow: hidden;
}
</style>

组件使用

<!-- src/components/HelloWorld.vue -->
<template>
 <el-button type="info"><svg-icon icon-class="block"/>SVG 本地图标</el-button>
</template>

整合 SCSS

一款CSS预处理语言,SCSS 是 Sass 3 引入新的语法,其语法完全兼容 CSS3,并且继承了 Sass 的强大功能。

安装依赖

npm i -D sass 

创建 variables.scss 变量文件,添加变量 $bg-color 定义,注意规范变量以 $ 开头

// src/styles/variables.scss
$bg-color:#242424;

Vite 配置导入 SCSS 全局变量文件

// vite.config.ts
css: {
    // CSS 预处理器
    preprocessorOptions: {
        //define global scss variable
        scss: {
            javascriptEnabled: true,
            additionalData: `@use "@/styles/variables.scss" as *;`
        }
    }
}

style 标签使用SCSS全局变量

<!-- src/components/HelloWorld.vue -->
<template>
  <div class="box" />
</template>

<style lang="scss" scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: $bg-color;
}
</style>

上面导入的 SCSS 全局变量在 TypeScript 不生效的,需要创建一个以 .module.scss 结尾的文件

// src/styles/variables.module.scss

// 导出 variables.scss 文件的变量
:export{
    bgColor:$bg-color
}

TypeScript 使用 SCSS 全局变量

<!-- src/components/HelloWorld.vue -->
<script setup lang="ts">
  import variables from "@/styles/variables.module.scss";
  console.log(variables.bgColor)  
</script>

<template>
  <div style="width:100px;height:100px" :style="{ 'background-color': variables.bgColor }" />
</template>

整合 UnoCSS

UnoCSS 是一个具有高性能且极具灵活性的即时原子化 CSS 引擎 。

参考:Vite 安装 UnoCSS 官方文档

安装依赖

npm install -D unocss

vite.config.ts 配置

// vite.config.ts
import UnoCSS from 'unocss/vite'

export default {
  plugins: [
    UnoCSS({ /* options */ }),
  ],
}

main.ts 引入 uno.css

// src/main.ts
import 'uno.css'

VSCode 安装 UnoCSS 插件

再看下具体使用方式和实际效果:

代码 效果
image-20230222220856251

如果UnoCSS 插件智能提示不生效,请参考:VSCode插件UnoCSS智能提示不生效解决

整合 Pinia

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。

参考:Pinia 官方文档

安装依赖

npm install pinia

main.ts 引入 pinia

// src/main.ts
import { createPinia } from "pinia";
import App from "./App.vue";

createApp(App).use(createPinia()).mount("#app");

定义 Store

根据 Pinia 官方文档-核心概念 描述 ,Store 定义分为选项式组合式 , 先比较下两种写法的区别:

选项式 Option Store 组合式 Setup Store

至于如何选择,官方给出的建议 :选择你觉得最舒服的那一个就好

这里选择组合式,新建文件 src/store/counter.ts

// src/store/counter.ts
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", () => {
  // ref变量 → state 属性
  const count = ref(0);
  // computed计算属性 → getters
  const double = computed(() => {
    return count.value * 2;
  });
  // function函数 → actions
  function increment() {
    count.value++;
  }

  return { count, double, increment };
});

父组件

<!-- src/App.vue -->
<script setup lang="ts">
import HelloWorld from "@/components/HelloWorld.vue";

import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>

<template>
  <h1 class="text-3xl">vue3-element-admin-父组件</h1>
  <el-button type="primary" @click="counterStore.increment">count++</el-button>
  <HelloWorld />
</template>

子组件

<!-- src/components/HelloWorld.vue -->
<script setup lang="ts">
import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>

<template>
  <el-card  class="text-left text-white border-white border-1 border-solid mt-10 bg-[#242424]" >
    <template #header> 子组件 HelloWorld.vue</template>
    <el-form>
      <el-form-item label="数字:"> {{ counterStore.count }}</el-form-item>
      <el-form-item label="加倍:"> {{ counterStore.double }}</el-form-item>
    </el-form>
  </el-card>
</template>

效果预览

环境变量

Vite 环境变量主要是为了区分开发、测试、生产等环境的变量

参考: Vite 环境变量配置官方文档

env配置文件

项目根目录新建 .env.development.env.production

  • 开发环境变量配置:.env.development

    # 变量必须以 VITE_ 为前缀才能暴露给外部读取
    VITE_APP_TITLE = 'vue3-element-admin'
    VITE_APP_PORT = 3000
    VITE_APP_BASE_API = '/dev-api'
    
  • 生产环境变量配置:.env.production

    VITE_APP_TITLE = 'vue3-element-admin'
    VITE_APP_PORT = 3000
    VITE_APP_BASE_API = '/prod-api'
    

环境变量智能提示

新建 src/types/env.d.ts文件存放环境变量TS类型声明

// src/types/env.d.ts
interface ImportMetaEnv {
  /**
   * 应用标题
   */
  VITE_APP_TITLE: string;
  /**
   * 应用端口
   */
  VITE_APP_PORT: number;
  /**
   * API基础路径(反向代理)
   */
  VITE_APP_BASE_API: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

使用自定义环境变量就会有智能提示,环境变量的读取和使用请看下一节的跨域处理中的 vite.config.ts的配置。

反向代理解决跨域

跨域原理

浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。

本地开发环境通过 Vite 配置反向代理解决浏览器跨域问题,生产环境则是通过 nginx 配置反向代理 。

vite.config.ts 配置代理

表面肉眼看到的请求地址: http://localhost:3000/dev-api/api/v1/users/me

真实访问的代理目标地址: http://vapi.youlai.tech/api/v1/users/me

整合 Axios

Axios 基于promise可以用于浏览器和node.js的网络请求库

参考: Axios 官方文档

安装依赖

npm install axios

Axios 工具类封装

//  src/utils/request.ts
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { useUserStoreHook } from '@/store/modules/user';

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: 50000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' }
});

// 请求拦截器
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const userStore = useUserStoreHook();
    if (userStore.token) {
      config.headers.Authorization = userStore.token;
    }
    return config;
  },
  (error: any) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, msg } = response.data;
    // 登录成功
    if (code === '00000') {
      return response.data;
    }

    ElMessage.error(msg || '系统出错');
    return Promise.reject(new Error(msg || 'Error'));
  },
  (error: any) => {
    if (error.response.data) {
      const { code, msg } = error.response.data;
      // token 过期,跳转登录页
      if (code === 'A0230') {
        ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', {
          confirmButtonText: '确定',
          type: 'warning'
        }).then(() => {
          localStorage.clear(); // @vueuse/core 自动导入
          window.location.href = '/';
        });
      }else{
          ElMessage.error(msg || '系统出错');
      }
    }
    return Promise.reject(error.message);
  }
);

// 导出 axios 实例
export default service;

登录接口实战

访问 vue3-element-admin 在线接口文档, 查看登录接口请求参数和响应数据类型

点击 生成代码 获取登录响应数据 TypeScript 类型定义

将类型定义复制到 src/api/auth/types.ts 文件中

/**
 * 登录请求参数
 */
export interface LoginData {
  /**
   * 用户名
   */
  username: string;
  /**
   * 密码
   */
  password: string;
}

/**
 * 登录响应
 */
export interface LoginResult {
  /**
   * 访问token
   */
  accessToken?: string;
  /**
   * 过期时间(单位:毫秒)
   */
  expires?: number;
  /**
   * 刷新token
   */
  refreshToken?: string;
  /**
   * token 类型
   */
  tokenType?: string;
}

登录 API 定义

// src/api/auth/index.ts
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { LoginData, LoginResult } from './types';

/**
 * 登录API 
 * 
 * @param data {LoginData}
 * @returns
 */
export function loginApi(data: LoginData): AxiosPromise<LoginResult> {
  return request({
    url: '/api/v1/auth/login',
    method: 'post',
    params: data
  });
}

登录 API 调用

// src/store/modules/user.ts
import { loginApi } from '@/api/auth';
import { LoginData } from '@/api/auth/types';

/**
 * 登录调用
 *
 * @param {LoginData}
 * @returns
 */
function login(loginData: LoginData) {
  return new Promise<void>((resolve, reject) => {
    loginApi(loginData)
      .then(response => {
        const { tokenType, accessToken } = response.data;
        token.value = tokenType + ' ' + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
        resolve();
      })
      .catch(error => {
        reject(error);
      });
  });
}

vue-router 动态路由

安装 vue-router

npm install vue-router@next

路由实例

创建路由实例,顺带初始化静态路由,而动态路由需要用户登录,根据用户拥有的角色进行权限校验后进行初始化

// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';

export const Layout = () => import('@/layout/index.vue');

// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/redirect',
    component: Layout,
    meta: { hidden: true },
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },

  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },

  {