如果你写过稍微复杂一点的uniapp应用,大概率遇到过这样一个尴尬的情况:你想在用户跳转某个页面前做点检查——比如验证登录状态、判断用户角色是否有权限访问——然后你翻开uniapp的官方文档,发现根本没有类似Vue Router那种全局前置守卫的API。官方给的方案是在每个页面的onShow里写判断逻辑。页面少的时候还能忍,一旦项目里有三四十个页面,每个页面都重复一段登录校验代码,维护起来会非常痛苦。而且一旦需求变动(比如某些页面需要新增角色判断),改起来更是灾难。
这篇文章会分享一套我在实际项目中验证过的方案:在不借助第三方框架的前提下,用uniapp原生的API构建一个可跨端使用的路由拦截器,并在此基础上实现动态权限分发。整个方案在微信小程序、App和H5端都经过了验证,不需要为不同平台写两套代码。
先弄清楚:uniapp的路由和Vue Router有什么不同
在标准的Vue项目中,路由由vue-router管理,全局前置守卫beforeEach可以拦截每一次路由跳转,判断条件后决定放行、重定向或取消。但uniapp为了兼容小程序的原生路由机制,没有采用vue-router,而是封装了一套自己的路由API——uni.navigateTo、uni.redirectTo、uni.switchTab等。
这套API最大的问题是:它们是“发射后不管”的。你调用uni.navigateTo后,页面直接就跳过去了,中间没有给你留一个拦截的钩子。小程序端的路由更底层,几乎不受前端框架控制。
所以要实现路由拦截,思路必须转变:不能指望在跳转动作发生前拦截,而是要在页面即将显示时做判断,如果条件不满足就立即重定向回来。说白了就是把拦截逻辑从“跳转前”挪到“跳转后、渲染前”。
这个思路的关键在于——我们需要一个统一的地方来放置这些判断逻辑,而不是分散在几十个页面的onShow里。
方案设计:用全局混入 + 路由元信息实现集中拦截
既然每个页面都要在onShow(或onLoad)时执行检查,我们可以利用Vue的全局混入机制,把检查逻辑注入到每一个页面的生命周期中。同时,为了区分哪些页面需要哪些权限,我们在pages.json中给每个页面添加自定义的元信息。
整体流程如下:
- 在
pages.json中为每个页面配置needLogin和allowedRoles等字段。 - 在
main.js中注册全局混入,拦截页面的onShow生命周期。 - 在拦截函数中读取当前页面的路由元信息,结合用户登录状态和角色做判断。
- 条件不满足时,立即
uni.redirectTo到登录页或403页面。
这个方案的优势在于:一旦配置好,后续新增页面只需要在pages.json中声明权限要求即可,不需要在页面组件里写任何额外代码。
下面我们逐步实现它。
第一步:扩展pages.json的元信息
uniapp的pages.json支持在每个页面配置中增加自定义字段,这些字段可以在页面实例的$vm.$page上获取到。我们利用这个特性来定义权限规则。
打开pages.json,给需要权限控制的页面添加routeMeta字段:
{
"pages": [
{
"path": "pages/index/index",
"style": { "navigationBarTitleText": "首页" },
"routeMeta": { "needLogin": false }
},
{
"path": "pages/user/user",
"style": { "navigationBarTitleText": "个人中心" },
"routeMeta": { "needLogin": true }
},
{
"path": "pages/admin/dashboard",
"style": { "navigationBarTitleText": "管理后台" },
"routeMeta": { "needLogin": true, "allowedRoles": ["admin"] }
},
{
"path": "pages/login/login",
"style": { "navigationBarTitleText": "登录" },
"routeMeta": { "needLogin": false, "isLoginPage": true }
}
]
}
这里定义了三个字段:
needLogin:是否需要登录才能访问。allowedRoles:允许访问的角色数组,空数组或未定义表示任意已登录用户均可访问。isLoginPage:标记当前页面是登录页本身,避免在登录页上也触发重定向造成死循环。
你可能会注意到,pages.json本身并不认识routeMeta这个字段。这没关系,uniapp在编译时会把自定义字段原样保留到页面实例上,我们可以在运行时读取它。
第二步:编写全局拦截逻辑
在main.js中注册全局混入。我们选择拦截onShow而不是onLoad,因为onShow在每次页面显示时都会触发(包括从后台切回前台、从子页面返回等场景),更适合做权限检查。
// main.js
import App from './App.vue';
import { createSSRApp } from 'vue';
export function createApp() {
const app = createSSRApp(App);
// 全局混入:在onShow中注入路由拦截逻辑
app.mixin({
onShow() {
this._checkRoutePermission();
},
methods: {
async _checkRoutePermission() {
// 获取当前页面的路由元信息
const pages = getCurrentPages();
if (!pages.length) return;
const currentPage = pages[pages.length - 1];
// 兼容不同平台获取routeMeta的方式
// 小程序端:currentPage.$page.routeMeta
// H5端:可能需要从currentPage.$vm.$page获取
const routeMeta = currentPage.$page?.routeMeta
|| currentPage.route?.meta
|| {};
// 如果是登录页,不作拦截
if (routeMeta.isLoginPage) return;
// 不需要登录的页面直接放行
if (!routeMeta.needLogin) return;
// 需要登录:检查token是否存在
const token = uni.getStorageSync('uni_id_token');
if (!token) {
// 未登录,重定向到登录页
uni.reLaunch({ url: '/pages/login/login' });
return;
}
// 检查角色权限(如果有配置)
if (routeMeta.allowedRoles && routeMeta.allowedRoles.length > 0) {
// 从本地缓存或store中获取用户角色
const userInfo = uni.getStorageSync('user_info') || {};
const userRole = userInfo.role || 'user';
if (!routeMeta.allowedRoles.includes(userRole)) {
// 角色不匹配,跳转到403页面或无权限提示
uni.showToast({
title: '你没有权限访问该页面',
icon: 'none',
duration: 2000
});
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack({ delta: 1 });
}, 1500);
return;
}
}
}
}
});
return { app };
}
这里有两个需要注意的地方:
第一,getCurrentPages()返回的页面栈在不同平台上的结构略有差异。小程序端routeMeta存放在$page对象上,H5端可能需要从route上读取。上面的代码做了兼容处理,你可以在实际项目中根据调试结果微调。
第二,重定向用了uni.reLaunch而不是uni.navigateTo。这是因为如果用户直接打开了一个需要登录的页面(比如通过分享链接进入),页面栈里只有一个页面,用navigateTo跳转到登录页后返回时又会回到这个需要登录的页面,形成循环。用reLaunch直接清空页面栈,用户体验更好。
第三步:处理登录成功后的回跳问题
上面这个方案拦截了未登录用户,但还有一个体验问题:用户从需要登录的页面被重定向到登录页,登录成功后应该自动跳回原来的页面。如果只是简单地跳转到首页,用户体验会打折扣。
我们可以在重定向到登录页时,把当前页面路径传递过去,登录成功后再根据这个路径跳转回来。
修改拦截函数中重定向登录页的部分:
if (!token) {
// 获取当前页面完整路径(包含参数)
const currentRoute = currentPage.route || currentPage.$page?.fullPath;
// 把目标页面路径传给登录页
uni.reLaunch({
url: `/pages/login/login?redirect=${encodeURIComponent(currentRoute)}`
});
return;
}
然后在登录页pages/login/login.vue中,登录成功后处理回跳:
// 登录成功后调用此方法
const handleLoginSuccess = (token, userInfo) => {
uni.setStorageSync('uni_id_token', token);
uni.setStorageSync('user_info', userInfo);
// 获取redirect参数
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// 兼容不同平台获取路由参数
const options = currentPage.$page?.options
|| currentPage.options
|| {};
const redirect = options.redirect;
if (redirect) {
// 解码并跳回原页面
uni.reLaunch({ url: decodeURIComponent(redirect) });
} else {
// 没有redirect参数则跳转首页
uni.switchTab({ url: '/pages/index/index' });
}
};
这样就形成了一个完整的闭环:用户访问需要登录的页面 → 被拦截跳转到登录页(携带原页面路径)→ 登录成功 → 自动跳回原页面。整个过程用户基本无感知。
第四步:封装成独立模块,方便多项目复用
上面的代码已经可以工作,但混在main.js里不够优雅。我们可以把整个路由拦截逻辑抽成一个独立的模块,方便在不同项目间复用。
在uni_modules目录下创建一个插件,或者在项目的common目录下新建routeGuard.js:
// common/routeGuard.js
// 获取当前页面的路由元信息(兼容多平台)
function getRouteMeta() {
const pages = getCurrentPages();
if (!pages.length) return {};
const currentPage = pages[pages.length - 1];
return currentPage.$page?.routeMeta
|| currentPage.route?.meta
|| {};
}
// 获取当前页面完整路径
function getCurrentRoute() {
const pages = getCurrentPages();
if (!pages.length) return '';
const currentPage = pages[pages.length - 1];
return currentPage.route || currentPage.$page?.fullPath || '';
}
// 核心拦截函数
export async function checkRoutePermission() {
const routeMeta = getRouteMeta();
if (routeMeta.isLoginPage) return true;
if (!routeMeta.needLogin) return true;
const token = uni.getStorageSync('uni_id_token');
if (!token) {
const currentRoute = getCurrentRoute();
uni.reLaunch({
url: `/pages/login/login?redirect=${encodeURIComponent(currentRoute)}`
});
return false;
}
if (routeMeta.allowedRoles && routeMeta.allowedRoles.length > 0) {
const userInfo = uni.getStorageSync('user_info') || {};
const userRole = userInfo.role || 'user';
if (!routeMeta.allowedRoles.includes(userRole)) {
uni.showToast({
title: '暂无访问权限',
icon: 'none',
duration: 2000
});
setTimeout(() => uni.navigateBack({ delta: 1 }), 1500);
return false;
}
}
return true;
}
// 注册全局混入的辅助函数
export function installRouteGuard(app) {
app.mixin({
onShow() {
checkRoutePermission();
}
});
}
然后main.js就变得非常简洁:
import { installRouteGuard } from '@/common/routeGuard.js';
export function createApp() {
const app = createSSRApp(App);
installRouteGuard(app);
return { app };
}
这样做的好处是,下一个项目如果需要同样的功能,直接复制routeGuard.js过去,调整一下pages.json中的路由元信息配置就能跑起来。
第五步:处理TabBar页面的特殊情况
在实际开发中你会遇到一个棘手的问题:TabBar页面不能使用uni.reLaunch和uni.redirectTo跳转,它们只能通过uni.switchTab打开。而我们的拦截逻辑中,如果用户未登录,需要跳转到登录页,但登录页通常不是TabBar页面,用switchTab跳不过去。
解决这个问题的方案有两种:
方案一:登录页也配置为TabBar页面(不推荐,登录页出现在底部导航栏里很奇怪)。
方案二(推荐):在拦截逻辑中判断当前页面是否为TabBar页面,如果是,就不要用reLaunch,而是先在页面上覆盖一个登录弹窗或提示,等用户手动跳转到“我的”Tab页去登录。或者更简单的方式——TabBar页面本身不做拦截,而是进入页面后根据登录状态展示不同的内容(未登录显示“请先登录”的占位,已登录显示用户信息)。
修改routeMeta的配置思路:TabBar页面的needLogin可以设为'optional',表示登录状态可选,页面本身不拦截,但页面内部根据状态渲染不同UI。
修改checkRoutePermission函数:
export async function checkRoutePermission() {
const routeMeta = getRouteMeta();
if (routeMeta.isLoginPage) return true;
if (!routeMeta.needLogin) return true;
// 对于登录状态可选的页面,不做拦截,由页面内部控制
if (routeMeta.needLogin === 'optional') return true;
const token = uni.getStorageSync('uni_id_token');
if (!token) {
const currentRoute = getCurrentRoute();
// 检查当前页面是否为tabBar页面
const isTabBar = routeMeta.isTabBar || false;
if (isTabBar) {
// TabBar页面不跳转,页面内部自行处理未登录状态
return false;
}
uni.reLaunch({
url: `/pages/login/login?redirect=${encodeURIComponent(currentRoute)}`
});
return false;
}
// ... 角色检查逻辑不变
}
然后在TabBar页面的组件中,检查登录状态:
// 在个人中心Tab页面中
const isLogin = computed(() => {
const token = uni.getStorageSync('uni_id_token');
return !!token;
});
模板中根据isLogin显示不同内容即可。这种处理方式在微信小程序等平台的审核中也被认为是合理的交互模式。
这个方案的局限性以及如何应对
任何方案都有适用边界,这里坦诚地说明几个限制:
1. 拦截发生在onShow阶段,页面已经加载。虽然用户看不到页面内容(因为onShow在渲染前触发),但页面的onLoad已经执行过了。如果你的onLoad中发起了网络请求,这些请求会在拦截发生前就发出。解决方法是把需要权限的数据请求放在onShow中,或者在请求前手动调用checkRoutePermission。
2. 无法拦截物理返回。用户点击手机上的返回按钮或手势返回时,我们的拦截逻辑不会触发。如果用户从管理后台页面返回到上一个页面,我们的方案无法阻止。对于这种场景,需要在目标页面(被返回的那个页面)的onShow中做权限复核——好在我们已经通过全局混入实现了这一点。
3. 不同平台的routeMeta获取方式有差异。上面代码中做了兼容处理,但如果uniapp底层更新导致$page的结构变化,可能需要微调。建议在项目初期就在各目标平台上做充分测试。
完整案例:一个带有角色权限的后台系统配置
最后,以一个模拟的“内容管理系统”为例,展示完整的pages.json配置和对应的角色体系:
{
"pages": [
{ "path": "pages/index/index", "routeMeta": { "needLogin": false } },
{ "path": "pages/article/list", "routeMeta": { "needLogin": true } },
{ "path": "pages/article/editor", "routeMeta": { "needLogin": true, "allowedRoles": ["editor", "admin"] } },
{ "path": "pages/user/list", "routeMeta": { "needLogin": true, "allowedRoles": ["admin"] } },
{ "path": "pages/login/login", "routeMeta": { "needLogin": false, "isLoginPage": true } },
{ "path": "pages/forbidden/403", "routeMeta": { "needLogin": false } }
],
"tabBar": {
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/article/list","text": "文章" }
]
}
}
配合我们之前写好的routeGuard.js,整个权限体系就运转起来了:普通注册用户能查看文章列表但不能编辑;编辑角色能写文章但不能管理用户;只有admin能进入用户管理页面。所有逻辑集中在pages.json的配置和一个独立的模块文件中,页面组件本身不需要感知权限逻辑。

