16

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

 3 years ago
source link: https://tech.youzan.com/you-zan-yi-dong-duan-dong-tai-lu-you-zu-jian-shi-xian-fang-an/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

“任意门”:一行配置实现页面跳转重定向。

背景 & 痛点 & 价值

动态路由组件,处理的是 App 中最最常见的一种行为的问题,那就是:跳转。

随着 App 技术栈的扩展,从原本最最简单的原生到原生的跳转,扩展到目前同一个 App 中包含原生页面、H5 页面、Weex 页面、Flutter 页面之间的跳转。

Yz2yI3R.png!mobile

随之而来的问题就是:随着 App 的版本迭代,很多原本原生实现的页面,需要通过新的 H5 或者 Weex 页面进行升级/降级。而这些原本都是硬编码的跳转逻辑,可能需要随着版本不停改动。总结下来,现有的,各个技术栈隔离的页面跳转逻辑面临的直接问题有:

  • 跳转逻辑跟着版本走,无法统一进行改动
  • 跨技术栈跳转的实现成本比较高,必须在桥接模块中进行特殊适配
  • 在一些 H5 需要使用专门 WebView 页面打开的场景下,很难去适配,也必须通过各个 Web 跳转的拦截做特殊处理

为了解决以上硬编码以及灵活性差的问题,我们决定梳理现有的各技术栈跳转逻辑,将这些跳转整合,能够满足动态性、可配置的需求。

得益于项目中原有的路由跳转组件,各种页面之间的页面都可以通过 URL 的方式进行路由,于是我们基于 URL 跳转,开发了一套动态路由组件,它完成的工作有 :

  • 承担 App 内所有跳转逻辑
  • 通过配置中心组件,支持获取/配置路由替换规则
  • 匹配所有的路由跳转规则,命中规则的,替换成新的目标路由地址
  • 将实际跳转目标地址传递给路由组件执行实际的跳转行为

YzUF3mI.png!mobile

实现方案

路由拦截+替换

微商城客户端目前已经有一套稳固的组件化实现方案,组件之间的页面跳转通过路由的方式进行解耦,这是一种比较常见的方式。

在微商城项目中,负责实现的路由组件为 ZanURLRouter ,它的职责很简单:

  • 启动时注册路由和页面
  • 找寻正确的页面进行跳转

ZJNNZbu.png!mobile

在不影响外部接口的前提下,我们在目标路由解析这一步,引入了动态路由

rYzu6re.png!mobile

对于移动端的路由重定向,实际上就是将一个路由转换为另一个路由,如:

youzan://orderlist?type=1&status=2

转换为:

wsc://orderlist/v2?type=1&status=2

跳转规则配置

路由的拦截和替换中的一个关键节点就是“配置”,我们需要一个路由规则列表来记录和下发匹配规则。为了方便下发路由规则表,我们将这份配置表存放在有赞移动配置中心,根据客户端的版本进行区分,动态地下发给不同版本的客户端。

一条路由规则,分为一个 Key 和对应的 Value,Key 为匹配方式,使用正则表达式进行匹配,Value 为替换方式,使用 JSON 格式定义。

实际代码实现中,我们将“路由规则”和“路由替换行为”分别抽象成实体类和接口方法。

抽象实体类

关于替换路由跳转的规则,我们可以这样配置:

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;
}

抽象接口

有了规则配置之后,就需要对动态路由的行为进行抽象,核心就是初始化规则、匹配规则和替换路由三个方法:

// 注册替换规则
fun initWithPattern(Rule rule)  
// 校验是否命中已经注册的路由配置的 pattern 正则
fun testWithRoute(String routeUrl): Boolean  
// 获取替换后的跳转地址
fun appliedWithRoute(String routeUrl): String

动态路由器会在应用启动阶段拉取正确的规则表,解析并记录下来:

zmMfqqu.png!mobile

ZanURLRouter 解析目标路由的时候,对每一个规则进行匹配测试,命中则应用匹配的规则,返回替换后的路由,再继续接下来的工作。

路由替换

实体类、接口类都抽象完成之后,就是动态路由的核心实现了,这里依赖到一个的核心工具就是:正则表达式。这里用到正则的场景有两个: - 正则验证是否命中规则 - 正则替换url文本

在 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;

疑难问题:参数处理

大部分情况下,跳转本身都是带参数的,那么动态替换跳转的 URL 之后,参数的获取就成了一个问题,尤其是原生和其他页面页面的跳转。

我们主要以 Android 为例,Android 原生跳转都是通过一个关键类:Intent 来实现参数的存取。这里需要注意的是,由于 Intent 传值存在多种复杂的数据接口,包括 Parcelable 这种复杂参数的场景,由于降级之后都是以 URL 的形式传值,所以我们目前约定动态路由的参数只支持基本数据类型,复杂参数类型的需要接入方来做兼容。

参数处理我们分两个典型的场景来讨论: - 原生跳转 H5 参数传递 - H5 跳转原生的参数传递

原生跳转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
                }
            }
        }
    }
}

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)
}

碰到的坑: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
}

实际应用案例

应用中心

微商城App应用中心,应该是应用动态路由的最佳场景,应用中心存在大量跳转的场景。

2YRjeeJ.png!mobile

先来说下使用动态路由的背景,应用中心中应用列表都是由服务端统一下发的,后端为每个应用配置的跳转地址是统一的,而 Android 和 iOS 本地路由配置的 URL 是不一致的,如果直接下发配置的话,会存在有一端无法跳转的问题。以店铺管理应用跳转为例:

  • iOS中店铺管理的路由 URL:wsc://shop/management
  • Android 中的路由URL:wsc://team/management
  • 服务端下发的URL:wsc://team/management

那么解决同一套配置跳转不同 URL 的这个问题,就交给动态路由来完成了,我只需要在iOS的动态路由添加一个规则,将 wsc://shop/management 动态替换成 wsc://team/management 就可以搞定!

订单项目

在微商城客户端的订单模块重构项目中,考虑到订单是使用频次很高的核心场景之一,且代码历史较久,所以新的模块上线后与旧订单列表模块共存,直到灰度完全结束。

由于微商城已经是组件化拆分,业务组件之间的跳转使用路由完成,我们在设计灰度方案时,利用动态路由来实时进行目标路由的映射:

3a2AJjR.png!mobile

具体可见 微商城订单模块重构实践》 一文。

总结

“上线只是开始”,随着业务迭代,历史业务也越来越多,为了保证不同平台版本的用户能够平滑过渡到新的功能上去,动态路由组件扮演了一个客户端的 URL 重定向服务的角色,避免因服务下线、功能更新、平台差异、项目重构等原因导致的功能不可用。

动态路由组件,核心就是非常简单的正则匹配和正则替换,而这个非常简单和核心代码逻辑,实现了业务场景下非常重要的路由重定向。这整套解决方案,也是有赞移动端在应用组件化、动态化的一个重要组成部分,我们也希望这个技术方案能够抛砖引玉,启发更多优秀的移动端动态化解决思路。

欢迎关注我们的公众号

bqyERrE.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK