其他分享
首页 > 其他分享> > 有赞移动应用如何给页面安上“任意门”

有赞移动应用如何给页面安上“任意门”

作者:互联网

 有赞技术 有赞coder 




图片作者:茄子 & 晓旭部门:有赞电商移动组
“任意门”:一行配置实现页面跳转重定向。

背景 & 痛点 & 价值

动态路由组件,处理的是 App 中最最常见的一种行为的问题,那就是:跳转。随着 App 技术栈的扩展,从原本最最简单的原生到原生的跳转,扩展到目前同一个 App 中包含原生页面、H5 页面、Weex 页面、Flutter 页面之间的跳转。图片随之而来的问题就是:随着 App 的版本迭代,很多原本原生实现的页面,需要通过新的 H5 或者 Weex 页面进行升级/降级。而这些原本都是硬编码的跳转逻辑,可能需要随着版本不停改动。总结下来,现有的,各个技术栈隔离的页面跳转逻辑面临的直接问题有:为了解决以上硬编码以及灵活性差的问题,我们决定梳理现有的各技术栈跳转逻辑,将这些跳转整合,能够满足动态性、可配置的需求。得益于项目中原有的路由跳转组件,各种页面之间的页面都可以通过 URL 的方式进行路由,于是我们基于 URL 跳转,开发了一套动态路由组件,它完成的工作有 :

一、实现方案

1.1 路由拦截+替换

微商城客户端目前已经有一套稳固的组件化实现方案,组件之间的页面跳转通过路由的方式进行解耦,这是一种比较常见的方式。在微商城项目中,负责实现的路由组件为 ZanURLRouter ,它的职责很简单:在不影响外部接口的前提下,我们在目标路由解析这一步,引入了动态路由image.png对于移动端的路由重定向,实际上就是将一个路由转换为另一个路由,如:youzan://orderlist?type=1&status=2转换为:wsc://orderlist/v2?type=1&status=2

1.2 跳转规则配置

路由的拦截和替换中的一个关键节点就是“配置”,我们需要一个路由规则列表来记录和下发匹配规则。为了方便下发路由规则表,我们将这份配置表存放在有赞移动配置中心,根据客户端的版本进行区分,动态地下发给不同版本的客户端。一条路由规则,分为一个 Key 和对应的 Value,Key 为匹配方式,使用正则表达式进行匹配,Value 为替换方式,使用 JSON 格式定义。实际代码实现中,我们将“路由规则”和“路由替换行为”分别抽象成实体类和接口方法。

1.2.1 抽象实体类

关于替换路由跳转的规则,我们可以这样配置:
Key: ^youzan://orderlist\?type=(\d+)&status=(\d+)$
Value: {"template": "wsc://orderlist/v2?type=$1&status=$2"}
即:一条匹配规则 + 一条替换模板。我们将之抽象为一个实体类, Rule
class Rule {
   // url 匹配规则(正则表达式)
   String pattern;
   // url 匹配规则(正则表达式)
   String template;
}

1.2.2 抽象接口

有了规则配置之后,就需要对动态路由的行为进行抽象,核心就是初始化规则、匹配规则和替换路由三个方法:
// 注册替换规则
fun initWithPattern(Rule rule)
// 校验是否命中已经注册的路由配置的 pattern 正则
fun testWithRoute(String routeUrl): Boolean
// 获取替换后的跳转地址
fun appliedWithRoute(String routeUrl): String
动态路由器会在应用启动阶段拉取正确的规则表,解析并记录下来:image.pngZanURLRouter 解析目标路由的时候,对每一个规则进行匹配测试,命中则应用匹配的规则,返回替换后的路由,再继续接下来的工作。

1.3 路由替换

实体类、接口类都抽象完成之后,就是动态路由的核心实现了,这里依赖到一个的核心工具就是:正则表达式。这里用到正则的场景有两个:在 Android 和 iOS 开发中,字符串正则相关的 API 都是自带的,开箱即用:
/* ------------ Android ------------ */

// 正则匹配校验方法
Pattern.matcher(String text)
// 正则匹配校验方法
Regex.replace(String input, String replacement)

/* ------------   iOS   ------------ */
(NSString *)stringByReplacingMatchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range withTemplate:(NSString *)templ;

1.4 疑难问题:参数处理

大部分情况下,跳转本身都是带参数的,那么动态替换跳转的 URL 之后,参数的获取就成了一个问题,尤其是原生和其他页面页面的跳转。我们主要以 Android 为例,Android 原生跳转都是通过一个关键类:Intent 来实现参数的存取。这里需要注意的是,由于 Intent 传值存在多种复杂的数据接口,包括 Parcelable 这种复杂参数的场景,由于降级之后都是以 URL 的形式传值,所以我们目前约定动态路由的参数只支持基本数据类型,复杂参数类型的需要接入方来做兼容。参数处理我们分两个典型的场景来讨论:

1.4.1 原生跳转H5

这里的方式主要是将 Intent 中的基本数值类型参数取出来,拼接成带参数的 URL 来实现将 Intent 里面的参数传递给 H5,主要实现代码如下:
fun appendBundleParams(strBuilder: StringBuilder, bundle: Bundle) {
   val ketSet = bundle.keySet()
   for (key in ketSet) {
       bundle[key]?.let { value ->
           when (value) {
               is Bundle -> appendBundleParams(strBuilder, value)
               is String, is Int, is Long, is Double, is Float, is Char, is Boolean, is Short
               -> {
                   if (strBuilder.isNotEmpty()) {
                       strBuilder.append("&")
                   }
                   strBuilder.append("$key=$value")
               }
               else -> {
                   // do nothing
               }
           }
       }
   }
}

1.4.2 H5跳转原生

同理的,H5 跳转原生做的就是将 URL 中携带的参数塞到 Intent 中来进行。这里比较关键的一个问题是:Intent 的取值都是带类型的,而 URL 的参数都是字符串。我们目前解决方案也很简单,就是封装 Intent 的取值方法,由于目前有赞 Android 主要使用 Kotlin 来开发,可以使用 Kotlin 的扩展函数特性来实现(Java 可以使用工具类的方式):
fun Intent.getIntFromRouter(key: String, defaultValue: Int): Int {
   val extras = this.extras;
   if (extras == null || !this.hasExtra(key)) {
       return defaultValue
   }
   return extras.get(key) as? Int ?: (this.getStringExtra(key)?.toInt() ?: defaultValue)
}

1.5 碰到的坑:UrlEncode

在匹配和替换 URL 规则的场景中,我们经常会碰到这么一种情况,URL 是被 UrlEncode 过的。由于字符串的正则匹配和正则替换是不会判断字符串是否被 UrlEncode 过,所以这里的逻辑需要由路由组件来实现。UrlEncode 字符串的正则匹配逻辑实现比较简单,即直接将字符串 Decode 之后进行匹配。比较复杂的是 UrlEncode 字符串的正则替换,有些情况下,路由中的url是必须进行 UrlEncode 的,如果直接 Decode 进行替换,那么可能会导致实际跳转的目标 URL 被错误地截断,导致无法跳转,所以这里的替换必须保留 UrlEncode 的字符。我们的解决思路是:记录 URLEncode 前后被 encode 字符的下标,然后再手动实现 replace 方法去挨个替换字符串中的字符,核心代码如下:
private fun getEncodeCharMap(url: String, encodeUrl: String): Map<Int, IntRange> {
   if (Uri.decode(encodeUrl) != url) {
       return mapOf()
   }
   val urlChars = url.toCharArray()
   val urlEncodeChars = encodeUrl.toCharArray()
   var i = 0
   var j = 0
   val encodeMap = mutableMapOf<Int, IntRange>()
   while (i < urlChars.size && j < urlEncodeChars.size) {
       // text:   [www:] => [www%23]
       // length: [0123] => [012345]
       if (urlChars[i] != urlEncodeChars[j]) {
           val s = Uri.encode(urlChars[i].toString())
           val range = IntRange(j, j + s.length - 1)
           encodeMap[i] = range
           j += s.length
       } else {
           j++
       }
       i++
   }
   return encodeMap
}

二、实际应用案例

2.1 应用中心

微商城App应用中心,应该是应用动态路由的最佳场景,应用中心存在大量跳转的场景。image.png先来说下使用动态路由的背景,应用中心中应用列表都是由服务端统一下发的,后端为每个应用配置的跳转地址是统一的,而 Android 和 iOS 本地路由配置的 URL 是不一致的,如果直接下发配置的话,会存在有一端无法跳转的问题。以店铺管理应用跳转为例:那么解决同一套配置跳转不同 URL 的这个问题,就交给动态路由来完成了,我只需要在iOS的动态路由添加一个规则,将 wsc://shop/management 动态替换成 wsc://team/management 就可以搞定!

2.2 订单项目

在微商城客户端的订单模块重构项目中,考虑到订单是使用频次很高的核心场景之一,且代码历史较久,所以新的模块上线后与旧订单列表模块共存,直到灰度完全结束。由于微商城已经是组件化拆分,业务组件之间的跳转使用路由完成,我们在设计灰度方案时,利用动态路由来实时进行目标路由的映射:image.png具体可见 《 微商城订单模块重构实践》一文。

三、总结

“上线只是开始”,随着业务迭代,历史业务也越来越多,为了保证不同平台版本的用户能够平滑过渡到新的功能上去,动态路由组件扮演了一个客户端的 URL 重定向服务的角色,避免因服务下线、功能更新、平台差异、项目重构等原因导致的功能不可用。动态路由组件,核心就是非常简单的正则匹配和正则替换,而这个非常简单和核心代码逻辑,实现了业务场景下非常重要的路由重定向。这整套解决方案,也是有赞移动端在应用组件化、动态化的一个重要组成部分,我们也希望这个技术方案能够抛砖引玉,启发更多优秀的移动端动态化解决思路。




标签:有赞,URL,规则,安上,跳转,替换,路由,页面
来源: https://blog.51cto.com/u_15127581/2748021