组件化系列 - 插拔与扩展方案

392 阅读7分钟

在阅读本篇文章前,推荐先阅读我之前写的一篇 组件化基础 的文章。

插拔

什么是插拔?

插拔是当我们不需要某些模块的时候,可以通过去掉依赖就直接拔除这个功能,同样的也能通过一行依赖直接让app拥有这个功能。我们先来看个图:

虽然这个图其实有点老了,但基本的架构没变,需要再强调的是无论是业务组件、业务基础组件还是基础组件,都要写一个接口层,需要对外暴露的能力都要写在接口层,然后在实现层实现接口,如 biz_home 作为实现层的话,那 biz_home_interface 可以作为接口层。虽然说基础组件一般不需要插拔,但我们做技术的是要对基础库做二次封装的,接口层也是一样的道理,对于其他模块来说,无论基础组件内部实现怎么变,调用的接口api都是不变的。

基本上做到其他模块只依赖接口层不依赖实现层,就代表我们已经实现了可插拔,一个功能模块可以根据需求决定是否打包进app内。

扩展

什么是扩展?

其实扩展并不是一个组件化项目必备的能力。但是当项目大到一定程度时,其他客户、开发者想要把你的app作为一个平台去开发定制自己的业务时,这个时候扩展能力就是公司需要考虑的了,比如现在的钉钉、企业微信、飞书等都拥有开发者平台。

扩展能力从大类上可以分为前端、后端、客户端。前端的扩展可以理解为一个个小程序或H5,后端的扩展就是对外开放的api,都是比较容易理解的,我们主要关注客户端的扩展。客户端的扩展其实并不会那么多,无论从研发效率上还是从业务快速迭代上来说,第三方开发者肯定是优先选用小程序/H5来开发自己的应用。而 native扩展更多情况是在已有的原生业务上,比如登录模块大多数是原生已有的,第三方想要接入自己的登录(这里提一嘴,做好扩展的前提是先做好插拔)。或者是只能用原生来实现的功能,比如接入自己的音视频、安全相关的等。

接下来介绍下如何开展安卓端扩展方面的工作。

技术方案

个人认为扩展开放最核心的技术是依赖注入,为什么是依赖注入呢?扩展的核心是一方开发者并不关心三方定制的逻辑,只需要让三方实现自己开放的接口,然后我们一方去调用这个接口方法,这个从设计上来讲不就是完全解耦嘛,我们只需要对接口实例化,不需要关心接口的实现类。

依赖注入

依赖注入框架的选型这里就不展开了,市面上的dagger是谷歌官方的框架,能力也非常强,但复杂度较高,看项目的诉求。大公司可能基本也是用自己研发的依赖注入框架。还有像 ARouter 之类的路由框架,也是一个简易版的依赖注入框架。本文就以 ARouter 来写一个模板代码,如对ARouter不熟悉,可以先看下组件化系列-ARouter原理及用法

① 一方对外暴露的Provider接口:

public interface IHelloProvider extends IProvider {

    String PATH_HELLO_THIRD = "/hello/third";

    void sayHello();
}

② 一方实现的IHelloProvider默认逻辑:

@Route(path = Constant.PATH_HELLO_MY)
public class MyHelloProvider implements IHelloProvider{
    @Override
    public void sayHello() {
        Log.d("MyHelloProvider", "sayHello my");
    }
    @Override
    public void init(Context context) {
        Log.d("MyHelloProvider", "init my");
    }
}

把我们自己内部的path都写到一个常量类里并不对外暴露,如 Constant.PATH_HELLO_MY。sayHello就是我们内部的默认逻辑。

③ 三方实现的IHelloProvider逻辑:

@Route(path = IHelloProvider.PATH_HELLO_THIRD)
public class ThirdHelloProvider implements IHelloProvider{
    @Override
    public void sayHello() {
        Log.d("ThirdHelloProvider", "sayHello third");
    }
    @Override
    public void init(Context context) {
        Log.d("ThirdHelloProvider", "init third");
    }
}

④ 最后我们需要做一段兼容代码,如果一方和三方的IHelloProvider同时存在,我们该怎么选呢?

public class HelloProviderHelper {
    public static IHelloProvider getHelloProvider() {
        IHelloProvider helloProvider = (IHelloProvider) ARouter.getInstance().build(IHelloProvider.PATH_HELLO_THIRD).navigation();
        if(helloProvider == null) {
            helloProvider = (IHelloProvider) ARouter.getInstance().build(Constant.PATH_HELLO_MY).navigation();
        }
        return helloProvider;
    }
}

从代码看出我们优先根据提供给三方的 path 取 IHelloProvider 实例,如果没有则取我们自己的实例。这样我们就完全不需要知道三方的任何代码逻辑、类名等,只需要让三方严格以我们提供的path注解即可。

最后贴下使用代码:

IHelloProvider helloProvider = HelloProviderHelper.getHelloProvider()
if(helloProvider != null) {
    helloProvider.sayHello()
}

这样就解决了最核心的依赖注入问题了,其实ARouter在设计上还是存在缺陷的,比如@Route注解的priority属性就没用上,否则我们不需要写上面这些帮助类,直接一个注解就可以搞定。如果真的要把ARouter作为核心依赖注入框架,推荐可以改造下,假设改造后priority可用,我们是不是直接可以这样处理:

@Route(path = Constant.PATH_HELLO_MY, priority = 0)
public class MyHelloProvider implements IHelloProvider{
}

// priority默认是-1,越小优先级越高。三方可以不关心priority
@Route(path = IHelloProvider.PATH_HELLO_THIRD)
public class ThirdHelloProvider implements IHelloProvider{
}

然后我们直接用@Autowired注解实例化对象:

@Autowired
IHelloProvider helloProvider;

helloProvider.sayHello();

如此简单就实现了优先取三方,没有优先级的情况下 @Autowired 获取到的实例是不确定的。

广播/事件

广播和事件也是完全解耦的,不需要感知三方的存在。有些情况,使用广播或事件可能更加合适,比如登录/登出等事件通知给三方,三方只需要做监听,也不需要实现接口之类的。广播和事件的技术框架很多,这里只是提下这种方案。

开放接口

什么是开放接口?

举例:三方在开发自己的业务时发现需要我们从文件服务器下载文件并解密给到他们。当然有一种方案是后端把类似的接口开给他们,但对于这些涉及到敏感信息或安全方面的能力,后端是不可能对外开放的。这种情况我们客户端需要开一个下载文件api给三方调用。当然这个前提是我们必须要做好接口层和实现层的拆分,只需要提供接口层给三方,决不能把我们自己的逻辑暴露给三方。

所以开放接口和依赖注入正好是相反的概念,开放接口是一方给三方提供功能,依赖注入是一方从三方那里获取功能。

以上能力基本上已经满足所有的扩展开发了,按照这个思路去做技术方案的选型即可。

\

开放平台

开放平台的建设不单单只是开发需要考虑的,PM、PD、安全等都需要去考虑。大致分为三块。

开发集成

  • 提供一个开发工具包,这个工具包大概率是一个工程,里面包含我们需要对外提供的接口层依赖

示例代码等。

  • 提供集成文档,各种服务、扩展实现的步骤。
  • 三方开发完成后打包的aar需要通过平台上传给到我们,我们最好也有一套自动化集成三方sdk的平台(没有的话就手动依赖aar)。

安全合规

安全合规更多是由安全的人负责,如何认证开发者、三方的sdk是否有漏洞等等。

常见问题

可以列一些开发中可能会遇到的常见问题,减少我们开发者的技术支持。