注册

统一路由,让小程序跳转更智能

我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:


// 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
wx.navigateTo({
url: "pages/somepage?id=1",
success: function (res) {},
});

但这里面存在几个问题:



  • 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的
  • 需要知道页面是否为 tabbar 页面(switchTab)
  • 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行
  • navigateBack 不支持传参

为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:



  • 页面别名声明使用注释方式,不侵入业务代码
  • 页面可以存在多个别名,方便新老版本页面的流量切换
  • 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心
  • 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)
  • 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制

实现思路


step1. 资源描述约定


小程序内的跳转类操作存在以下几种



  1. js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)
  2. js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)
  3. 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )

针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源



  1. 内部页面

https://host?cmd=${pagename}&param1=a  // 打开普通页面并传参,标准的H5容器也算在普通页面内


  1. 微信原生 API

https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456  // 拨打电话
https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调


  1. 需要借助按钮 open-type 的微信原生能力

https://host?cmd=nativeButtonAPI&openType=contact  // 在线客服


  1. 打开另一个小程序

https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid} 


小程序跳转需要携带更多的参数,所以做了cmd的区分,这里实际会解析成 nativeButtonAPI 运行



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 传参。


结语


上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。


作者:胖纳特
链接:https://juejin.cn/post/6930899487250448398

0 个评论

要回复文章请先登录注册