客户端路由动态配置——可能是最简单的热更新方案

Posted by beautifulSoup on December 2, 2016

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<Map<String, String>>() {
                    @Override
                    public void onResponse(Call<Map<String, String>> call, Response<Map<String, String>> response) {
                        if(response != null && response.body() != null) {									
                        		//RouterCache 用来缓存路由表
                            RouterCache.updateRouter(response.body());
                        } else {
                            Timber.w("路由更新失败");
                        }
                    }

                    @Override
                    public void onFailure(Call<Map<String, String>> call, Throwable t) {
                        Timber.e(t, "路由更新失败");
                    }
                });
    }

路由选择

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

	btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Map<String, String> map = new HashMap<String, String>();
                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

			<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="dynamic"/>
                <data android:scheme="dynamicWeb"/>
            </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项目地址

广告

欢迎Follow我的GitHub账号

欢迎关注我的简书账号

欢迎关注我的微薄账号