项目技术栈(习惯用yarn)
-
vue3 + 组合式Api+
pinia
-
axios
yarn add axios
-
持久化状态(token)插件
pinia-plugin-persistedstate
yarn add pinia-plugin-persistedstate
-
加载进度条
yarn add nprogress
-
ant-design-vue组件库
yarn add ant-design-vue
-
less
yarn add less
采用vite进行vue3项目的创建。
1. 创建项目
yarn create vue
# yarn create vue 是固定的 hrsaas是项目名称
- 选择工具
- 安装依赖、启动
cd hrsaas # 切换到对应目录
yarn # 安装依赖
yarn dev #启动服务
- 初始化git仓库
git init
git add .
git commit -m "初始化项目"
git remote add origin <远程仓库地址>
git push -u origin master
2. 配置eslint相关配置和vscode的配置
创建项目时,我们选择了esliint, 但是有一些配置阻碍我们愉快的创建组件和开发
- 如下的eslint配置写入到项目根目录下的 .eslinttrc.cjs中
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier'
// 当前项目不支持 ts
// '@vue/eslint-config-typescript/recommended'
],
env: {
'vue/setup-compiler-macros': true
},
rules: {
'vue/multi-word-component-names': [
'error',
{
ignores: ['index']
}
],
'vue/no-setup-props-destructure': ['off'],
// 支持对 defineProps 解构
'vue/no-mutating-props': ['off']
}
}
- settings.json配置,按需配置
{
// ts文件格式化
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// vue文件格式化
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 保存时格式化
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
},
// 开启 eslint 格式化
"eslint.enable": true,
"eslint.run": "onType",
"eslint.options": {
"extensions": [
".js",
".ts",
".vue",
".jsx",
".tsx"
]
},
// 操作时作为单词分隔符的字符
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
// 一个制表符等于的空格数
"editor.tabSize": 2,
// 文件行尾符号
"files.eol": "\n",
// 是否以紧凑形式展示文件夹
"explorer.compactFolders": false,
//vscode文件图标
"workbench.iconTheme": "material-icon-theme",
"window.zoomLevel": 1,
"liveServer.settings.donotShowInfoMsg": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
//有时候我们的路径提示会失败,识别不出来@代表的是src,所以要加这个
"path-intellisense.mappings": {
"@": "${workspaceFolder}/src",
},
// 自动保存
// "files.autoSave": "afterDelay",
// 鼠标滚动放大缩小字体
"editor.mouseWheelZoom": true,
}
3.调整项目目录
- 调整完目录后、调整如下文件:
- 调整App.vue
- 调整router/index.js
- 删除views/AboutView.vue views/HomeView.vue
- 新建views/login/index.vue views/layout/index.vue
- 删除assets下的内容, 删除components下所有的组件
4.ant-design-vue主题定制
ant-desigin-vue是蚂蚁金服提供的一套基于Vue的pc组件库,支持最新版的Vue3.x。是主流的应用UI框架
- 在main.js中进行全局的导入和样式的引入、使用app实例对组件库进行注册
import AntD from 'ant-design-vue'
import 'ant-design-vue/dist/antd.less'
app.use(AntD) // 全局注册antD
- 在vite.config.js中配置主题
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
changeOrigin: true,
target: 'https://xxxxxx/api',
rewrite: (path) => path.replace(/^\/api/, '')
}
},
port: 5173
},
// 配置主题
css: {
preprocessorOptions: {
less: {
modifyVars: {
'primary-color': '#7094ff' // 配置主题的主色调
},
javascriptEnabled: true
}
}
}
})
- 修改完毕之后,自动生效
5. 完成登录表单的设计- ant-design-vue表单
- 基于组件库的规范要求,我们可以设计如下的表单结构
<a-form :model="loginForm" autocomplete="off">
<a-form-item name="mobile" :rules="[{
required: true, message: '手机号不能为空',
trigger: ['change', 'blur']
}, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', , trigger: ['change', 'blur'] }
]">
<!-- 手机号 -->
<!-- v-model原理实现 v2-v3的变化 v-model :value-> modelValue -->
<!-- v-model实现原理 :value @input -->
<a-input size="large" v-model:value="loginForm.mobile"></a-input>
</a-form-item>
<a-form-item name="password" :rules="[{
required: true, message: '密码不能为空', trigger: ['change', 'blur']
}]">
<a-input-password size="large" v-model:value="loginForm.password"></a-input-password>
</a-form-item>
<a-form-item name="isAgree">
<a-checkbox v-model:checked="loginForm.isAgree">用户平台使用协议</a-checkbox>
</a-form-item>
<a-form-item>
<a-button size="large" type="primary" block>登录</a-button>
</a-form-item>
</a-form>
- autocomplete属性为自动填充表单数据,我们设置为off
- rules规则如下
6.安装请求工具并封装request
import axios from "axios"
// 使用axios创建实例 new Vue() createApp()
const serive = axios.create({
// 初始化参数
}) // service和axios的功能一摸一样
// 请求拦截器
serive.interceptors.request.use()
// 响应拦截器
serive.interceptors.response.use()
export default serive // 导出工具
- 在这一步的操作中,我们后续可以在这里处理若干的问题,比如token的统一植入,数据的统一处理,异常的统一捕获
7.通过vite配置代理,解决跨域问题
- 在前后分离模式下,我们是无法在项目中正常访问其他服务的接口的,因为这里形成了跨域,跨域的解决方案在项目中,最简单的方式就是代理
- 原理:也就是我们通过自己的前端-向自己的后端发出一个请求,让我们自己的后端代替我们发出请求,从而间接拿到我们想要的拿到的数据,因为后端是不受浏览器同源策略的影响的
- 配置
vite.config.js
如下配置:
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// 在这里配置server 解决跨域
server: {
proxy: {
'/api': {
changeOrigin: true,
target: 'https://xxxxxxxxx/api',
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
'primary-color': '#7094ff' // 配置主题的主色调
},
javascriptEnabled: true
}
}
}
})
- src/utils/request.js配置:
import axios from "axios"
// 使用axios创建实例 new Vue() createApp()
const serive = axios.create({
// 初始化参数
// /sys/login => /api/sys/login
baseURL: '/api'
}) // service和axios的功能一摸一样
// 请求拦截器
serive.interceptors.request.use()
// 响应拦截器
serive.interceptors.response.use()
export default serive // 导出工具
8.token的处理
- pinia- 管理状态的工具
- 在src/stores 下新建一个token.js
// 专门来管理状态- 状态只有一份
import { defineStore } from "pinia" // 引入定义状态仓库的工具
import { ref } from 'vue'
// 1.仓库的标识 2. 仓库的需要管理的状态
const useToken = defineStore("token", () => {
// 回调函数 进行返回的状态就是要管理的状态
const token = ref(null) // 需要在token变化的时候 通知组件
// 修改token的方法
const updateToken = (val) => token.value = val
// 删除token的方法
const removeToken = () => token.value = null
return { token, updateToken, removeToken }
})
// 导出这个方法
export default useToken
- 通过pinia提供的defineStore的处理,我们就可以实现公共仓库的建立
- 前端浏览器刷新之后,所有的数据都会重新渲染,token也会随之消失,所以我们需要将公共状态进行可持久化
- 安装pinia可持久化插件(pinia-plugin-persistedstate)
- 在main.js中应用该插件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import AntDesign from 'ant-design-vue' // 引入全局包
import 'ant-design-vue/dist/antd.less' // less - css的预处理器 可以写嵌套语法 可以写变量
import PluginState from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const piniaApp = createPinia()
piniaApp.use(PluginState) //注册持久化插件
app.use(piniaApp) // 注册pinia
app.use(router)
app.use(AntDesign) // 注册全局组件
app.mount('#app')
- 在stores/token.js中设置可持久化标记
// 专门来管理状态- 状态只有一份
import { defineStore } from "pinia" // 引入定义状态仓库的工具
import { ref } from 'vue'
// 1.仓库的标识 2. 仓库的需要管理的状态
const useToken = defineStore("token", () => {
// 回调函数 进行返回的状态就是要管理的状态
const token = ref(null) // 需要在token变化的时候 通知组件
// 修改token的方法
const updateToken = (val) => token.value = val
// 删除token的方法
const removeToken = () => token.value = null
return { token, updateToken, removeToken }
}, {
persist: true // 可持久化的标记- 将我们的数据 持久化到前端缓存中
})
// 导出这个方法
export default useToken
9. 封装请求模块的api-处理响应拦截器
- 在axios这个工具中,它会将我们的数据默认包裹一层data再返回给我们,所以我们需要处理
10. 登录-存储token-跳转主页
11.基于token权限的导航守卫
- 我们已经完成了登录的过程,并且存储了token,但是此时主页并没有因为token的有无而被控制访问权限.
- 在src下新建permission.js
- 权限实际上是针对路由控制,操作的实际上是路由
// 做权限控制
import router from '@/router' // 可以不写index.js
import useToken from '@/stores/token'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css' // 引入进度条样式
// import { useRouter } from 'vue-router' 这个方法只能用在组件中
// 前置导航守卫
// 只要是路由发生跳转-就会执行- 跳转之前执行
// to 到哪里去
// from 从哪里来
// next 必须执行的一个函数
const whiteList = ['/login', '/404'] // 登录页 404白名单
router.beforeEach((to, from, next) => {
nprogress.start() // 开启进度条
const { token } = useToken()
if (token) {
// 有token的情况下
if (to.path === '/login') {
// 就是登录页
next('/') // 跳转到主页
} else {
next() // 放行的意思
}
} else {
// 没有token的情况下
if (whiteList.includes(to.path)) {
// 在白名单中 直接放行
next()
} else {
next('/login')
}
}
})
// 后置导航守卫
router.afterEach(() => nprogress.done()) // 关闭进度条
- permission.js建好之后,为了让其生效,需要在main.js中引入
- 为了体现页面进度的效果,我们可以安装一个进度条的修饰器(nprogress)
- 在前置守卫-开启
- 在后置后卫-关闭
12. 菜单组件的封装
- 菜单是根据当前路由中所包含的页面循环出来的,所以我们需要循环当前路由的信息,循环生成若干的菜单
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter() // 获取路由实例
// 计算属性-测算需要展示的路由的信息
const routes = computed(() => {
return router.options.routes.filter((item) => !item.hidden) // 找出所有的hidden为false的路由
})
const getMeta = (obj) => {
// 判断当前有没有子节点
if (obj.children && obj.children.length) {
// 如果有子节点 就读取 子节点中第一个的meta属性
// meta- 路由的元信息-存储信息的地方
// 有节点
return obj.children.find((item) => !item.hidden).meta
}
return obj.meta
}
</script>
- 路由的格式
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/views/layout/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
hidden: true
},
{
path: '/',
redirect: '/dashboard',
component: Layout,
// 子节点
children: [
{
path: 'dashboard', // 二级路由的地址
component: () => import('@/views/dashboard/index.vue'),
// 路由元信息
meta: {
title: '数据看板',
icon: 'HomeOutlined'
}
}
]
}
]
})
export default router
- 我们不想展示某个路由时,就可以设置路由信息下的hidden为true
13. 图标自封装注册
- ant-design-vue的图标需要单独安装和引入
yarn add @ant-design/icons-vue
- 在src/components/Icons/index.js中实现所有图标的统一注册
- app.use(对象), 会调用对象中的install方法
import {
HomeOutlined,
PartitionOutlined,
SettingOutlined,
TeamOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
PoweroffOutlined,
LockOutlined
} from '@ant-design/icons-vue'
// 以上图标都需要全局注册
const icons = [
HomeOutlined,
PartitionOutlined,
SettingOutlined,
TeamOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
PoweroffOutlined,
LockOutlined
]
export default {
install: (app) => icons.forEach((item) => app.component(item.displayName, item))
// 全局注册引入的所有图标
}
- 在main.js中统一注册
import Icons from '@/components/Icons'
app.use(Icons)
14.token超时处理
- token是有有效期的,一般为1个半小时-2个小时,所以当时间到了,我们请求接口的时,后端会返回401状态码,此时要根据状态码进行登出操作
import router from '@/router'
serive.interceptors.response.use(
(response) => {
const { success, message, data } = response.data // axios默认加了一层data
if (success) {
// 表示执行成功
return data // 返回需要的业务数据
}
// 提示消息
Msg.error(message)
// 报错
return Promise.reject(new Error(message))
},
(error) => {
if (error.response.status === 401) {
// 此时说明token超时了- 超时和没有token是没有任何区别的
const { removeToken } = useToken()
removeToken() // 删除token
// 回到登录
router.push('/login')
}
return Promise.reject(error)
}
)