背景
我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:
// 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
wx.navigateTo({
url: "pages/somepage?id=1",
success: function (res) {},
});
但这里面存在几个问题:
- 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的
- 需要知道页面是否为 tabbar 页面(switchTab)
- 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行
- navigateBack 不支持传参
为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:
- 页面别名声明使用注释方式,不侵入业务代码
- 页面可以存在多个别名,方便新老版本页面的流量切换
- 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心
- 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)
- 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制
实现思路
step1. 资源描述约定
小程序内的跳转类操作存在以下几种
- js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)
- js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)
- 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )
针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源
- 内部页面
https://host?cmd=${pagename}¶m1=a // 打开普通页面并传参,标准的H5容器也算在普通页面内
- 微信原生 API
https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456 // 拨打电话
https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调
- 需要借助按钮 open-type 的微信原生能力
https://host?cmd=nativeButtonAPI&openType=contact // 在线客服
- 打开另一个小程序
https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid}
step2. 在页面内定义需要的数据
在每个页面的顶部添加注释,注意 cmd 不能重复,支持多个 cmd。为了方便后续解析,我们的注释大体上遵循 JSDoc 注释规范
// pages/detail/index.tsx
/**
* @cmd detail, newdetail
* @description 详情
* @param skuid {number} skuid
*/
step3. 在编译阶段扫描并生成配置文件
根据入口文件的页面定义,匹配出需要的注释部分,使用 doctrine 解析需要的数据,解析后的数据如下:
// config/router.config.ts
export default {
index: {
description: "首页", // 页面描述
path: "/pages/index/index", // 真实路径
isTabbar: true, // 是否tabbar页面
ensureLogin: false, // 是否需要强制登录
},
detail: {
description: "详情",
path: "/pages/detail/index",
isTabbar: false,
ensureLogin: true,
},
};
这里顺便可以使用 param 等生成详细的页面名称及入参文档,提供给其他研发或运营同学使用。
step4. 资源描述解析为标准数据
根据上面的资源描述约定及扫描得到的配置文件,我们可以将其转换为方便在小程序内解析的数据定义,基本格式如下
{
origin: 'https://host?cmd=detail&skuid=1', // 原始数据
parsed: {
type: 'PAGE', // 类型,PAGE,NATIVE_API,NATIVE_BUTTON_API,UNKNOW
data: {
path: 'pages/detail/index', // 实际的页面路径,如果type是PAGE则会解析出此字段
action: undefined, // 动作,scanCode,makePhoneCall,openType,miniprogram ……。如果type是NATIVE_API,NATIVE_BUTTON_API,则会解析出此字段
params: {
skuid: '1' // 需要携带的参数
}
}
}
}
step5. 根据标准数据执行对应逻辑
由于我们的项目使用的是 Taro 框架,以下伪代码都是以 Taro 为例。
// utils/router.ts
// 用于解析原始链接为标准数据
const parseURL = (origin) => {
// balabala,一顿操作格式化成上文的数据
const data = {
...
};
return data;
};
// 执行除 NATIVE_BUTTON_API 之外的跳转
const routeURL = (origin) => {
const parsedData = parseURL(origin)
const {parsed: {type, data}} = parsedData
switch(type){
case 'PAGE':
...
break;
case 'NATIVE_API':
...
break;
case 'UNKNOW':
...
break;
}
};
export default {
parseURL,
routeURL,
};
对于需要点击的类型,我们需要借助 UI 组件实现
// components/router.tsx
import router from "/utils/router";
import { Button } from "@tarojs/components";
import Taro, { Component, eventCenter } from "@tarojs/taro";
export default class Router extends Component {
componentWillMount() {
const { path } = this.props;
const data = router.parseURL(path);
const { parsed, origin } = data;
const openType =
(parsed &&
parsed.data &&
parsed.data.params &&
parsed.data.params.openType) ||
false;
this.setState({
parsed,
openType,
});
}
// 点击事件
async handleClick(parsed, origin) {
// 点击执行动作
let {
type,
data: { action, params },
} = parsed;
if (!type) {
return;
}
// 内部页面
if (["PAGE", "CMD_UNKNOW"].includes(type)) {
console.log(`CMD_NATIVE_PAGE 参数:`, origin, options);
router.routeURL(origin);
return;
}
// 拨打电话、扫码等原生API
if (["NATIVE_API"].includes(type) && action) {
if (action === "makePhoneCall") {
let { phoneNumber = "" } = params;
if (!phoneNumber || phoneNumber.replace(/\s/g, "") == "") {
Taro.showToast({
icon: "none",
title: "未查询到号码,无法呼叫哦~",
});
return;
}
}
let res = await Taro[action]({ ...params });
// 扫码事件,需要在扫码完成后发送全局广播,业务内自行处理
if (action === "scanCode" && params.callback) {
let eventName = `${params.callback}_event`;
eventCenter.trigger(eventName, res);
}
}
// 打开小程序
if (
["NATIVE_BUTTON_API"].includes(type) &&
["miniprogram"].includes(action)
) {
await Taro.navigateToMiniProgram({
...params,
});
}
}
render() {
const { parsed, openType, origin } = this.state;
return (
<Button
onClick={this.handleClick.bind(this, parsed, origin)}
hoverClass="none"
openType={openType}
>
{this.props.children}
</Button>
);
}
}
在具体业务中使用
// pages/index/index.tsx
import router from "/utils/router";
import Router from "/components/router";
// js方式直接跳转
router.routeURL('https://host?cmd=detail&skuid=1')
// UI组件方式
...
render(){
return <Router path='https://host?cmd=detail&skuid=1'></Router>
}
...
当然这里面可以附加你自己需要的功能,比如:增加跳转方式控制、数据处理、埋点、加锁防连续点击,相对来说并不复杂。甚至你还可以顺手实现一下上面提到的 navigateBack 传参。
结语
上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!