- 前端
- 小程序
- 开发工具
【免费下载链接】wepy
小程序组件化开发框架 - 已归档
导读
本文以 wepy 框架官方路由模块(packages/router)的使用文档为骨架,系统讲解如何用createRouter创建路由管理器、以结构化routeMap取代散落的 URL 字符串、通过 Promise 化 API 完成页面跳转,并用参考 vue-router 设计的导航守卫处理预加载、登录限制、非正常流程跳转等真实业务场景。读完本文,你将能在 wepy 项目中落地一套支持任意类型传参、守卫可编程、tab 页与分包页统一管理的路由体系,并理解其底层实现原理。
一、模块定位与核心特性
packages/router是 wepy 生态中用于替代原生微信路由跳转的独立插件包。它“对微信路由进行了封装和扩展”,设计思路主要参考 vue-router,为开发者带来以下四大能力(对应 usage.md):
- 结构化路由配置:所有页面集中在
routeMap中声明,跳转时按页面名(name)定位,不再手工拼接/pages/xxx/yyy字符串路径; - API 支持 Promise:
navigateTo、redirectTo、switchTab、navigateBack、reLaunch五个接口全部 Promise 化,可以await跳转结果,也支持传入success/fail/complete回调; - 任意类型的路由传参:跳转时通过
query传入对象、函数、Promise 等任意类型数据,页面侧从this.$route.query取回完整数据,不受微信原生 URL 只能携带字符串参数的限制; - 导航守卫:提供
beforeRouteLeave、beforeEach、beforeEnter、beforeRouteEnter、beforeResolve、afterEach、beforeRouteUpdate七种守卫,覆盖全局、路由级、组件级三个维度。
从源码看,模块入口 index.js 对外导出createRouter、routerPlugin以及encodeParams/encodeUrl/decodeParams/decodePage/decodeUrl五个编解码工具函数。核心实现位于 core/createRouter.js、core/routerApi.js 与 install.js 三处,下文会逐步对照源码展开。
二、创建路由管理器
使用前先构造一份路由配置,再调用createRouter生成实例,并通过wepy.use注入插件:
const homePage = 'SplashScreen' const tabPages = ['CourseList'] const routeMap = { SplashScreen: '/pages/SplashScreen', CourseList: '/pages/tabBar/courseList/CourseList', CourseDetail: '/pages/CourseDetail', CourseOnlineDetail: { name: 'CourseDetail', query: { courseType: 'online' } }, } const config = { homePage, tabPages, routeMap } const router = createSmRouter(config) wepy.use(routerPlugin, router); export { router }配置中的三个字段分别承担不同职责:
| 字段 | 类型 | 说明 |
|---|---|---|
homePage | string | 首页(启动页)的页面名,供backHome、reLaunch等回退场景定位 |
tabPages | List<string> | tab 页页面名列表,因为 tab 页的跳转 API 与普通页面不同(详见下文) |
routeMap | Object | 页面路由配置表,key 为页面名,value 为路由配置 |
从源码看 createRouter 做了什么
createRouter.js 中createRouter(config)做了三件事:
- 调用
normalizeRouteMap把routeMap的三种取值(string / function / object)统一归一化为{ handler, beforeEnter }结构,并同步建立路由级守卫(beforeEnter)注册表; - 通过
setConfig将配置写入全局 core/config.js 中的config单例; - 返回一个合并了
config、routerApi、route/currentVm两个 getter 以及beforeEach/beforeResolve/afterEach三个全局守卫注册方法的对象。
值得注意的是 normalizeRouteMap 对字符串配置的解析逻辑:handleStrResult判断字符串是否包含/,包含则视为path,不包含则视为页面name。这正是后面“config(string)”一节“有/的被认为是 path”的源码依据。
插件安装与实例注入
wepy.use(routerPlugin, router)执行的是 install.js 的install方法,它会:
- 注册
__routerHandler合并策略,在wepy.page({ name, beforeRouteLeave, beforeRouteEnter, beforeRouteUpdate })声明时把对应守卫登记进componentGuardMap; - 注入全局 mixin,为每个页面/组件实例提供
$router、$route、$encodeUrl、$decodeUrl等工具; - 在
created/onShow/onUnload生命周期中维护路由栈(routeManager.routes)、当前路由(currentRoute)、当前实例(currentVm)以及守卫的执行时机。
因此,在页面中你可以直接通过this.$router(等同全局router对象)与this.$route(等同router.route)访问路由能力,详见 instance.md。
三、routeMap 路由配置详解
routeMap是一个对象(key/value),所有页面的路由都在此配置,其中 key 为页面名(name),value 为路由配置信息(config)。一个关键前提是:页面名必须在页面里定义wepy.page({ name: '页面名' })。
完整的配置示例如下(来自 config.md):
homePage = 'SplashScreen' tabPages = ['CourseList'] routeMap = { SplashScreen: '/pages/SplashScreen', CourseList: '/pages/tabBar/courseList/CourseList', ClassList: { name: 'CourseList', query: { courseType: 'class' } }, BookList: { path: '/pages/BookList', query: { info: { name: 'roma' } }, meta: 2 }, CityList: { handler: ({ query }) => ({ path: '/pages/CityList', query }) }, Courses: 'CourseList' }config 支持三种数据类型来配置一个路由:Object、function、string。
config(Object):第一种结构
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string | 否 | 页面绝对路径 |
| name | string | 否 | 页面名 |
| query | Object | 否 | 路由传参参数 |
| meta | any | 否 | 不属于 data 的额外信息 |
| beforeEnter | function | 否 | 路由级守卫,见导航守卫章节 |
注:
path、name 二选一(原文档此处写为 “path、page 二选一”,结合源码与上下文,二者等价于指定目标页面),name 应与wepy.page中定义的页面名一致。
config(Object):第二种结构(handler 动态生成)
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| handler | function | 是 | ({ query }) => { path, name, query, meta },具体含义参考上面 |
| beforeEnter | function | 否 | 路由级守卫 |
handler的返回值可以是一条完整路由,也可以继续返回name实现“链式别名”,最终解析到某个物理页面(见下文别名解析)。
config(function)
({ query }) => ({ path | name, query(可选), meta(可选) } | string)函数形式接收当前跳转携带的query,返回一个路由对象或字符串;返回字符串时同样遵循“有/为 path,否则为 name”的规则。
config(string)
path 或者 name,有 / 的被认为是 path字符串形式最为精简:'CourseList'视为按页面名跳转,'/pages/SplashScreen'视为按绝对路径跳转。
别名与路径解析的底层实现
路由配置最终都归一化为{ handler, beforeEnter }(见 createRouter.js),真正解析发生在 getRealPageInfo.js:
- 先在
config.routeMap中按 name 查找,找不到会抛出routeMap 中未找到 ${name} 页面错误; - 调用
handler({ query })得到结果:如果结果含path,则返回物理路径pagePath以及合并后的pageData、pageMeta,同时把链路中的页面名都压入pages数组; - 如果结果只有
name(即别名),则递归继续解析,直到拿到真实物理页面。
query与meta的合并遵循“右值覆盖左值”的mergeRight规则,也就是说跳转时传入的query会覆盖 routeMap 中预置的同名字段——这正是别名跳转示例中courseType: 'online'与外部传入course能共存的原因。
四、页面跳转 API 与任意类型传参
四个典型使用场景
文档 usage.md 给出了具体使用场景,下面逐一展开。
1. 页面跳转(基础)
// 其它页 const course = { id: '123', text: '课程' }; router.navigateTo({ name: 'CourseDetail', query: { course } }) // CourseDetail 页 wepy.page({ name: 'CourseDetail', onLoad() { // params = { course: { id: '123', text: '课程' } } const params = this.$route.query; } })跳转方把整个course对象塞进query,接收方在this.$route.query拿到的是完整对象,而非微信原生 URL 解码后的字符串——这就是“任意类型传参”能力的直接体现。
2. 别名跳转
// 其它页 const course = { id: '233', text: '线上课程' }; router.navigateTo({ name: 'CourseOnlineDetail', query: { course } }) // CourseDetail 页 wepy.page({ name: 'CourseDetail', onLoad() { // params = { course: { id: '233', text: '线上课程' }, courseType: 'online' } const params = this.$route.query; } })CourseOnlineDetail在 routeMap 中配置为{ name: 'CourseDetail', query: { courseType: 'online' } },跳转时传入的course与预置的courseType合并后一起到达物理页面CourseDetail。业务上可以把“同一个物理页面 + 不同参数”抽象为多个逻辑页面名,便于在守卫和埋点中区分入口。
3. 复杂情况下的页面数据交互(Promise 回传)
把“选择代金券”这类跨页面取数封装成一个服务,让组件像调用本地函数一样等待页面返回结果:
// 代金券选择组件 wepy.component({ /** 页面/组件名 */ name: 'BookingConfirmRowSelectTicket', methods: { async goSelectTicket() { this.ticketId = await this._getTicketId() || '' }, _getTicketId() { return new Promise((resolve, reject) => { this.$router.navigateTo({ name: 'UseTicket', query: { ...params, resolve, reject } }) }) } }, }) // 选择代金券页面 wepy.page({ /** 页面/组件名 */ name: 'UseTicket', methods: { onSelectTicket({ ticketId }) { this.ticketId = ticketId this.$router.navigateBack({ delta: 1 }) } }, onUnload() { this.$route.query.resolve(this.ticketId) } })原理在于:query支持任意类型,因此可以把resolve/reject两个函数直接放进query传给目标页;目标页在onUnload时把结果通过resolve回传,发起方await的 Promise 即被兑现。复杂页面交互可以完全抽象为“可等待的服务调用”,页面间不再需要全局事件总线或存储层做中转。
4. 预加载数据
为了提升页面加载速度、减少用户等待时间,可以用守卫beforeRouteEnter在进入页面前发起请求,页面实例化后再把数据“搬”进实例:
<script> wepy.page({ name: 'xxx', _pData: null, beforeRouteEnter(to, from, next) { const pData = api.getXXX() next(vm => { vm.$options._pData = pData }) }, onLoad() { this._fetchData() }, methods: { async _fetchData() { try { if (this.$options._pData) { this._renderingData = await this.$options._pData } else { this._renderingData = await api.getXXX() } } catch(e) { // 错误处理 } finally { this.$options._pData = null } } } }) </script>这里用到了beforeRouteEnter特有的next(vm => {})回调:进入页面的实例尚未创建,守卫函数内无法使用this,只能把对实例的操作包在传给next的函数里,等页面实例化后由路由内部调用(对应 guard.md 中“this 由于进入页面还未实例化,因此不能使用,由 vm 代替”的说明)。onLoad时优先取$options._pData,用后置空,避免二次进入时使用过期缓存。
完整 API 清单与参数说明
router对象的导航方法对应微信原生 API 的五个封装(router.md),统一形式为方法(object, encode):
| 方法 | 对应 wx API | 行为 | 特有参数 |
|---|---|---|---|
navigateTo | wx.navigateTo | 保留当前页面,跳转到应用内页面 | name、query |
redirectTo | wx.redirectTo | 关闭当前页面,跳转到应用内页面 | name、query |
switchTab | wx.switchTab | 跳转到 tabBar 页面 | name、query |
navigateBack | wx.navigateBack | 关闭当前页面,返回上一级或多级页面 | delta(默认 1,超过栈深则回首页) |
reLaunch | wx.reLaunch | 关闭所有页面,打开应用内某个页面 | name、query |
通用参数说明:
{Object} object内含name(页面名)、query(页面参数)、success/fail/complete(接口调用回调,complete无论成败都会执行);navigateBack用delta代替name/query;{bool} encode是否对参数进行编码(encodeURIComponent),默认false;- 返回值:Promise,根据调用成功/失败设置状态。
关于参数编码与过滤,文档特别指出(router.md):
page 会根据 routeMap 找到真实页面路径(path);data 会转换成查询参数(
key=value&key2=value2),如果 encode 为 true,则会使用encodeURIComponent编码,由于转换后是字符串,query 里如果有非 string 和 number 将会被过滤掉,可以从 route.query 里拿到完整的 query。
这段话的源码依据在 urlParse.js 的encodeParams:只有typeof obj[key]为'string'或'number'的字段才会被拼进 URL 查询串。也就是说:传给微信原生层的 url 只能携带字符串/数字,而对象、函数等复杂数据靠的是路由内部维护的完整query对象(routeManager缓存),并不会真正出现在 URL 上。这也是“任意类型传参”与“fullPath 只带字符串/数字参数”(见 router.md 对route.fullPath的描述)能够同时成立的原因。
routerApi还提供了两个便捷能力(routerApi.js):
router.onError(handler):注册全局错误回调,守卫中返回Error或next(error)时触发;router.backHome():一键switchTab回到homePage配置的首页。
此外,源码中 getRealMethod 会自动校正跳转方式:当目标页位于tabPages中、而调用方用的是navigateTo/redirectTo时,会自动改写为switchTab,避免因 tab 页 API 差异导致跳转失败——这就是tabPages配置存在的意义。
五、导航守卫:七种守卫与完整解析流程
导航守卫参考 vue-router 设计,基本保持接口对齐,但有三个区别(guard.md):
- 支持 promise,可让函数返回一个 promise 代替 next;
to、from的具体内容不同;- 解析流程有部分区别。
守卫参数约定
除afterEach外,各守卫形式均为(to, from, next):
{Object} to目标路由:name(页面名)、query(跳转页面携带数据)、meta(携带额外信息)、jumpMethodName(跳转方式:'navigateBack' | 'navigateTo' | 'redirectTo' | 'reLaunch' | 'switchTab')、encode(是否编码);{Object} from原路由:name、query、meta;{function} next:next():进入下一个守卫;next(false):中断当前的导航;next({ name, query, meta, jumpMethodName, encode }):跳转到另一个页面,当前导航被中断;next(error):传入Error实例则导航终止,错误被传递给router.onError()注册的回调;beforeRouteEnter额外支持next(vm => {}),vm 为进入页面的实例。
返回值(afterEach除外):promise,无论 rejected 还是 fulfilled,其值的处理逻辑相同——true进入下一守卫;false中断导航;返回路由对象则重定向;返回Error则终止并交给onError;beforeRouteEnter还可返回vm => {}函数。
⚠️ 注意:next 和返回值二选一,否则跳转会卡住。
源码中 routerApi.js 的 handleNext 对返回值做了类型分发:false会 reject 一个Error('router to: false');Object类型视为重定向;Function类型视为beforeRouteEnter的回调(存入routeManager.setNext);其余情况视为放行。
七种守卫速查
| 守卫 | 级别 | 触发时机 | this 指向 |
|---|---|---|---|
beforeRouteLeave | 组件级 | 离开页面时 | 离开页面的实例 |
beforeEach | 全局 | 每次导航开始(离开页面时) | 离开页面的实例 |
beforeEnter | 路由级 | 进入配置该守卫的路由前 | 离开页面的实例 |
beforeRouteEnter | 组件级 | 进入页面时 | 无(页面未实例化,用next(vm => {})) |
beforeResolve | 全局 | 以上守卫执行后、页面跳转前 | 无 |
afterEach | 全局 | 进入页面后(形式为(to, from)) | 进入页面的实例 |
beforeRouteUpdate | 组件级 | 跳转目标为当前页面时 | 当前页面的实例 |
完整页面导航解析流程
不同页面(离开页 ≠ 进入页):
- 从一个页面跳转到另一页面;
- 在离开页面里调用
beforeRouteLeave; - 调用全局的
beforeEach; - 调用路由配置里的
beforeEnter; - 调用即将进入页面的
beforeRouteEnter; - 调用全局的
beforeResolve; - 页面进入后;
- 调用全局的
afterEach; - 调用
beforeRouteEnter中传给next的回调函数(如果定义了); - 如果某个守卫改变了页面跳转,则中断以上流程并重新开始导航解析。
同一个页面(离开页 = 进入页):
- 从一个页面跳转到另一页面;
- 离开页和进入页为同一个页面;
- 调用当前页的
beforeRouteUpdate。
跳转成功以后,route 对象会更新。
这一流程在 routerApi.js 的 getGuardQueue 中有精确对应:同页跳转时守卫队列只有beforeRouteUpdateGuards;跨页跳转时守卫队列依次为beforeRouteLeaveGuards → beforeEachGuards → beforeEnterGuards(含别名链上所有页面)→ beforeRouteEnterGuards → beforeResolveGuards,守卫通过runQueue(queue.js)串行执行,任一步返回对象都会在 runGuard 的 catch 中触发递归重跑导航。
六、实战:五大典型业务场景
场景一:非正常流程页面跳转(beforeRouteLeave)
要求符合条件 A 的用户无论点击哪个按钮都跳到某个指定页面:
<script> wepy.page({ name: 'xxx', beforeRouteLeave(to, from, next) { // 如果不加 to.name !== 'xxxPage',会进入死循环 if (this.condition && to.name !== 'xxxPage') { next({ name: 'xxxPage', jumpMethodName: 'navigateTo' }) } else { next() } } }) </script>通过next({ name, jumpMethodName })重定向,并用to.name !== 'xxxPage'做终止条件,防止守卫反复触发自身造成死循环。
场景二:限制进入某个页面(beforeEnter)
进入页面需要满足某个条件,否则拦截并提示:
routerConfig = { xxxPage: { path: '/xxxPath', beforeEnter: (to, from, next) => { if (condition) { next() } else { this.$showToast('需要 xxx 条件方可进入哦') next(false) } } } }beforeEnter是路由级守卫,直接写在 routeMap 对应页面的配置对象里,适合做页面级鉴权。
场景三:页面内不同标签页之间跳转(beforeRouteUpdate)
某个页面由不同业务组成、分成不同标签页时,在定义路由时可根据标签页定义不同的逻辑页面(映射到同一个物理页面),页面内跳转时用beforeRouteUpdate处理:
routeMap = { xxxPage: '/xxxPath', APage: { name: 'xxxPage', query: { type: 'A' } }, BPage: { name: 'xxxPage', query: { type: 'B' } } } <script> wepy.page({ name: 'xxxPage', data: { type: 'A' }, beforeRouteUpdate(to, from, next) { if (to.query.type === 'B') { // 处理 B 业务 } next() } }) </script>由于APage/BPage解析后仍是xxxPage这个物理页面,导航走的是“同一个页面”的守卫分支,beforeRouteUpdate在页面不销毁的情况下完成标签页切换(文档注明可参考启动页、课表页的实现)。同页跳转时 routerApi.js 会直接调用routeManager.updateCurrentRoute({ query, meta })更新当前路由并返回true,不再发起真实的 wx 导航。
场景四:全局守卫(beforeEach / beforeResolve / afterEach)
<script> import router from 'router' // 全局前置守卫 router.beforeEach(function beforeEach(to, from, next) { next() }) // 或者返回 promise 代替 next router.beforeEach(function beforeEach(to, from) { return true }) </script>// beforeResolve:所有守卫执行后、跳转前 router.beforeResolve((to, from, next) => next()) router.beforeResolve((to, from) => true) // afterEach:进入页面后 router.afterEach((to, from) => {})全局守卫通过router.beforeEach(cb)/router.beforeResolve(cb)/router.afterEach(cb)注册(对应 globalGuard.js),适合做埋点上报、登录态校验、页面访问统计等横切逻辑。
场景五:路由级守卫(beforeEnter)
routerConfig = { PersonalDetail: { path: '/pagesSubPackage/personal/pages/PersonalDetail', beforeEnter: (to, from, next) => next() } }注意此场景路由配置的是分包路径(/pagesSubPackage/...),说明 routeMap 同样覆盖分包页面,只需把分包内页面的绝对路径配进path即可。
七、注意事项与已知边界
文档 usage.md 明确了两条限制,务必在实际开发中留意:
- 路由管理器基于微信提供的 API,如果用户的某个路由跳转行为不是基于 API(例如手势滑动返回、点击 tab、从外部跳转进入小程序),那么守卫将无法处理。源码侧 install.js 的
onShow对这类“非 API 导航”做了尽力补偿:检测到routeManager.route !== this.$_route时手动同步路由栈并补跑守卫,但本质上这属于事后修正,无法在跳转发生前拦截; - 首次分包加载时
beforeRouteEnter守卫无法终止路由跳转:因为导航守卫是在运行时处理的,首次分包加载前尚无跳转页面代码,拿不到此守卫。
另外还有两个来自文档的使用提醒:beforeRouteUpdate成功后 route 对象会更新;守卫函数内next与返回值必须二选一。
八、route 对象与实例能力速览
route 对象(this.$route/router.route)
当前页面对应的路由对象,属性如下(router.md):
| 属性 | 类型 | 说明 |
|---|---|---|
route.id | string | 当前页面的唯一 id |
route.fullPath | string | 当前页面 url,带查询参数(只带数字和字符串类型的参数) |
route.path | string | 当前页面 url,不带查询参数 |
route.name | string | 当前页面名 |
route.query | Object | 当前页面的完整查询参数数据 |
route.meta | any | 当前页面非查询参数的其它数据 |
route.routes | [Route, ..., Route] | 当前页面的路由栈 |
route.referrerRoute | Route | 访问轨迹中当前页面的上一个页面(例如 A → B → C 再返回 B,当前为 B、lastRoute 为 A、referrerRoute 为 C),没有则为 null |
route.redirectedFrom | string | 重定向页面名 |
从 routeManager.js 的createRoute可以看到,path由fullPath.split('?')[0]派生,routes/referrerRoute由路由栈与显示栈(_showRoutes)维护。
实例上的挂载(instance.md)
this.$router:router 对象实例,与全局router对象一致;this.$route:route 对象实例,与全局router.route一致。
在 install.js 中,mixin 还会为实例注入$encodeParams/$encodeUrl/$decodeParams/$decodePage/$decodeUrl五个编解码工具;同时约定:组件($id末尾为1)访问$route时会委托到this.$root.$route,保证组件内拿到的是所属页面的路由。
九、继续深入:配套文档与源码地图
本文以使用文档为核心展开,以下文件可供进一步阅读与验证:
- packages/router/doc/usage.md —— 本文主体:特性、创建路由管理器、典型使用场景、注意事项;
- packages/router/doc/config.md —— routeMap 的三种配置类型与字段说明;
- packages/router/doc/guard.md —— 七种导航守卫的完整参数、返回值与解析流程;
- packages/router/doc/router.md —— router 对象属性与五个导航方法的 API 参考;
- packages/router/doc/instance.md —— 页面/组件实例上的
$router与$route; - packages/router/core/createRouter.js —— 路由实例创建与 routeMap 归一化;
- packages/router/core/routerApi.js —— 导航 API 的 Promise 化、守卫队列编排与 tab 页自动校正;
- packages/router/core/utils/getRealPageInfo.js —— 别名递归解析与 query/meta 合并;
- packages/router/core/utils/urlParse.js —— URL 编码/解码与字符串/数字参数过滤;
- packages/router/install.js —— 插件安装、守卫登记与路由栈维护;
- packages/router/core/routeManager.js —— 路由栈、当前路由、当前实例的集中管理。
这套路由方案把微信原生路由的“URL 字符串 + 全局 API”改造成“页面名 + 任意参数 + 可编程守卫”的工程化体系,配合 wepy 的wepy.use(routerPlugin, router)即可在项目中一键启用。安装与使用方式为:从仓库 packages/router 目录引入createRouter与routerPlugin,按上文配置homePage/tabPages/routeMap后创建实例并注入即可,无需修改仓库任何文件。
- 前端
- 小程序
- 开发工具
【免费下载链接】wepy
小程序组件化开发框架 - 已归档
相关推荐
yuzu模拟器下载安装完全指南:从新手到高手的完整教程
yuzu模拟器下载安装完全指南:从新手到高手的完整教程 想要在电脑上畅玩Switch游戏吗?yuzu模拟器是你的最佳选择!作为全球最受欢迎的开源Nintendo
游戏开发Angular路由与导航精通:基于angular-interview-questions项目的路由配置与守卫实现
Angular路由与导航精通:基于angular interview questions项目的路由配置与守卫实现 你是否在构建Angular应用时遇到路由跳转混
前端知识库prompt-optimizer路由系统:前端路由与导航守卫实现
prompt optimizer路由系统:前端路由与导航守卫实现 引言:单页面应用的路由挑战 在现代化的Web应用中,路由系统是构建复杂用户界面的核心基础设施。
人工智能大模型提示工程AI 应用AI 评测
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考