这套题还不错,感兴趣的猿可以试一试:前端开发工程师
前言
当我们的项目依赖都配置完毕之后,最重要的一步就来了,那就是如何去处理用户权限,给用户分配指定的菜单。首先需要知道的一点是,我们的路由表只会配置一小部分,这部分是不需要任何权限就能访问的,也就是白名单,比如登录路由
、404路由
、500路由
等等,其它的我们都可以从接口里面去获取,这样我们就可以根据用户权限来做一些过滤处理,从而达到不同用户显示不同菜单和路由的目的。
当然也有其它方式来做这些处理,个人目前还是比较喜欢这种方式。好了,话不多说,直接开撸。
配置axios拦截器
像配置axios
拦截器,一千个人估计有一千种写法,不过核心的逻辑是不会变的,无非就是做一些设置一些请求头和对响应数据做一些处理。
创建实例
首先我们创建一个 axios 实例,通过 axios.create(config)
方法。baseURL
将自动加在我们的 url
前面(除非url是一个绝对URL
),timeout
表示超时时间,如果请求超过了这个时间还没有响应,请求会被中断。
const service = axios.create({
baseURL: defaultConfig.zhtApiUrl,
timeout: timeout * 1000
})
复制代码
创建好实例之后,就可以配置拦截器了。
请求拦截器
在请求拦截器中,我们可以对请求头做一些处理。比如我们可以在请求拦截器上添加 TOKEN
,可以这样做:
service.interceptors.request.use(
config => {
const TOKEN = getToken()
if (TOKEN) {
config.headers.Authorization = TOKEN
}
return config
},
error => {
Promise.reject(error)
}
)
复制代码
响应拦截器
在响应拦截器中,主要是对响应数据做处理,比如响应的状态,响应的数据,以及一些错误处理。
service.interceptors.response.use(
response => {
const { url: apiUrl } = response.config
const { status, statusText, data } = response
if (status === 200) {
if (apiUrl!.includes('auth/social/token')) {
// 获取 user token 的接口,返回格式跟其它接口有区别,
return {
code: 0,
msg: 'success',
data
}
} else {
return data
}
} else {
// 报错
message.error(`出错了哦 status: ${status} - statusText: ${statusText}`)
return Promise.reject({
code: 1,
msg: statusText
})
}
},
error => {
// { response, message, request, config } = error
if (error.response) {
const { status, statusText } = error.response
if (status === 401) {
message.error(`接口没有权限访问,请检查接口或者参数! status: ${status} - statusText: ${statusText}.`)
} else if (status === 500) {
message.error(`请检查接口或服务器状态! status: ${status} - statusText: ${statusText}.`)
router.push('/portal-error500')
} else {
message.error(`出错啦! message: ${error.message} - statusText: ${statusText}.`)
}
} else {
message.error(`禁止访问! 错误信息: ${error}`)
}
}
)
复制代码
每个人或者说不同的项目,可能配置都不一样,所以,这里的配置仅供参考,具体的配置还需要根据业务需求来。
取消拦截器
如果你想在什么时候取消拦截器,可以这样做:
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
复制代码
导航守卫
vue-router
提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
我们可以创建一个 permissions.ts
文件,来写一些全局的导航守卫。
全局前置守卫
你可以使用 router.beforeEach
注册一个全局前置守卫。
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
复制代码
在 permissions.ts
文件中,我们就可以这样去配置,在导航跳转前,我们需要判断当前用户是否登录,如果他想去的不是登陆页面,那么我们就让他跳转到登录页,如果登录了,就判断是否他已经获取了权限,如果没有获取权限,我们就去调接口,否则我们就直接进入到项目的首页(如果记住了跳转的页面,就 redirect
到那个页面,而不是首页)。
我们的登录接口仅仅只是返回了跟 TOKEN
相关的信息,那么还需要根据 token
去获取此用户的权限,以及其它信息。
router.beforeEach(async (to, from, next) => {
const token = getToken()
const isWhite = whiteList.findIndex(w => w === to.path)
NProgress.start()
if (token) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取应用
await store.dispatch(`userModule/${userAction.GetApplictions}`)
// 获取用过户信息权限
const roles = await store.dispatch(`userModule/${userAction.GetInfo}`)
// 获取路由配置,可以根据 roles 来过滤
const accessRoutes = await store.dispatch(`permissionsModule/${permissionAction.GenerateRoutes}`, roles)
next({ path: '/', replace: true })
} catch (error) {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (to.path === '/login') {
next()
NProgress.done()
} else if (isWhite > -1) {
// 在白名单范围之内
next()
} else {
next('/login')
}
}
})
复制代码
全局后置钩子
我们也可以创建全局后置钩子,它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from) => {
NProgress.done()
})
复制代码
状态管理 Store
权限模块
创建一个用户模块(store/modules/permissions/index.ts
),这里作为用来存储、权限处理路由的中心。
state 类型
首先我们定义好 permissions
里面的 state
类型,我在这个里面只有一个 routes
数据,所有只需要定义这个 routes
类型就可以了。
export type ChildRouteType= Array<RouteType>
export interface RouteType {
name: string,
path: string,
component: string,
redirect?: string,
children?: ChildRouteType
}
export default interface PermissionsStateTypes {
routes: Array<RouteType>
}
复制代码
mutations 和 actions 常量
接着定义好我们的 MutationTypes
和 ActionTypes
类型,当然如果不定义也可以,只是页面中可能就导出充斥着魔法字符串了。(官方也是建议我们这样去定义一些常量,后续就改动的话也只需要改一处地方了)
export enum MutationTypes {
SetRoutes = "SET_ROUTES"
}
export enum ActionTypes {
GenerateRoutes = "GENERATE_ROUTES"
}
复制代码
mutations 和 actions 方法类型
定义好这些之后,我们还需要定义 mutations
和 actions
方法类型。
export type Mutations<T = PermissionsStateTypes> = {
[MutationTypes.SetRoutes](state: T, routes: RouteType[]): void
}
type ActionArgs = Omit<ActionContext<PermissionsStateTypes, RootState>, 'commit'> & {
commit<k extends keyof Mutations>(
key: k,
payload: Parameters<Mutations[k]>[1]
): ReturnType<Mutations[k]>
}
export type Actions = {
[ActionTypes.GenerateRoutes]({ commit }: ActionArgs, roles: string[]): void
}
复制代码
实现
定义好所有的类型之后,就可以开始实现 permissions
里面的核心逻辑了。
在 actions
中,我们去获取接口中的路由数据,然后通过我们设置的 roles
可以去做过滤处理,如果有权限的路由,我们就可以通过 router.addRoute
去动态添加。
由于接口返回的路由并不是真正的组件,
const state: PermissionsStateTypes = {
routes: []
}
const mutations: MutationTree<PermissionsStateTypes> & Mutations = {
[MutationTypes.SetRoutes](state: PermissionsStateTypes, routes: RouteType[]) {
state.routes = routes
}
}
const handleParseChildRoutes = (childs: ChildRouteType, prePath: string): ChildRouteType => {
if (childs) {
// @ts-ignore
return childs.map((c: RouteType) => {
return {
name: c.name,
path: c.path,
component: c.component === 'RouterView' ? RouterView : () => import(`@/views${prePath}/index.vue`)
}
})
} else {
return []
}
}
const actions: ActionTree<PermissionsStateTypes, RootState> & Actions = {
[ActionTypes.GenerateRoutes]({ commit }, roles: string[]) {
// 可以根据 roles 来选择性返回 route
// console.log('permissions roles', roles)
return new Promise<void>((resolve, reject) => {
// 这里的 asyncRoutesMap 是静态的数据,模拟从后台接口调用数据
asyncRoutesMap.forEach((route: RouteType) => {
router.addRoute({
name: route.name,
path: route.path,
redirect: route.redirect ? route.redirect : undefined,
component: () => import(`@/${route.component.toLowerCase()}/index.vue`),
children: handleParseChildRoutes((route.children as ChildRouteType), route.path)
})
})
// 经过处理之后,可以返回 accessRoutes
commit(MutationTypes.SetRoutes, asyncRoutesMap)
resolve(asyncRoutesMap)
})
}
}
复制代码
用户模块
创建用户模块(store/modules/user/index.ts
),用来存储用户信息、用户的权限以及应用和菜单的中心。
state类型
用户模块的 state
包含了用户姓名、权限、头像、介绍、当前中台的应用对象(currentApp)以及所有应用的数组集合。
export interface ApplicationType {
id?: number
appId?: number
path?: string
menuName?: string
childMenu?: Array<ApplicationType>
[propName:string]: any
}
export default interface UserModuleStateType {
name: string
roles: Array<string>
currentApp: ApplicationType
applications: Array<ApplicationType>
avatar?: string
introduction?: string
}
复制代码
mutations 和 actions 常量
export enum MutationTypes {
SetRoles = "SET_ROLES",
SetApplications = "SET_APPLICATIONS",
SetCurrentApp = "SET_CURRENT_APP"
}
export enum ActionTypes {
GetInfo = "GET_INFO",
GetApplictions = "GET_APPLICATONS"
}
复制代码
mutations 和 actions 方法类型
type ActionArgs = Omit<ActionContext<UserModuleStateType, RootState>, 'commit'> & {
commit<k extends keyof Mutations>(
key: k,
payload: Parameters<Mutations[k]>[1]
): ReturnType<Mutations[k]>
}
export type Mutations<T = UserModuleStateType> = {
[MutationTypes.SetRoles](state: T, roles: Array<string>): void
[MutationTypes.SetApplications](state: T, apps: Array<ApplicationType>): void
[MutationTypes.SetCurrentApp](state: T, app: ApplicationType): void
}
export type Actions = {
[ActionTypes.GetInfo]({ commit }: ActionArgs): void
[ActionTypes.GetApplictions]({ commit }: ActionArgs): void
}
复制代码
实现
定义好所有的类型之后,就需要去实现具体的逻辑了。
这里面包括设置当前用户角色 SET_ROLSE
(即权限),获取应用数据集合 SET_APPLICATIONS
以及设置当前中台所接入的应用对象 SET_CURRENT_APP
。其中数据都是静态数据,模拟从后台接口调用的数据。
const state: UserModuleStateType = {
name: '',
avatar: '',
introduction: '',
roles: [],
applications: [],
currentApp: {}
}
const mutations: MutationTree<UserModuleStateType> & Mutations = {
[MutationTypes.SET_ROLES](state: UserModuleStateType, roles: Array<string>) {
state.roles = roles
},
[MutationTypes.SET_APPLICATIONS](state: UserModuleStateType, apps: Array<ApplicationType>) {
state.applications = apps
},
[MutationTypes.SET_CURRENT_APP](state: UserModuleStateType, app: ApplicationType) {
state.currentApp = app
}
}
const actions: ActionTree<UserModuleStateType, RootState> & Actions = {
[ActionTypes.GetInfo]({ commit }) {
return new Promise((resolve, reject) => {
// getInfo() 获取接口
const roles = ['admin', 'editor']
commit(MutationTypes.SET_ROLES, roles)
resolve(roles)
})
},
[ActionTypes.GetApplictions]({ commit }) {
return new Promise<void>((resolve, reject) => {
commit(MutationTypes.SET_APPLICATIONS, applications)
commit(MutationTypes.SET_CURRENT_APP, applications[0])
resolve()
})
}
}
const UserModule: Module<UserModuleStateType, RootState> = {
namespaced: true,
state,
mutations,
actions
}
export default UserModule
复制代码
到这里,我们的用户模块也就配置好了。
总结
在 vuex
中我们采用了大量 TypeScript
语法,很多都是 TypeScript
中内置工具类型,比如:
-
Pick<T, K>
能够从已有对象类型中选取给定的属性及其类型,然后构建出一个新的对象类型。
-
Omit<T, K>
与“Pick<T, K>”工具类型是互补的,它能够从已有对象类型中剔除给定的属性,然后构建出一个新的对象类型。
还有很多具有实用性的工具类型待我们去发现和使用,在使用 TypeScript
的过程中,可能会遇到很多问题,这个是需要时间去打磨的,写得越多也就越熟练。