Abstract

说到热更新,大多数人的第一印象肯定是AndFix或者HotFix等热更新框架。但是一来这些框架学习成本较高,坑较多,二来对于大的模块更新支持不好。所以在实际开发功能中,对于一些紧急的功能上线或者bug修复,使用H5页面替换原生页面是一个较为简单和方便的方案。本文主要讲解如何使用路由动态配置思想实现App内任意页面的替换。同时会给出一个Demo工程,方便大家学习和实现。

Url规范

首先为了实现原生页面和H5页面之间的任意跳转,我们需要定义一套统一的Url规范。我们知道一个Url主要由Schema、Host、path以及QueryParameter等构成。以下分条给出定义。

Schema

为了实现从浏览器直接跳入App,我建议使用私有Schema。同时用不同的Schema来表示不同的动作。以下是我定义的两个Schema。

dynamic: 用此Schema的url表示如果能用原生页面打开,则用原生页面打开,如果没有,则经过一些转化变成http或者https协议连接使用WebView打开。主要用来上线一些原生没有的页面。比如老版本没有这个页面就用h5页面替换,新版本还是用原生的。

dynamicWeb: 用此Schema的url表示总是用WebView打开。主要用来替换原生的一些页面。比如某个运营活动明天就上线了,但是临时更版肯定来不及了,就临时把某一个页面都替换为H5页面。

Path

Path包括Host部分来区分某一个页面。某些同学可能会问,在H5那边Host是用来表示服务器域名的,这不就不兼容了吗,如果手机没有安装App,用我们这种规范定义的url肯定就无法打开了。这确实是一个问题,但是我想如果为了兼容H5在客户端页面的url中加上服务器的域名感觉也很奇怪。所以作为一个客户端程序员来说,这样比较方便,就暂且这么定。而为了解决没有安装客户端的情况,需要给H5页面的跳转链接加上备选链接。为了解决客户端原生页面跳转H5页面的情况,需要为我们定义的Url加上H5服务器的域名。这是本方案不太优雅的地方,各位有什么建议欢迎提出。

QueryParameter

所有的参数都放在QueryParameter中。对于一些Object,就只能序列化一下也放在QueryParameter中了。

路由框架

为了方便的通过Url打开Activity或者WebView,我们需要一个路由框架。这里我推荐我开源的一个路由框架 AndRouter。AndRouter会根据Url的Schema选择不同的方式来打开这个url。同时它提供了ActivityRouter(将以activity为Schema的url转为Intent并打开对应Activity)的实现和BrowserRouter(用浏览器打开以http和https为Schema的url)的实现。为了让AndRouter支持我们自定义的dynamic和dynamicWeb为Schema的连接,我们需要自定义两个Router如下。

public class DynamicRouter implements IRouter {
private static final String DEFAULT_SCHEME = "dynamic";
...
protected boolean open(Context context, IRoute route){
String routeUrl = route.getUrl();
boolean isSuccess = false;
if(canOpenTheRoute(route)) {
//首先尝试用原生的Activity打开,如果无法打开,则使用WebView打开
try {
Uri uri = Uri.parse(routeUrl)
.buildUpon()
.scheme("activity")
.build();
//尝试Activity打开
if (!Router.open(context, uri.toString())) {
//失败,换用WebView打开,但是需要给Url加上H5域名
String path = UrlConfig.BASE_URL + UrlUtils.getHost(routeUrl);
Timber.i("path : %s", path);
String url = UrlUtils.addQueryParameters(path, UrlUtils.getParameters(routeUrl));
isSuccess = Router.open(context, url);
} else {
isSuccess = true;
}
} catch (Exception e) {
Timber.e(e, "");
}
}
return isSuccess;
}
...
}

复制代码

DynamicWebRouter 同理,不过不会尝试用Activity打开。

public class DynamicWebRouter extends BaseRouter {
private static final String DEFAULT_SCHEMA = "dynamicWeb";
...
protected boolean open(Context context, IRoute route){
boolean isSuccess = false;
if(canOpenTheRoute(route)){
//强制使用WebViewActivity打开
String routeUrl = route.getUrl();
try {
String host = UrlUtils.getHost(routeUrl);
String path = UrlConfig.BASE_URL + host;
Timber.i("path %s", path);
String url = UrlUtils.addQueryParameters(path, UrlUtils.getParameters(routeUrl));
isSuccess = Router.open(context, url);
} catch (Exception e){
Timber.e(e, "");
}
}
return isSuccess;
}
...
}

复制代码

给路由框架加上以上两个Router的实现,然后AndRouter就支持打开我们自定义的Schema Url了。

Router.initActivityRouter(getContext());
Router.addRouter(new WebRouter());
Router.addRouter(new DynamicRouter());
Router.addRouter(new DynamicWebRouter());

复制代码

### 路由动态刷新

为了让后台控制客户端的页面跳转,App在初始化的时候需要同步一下动态路由表接口,并缓存下来,每次需要做页面跳转的时候去表里查一下有没有动态配置项,如果有,则需要使用动态路由来做页面跳转。为了方便大家测试,我写了一个Spring项目,用来提供该接口,该项目同时也在github上开源了 RouterSender。

//客户端更新路由表
void refreshRouter(){
service.getRouters()
.enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
if(response != null && response.body() != null) {
//RouterCache 用来缓存路由表
RouterCache.updateRouter(response.body());
} else {
Timber.w("路由更新失败");
}
}
@Override
public void onFailure(Call> call, Throwable t) {
Timber.e(t, "路由更新失败");
}
});
}

复制代码

路由选择

在每次跳转的时候进行的路由选择代码如下:

btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Map map = new HashMap();
map.put(TwoActivity.KEY_NAME, "Tango");
//如果有动态配置,则用动态url代开,某则用activity://three 对应的页面打开 并带上map中的参数
RouterTry.tryOpenOr(MainActivity.this, RouterCache.getRoute(KEY_ACTION_TWO), "activity://three", map);
}
});

复制代码

网页到App的统一入口

为了能够从网页跳入原生页面,我们设置了一个统一的Activity入口,不管是从浏览器跳入App还是从WebView跳到某一个原生页面,都从该Activity中转。这主要是为了方便做一些过滤,以及实施一些安全策略。

public class RouterDispatchActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
Uri uri = intent.getData();
Router.open(RouterDispatchActivity.this, uri.toString());
finish();
}
}

复制代码

该Activity需要添加如下Intent-filter

复制代码

同时我们需要给WebView设置WebViewClient拦截网页内部的页面跳转,在发现是我们自定义协议链接,使用上面的RouterDispatchActivity来打开该Url。

private class CaptureWebViewClient extends WebViewClient {
...
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if(TextUtils.equals(UrlUtils.getScheme(url), "dynamic") || TextUtils.equals(UrlUtils.getScheme(url), "dynamicWeb")) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
mContext.startActivity(intent);
return true;
} else {
return false;
}
}
}

复制代码

可能安全隐患

外部网页通过自定义Schema的链接在App内部打开一些恶意网页,引导用户进行一些危险性操作(比如输入用户名和密码)

参数泄露以及跨站攻击

Intent Schema Url攻击

规避策略

在UrlDispathActivity中对Url进行过滤和安全处理。不过我感觉问题不大,这里只是给大家提供个思路,大家可以想想会不会有问题。

总结

以上即为我的路由动态配置思路和实现。如有问题和建议,欢迎提出。

Demo项目地址

[服务器实现] (https://github.com/beautifulSoup/RouterSender)