vben admin从登录到显示菜单都做了什么

1,477 阅读4分钟

从入口文件main.ts说起

pinia文档:pinia.vuejs.org/

async function bootstrap() {
  //声明app实例
  const app = createApp(App);

  // 配置pinia状态管理,pinia是Vue.js团队核心成员开发的新一代状态管理器
  setupStore(app);

  // 初始化系统的配置、项目配置、样式主题、持久化缓存等等
  initAppConfigStore();

  // 引入全局使用的组件
  registerGlobComp(app);

  // 多语言配置
  // Asynchronous case: language files may be obtained from the server side
  await setupI18n(app);

  // 配置路由
  setupRouter(app);

  // 路由守卫、权限判断、初始化缓存数据
  setupRouterGuard(router);

  // 注册全局指令
  setupGlobDirectives(app);

  // 全局错误处理
  setupErrorHandle(app);

  // https://next.router.vuejs.org/api/#isready
  // await router.isReady();

  app.mount('#app');
}

bootstrap();

image.png 出现登录页面后输入账号密码并点击登录按钮触发登录事件handleLogin

image.png

handleLogin->login

    async login(
      params: LoginParams & {
        goHome?: boolean;
        mode?: ErrorMessageMode;
      },
    ): Promise<GetUserInfoModel | null> {
      try {
        debugger;
        const { goHome = true, mode, ...loginParams } = params;
        //loginParams = {password: "123456",username: "vben"},mode=null
        //模拟请求登录接口并获取接口返回token
        const data = await loginApi(loginParams, mode);
        const { token } = data;

        // 在pinia状态管理器中存入token
        // actions: {
        // setToken(info: string | undefined) {
        //   this.token = info ? info : ''; // for null or undefined value
        //   setAuthCache(TOKEN_KEY, info);
        // },}
        this.setToken(token);
        return this.afterLoginAction(goHome);
      } catch (error) {
        return Promise.reject(error);
      }
    },

handleLogin->login->afterLoginAction

    async afterLoginAction(goHome?: boolean): Promise<GetUserInfoModel | null> {
      if (!this.getToken) return null;
      // get user info
      const userInfo = await this.getUserInfoAction();
      
      const sessionTimeout = this.sessionTimeout;
      if (sessionTimeout) {
        this.setSessionTimeout(false);
      } else {
        //定义一个pinia的状态管理器:usePermissionStore
        const permissionStore = usePermissionStore();
        if (!permissionStore.isDynamicAddedRoute) {
          //获取路由配置、生成菜单配置
          const routes = await permissionStore.buildRoutesAction();
          routes.forEach((route) => {
            router.addRoute(route as unknown as RouteRecordRaw);
          });
          router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
          permissionStore.setDynamicAddedRoute(true);
        }
        goHome && (await router.replace(userInfo?.homePath || PageEnum.BASE_HOME));
      }
      return userInfo;
    },

handleLogin->login->afterLoginAction->getUserInfoAction

获取 token 之后调用 getUserInfoAction 获取用户信息。 其中,getUserInfoAction为:

      async getUserInfoAction(): Promise<UserInfo | null> {
      if (!this.getToken) return null;
      //模拟真实环境获取用户路由,角色等信息
      const userInfo = await getUserInfo();
      const { roles = [] } = userInfo;
      if (isArray(roles)) {
        const roleList = roles.map((item) => item.value) as RoleEnum[];
        //在pinia状态管理器中存入roleList
        //action:{
        // setRoleList(roleList: RoleEnum[]) {
        //   this.roleList = roleList;
        //   setAuthCache(ROLES_KEY, roleList);
        // },}
        this.setRoleList(roleList);
      } else {
        userInfo.roles = [];
        this.setRoleList([]);
      }
      //在pinia状态管理器中存入roleList
      //action:{
      // setUserInfo(info: UserInfo | null) {
      //   this.userInfo = info;
      //   this.lastUpdateTime = new Date().getTime();
      //   setAuthCache(USER_INFO_KEY, info);
      // },}
      this.setUserInfo(userInfo);
      return userInfo;
    },

userInfo值如下:

image.png

handleLogin->login->afterLoginAction->buildRoutesAction

接着调用 buildRoutesAction 获取路由配置、生成菜单配置。

  async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
      debugger;
      const { t } = useI18n();
      const userStore = useUserStore();
      const appStore = useAppStoreWithOut();

      let routes: AppRouteRecordRaw[] = [];
      //从状态管理器中读取角色权限生成对应路由
      const roleList = toRaw(userStore.getRoleList) || [];
      // 获取权限模式
      const { permissionMode = projectSetting.permissionMode } = appStore.getProjectConfig;

      const routeFilter = (route: AppRouteRecordRaw) => {
        const { meta } = route;
        const { roles } = meta || {};
        if (!roles) return true;
        //返回包含该角色权限的路由
        return roleList.some((role) => roles.includes(role));
      };

      const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => {
        const { meta } = route;
        const { ignoreRoute } = meta || {};
        return !ignoreRoute;
      };
       switch (permissionMode) {
        case PermissionModeEnum.ROLE:
          // 前端方式控制(菜单和路由分开配置)
          //根据角色权限过滤路由
          // const routeFilter = (route: AppRouteRecordRaw) => {
          //   const { meta } = route;
          //   const { roles } = meta || {};
          //   if (!roles) return true;
          //   //返回包含该角色权限的路由
          //   return roleList.some((role) => roles.includes(role));
          // };
          routes = filter(asyncRoutes, routeFilter);
          routes = routes.filter(routeFilter);
          //将多级路由转换为二级路由
          // Convert multi-level routing to level 2 routing
          routes = flatMultiLevelRoutes(routes);
          break;

        case PermissionModeEnum.ROUTE_MAPPING:
          //前端方式控制(菜单由路由配置自动生成)
          routes = filter(asyncRoutes, routeFilter);
          routes = routes.filter(routeFilter);
          //将路由转换为菜单
          const menuList = transformRouteToMenu(routes, true);
          routes = filter(routes, routeRemoveIgnoreFilter);
          routes = routes.filter(routeRemoveIgnoreFilter);
          menuList.sort((a, b) => {
            return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0);
          });
          //存储菜单
          this.setFrontMenuList(menuList);
          // Convert multi-level routing to level 2 routing
          routes = flatMultiLevelRoutes(routes);
          break;

        //  If you are sure that you do not need to do background dynamic permissions, please comment the entire judgment below
        case PermissionModeEnum.BACK:
          //后台方式控制
          const { createMessage } = useMessage();

          createMessage.loading({
            content: t('sys.app.menuLoading'),
            duration: 1,
          });

          // !Simulate to obtain permission codes from the background,
          // this function may only need to be executed once, and the actual project can be put at the right time by itself
          let routeList: AppRouteRecordRaw[] = [];
          try {
            this.changePermissionCode();
            routeList = (await getMenuList()) as AppRouteRecordRaw[];
          } catch (error) {
            console.error(error);
          }

          // Dynamically introduce components
          routeList = transformObjToRoute(routeList);

          //  Background routing to menu structure
          const backMenuList = transformRouteToMenu(routeList);
          this.setBackMenuList(backMenuList);

          // remove meta.ignoreRoute item
          routeList = filter(routeList, routeRemoveIgnoreFilter);
          routeList = routeList.filter(routeRemoveIgnoreFilter);

          routeList = flatMultiLevelRoutes(routeList);
          routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];
          break;
      }
}

最后回到handleLogin方法

到此登录并生成路由流程已完成。

生成菜单

// src/router/menus/index.ts
// 自动加载 `modules` 目录下的菜单模块
const modules = import.meta.globEager("./modules/**/*.ts");
const staticMenus = transformMenuModule(modules); // 简化处理

async function getAsyncMenus() {
  const permissionStore = usePermissionStore();
  // 后端模式 BACK
  if (isBackMode()) {
    // 获取 this.setBackMenuList(menuList) 设置的菜单
    return permissionStore.getBackMenuList.filter(item => !item.meta?.hideMenu && !item.hideMenu);
  }
  // 前端模式(菜单由路由配置自动生成) ROUTE_MAPPING
  if (isRouteMappingMode()) {
    // 获取 this.setFrontMenuList(menuList) 设置的菜单
    return permissionStore.getFrontMenuList.filter(item => !item.hideMenu);
  }
  // 前端模式(菜单和路由分开配置) ROLE
  return staticMenus;
}

在菜单组件中获取菜单配置渲染。

image.png

// src/layouts/default/menu/index.vue
function renderMenu() {
  const { menus, ...menuProps } = unref(getCommonProps);
  if (!menus || !menus.length) return null;
  return !props.isHorizontal ? (
    <SimpleMenu {...menuProps} isSplitMenu={unref(getSplit)} items={menus} />
  ) : (
    <BasicMenu
      {...(menuProps as any)}
      isHorizontal={props.isHorizontal}
      type={unref(getMenuType)}
      showLogo={unref(getIsShowLogo)}
      mode={unref(getComputedMenuMode as any)}
      items={menus}
    />
  );
}

路由守卫

之前项目里的路由守卫思路如下

image.png

image.png 需要注意的是:next()含参的话会在跳转之后再次出发拦截器钩子,而无参数的时候不会再次触发,但是拦截器一定要触发next()才能resolve,所以如果无token登录的话不能直接next('/login'),这里的做法呢是创建一个白名单路由,也就是类似于访客模式,只有少数路由如登录路由是可以访问到的

其实vben的路由守卫也是这种白名单+动态路由的方式

// src/router/guard/permissionGuard.ts
export function createPermissionGuard(route) {
  const userStore = useUserStoreWithOut();
  const permissionStore = usePermissionStoreWithOut();
  router.beforeEach(async (to, from, next) => {
    // 白名单
    if (whitePathList.includes(to.path)) {
      next();
      return;
    }

    // 如果 token 不存在,从定向到登录页
    const token = userStore.getToken;
    if (!token) {
      if (to.meta.ignoreAuth) {
        next();
        return;
      }

      // redirect login page
      const redirectData: { path: string; replace: boolean; query?: Recordable<string> } = {
        path: LOGIN_PATH,
        replace: true
      };
      if (to.path) {
        redirectData.query = {
          ...redirectData.query,
          redirect: to.path
        };
      }
      next(redirectData);
      return;
    }

    // 获取用户信息 userInfo / roleList
    if (userStore.getLastUpdateTime === 0) {
      try {
        await userStore.getUserInfoAction();
      } catch (err) {
        next();
        return;
      }
    }

    // 根据判断是否重新获取动态路由
    if (permissionStore.getIsDynamicAddedRoute) {
      next();
      return;
    }
    const routes = await permissionStore.buildRoutesAction();
    routes.forEach(route => {
      router.addRoute(route);
    });
    router.addRoute(PAGE_NOT_FOUND_ROUTE);
    permissionStore.setDynamicAddedRoute(true);
  });
}