目录
背景
大家如果经历过Hybrid项目的开发,即项目中涉及到H5与Native之间的交互,那么很有可能会遇到各种各样的H5容器。为什么会有那么多各种各样的容器呢...这也是轮子多的通病了,轮子多到业务方不知道选哪个。当然,也有可能大家压根就不会使用到H5容器,直接用系统WebView就完事儿了,比如我的前东家就是这样做的。那这篇文章的主题就是与前者相关的:当项目中拥有很多个H5容器时,怎样封装才能让业务侧用得爽呢?
下面按惯例,抛出这篇文章要解决的三个问题:
- 如何优雅地提供接口调用?
- 怎样封装多个不同类型的H5容器?
- 这样的架构能带来什么样的好处?
术语对齐
术语 | 描述 |
---|---|
H5框架容器 | 特指目前项目中正在使用的二方/三方H5容器。 |
H5通用容器 | 特指将项目中所有的框架容器抽象到一个容器中,让业务侧不需要感知到具体的框架细节。 |
A页面 | url query参数中带a=true的页面,项目中用A容器承载。 |
B页面 | url query参数中带b=true的页面,项目中用B容器承载。 |
探索
如何优雅地提供接口调用?
业务侧调用的接口其实很有限,可以说90%的业务都只是打开一个网页而已。
我们根据业务需要新建一个接口类:
/** * WebPage服务接口类 */ public interface IWebPageService { /** * 打开url * @param url */ void openUrl(String url); /** * 创建Fragment内置到某页面中使用 * @return */ Fragment buildFragment(String url, Context context); /** * 获取当前Url * @return */ String getCurrentUrl(); /** * 给h5发通知 * @param eventData * @param eventName */ void postNotificationToJS(String eventName, String eventData); }
以上列举了4个非常常见的接口。如果有需要,还可以扩展其它的,比如设置容器生命周期相关的监听、设置H5发通知来时的监听等等。拿openUrl
的实现举例,实现类通过url中的参数判断需要打开A容器
还是B容器
。
@Override public void openUrl(String url) { // 若url中含有A参数,则用A容器打开 // if (urlParamHasA) { // startAContainer(); // } else { // 否则用B容器打开 // startBContainer(); //} }
这就从接口层这一层面屏蔽了内部框架容器。其它接口也是类似,原则就是不让业务侧感知到具体的实现细节,而不是跳个页面还需要知道这个url是要用A容器打开还是B容器打开。在应用启动时,可以将实现类注入到公共依赖中去,也可以通过ARouter等框架实现依赖注入,方便业务侧的调用。
怎样封装多个不同类型的H5容器容器?
整体架构
先上一张图:
接口层上面已经讲过了,这里我们直接看容器层。
通用容器
第一层就是我们抽出来的通用容器,WebPageActivity与WebPageFragement。项目中,不管是哪个H5框架容器,势必都是用Activity或者Fragment来承载的,所以我们相应的也需要有这两者作为父类去进行封装。而通用容器中,最重要的职责就是去执行通用逻辑,即每个框架容器都需要执行的逻辑。
通用职责可以有以下几点:
- 设置页面属性,包括window flag、activity的主题等。
- 解析url,将url中的关键参数记录下来,从而决定以哪种策略渲染UI。比如可以通过disableNav参数决定是否显示导航栏,通过statusBarStyle参数决定状态栏的风格等。
- 控制通用UI的渲染,包括何时改变TitleBar、何时加载/隐藏Loading,何时渲染异常态UI等。
- 决定框架容器加载时的逻辑顺序,如setContentView()->loadUrl()。
- 接收生命周期事件与系统回调事件,并且分发回调。生命周期就不用说了,系统回调这里特指
onActivityResult
、onRequestPermissionResult
这类事件,然后将结果分发给各listener。
拿WebPageActivity#onCreate
举例:
@Override protected void onCreate(Bundle savedInstanceState) { // setWindowFlags(); 设置window属性 // setTheme(); 设置主题 // setTransition(); 设置转场动画 super.onCreate(savedInstanceState); // parseUrl(); 解析url // initCommonUI(); 渲染通用UI 比如TitleBar、底部导航栏等 setContentView(); loadUrl(); } protected abstract void setContentView(); protected abstract void loadUrl();
我们可以将设置window属性、设置主题这些通用行为放在通用容器中执行,而setContentView
与loadUrl
交由框架容器去执行。setContentView
是因为不同的容器,layout xml可能是不一样的。loadUrl
是因为每个容器用WebView去加载url的方式也可能不同,无法统一。类似的行为,若执行时机可统一,则由通用容器统一执行时机,比如setContentView
与loadUrl
。若时机也没法确定,甚至逻辑只会存在于某个框架容器中,那么就要写到子类中去了。
框架容器
图中第二层的容器A、容器B、容器C指的就是框架容器。有了通用容器承载共同的逻辑后,框架容器需要做的事情就比较少了。只需要加载各自的布局,初始化WebView,然后将url加载到WebView中就可以了(除非有某个框架容器需要特殊适配,那就得写些额外的代码)。具体的实现需要根据项目中框架容器的使用方式来确定。
基础组件
第三层表示基础组件。这里只列一些常见的基础组件。
- Components:各类UI组件,比如TitleBar与异常态控件。
- JS Bridge:JS Bridge可以单独放到一个类中做收口,然后分发给各个listener。listenr通过接口层进行注入。
- WebViewClient:WebViewClient在日常开发中算是维护的非常频繁的一个类,职责主要是在
shouldOverrideUrlLoading
重定向时对url进行拦截,以及根据相应的生命周期回调(onPageStarted、onPageFinished、onReceviedError
等)进行loading或异常态UI的渲染时。若每个框架容器可以设置同一个WebViewClient,那就非常方便了,只要改一处地方,所有容器就都生效了。 - Interceptors:即用于WebView重定向时的拦截器。像我的项目中会有很多拦截相关的逻辑,比如将A页面拦截跳转到原生页面,就需要在重定向时进行拦截。但逻辑堆叠太多会提高维护的成本,因此建议用责任链模式(没用过的同学可以百度一下,很简单滴~)去处理拦截的这块逻辑。
- Managers、Listeners:容器中总是需要很多辅助类去来帮助我们去管理一些东西,比如WebView的管理,url的管理。这里就不延伸了,根据实际需要去加。Listener也一样,关于容器生命周期的Listener,与JS Bridge交互相关的Listener,都可以通过接口层去实现注入与销毁。
当然,除此之外,根据每个项目的完整度与负责度,还会衍生出很多基础组件。比如H5的监控体系,H5的预加载体系,以及WebView预创建相关的缓存体系等。他们作为框架容器共同的底座承担相应的职责。
这样的架构能带来什么样的好处?
- 对业务侧而言,接口统一收口到IWebPageService中去,调用方式更加简单明了,不需要再关注容器的具体类型。
- 对容器侧而言,代码逻辑性清晰的同时,有效降低了日后的维护成本与开发成本。Loading、异常态不需要再在各类容器中重复写N遍,各种url params都统一收口到通用容器中去,重定向拦截逻辑也不再需要重复写N遍...总而言之,通用逻辑统一给通用容器与基础组件去承担,框架容器的代码,都是各框架容器所“独有”的。