WMRouter:美团外卖 Android 开源路由框架 (1),看完老板哭着让我留下来
Native+H5 混合开发模式,需要进行页面之间的互相跳转,或进行灵活的运营跳转链接下发。可以利用 WMRouter 统一页面跳转逻辑,根据不同的协议(HTTP、HTTPS、用于 Native 页面的自定义协议)跳转对应页面,且在跳转过程中可以使用 UriInterceptor 对跳转链接进行修改,例如跳转 H5 页面时在 URL 中加参数。
统一管理来自 App 外部的 URI 跳转。来自 App 外部的 URI 跳转,如果使用 Android 原生的 Manifest 配置,会直接启动匹配的 Activity,而很多时候希望先正常启动 App 打开首页,完成常规初始化流程(例如登录、定位等)后再跳转目标页面。此时可以使用统一的 Activity 接收所有外部 URI 跳转,到首页时再用 WMRouter 启动目标页面。
页面跳转有复杂判断逻辑的场景。例如多个页面都需要先登录、先定位后才允许打开,如果使用常规方案,这些页面都需要处理相同的业务逻辑;而利用 WMRouter,只需要开发好 UriInterceptor 并配置到各个页面即可。
多工程、组件化、平台化开发。多工程开发要求各个工程之间能互相通信,也可能遇到和外卖 App 类似的代码复用、依赖注入、编译等问题,这些问题都可以利用 WMRouter 的 URI 分发和 ServiceLoader 模块解决。
对业务埋点需求较强的场景。页面跳转作为最常见的业务逻辑之一,常常需要埋点。给每个页面配置好 URI,使用 WMRouter 统一进行页面跳转,并在全局的 OnCompleteListener 中埋点即可。
对 App 可用性要求较高的场景。一方面,可以对页面跳转失败进行埋点监控上报,及时发现线上问题;另一方面,页面跳转时可以执行判断逻辑,发现异常(例如服务端异常、客户端崩溃等)则自动打开降级后的页面,保证关键功能的正常工作,或给用户友好的提示。
页面 A/B 测试、动态配置等场景。在 WMRouter 提供的接口基础上进行少量开发配置,就可以实现:根据下发的 A/B 测试策略跳转不同的页面实现;根据不同的需要动态下发一组路由表,相同的 URI 跳转到不同的一组页面(实现方面可以自定义 UriInterceptor,对匹配的 URI 返回 301 的 UriResult 使跳转重定向)。
基本概念解释
下面开始介绍 WMRouter 的发展背景和过程。为了方便后文的理解,我们先简单了解和回顾几个基本概念。
路由
根据维基百科的解释,路由(routing)可以理解成在互联的网络通过特定的协议把信息从源地址传输到目的地址的过程。一个典型的例子就是在互联网中,路由器可以根据 IP 协议将数据发送到特定的计算机。
URI
URI(Uniform Resource Identifier,统一资源标识符)是一个用于标识某一互联网资源名称的字符串。URI 的组成如下图所示。
一些常见的 URI 举例如下,包括平时经常用到的网址、IP 地址、FTP 地址、文件、打电话、发邮件的协议等。
file:///Users/
tel:863-1234
在 Android 中也提供了android.net.Uri
工具类用于处理 URI,Android 中 URI 常用的几个部分主要是 scheme、host、path 和 query。
Android 中的 Intent 跳转
在 Android 中的 Intent 跳转,分为显式跳转和隐式跳转两种。
显式跳转即指定 ComponentName(类名)的 Intent 跳转,一般通过 Bundle 传参,示例代码如下:
Intent intent = new Intent(context, TestActivity.class);intent.putExtra("param", "value")startActivity(intent);
隐式跳转即不指定 ComponentName 的 Intent 跳转,通过 IntentFilter 找到匹配的组件,IntentFilter 支持 action、category 和 data 的匹配,其中 data 就是 URI。例如下面的代码,会启动系统默认的浏览器打开网页:
Intent intent = new Intent(Intent.ACTION_VIEW);intent.setData(Uri.parse("http://www.meituan.com"))startActivity(intent);
Activity 通过 Manifest 配置 IntentFilter,例如下面的配置可以匹配所有形如demo_scheme://demo_host/***
的 URI。
<activity android:name=".app.UriProxyActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/><category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="demo_scheme" android:host="demo_host"/></intent-filter></activity>
URI 跳转
在美团外卖 C 端早期开发过程中,产品希望通过后台下发 URI 控制客户端跳转指定页面,从而实现灵活的运营配置。外卖 App 采用了 Native+H5 的混合开发模式,Native 页面定义了专用的 URI,而 H5 页面则使用 HTTP/HTTPS 链接在专门的 WebView 容器中加载,两种链接的跳转逻辑不同,实现起来比较繁琐。
Native 页面的 URI 跳转最开始使用的是 Android 原生的 IntentFilter,通过隐式跳转启动,但是这种方式存在灵活性差、功能缺失、Bug 多等问题。例如:
从外部(浏览器、微信等)跳转外卖的 URI 时,系统会直接打开相应的 Activity,而没有经过欢迎页的正常启动流程,一些代码逻辑可能没有执行,例如定位逻辑。
有很多页面在打开前需要确保用户先登录或先定位,每个页面都写一遍判断登录、定位的逻辑非常麻烦,提高了开发维护成本。
运营人员可能会配错 URI,页面跳转失败,有些跳转的地方没有做 try-catch 处理,会产生 Crash;有些地方虽然加了 try-catch,但跳转失败后没有任何响应,用户体验差;跳转失败没有监控,不能及时发现和解决线上业务异常。
为了解决上述问题,我们希望有一个 Android 的 URI 分发组件,可以根据 URI 中不同的 scheme、host、path,进行不同的处理,同时能够在页面跳转过程中进行更灵活的干预。调研发现,现有的一些 Android 路由组件主要都是在解决多工程之间解耦的问题,而 URI 往往只支持通过 path 分发,页面跳转的配置也不够灵活,难以满足实际需要。于是我们决定自行设计实现。
核心设计思路
下图展示了 WMRouter 中 URI 分发机制的核心设计思路。借鉴网络请求的机制,WMRouter 中的每次 URI 跳转视为发起一个 UriRequest;URI 跳转请求被 WMRouter 逐层分发给一系列的 UriHandler 进行处理;每个 UriHandler 处理之前可以被 UriInterceptor 拦截,并插入一些特殊操作。
页面跳转来源
常见的页面跳转来源如下:
来自 App 内部 Native 页面的跳转
来自 App 内 Web 容器的跳转,即 H5 页面发起的跳转
从 App 外通过 URI 唤起 App 的跳转,例如来自浏览器、微信等
从通知中心 Push 唤起 App 的跳转
对于来自 App 内部和 Web 容器的跳转,我们把所有跳转代码统一改成调用 WMRouter 处理,而来自外部和 Push 通知的跳转则全部使用一个独立的中转 Activity 接收,再调用 WMRouter 处理。
UriRequest
UriRequest 中包含 Context、URI 和 Fields,其中 Fields 为 HashMap<String, Object>,可以通过 Key 存放任意数据。简单起见,UriRequest 类同时承担了 Response 的功能,跳转请求的结果,也会被保存到 Fields 中。Fields 可以根据需要自定义,其中一些常见字段举例如下:
Intent 的 Extra 参数,Bundle 类型
用于 startActivityForResult 的 RequestCode,int 类型
用于 overridePendingTransition 方法的页面切换动画资源,int[]类型
本次跳转结果的监听器,OnCompleteListener 类型
每次 URI 跳转请求会有一个 ResultCode(类似 HTTP 请求的 ResponseCode),表示跳转结果,也存放在 Fields 中。常见 Code 如下,用户也可以自定义 Code:
200:跳转成功
301:重定向到其他 URI,会再次跳转
400:请求错误,通常是 Context 或 URI 为空
403:禁止跳转,例如跳转白名单以外的 HTTP 链接、Activity 的 exported 为 false 等
404:找不到目标(Activity 或 UriHandler)
500:发生错误
总结来说,UriRequest 用于实现一次 URI
跳转中所有组件之间的通信功能。
UriHandler
UriHandler 用于处理 URI 跳转请求,可以嵌套从而逐层分发和处理请求。UriHandler 是异步结构,接收到 UriRequest 后处理(例如跳转 Activity 等),如果处理完成,则调用callback.onComplete()
并传入 ResultCode;如果没有处理,则调用callback.onNext()
继续分发。下面的示例代码展示了一个只处理 HTTP 链接的 UriHandler 的实现:
public interface UriCallback {
/**
处理完成,继续后续流程。*/void onNext();
/**
处理完成,终止分发流程。
@param resultCode 结果*/void onComplete(int resultCode);}
public class DemoUriHandler extends UriHandler {public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {Uri uri = request.getUri();// 处理 HTTP 链接 if ("http".equalsIgnoreCase(uri.getScheme())) {try {// 调用系统浏览器 Intent intent = new Intent();intent.setAction(Intent.ACTION_VIEW);intent.setData(uri);request.getContext().startActivity(intent);// 跳转成功 callback.onComplete(UriResult.CODE_SUCCESS);} catch (Exception e) {// 跳转失败 callback.onComplete(UriResult.CODE_ERROR);}} else {// 非 HTTP 链接不处理,继续分发 callback.onNext();}}// ...}
UriInterceptor
UriInterceptor 为拦截器,不做最终的 URI 跳转操作,但可以在最终的跳转前进行各种同步/异步操作,常见操作举例如下:
URI 跳转拦截,禁止特定的 URI 跳转,直接返回 403(例如禁止跳转非 meituan 域名的 HTTP 链接)
URI 参数修改(例如在 HTTP 链接末尾添加 query 参数)
各种中间处理(例如打开登录页登录、获取定位、发网络请求)
……
每个 UriHandler 都可以添加若干 UriInterceptor。在 UriHandler 基类中,handle()方法先调用抽象方法shouldHandle()
判断是否要处理 UriRequest,如果需要处理,则逐个执行 Interceptor,最后再调用handleInternal()
方法进行跳转操作。
public abstract class UriHandler {
// ChainedInterceptor 将多个 UriInterceptor 合并成一个 protected ChainedInterceptor mInterceptor;
public UriHandler addInterceptor(@NonNull UriInterceptor interceptor) {if (interceptor != null) {if (mInterceptor == null) {mInterceptor = new ChainedInterceptor();}mInterceptor.addInterceptor(interceptor);}return this;}
public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {if (shouldHandle(request)) {if (mInterceptor != null) {mInterceptor.intercept(request, new UriCallback() {@Overridepublic void onNext() {handleInternal(request, callback);}
@Overridepublic void onComplete(int result) {callback.onComplete(result);}});} else {handleInternal(request, callback);}} else {callback.onNext();}}
/**
是否要处理给定的 uri。在 Interceptor 之前调用。*/protected abstract boolean shouldHandle(@NonNull UriRequest request);
/**
处理 uri。在 Interceptor 之后调用。*/protected abstract void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback);}
URI 的分发与降级
在外卖 C 端 App 中的 URI 分发示意如下图。所有 URI 跳转都会分发到 RootUriHandler,然后根据不同的 scheme 分发到不同的子 Handler。例如 waimai 协议分发到 WmUriHandler,然后进一步根据不同的 path 分发到子 Handler,从而启动相应的 Activity;HTTP/HTTPS 协议分发到 HttpHandler,启动 WebView 容器;对于其他类型 URI(tel、mailto 等),前面的几个 Handler 都无法处理,则会分发到 StartUriHandler,尝试使用 Android 原生的隐式跳转启动系统应用。
每个 UriHandler 都可以根据实际需要实现降级策略,也可以不作处理继续分发给其他 UriHandler。RootUriHandler 中提供了一个全局的分发完成事件监听器,当 UriHandler 处理失败返回异常 ResultCode 或所有子 UriHandler 都没有处理时,执行全局降级策略。
平台化与两端复用
随着外卖 C 端业务的演进,团队成员扩充了数倍,商超生鲜等垂直品类的拆分,以及异地研发团队的建立,客户端的平台化被提上日程。关于外卖平台化更详细的内容,可参考团队之前已经发布的文章 美团外卖Android平台化架构演进实践。
评论