从入口文件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();
出现登录页面后输入账号密码并点击登录按钮触发登录事件
handleLogin
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值如下:
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;
}
在菜单组件中获取菜单配置渲染。
// 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}
/>
);
}
路由守卫
之前项目里的路由守卫思路如下
需要注意的是: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);
});
}