美文网首页我爱编程
组件化方案:JIMU之UI路由(二)

组件化方案:JIMU之UI路由(二)

作者: leobert | 来源:发表于2018-04-13 23:08 被阅读378次

    前言

    在上一篇组件化方案:JIMU之UI路由(一)中,我们简单介绍了JIMU中用于UI跳转的UIRouter,较为扼要的阐述了它存在的原因,如何集成,如何使用基础功能以及一些常见的排错。

    这一篇中,会较为细致的阐述其原理,功能特性。

    原理

    如果不使用HOOK技术,那么此类路由最终都会回归到系统API
    -- Leobert

    在展开讨论之前,一定要记住我上面这句话,无论是谁给出的UI路由方案,在Activity跳转上,最终都会体现为

    Context#startActivity(Intent intent)
    

    JIMU中的实情

    仔细了解过JIMU的朋友们都知道:JIMU是编译期隔离、运行期不隔离的。这一点决定了我们不需要使用ClassLoader,我们需要做的是采用一种映射技术:在编译期自动创建映射、按照映射编写可以通过编译的代码,在运行期遵循映射执行相应的逻辑。这样就满足了组件化的核心:“隔离与发现”

    严格的数学上的映射:两个非空集合A与B间存在着对应关系f,而且对于A中的每一个元素x,B中总有有唯一的一个元素y与它对应,就这种对应为从A到B的映射,记作f:A→B。

    而且假如B到A也满足该条件,就是一种特殊的情况:一一映射(或称双射)

    而我设计的UIRouter是一种一一映射的场景:Enum(host+path) <--> Enum(Activity)。

    为什么废弃了UIRouter中priority的设计:我们在编码阶段所知的是host和path,需要根据它找到Activity,所以是Enum(host+path) --> Enum(Activity),而加入priority,则升维成:Enum(host+path+priority) --> Enum(Activity)。这并不是无法实现,而是增加了集成和使用的成本,加入priority之后,按照设计常理,不适合做成双射。一般而言会按照Chain of Responsibility进行设计,对priority做范围划分,这就破坏了设计的初衷

    实现host+path到Activity的映射

    在UIRouter中,我们使用RouteNode注解来为Activity指定他的Path,而Host根据Module环境确定,一个Module中,最终生成一张路由表,例如:

    public class AppUiRouter extends BaseCompRouter {
      @Override
      public String getHost() {
        return "app";
      }
    
      @Override
      public void initMap() {
        super.initMap();
        routeMapper.put("/main",MainActivity.class);
        routeMapper.put("/uirouter/demo/3",Demo3Activity.class);
        paramsMapper.put(Demo3Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
        routeMapper.put("/uirouter/demo/4",Demo4Activity.class);
        paramsMapper.put(Demo4Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
        routeMapper.put("/uirouter/demo/5",Demo5Activity.class);
        paramsMapper.put(Demo5Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 9); }});
        routeMapper.put("/uirouter/demo/2",Demo2Activity.class);
        paramsMapper.put(Demo2Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
        routeMapper.put("/uirouter/demo/8",Demo8Activity.class);
        paramsMapper.put(Demo8Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); }});
        routeMapper.put("/uirouter/demo/7",Demo7Activity.class);
        paramsMapper.put(Demo7Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); }});
        routeMapper.put("/uirouter/demo/6",Demo6Activity.class);
        paramsMapper.put(Demo6Activity.class,new java.util.HashMap<String, Integer>(){{put("EXTRA_OBJ_FOO", 10); }});
        routeMapper.put("/uirouter/demo/1",Demo1Activity.class);
        routeMapper.put("/uirouter/demo",UiRouterDemoActivity.class);
      }
    }
    

    我们将路由信息存入了routeMapper:

    protected Map<String, Class> routeMapper = new HashMap<>();
    

    最终回归到系统API:

     if (routeMapper.containsKey(path)) {
                Class target = routeMapper.get(path);
                if (bundle == null) {
                    bundle = new Bundle();
                }
                Map<String, String> params = UriUtils.parseParams(uri);
                Map<String, Integer> paramsType = paramsMapper.get(target);
                UriUtils.setBundleValue(bundle, params, paramsType);
                Intent intent = new Intent(context, target);
                intent.putExtras(bundle);
                if (requestCode > 0 && context instanceof Activity) {
                    ((Activity) context).startActivityForResult(intent, requestCode);
                    return true;
                }
                context.startActivity(intent);
                return true;
            }
    

    如何处理参数?

    再回忆一下我开始说的内容,一定会回归到系统API,通过Intent跳转Activity时,我们携带参数是利用了Bundle,这里也不会例外。
    JIMU中,可以直接使用Bundle携带参数以及像往常一样从Intent中获取参数。但是这还不够。

    实际情景中,为了扩大APP体量以及利用社交享受社交红利,会利用社交媒体分享mobile-web站点页面,为了给用户更沉浸的体验以及引导转化,会要求从mobile-web站点回流到APP,也就是我们常说的web唤醒

    而可行的技术就是通过向系统注册APP可以处理某类数据(特定的uri)然后从web中发出处理相应uri的请求。

    从UX的角度来说,当然希望用户继续按照刚才的页面操作下去,这时候一个问题摆在眼前:如何通过Uri传递参数给相应的Activity?

    从之前文章中的讨论以及一些常识,我们知道外部唤醒使用的是一个特定协议的Url。传递参数的问题就变为:
    如何使用Url包装入参?
    我们知道,一个Url可以拆解为以下部分

    [schema]//[host]:[port][path][queryString][#hash]

    可以用于包装参数的无非:

    • path
    • queryString
      而框架中采用的是queryString。

    理由:
    path已经用于映射,在path中采用参数表达式不够自由且容易出bug,例如PHP的一款支持restful风格的框架laravel就在其路由匹配上出现过各种bug;
    qs足够清晰明了。

    从queryString到Bundle

    在回忆一下前文提到的:“一定会回归到系统API”,Activity想要获取Bundle参数,一定会牵涉到类型,向Bundle写入时,也会牵涉到类型。故而:一定需要知道需要的参数的类型。Autowired注解应运而生。

    所以利用注解生成了如下的参数关系:

     paramsMapper.put(Demo6Activity.class,new java.util.HashMap<String, Integer>(){{put("EXTRA_OBJ_FOO", 10); }});
    

    "EXTRA_OBJ_FOO"是Bundle的key,10代表了参数类型。过多的细节不做深入。需要注意的是,JIMU目前支持以下类型:

    • 基本类型
    • 基本类型对应的装箱类型
    • String
    • Parcelable接口实现类
    • Object,通过Gson(最老版本使用了fastjson)对Object进行序列化和反序列化。

    注意,不支持其他的Serializable的实现类,例如,Foo类就是不可以的:

    public class Foo implement Serializable {
        public String bar;
    }
    

    对比以下两个Bundle类的API,就会理解原因

      public <T extends Parcelable> T getParcelable(@Nullable String key) 
    
     public Serializable getSerializable(@Nullable String key) 
    

    而JIMU中取参数赋值用的是对成员变量直接赋值:

      substitute.foo = substitute.getIntent().getStringExtra("foo");
      substitute.bar = substitute.getIntent().getStringExtra("EXTRA_STR_BAR");
    

    注入代码生成的路径在一个包内:

    package com.luojilab.componentdemo.router.cases;
    
    /**
     * Auto generated by AutowiredProcessor */
    public class Demo2Activity$$Router$$Autowired implements ISyringe {}
    
    package com.luojilab.componentdemo.router.cases;
    @RouteNode(path = "/uirouter/demo/2", desc = "使用bundle传递参数")
    public class Demo2Activity extends TestActivity {}
    

    故而:Field需要声明为public或者default

    这里扯开一句,其实在去年我编写了一个完全由兴趣驱动的项目:MagicBox,用来简化以及规范项目中对InstanceState的处理,因为后续的商业项目都不是从零开始的,就没有考虑投入商业实战。在这个库中,几乎实现了Bundle支持的所有的参数的读写,默认值填充,不限制可见性修饰符,处理父类中的参数,处理引用对象内的参数,处理引用对象父类中声明的参数。但是我也不鼓励大家去研究这个项目,当时未考虑将其加入JIMU的原因有几点:JIMU中的方法已经足够了;MagicBox采用的反射,一定程度影响效率;MagicBox的使用更复杂。

    UIRouter的一些特性

    其实在前文中已经零零碎碎说了不少了。新版本中对Log输出的支持进行了大力改进。新版本的Demo代码也提供了相应的sample,如果本周没有合并到master分支的话,请关注dev分支

    sample:

    • Demo1Activity 无参数跳转
    • Demo2Activity 使用bundle传递参数
    • Demo3Activity 使用Url传参
    • Demo4Activity Url和Bundle同时包含参数,以url为准
    • Demo5Activity Parcelable和Serializable
    • Demo6Activity 使用json字符串传参
    • Demo7Activity 必须参数,必须参数缺失,不使用抛出功能,可以看到log输出的信息。
    • Demo8Activity 必须参数2,必须参数缺失,使用抛出异常功能,以及使用安全模式。

    这里就不给大家详细展开了,Demo中也进行了一定的解释,大家也可以进行一些额外的测试,例如:url的path不匹配(错误的path),未加载mapping的情况下进行跳转,错误的参数值(无法parse,Json结构异常等)

    关于外部唤醒

    此次Demo中包含了一种特殊情况的处理策略,注意它并不是一个规范,只是一种特殊情况的可行性策略。不要引起误会。外部唤醒,需要实际情况实际分析,如有必要将通过issue进行讨论,并最终进行总结,不在本文展开。

    PS:如果继续收到大家对UIRouter使用的困惑,再跟新本文做更详细的展开


    下一阶段计划:

    • 对组件化的集成再做一下梳理,让文档更加适合初步认识JIMU的朋友。
    • 添加事件库,为组件间事件通知提供更加轻量级的方案。
    • 让组件加载支持异步

    JIMU的讨论群,群号693097923,欢迎大家加入:

    image

    qq群中有很多热心的朋友,一些重要的讨论,往往从群里面展开,最后转移到项目的issue中展开讨论以及总结。

    相关文章

      网友评论

        本文标题:组件化方案:JIMU之UI路由(二)

        本文链接:https://www.haomeiwen.com/subject/fkzmhftx.html