前言
完犊子了,刚写完发布上去的文章被我给当草稿给误删了,真的服了。可能自己都觉得这篇文章写的不太好吧,那就换个思路重写吧!上篇文章写了基础的配置,如代码格式化,项目打包等相关,本篇文章继续深入开发细节,对开发规范,动态路由,pinia
状态管理器,axios
配 ts
二次封装等进行讲述!
1 开发规范
每个团队都有自己的开发规范,能保持代码的一致性,后面更好维护。当然有的项目比较久远,各写各的,也就无法形成有效的规范。所以从开始搭建项目框架时就要配置一套行之有效的规则。下文规范 仅供参考
# 1 目录命名
采用小写方式, 以中划线分隔,有复数结构时,要采用复数命名法, 缩写不用复数
# 2 文件名称 采用小写方式, 以中划线分隔 (JS、CSS、SCSS、HTML、PNG)
例 render-dom.js / signup.css / index.vue / company-logo.png
# 3 参数名、变量都统一使用 lowerCamelCase 风格,采用小写驼峰命名 lowerCamelCase 代码中的命名均不能以下划线,也不能以下划线或美元符号结束
例 userName,userAge (后台接口需要字段除外) 如登录 params:{username:'',password:''}
# 4 方法名 method 方法命名必须是 动词 或者 动词+名词 形式**
例 saveShopCarData /openShopCarInfoDialog
**增删查改,详情统一使用如下 5 个单词,不得使用其他(目的是为了统一各个端)**
create / delete / update / query
例 addUser updateCompany queryUserInfo deleteCompany
# 5条件判断和循环最多三层
条件判断能使用三目运算符和逻辑运算符解决的,就不要使用条件判断,但是谨记不要写太长的三目运算符。
如果超过 3 层请抽成函数,并写清楚注释
## vue相关 ##
#1 组件名为多个单词
例 export default {
name: 'TodoItem'
// ...
}
#2 组件文件名为 pascal-case 格式 index除外 禁止使用小驼峰格式
正例 components/my-component.vue
#3 全局组件在文件夹components内定义,以App开头 使用完整单词而不是缩写
命名 components/AppaArea.vue
使用 <AppaArea /> (更好的与局部组件区分)
#4 和父组件紧密耦合的子组件应该以父组件名作为前缀命名
components/
|- todo-list.vue
|- todo-list-item.vue
|- todo-list-item-button.vue
#5 在 Template 模版中使用组件,应使用 PascalCase 模式,并且使用自闭合组件。
例:
<my-component :data="data" />
常量用大写字母定义,下划线进行字段间隔
例:BUSINESS_MODEL
#6 路由路径 使用 pascal-case 格式 (vite 使用热更新时,如果路径使用PascalCase 可能会导致热更新出现问题)
const router=[
name:'home',
url:'home',
children:[
{
name:'myCompany',
url:'my-company',
}
]
]
2 动态路由
1 基础路由与打包指向注意事项
我们把默认的路由配置全部删除,只定义了登录路由
这里面 import.meta.env.BASE_URL
默认为'/',如果使用默认,怎我们在打包时也要加下配置。这个发布路径根据我们自己项目来,如果是域名根目录就为 '/', 如果是想要再指向某个路径,我们再加上同样的配置就行
2 动态路由的实现
我们先来定义一下数据,当做后台返回的权限字段,拿到数据后进行路由渲染,实现效果为根据权限对左侧菜单进行渲染,以及按钮层级的渲染。
export const asyncRoute = [
{
name: 'home',
path: 'home',
component: 'layout',
children: [
{
name: 'myCompany',
path: 'my-company',
component: 'home/index',
children: [
{
name: 'myCompanyViews',
path: '',
component: 'home/company/index' //主页面 也是查询按钮权限
},
{
name: 'myCompanyAdd',
path: '',
component: '' // 如果是按钮级别,该component 设置为空,不添加路由 通过自定义指令来实现按钮的显示与隐藏
},
{
name: 'myCompanyDelete',
path: '',
component: ''
}
]
}
]
}
]
1 动态导入前置工具
我们采用 import.meta.glob
的方式进行文件的动态导入。(为什么不使用 import + 箭头函数直接导入呢,在 vite 里面不支持动态字符这种方式导入)
const modules = import.meta.glob('@/views/**/*.vue')
导入该规则下的所有文件,然后我们进行对应匹配
2 递归方式导入
我们在 src 下定义一个 promission.ts
,在 main.ts
中引用,来进行路由拦截和动态路由的实现。
对路由数据进行遍历,如果 component
是 'layout'
则说明它是第一层级,直接使用我们的 'layout'
组件,如果不是,则我们对 component
进行对比匹配,拿到对应的文件,或者无需加载文件,通过 addRoute (router4)已经无法使用 addRouters
直接进行数组添加)
addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void;
addRoute(route: RouteRecordRaw): () => void;
addRouter
接收一个或者两个参数,如果是一个对象则为第一层路由,如果是路由名称和路由对象则在对应路由名称下添加该路由。子级对路由依次进行递归遍历实现动态路由的添加!
const filterRouter = (promissionList: Irouter) => {
promissionList.map((item: any) => {
if (item.component === 'layout') {
item.componentVue = layout
} else {
// 如果有component 则需要渲染路由,否则则不需要(如弹窗类新增操作,删除操作,接口返回树结构进行按钮权限判断)
item.componentVue = item.component ? modules[`/src/views/${item.component}.vue`] : ''
}
const obj = {
path: item.url,
name: item.routeName,
component: item.componentVue,
}
if (item.component === 'layout') { // 如果component 为layout则该组件为根路由
router.addRoute(obj)
} else if (item.component) {
router.addRoute(item.parent, obj) // 无component 无需动态添加路由
}
if (item.children && item.children.length > 0) {
item.children = item.children.map((ele: any) => {
ele.parent = item.routeName
})
item.children = filterRouter(item.children)
}
return item
})
return accessedRouters
}
3 添加路由拦截配置
我们在 router.beforeEach
里面进行路由拦截,如果无 token 则直接跳转登录,否则,可以通过 router.getRoutes()
的长度判断是否已经加载了路由,如果无添加路由,则通过我们上面定义的方法进行添加,如果此时跳转的登录页,我们取值路由首页路由路径进行跳转,如果不是登录,直接放行通过即可!
router.beforeEach(async (to, from, next) => {
const token: string | null = localStorage.getItem('token')
if (token) {
const routerPath = `首页路由路径`
if (router.getRoutes().length <= 4) {
await filterRouter(users.authorityTrees)
if (to.path !== '/login') {
next({ ...to, replace: true })
} else {
next(routerPath) // 无权限页面跳转到首页
}
} else if (to.path !== '/login') {
next()
} else {
next(routerPath)
}
} else if (to.path === '/login') {
localStorage.removeItem('token')
next()
} else {
localStorage.removeItem('token')
next('/login')
}
})
pinia 状态管理与数据持久化
pinia 配置与使用
我们通过脚手架创建项目时已经帮我们安装配置了 pinia store/users.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('users', {
state: () => ({
user: {
name: '李白',
age: 18888
}
}),
actions: {
setUser(obj: any) {
this.$state.user = { ...this.$state.user, ...obj }
}
}
})
vue文件中的几种使用方式
<template>
<div>我是定义的 pinia值 {{ countPinia }}</div>
<a-button @click="handelPinia" type="primary">修改Pinia的值</a-button>
</template>
<script lang="ts" setup>
import { useCounterStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// 使 pinia数据实现响应式
// const countPinia = computed(() => counter.user) //第一种
const countPinia = storeToRefs(counter) // 第二种
// 修改pinia数据
const handelPinia = () => {
// 第一种 直接修改state 中的值
// counter.user = {
// name: '甄姬',
// age: 8888
// }
// 第二种,直接替换state
// counter.$state = {
// user: {
// name: '妲己',
// age: 8888
// }
// }
// 第四种,调用$patch方法
// counter.$patch({
// user: {
// name: '妲己',
// age: 8888
// }
// })
// 第四种 使用store 暴露出来的方法 如 vuex 中 dispatch
counter.setUser({
name: '妲己',
age: 8888
})
}
</script>
pinia 数据持久化
使用 pinia-plugin-persist
插件进行
npm install pinia-plugin-persist
我们创建 store/index.ts
将 pinia 处理一下暴露出来,因为不只是一个地方要使用
// store/index.ts action 后添加配置
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
// main.ts 代码更新
import pinia from './stores'
import piniaPersist from 'pinia-plugin-persist'
pinia.use(piniaPersist)
app.use(pinia)
// store/users.ts
persist: {
enabled: true, // 开启存储
// **strategies: 指定存储位置以及存储的变量都有哪些,该属性可以不写,不写的话默认是存储到sessionStorage里边,默认存储state里边的所有数据**
strategies: [
{ storage: localStorage }
// storage 存储到哪里 sessionStorage/localStorage
// paths是一个数组,要存储缓存的变量,当然也可以写多个
// paths如果不写就默认存储state里边的所有数据,如果写了就存储指定的变量
]
}
非 vue 文件使用 pinia 比如在路由拦截里面需要使用权限数据
import pinia from '@/stores'
import { userStore } from './stores/users'
// router.beforeEach
router.beforeEach(async (to, from, next) => {
const { users } = userStore(pinia)
// beforeEach 之前 store 还未挂载,不能获取实例,所以在beforeEach 内部获取
})
axios 配置
src/api/index.ts
主要是对出参数据加类型配置
// 引入axios
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
// 使用指定配置创建axios实例
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_API, //env 文件中定义 VITE_BASE_API 接口域名
timeout: 30000
// ....其他配置
})
instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token: string | null = localStorage.getItem('token')
config.headers = {
...{ 'Content-Type': 'application/json', 'Accept-Language': 'zh-CN' },
...config.headers
}
if (token) {
config.headers['Authorization'] = `bearer ${token}`
}
return config
},
(err: AxiosError) => Promise.reject(err)
)
// 后台给我们的数据类型如下
// 泛型T指定了Response类型中result的类型,默认为any
type Response<T = any> = {
// 描述
desc: string
message: string
msgParam: string
status: string
data: T
}
// AxiosRequestConfig从axios中导出的,将config声明为AxiosRequestConfig,这样我们调用函数时就可以获得TS带来的类型提示
// 泛型T用来指定Reponse类型中result的类型
export default <T>(config: AxiosRequestConfig) =>
// 指定promise实例成功之后的回调函数的第一个参数的类型为Response<T>
new Promise<Response<T>>((resolve, reject) => {
// console.log(config)
// instance是我们在上面创建的axios的实例
// 我们指定instance.request函数的第一个泛型为Response,并把Response的泛型指定为函数的泛型T
// 第二个泛型AxiosResponse的data类型就被自动指定为Response<T>
// AxiosResponse从axios中导出的,也可以不指定,TS会自动类型推断
instance
.request<Response<T>>(config)
.then((response: AxiosResponse<Response<T>>) => {
// console.log(response.data)
// response类型就是AxiosResponse<Response<T>>,而data类型就是我们指定的Response<T>
// 请求成功后就我们的数据从response取出并使返回的promise实例的状态变为成功的
resolve(response.data)
})
.catch((err: AxiosError) => {
switch(err.response?.status){
...
}
})
})
src/api/login.ts
中引入,将我们封装好的 login
接口方法暴露出去使用
// 登录相关接口
import request from './index'
// 登录
interface ILogin {
username: string
password: string
}
interface Ires{
userId:string
token:string
....
}
// 登录
export const login = (data: ILogin) =>
// 指定我们封装的request函数的第一个泛型的类型为Category[],也就是指定 Response<T> 中T的类型
request<Ires>({ //any换为我们自定义的出参类型
url: 'xxx/login',
method: 'POST',
data
})
最后
到这里配置完成以后,我们的项目基本完成了整体流程的配置。后续文章会进行常用组件的封装, antd Ui
框架的使用以及遇到的问题整理记录