小程序的路由系统与浏览器 URL 路由有本质区别:小程序维护一条「页面栈」,所有跳转都是对栈的压栈、出栈、替换操作。理解页面栈的行为模型,是设计导航架构的前提。本文将从页面栈机制讲起,覆盖五大路由 API、参数传递与 EventChannel 回传、自定义导航栏适配、路由分层与防跳转,以及分包与路由的协同设计。
一、页面栈机制与栈深度限制
小程序运行时维护一条页面栈,每个页面实例对应一个栈元素。所有路由 API 的本质都是对栈的操作:
当前页面栈(从底到顶):
[ index ] → [ list ] → [ detail ] 栈顶是当前展示的页面
核心约束:
- 栈深度上限 10 层:超过 10 层再
navigateTo会失败(errMsg: navigateTo:fail webview count limit)。 - tabBar 页面特殊:tabBar 页面之间切换不压栈,且不能
redirectTo。 - 首页(栈底)不能被关闭:不能对栈底页面执行出栈操作。
// 查看当前页面栈
const pages = getCurrentPages();
console.log('当前栈深度:', pages.length);
console.log('栈底(首页):', pages[0].route);
console.log('栈顶(当前页):', pages[pages.length - 1].route);
一句话:把小程序路由想成「只能操作栈顶、深度上限 10 的栈」,几乎所有导航坑都能从这个模型推出答案。
二、五大路由 API 对比
| API | 栈行为 | 是否可跳 tabBar 页 | 适用场景 |
|---|---|---|---|
wx.navigateTo | 压栈(保留当前页) | 否 | 打开详情、进入下一级页面 |
wx.redirectTo | 替换栈顶(关闭当前页) | 否 | 表单提交后替换到结果页 |
wx.switchTab | 关闭所有非 tab 页 | 是 | 切换底部导航 |
wx.reLaunch | 清空整条栈再开新页 | 是 | 登录后重置、跳转首页 |
wx.navigateBack | 出栈(返回) | - | 返回上一页 |
// 典型场景
wx.navigateTo({ url: '/pages/detail/detail?id=1001' });
wx.redirectTo({ url: '/pages/result/result' });
wx.switchTab({ url: '/pages/home/home' });
wx.reLaunch({ url: '/pages/index/index' });
wx.navigateBack({ delta: 1 }); // 返回上一页,delta 支持多级
2.1 跳转失败兜底
栈满时 navigateTo 会失败,业务上需要降级:
function safeNavigate(url) {
const pages = getCurrentPages();
if (pages.length >= 10) {
wx.redirectTo({ url }); // 栈满时用替换代替压栈
} else {
wx.navigateTo({ url });
}
}
三、参数传递与 EventChannel 回传
3.1 URL Query 传参
跳转时通过 URL query 传参,目标页在 onLoad(options) 接收:
// A 页跳转
wx.navigateTo({ url: `/pages/detail/detail?id=${id}&from=home` });
// B 页接收
Page({
onLoad(options) {
console.log(options.id, options.from); // 所有参数均为字符串
}
});
参数需要 URL 编码:encodeURIComponent 处理含特殊字符的值;参数只能传「可序列化」的标量,不能传函数或大数据对象。
3.2 EventChannel 页面间通信
wx.navigateTo 支持 events 配置,配合目标页 getOpenerEventChannel() 实现「回传数据」:
// 打开页面,并监听目标页的事件
wx.navigateTo({
url: '/pages/editor/editor?type=avatar',
events: {
// 目标页通过 eventChannel.emit 触发
onSaved(data) {
console.log('编辑结果已回传:', data);
}
}
});
// 目标页 editor.js
Page({
onLoad() {
this._eventChannel = this.getOpenerEventChannel();
},
save() {
const result = { url: this.data.previewUrl };
this._eventChannel.emit('onSaved', result); // 回传给来源页
wx.navigateBack();
}
});
| 通信方式 | 方向 | 特点 |
|---|---|---|
| URL query | 打开方 → 目标页 | 单向、仅标量、需编码 |
| EventChannel | 目标页 → 打开方 | 双向事件、可回传对象 |
| 全局 Store | 任意 | 跨页共享,需订阅清理 |
一句话:正向传参用 URL query,回传数据用 EventChannel,跨页面共享状态才用全局 Store——三者各司其职,避免把所有通信都堆到 Store。
3.3 参数安全与回跳保护
路由参数天然「不可信」——来源可能是分享卡片、二维码或用户手工构造。需要做两层防护:
- 参数合法性校验:目标页对
id、type等参数做格式与存在性校验,非法参数直接重定向到兜底页。 - 回跳防开放重定向:登录后按
redirect参数回跳时,校验其是否为本小程序内的合法页面路径,防止被构造跳转到任意页面。
// utils/router.js
const WHITE_PAGES = ['/pages/index/index', '/pages/order/order'];
function safeRedirect(raw) {
if (!raw) return '/pages/index/index';
const decoded = decodeURIComponent(raw);
// 只允许站内路径:以 / 开头、不含协议跳转、且在页面白名单中
const valid = decoded.startsWith('/') &&
!decoded.includes('//') &&
(WHITE_PAGES.includes(decoded) || isValidPage(decoded));
return valid ? decoded : '/pages/index/index';
}
四、自定义导航栏适配
默认导航栏样式受限(不能自定义颜色渐变、双行标题等),因此「自定义导航栏」成为中大型应用标配。
4.1 开启自定义导航
// 页面级配置,仅对当前页生效
{
"navigationStyle": "custom"
}
或在 app.json 全局开启,再逐页关闭。
4.2 胶囊按钮适配
状态栏与胶囊按钮位置由设备决定,必须运行时计算:
// utils/navbar.js
function getNavBarMetrics() {
const win = wx.getWindowInfo(); // 基础库 2.20.1+ 推荐
const capsule = wx.getMenuButtonBoundingClientRect();
const statusBarHeight = win.statusBarHeight;
const navBarHeight = (capsule.top - statusBarHeight) * 2 + capsule.height;
return {
statusBarHeight,
navBarHeight,
capsuleWidth: capsule.width,
navBarRight: win.windowWidth - capsule.left, // 标题可用宽度
};
}
<!-- 自定义导航模板 -->
<view class="navbar" style="padding-top: {{statusBarHeight}}px; height: {{navBarHeight}}px;">
<view class="navbar-title">{{title}}</view>
</view>
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background: #ffffff;
}
.navbar-title {
text-align: center;
line-height: 44px;
font-weight: 600;
}
一句话:自定义导航必须用
wx.getMenuButtonBoundingClientRect()动态计算胶囊与状态栏位置,硬编码高度在刘海屏/不同机型上必然错位。
五、路由分层与防跳转
5.1 路由分层设计
把页面分为三类,控制跳转方向,避免环路:
| 层级 | 页面示例 | 跳转规则 |
|---|---|---|
| 入口层 | 首页、tab 页 | 可跳任意层级 |
| 业务层 | 列表、详情、编辑 | 只能向下跳或返回 |
| 结果层 | 成功页、分享页 | 禁止再向下跳,只能返回/重置 |
5.2 登录守卫
统一在跳转封装里做登录校验,避免每个页面重复写:
// utils/router.js
const { isLoggedIn } = require('./auth');
function requireLogin(url) {
if (!isLoggedIn()) {
wx.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent(url) });
return false;
}
wx.navigateTo({ url });
return true;
}
// 登录成功后按 redirect 回跳
const redirect = options.redirect ? decodeURIComponent(options.redirect) : '/pages/index/index';
5.3 防重复跳转
快速双击导致的重复跳转用「跳转锁」拦截:
let lastNavTime = 0;
function navigateWithLock(url) {
const now = Date.now();
if (now - lastNavTime < 300) return; // 300ms 内忽略重复点击
lastNavTime = now;
wx.navigateTo({ url });
}
六、分包与路由的关系
分包直接影响路由规则:
- tabBar 页面必须在主包:
tabBar.list指向的页面不能放在分包中。 - 分包页面可通过 URL 直接跳转:路由目标指向分包页面时,微信自动下载对应分包。
- 独立分包可脱离主包直达:
independent分包页面可作为独立入口,不加载主包。 - 路由预下载:在
preloadRule中预下载目标分包,减少跳转等待。
// app.json 示例
{
"pages": ["pages/index/index"],
"tabBar": {
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/user/user", "text": "我的" }
]
},
"subpackages": [
{ "root": "package-detail", "pages": ["pages/detail/detail"] }
],
"preloadRule": {
"pages/index/index": { "network": "all", "packages": ["package-detail"] }
}
}
一句话:路由设计要提前与分包规划对齐:tabBar 页固定主包、低频大页面放分包并用
preloadRule预热、独立分包支撑分享直达场景。
七、tabBar 配置与自定义 tabBar
7.1 原生 tabBar 配置
{
"tabBar": {
"color": "#999999",
"selectedColor": "#ff5500",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{ "pagePath": "pages/home/home", "text": "首页", "iconPath": "img/home.png", "selectedIconPath": "img/home_active.png" }
]
}
}
list 至少 2 项、最多 5 项,图标只支持本地路径,大小建议 81px × 81px。
7.2 自定义 tabBar
当需要渐变、中部凸起「+」按钮等特殊样式时,使用自定义 tabBar(custom: true + 自定义组件):
{
"tabBar": {
"custom": true,
"list": [
{ "pagePath": "pages/home/home", "text": "首页" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}
自定义 tabBar 组件监听 wx.switchTab 时,需自行维护选中态,且每个 tab 页面都要引入该组件。
八、常见问题
- navigateTo 失败:检查是否栈满(>10)、目标是否为 tabBar 页、路径是否在
app.json.pages或分包 pages 中。 - 参数丢失:URL 中的
&、?、中文等特殊字符未编码。 - onLoad 只执行一次:从 A 返回 B 再进 B,
onLoad不重跑,用onShow处理刷新。 - getCurrentPages 在非页面上下文报错:只能在页面/组件的页面生命周期中使用。
- 自定义导航与下拉刷新冲突:
navigationStyle: custom后原生下拉失效,需配合enablePullDownRefresh自行实现。
九、总结
| 环节 | 核心机制 | 关键要点 |
|---|---|---|
| 页面栈 | 栈模型 + 10 层上限 | 所有 API 都是栈操作 |
| 路由 API | navigateTo/redirectTo/switchTab/reLaunch | 明确各自栈行为 |
| 参数传递 | URL query + EventChannel | 正传 query,回传 channel |
| 自定义导航 | 胶囊动态计算 | 硬编码高度必错位 |
| 路由守卫 | 登录校验 + 跳转锁 | 收敛公共跳转逻辑 |
| 分包路由 | 主包固定 tabBar + preloadRule | 路由与分包协同设计 |
导航架构是小程序信息架构的外在表现。从页面栈模型出发,正确使用五大路由 API,用 EventChannel 解决跨页回传,用动态计算适配自定义导航栏,再辅以路由分层、登录守卫与防跳转,最后与分包规划对齐——这样一套导航体系足以支撑大型小程序的复杂页面关系,并保持清晰、可预测的跳转行为。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。