Code前端首页关于Code前端联系我们

【中泰】美少年请陪我幕后玩吧~(系列第三部:认证、动态路由)

terry 2年前 (2023-09-08) 阅读数 142 #Vue

前言

配置完项目依赖项后,接下来就是最重要的一步,即如何处理权限并将命名菜单分配给用户。首先要知道的是,我们的路由表中只有一小部分是无需权限即可访问的,即白名单,比如登录路由404路由500路由等,我们可以拿到剩下的界面,这样我们就可以根据用户的权限做一些过滤,实现对不同的用户显示不同的菜单和路线。

当然还有其他方法可以完成这些过程,目前我个人更喜欢它。好了,废话不多说,我们开始自慰吧。

配置axios拦截器

就像定义axios拦截器一样,一千个人估计有一千种写法,但核心逻辑不变。无非就是设置请求头和处理响应数据。

创建实例

首先,使用axios.create(config)方法创建一个axios实例。 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/modules/permissions/index.ts),作为路由存储和权限处理的中心。

状态类型

首先我们用字母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>
}
 

突变和作用常数

然后定义我们的 MutationTypesActionTypes 类型。当然,不指定也可以,但是页面可以导出满魔法集。 (官方也建议我们这样定义一些常量,以后如果改的话只需要改一处即可)

export enum MutationTypes {
  SetRoutes = "SET_ROUTES"
}

export enum ActionTypes {
  GenerateRoutes = "GENERATE_ROUTES"
}

 

突变类型和功能

定义完这些之后,我们还需要定义方法类型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包含用户名、权限、头像、简介、中台当前应用对象(currentApp)以及所有应用的批量集合。

突变和作用常数

export enum MutationTypes {
  SetRoles = "SET_ROLES",
  SetApplications = "SET_APPLICATIONS",
  SetCurrentApp = "SET_CURRENT_APP"
}

export enum ActionTypes {
  GetInfo = "GET_INFO",
  GetApplictions = "GET_APPLICATONS"
}
 

突变和操作的方法类型

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”。它可以从现有对象类型中删除特定属性,然后构造新的对象类型。

我们有很多实用的工具等待我们去发现和使用。在使用TypeScript应用程序时,您可能会遇到很多问题。这需要时间去打磨。写得越多越好。熟练。

一起交流吧

无论你面临什么问题或者想交朋友一起讨论技术(=.=),都可以加入我们的组织,和我们一起工作~

如果您喜欢这部分内容,请加入我们的QQ群,和大家一起交流技术~

QQ群1032965518

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门