vue3 admin 保姆教学指南 | 一文让你彻底上手vue3全家桶,集成pinia+element-plus+vue-router@4

4,354 阅读9分钟

本文涉及到的内容

  • 如何配置项目别名
  • 如何配置环境变量
  • 集成element-plus和自定义Svg图标
  • 集成vue-router
  • 集成pinia,使用Pinia管理用户信息
  • 集成axios
  • 集成Mock,如何Mock用户相关的信息,如何使用Token做用户鉴权
  • 如何进行全局组件的注册

本文接上期文章# vue3 admin 保姆教学指南 | 项目规范集成教程,看完秒懂项目中各种奇怪的文件和配置,在此基础上迭代。

不废话,直接上干货。

集成Element-plus

1.安装Element Plus和图标组件

pnpm install element-plus @element-plus/icons-vue

2.全局注册组件

// main.ts
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'

createApp(App)
    .use(ElementPlus)
    .mount('#app')

23Element Plus全局组件类型声明

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["element-plus/global"]
  }
}

4.页面使用 Element Plus 组件和图标

<!-- src/App.vue -->
<template>
  <HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
  <div style="text-align: center;margin-top: 10px">
    <el-button :icon="Search" circle></el-button>
    <el-button type="primary" :icon="Edit" circle></el-button>
    <el-button type="success" :icon="Check" circle></el-button>
    <el-button type="info" :icon="Message" circle></el-button>
    <el-button type="warning" :icon="Star" circle></el-button>
    <el-button type="danger" :icon="Delete" circle></el-button>
  </div>
</template>

<script lang="ts" setup>
     import HelloWorld from '/src/components/HelloWorld.vue'
     import {Search, Edit,Check,Message,Star, Delete} from '@element-plus/icons-vue'
</script>

路径别名配置

使用 @ 代替 src

1. Vite配置

// vite.config.ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'

import path from 'path'

export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
        }
    }
})

引用path的时候会报类型错误,记得pnpm add -D @types/node,安装完以后会在多一个文件tsconfig.node.json

2. TypeScript 编译配置

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

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

4.别名使用

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

可以直接cmd+鼠标左键跳转到对应的文件目录。

如果遇到无法导入的情况,重启一下vscode

环境变量配置

1. env配置文件

项目根目录分别添加 开发、生产和模拟环境配置文件,如下图所示

文件内容

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '尚品汇商城后台管理系统'
VITE_APP_PORT = 3002
VITE_APP_BASE_API = '/api'
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_TITLE = '尚品汇商城后台管理系统'
VITE_APP_PORT = 3002
VITE_APP_BASE_API = '/prod-api'
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '尚品汇商城后台管理系统'
VITE_APP_PORT = 3002
VITE_APP_BASE_API = '/test-api'

默认我们运行pnpm dev的时候NDOE_ENV='development',运行pnpm build的时候NODE_ENV='production',多了一个test环境以后,我们就需要上面的方式额外添加一个test环境变量

配置运行命令

"scripts": {
		"dev": "vite", // dev环境不需要添加 --mode,默认就是 development
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
}

获取NODE_ENV

获取环境变量可以通过process.env.NODE_ENV来获取,后面我们就可以这个变量来区分不同环境了。

获取其他环境变量

import { defineConfig, loadEnv } from 'vite'
export default defineConfig((config) => {
   // 根据当前工作目录中的 `mode` 加载 .env 文件
  // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
  const { command, mode } = config
  const env = loadEnv(mode, process.cwd(), '')
  console.log(env.VITE_APP_TITLE)
})

通过loadEnv()函数可以获取配置文件中的参数

SVG图标配置

安装依赖

pnpm install vite-plugin-svg-icons -D

使用

vite.config.ts中配置插件

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'

export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // Specify symbolId format
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}

main.ts导入

import 'virtual:svg-icons-register'

封装/src/components/SvgIcon.vue组件

<template>
  <svg
    aria-hidden="true"
    :class="['svg-icon', spin && 'svg-icon-spin']"
    :style="getStyle"
  >
    <use :xlink:href="symbolId" :fill="color" />
  </svg>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import type { CSSProperties } from 'vue'
const props = defineProps({
  prefix: {
    type: String,
    default: 'icon',
  },
  name: {
    type: String,
    required: true,
  },
  color: {
    type: String,
    default: '',
  },
  size: {
    type: [Number, String],
    default: 20,
  },
  spin: {
    type: Boolean,
    default: false,
  },
})

const symbolId = computed(() => `#${props.prefix}-${props.name}`)
const getStyle = computed((): CSSProperties => {
  const { size } = props
  let s = `${size}`
  s = `${s.replace('px', '')}px`
  return {
    width: s,
    height: s,
  }
})
</script>

<style scoped>
.svg-icon {
  display: inline-block;
  overflow: hidden;
  vertical-align: -0.15em;
  fill: currentColor;
}
.svg-icon-spin {
  animation: loadingCircle 1s infinite linear;
}

/* 旋转动画 */
@keyframes loadingCircle {
  0% {
    transform: rotate(0);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>

上面我们定义的Svg组件可以配置以下功能:

  • 名称
  • 大小
  • 颜色
  • 是否loading效果

我们求阿里巴巴图标库下载一个icon,如下图:

找一个喜欢的图标,然后点击复制SVG代码。

在项目目录src/assets/icon下面创建一个refresh.svg文件,然后把刚才复制的代码粘贴到里面。

使用

<template>
  <div>
    <div>
      <svg-icon name="refresh" spin></svg-icon>
    </div>
  </div>
</template>

<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'
</script>

效果如下:

你可以根据自己的需求更改它的大小或者颜色,是否让它旋转。而且我们使用SVG以后,页面上加载的不再是图片资源,而是下面这样的:

这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。

全局注册组件

上面的SvgIcon组件我们在使用的都需要手动的引入一下,如果我们自定义的组件很多的时候,这样就显得很不方便了,所以我们来把上面我们的组件改造一下,使用全局注册的方式来。

定义组件改造,我们把组件目录修改成/components/SvgIcon/src/SvgIcon.vue

/components/SvgIcon下新建一个index.ts文件,暴露出组件

import SvgIcon from './src/SvgIcon.vue'
export { SvgIcon }

components下新建index.ts文件,用来把所有的组件引入,然后提供一个install方法

import type { App, Component } from 'vue'

// 当组件很多的时候,可以使用
import { SvgIcon } from './SvgIcon'

// 这个地方
const Components: {
  [propName: string]: Component
} = { SvgIcon }

export default {
  install: (app: App) => {
    Object.keys(Components).forEach((key) => {
      app.component(key, Components[key])
    })
  },
}

install是专门用来提供安装插件的一个方法,这样我们就可以使用app.use()用来注册所有的全局组件了。

main.ts

import { createApp } from 'vue'
import './style.less'
import App from './App.vue'
import registerGlobComp from '@/components'

const app = createApp(App)

app.use(registerGlobComp)

app.mount('#app')

这样我们在使用SvgIcon组件的时候,就不用再引入一次了

<template>
   <svg-icon name="refresh" spin></svg-icon>
</template>

<script setup lang="ts">
// import SvgIcon from '@/components/SvgIcon/src/SvgIcon.vue'
</script>

<style scoped lang="less">
</style>

集成Mock

1.安装依赖

pnpm add -D vite-plugin-mock mockjs

2.在 vite.config.js 配置文件启用插件。

Mock 服务通常只用于开发阶段,因此我们需要在配置文件中判断当前所处环境。

在 webpack 中通常会配置一个 NODE_ENV 的环境变量。而在 Vite 中,不用开发者进行设置,它提供了一种方便的判断开发环境和生产环境的方式,如下:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig((config) => {
  const { command } = config
  return {
    plugins: [
      vue(),
      viteMockServe({
        // 只在开发阶段开启 mock 服务
        localEnabled: command === 'serve'
      })
    ]
  }
})

dev环境下command='serve',build环境下command='build'

3.创建API

在根目录创建mock文件夹,然后创建user.ts文件,添加用户相关的接口

import { resultError, resultSuccess, getRequestToken } from './_utils'
// mock/user.ts

function createUserList() {
  return [
    {
      userId: 1,
      avatar:
        'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
      username: 'admin',
      password: '111111',
      desc: '平台管理员',
      roles: ['平台管理员'],
      buttons: ['cuser.detail'],
      routes: ['home'],
      token: 'Admin Token',
    },
    {
      userId: 2,
      avatar:
        'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
      username: 'system',
      password: '111111',
      desc: '系统管理员',
      roles: ['系统管理员'],
      buttons: ['cuser.detail', 'cuser.user'],
      routes: ['home'],
      token: 'System Token',
    },
  ]
}

export default [
  // 用户登录
  {
    url: '/api/user/login',
    method: 'post',
    response: ({ body }) => {
      const { username, password } = body
      const checkUser = createUserList().find(
        (item) => item.username === username && item.password === password,
      )
      if (!checkUser) {
        return resultError('Incorrect username or password!')
      }
      const { token } = checkUser
      return resultSuccess({
        token,
      })
    },
  },
  // 获取用户信息
  {
    url: '/api/user/info',
    method: 'get',
    response: (request) => {
      const token = getRequestToken(request)
      console.log(token)

      const checkUser = createUserList().find((item) => item.token === token)
      if (!checkUser) {
        return resultError(
          'The corresponding user information was not obtained!',
        )
      }
      return resultSuccess(checkUser)
    },
  },
]

上面我们写了两个接口,第一个是用户登陆接口,接收usernamepassword参数,然后在createUserList()做匹配,返回给前端。第二个是获取用户信息接口,接收token,然后从headers从拿到token信息,再从createUserList()做匹配,返回给前端。

然后我们就可以直接跟正常请求api一样,去请求对应的接口了

接下来我们封装一下axios,然后测试我们的mock接口

集成Axios

我们的axios封装放在目录src/utils/http下面,创建一个index.ts文件

import axios from 'axios'
import type {
  AxiosInstance,
  AxiosError,
  AxiosRequestConfig,
  AxiosResponse,
} from 'axios'
import { ElMessage } from 'element-plus'
import { localGet } from '../cache'
import { TOKEN_KEY } from '../../enums/cacheEnum'

const service: AxiosInstance = axios.create({
  baseURL: '/api',
  timeout: 0,
})

/* 请求拦截器 */
service.interceptors.request.use(
  (config) => {
    const token = localGet(TOKEN_KEY)
    if (token) {
      config.headers.Authorization = `${token}`
    }
    return config
  },
  (error: AxiosError) => {
    ElMessage.error(error.message)
    return Promise.reject(error)
  },
)

/* 响应拦截器 */
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, message, data } = response.data

    // 根据自定义错误码判断请求是否成功
    if (code === 0) {
      // 将组件用的数据返回
      return data
    } else {
      // 处理业务错误。
      ElMessage.error(message)
      return Promise.reject(new Error(message))
    }
  },
  (error: AxiosError) => {
    // 处理 HTTP 网络错误
    let message = ''
    // HTTP 状态码
    const status = error.response?.status
    switch (status) {
      case 401:
        message = 'token 失效,请重新登录'
        // 这里可以触发退出的 action
        break
      case 403:
        message = '拒绝访问'
        break
      case 404:
        message = '请求地址错误'
        break
      case 500:
        message = '服务器故障'
        break
      default:
        message = '网络连接故障'
    }

    ElMessage.error(message)
    return Promise.reject(error)
  },
)

/* 导出封装的请求方法 */
const http = {
  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return service.get(url, config)
  },

  post<T = any>(
    url: string,
    data?: object,
    config?: AxiosRequestConfig,
  ): Promise<T> {
    return service.post(url, data, config)
  },

  put<T = any>(
    url: string,
    data?: object,
    config?: AxiosRequestConfig,
  ): Promise<T> {
    return service.put(url, data, config)
  },

  delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return service.delete(url, config)
  },
}

export default http

集成Pinia

安装依赖

pnpm add pinia

引入pinia

创建文件store/index.ts,添加如下内容

import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

然后在main.ts中使用一下

import { createApp } from 'vue'
import './style.less'
import App from './App.vue'
import pinia from '@/store'
createApp(App).use(pinia).mount('#app')

封装userState信息

import { defineStore } from 'pinia'
import { login, getUserInfo } from '@/api'
import { LoginParams } from './model/userModel'
import { localSet, localGet } from '@/utils/cache'
import { TOKEN_KEY, USER_INFO_KEY } from '@/enums/cacheEnum'
import type { UserState } from './model/userModel'
import type { UserInfo } from '@/types/store'
export const useUserStore = defineStore({
  id: 'app-user',
  state: (): UserState => ({
    userInfo: null,
    token: undefined,
  }),
  getters: {
    getUserInfo(): UserInfo {
      return (this.userInfo as UserInfo) || localGet(USER_INFO_KEY) || {}
    },
    getToken(): string {
      return (this.token as string) || localGet(TOKEN_KEY) || ''
    },
  },
  actions: {
    setToken(token: string | undefined) {
      this.token = token ? token : ''
      localSet(TOKEN_KEY, token)
    },
    setUserInfo(info: UserInfo) {
      this.userInfo = info
      localSet(USER_INFO_KEY, info)
    },
    async login(params: LoginParams) {
      try {
        const data = await login(params)

        const { token } = data
        this.setToken(token)
        this.getUserInfoAction()
      } catch (error) {
        return Promise.reject(error)
      }
    },
    async getUserInfoAction() {
      try {
        const data = await getUserInfo()
        this.setUserInfo(data)
      } catch (error) {
        return Promise.reject(error)
      }
    },
  },
})

这里我们使用pinia对用户信息的操作封装了一下,登陆成功以后,会缓存token,或者用户信息以后,缓存。

使用useUserStroe

login页面,添加下面逻辑

import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()

const submitForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.validate((valid) => {
    if (valid) {
      userStore.login({
        ...ruleForm,
      })
    } else {
      console.log('error submit!')
      return false
    }
  })
}

每个我们定义的pinia,比如上面的useUserStore,都有一个唯一的id:app-user(不允许重复),在vue文件中使用的时候,可以通过const userStore = useUserStore(),获取到对应store的所有信息,包活state、action、gettter等。比之前的vuex简单多了。

集成router

官方文档:router.vuejs.org/

安装vue-router

pnpm add vue-router@4

创建路由实例

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

export const constantRoutes: Array<RouteRecordRaw> = [
  {
    path: '/',
    component: () => import('@/views/home/index.vue'),
  },
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
  },
  {
    path: '/401',
    component: () => import('@/views/error-page/index.vue'),
  },
]

// 创建路由实例
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 }),
})

export default router

路由实例全局注册

// main.ts
import router from "@/router";

const app = createApp(App)

app.use(router)

app.mount('#app')

在页面访问//login/401路由的时候已经切换了。

此时,我们的项目目录为下面这样,

image.png

本文到此结束,下期预告:

  • admin登陆流程
  • Layout配置
  • 路由权限控制,按钮权限控制

文章持续更新中。。。

代码地址

gitee.com/guigu-fe/gu… (欢迎star,欢迎PR)

文章教程系列