这套题还不错,感兴趣的猿可以试一试:前端开发工程师


前言

当我们的项目依赖都配置完毕之后,最重要的一步就来了,那就是如何去处理用户权限,给用户分配指定的菜单。首先需要知道的一点是,我们的路由表只会配置一小部分,这部分是不需要任何权限就能访问的,也就是白名单,比如登录路由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 常量

接着定义好我们的 MutationTypesActionTypes 类型,当然如果不定义也可以,只是页面中可能就导出充斥着魔法字符串了。(官方也是建议我们这样去定义一些常量,后续就改动的话也只需要改一处地方了)

export enum MutationTypes {
  SetRoutes = "SET_ROUTES"
}

export enum ActionTypes {
  GenerateRoutes = "GENERATE_ROUTES"
}

复制代码

mutations 和 actions 方法类型

定义好这些之后,我们还需要定义 mutationsactions 方法类型。

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 的过程中,可能会遇到很多问题,这个是需要时间去打磨的,写得越多也就越熟练。