业务场景
UI开发不可避免需要和后端进行交互,部分灵活的页面需要由后端配置进行页面跳转,或者行为触发。
方案一:字段解析,按照对应的开关跳转或者触发不同的行为
字段解析一般是最简单想到的方法,和后台定义携带的参数,像必要的id,是否跳转等等。前端直接解析对应字段,按照必要的逻辑进行处理。
优点
绝大部分都是直接使用定义字段进行处理的,思路比较简单,在开关类场景十分常见,约定了对应的字段进行解析操作
缺点
缺点主要在业务迭代情况下,由开关类的场景转换为内容参数类场景,这时候这种前期定义的参数就会出现很多问题。
如果之前约定的字段是boolean类型,那么可拓展就很难,需要新增boolean类型来做兼容,那么就会出现2X2>3的情况,本来只是多一个场景,却多了两个可能。
如果之前约定的字段是非boolean类型的,那么可拓展比较容易,int类型直接加一个,字符串或者别的类型也可以做变化。
如果之前约定的字段没有,那么需要拓展,需要变更json解析逻辑。
相对来讲如果需要用参数定义,那么最好是非boolean类型的,不过由于业务变更的不确定性,最好同步增加版本号。
方案二:使用Uri做字段定义解析器
这个思路其实是基于方案一的升级
每个需要路由跳转的地方,新增的字段类型是string,内容是编码后的Uri
使用Uri主要是Uri拥有比较完善的解析方式,巧用可以完成像协议,版本号,意图,参数等行为的封装
Uri格式
协议名://[用户名]:[密码]@[服务器地址]:[服务器端口号]/[路径]?[查询字符串]#[片段ID]
完全可以按照这种格式进行处理。
首先是协议名,一般是http,https,files等,可以变成客户端代号,像我在喜马拉雅,安卓项目用的就是tingcar,小程序用的就是miniTingCar,鸿蒙就是hongmengTingCar。这个主要是可以用来区分该协议针对哪个端。
其次是用户名,一般是域名,但是业务上面可以变成协议的版本号,这个版本号可以是大版本的区分,也可不必区分,这个可以业务上面自己做决定
密码/服务器端口号:这个暂时不需要使用
路径:路径可以设计为意图,比较关键,这一次动作的整体行为,比如说跳转哪个页面,执行哪个动作等。
查询关键字/片段id:这里直接添加query,像跳转页面需要的参数,执行动作需要的参数等
客户端需要做的事情
主要是对这个Uri进行安全校验,版本校验,然后意图解析,动作行为执行。
不过有一点,这个动作行为执行需要提前封装。这个需要有一定的意识,在新建页面的时候,对其一些对外的行为就进行封装。
封装的过程不单单局限于参数的传递,还有上下文的获取。这个上下文最好不要做成参数进行传递,而是需要时实时从环境中读取。这样行为封装更为彻底。
优点
1、版本升级,只要约定的协议不变,后续的仍然可以使用该协议进行拓展。
2、携带的参数可以无限制扩大,对于可变参数,例如页面需要走网络请求带的头参,可以使用遍历query一股脑加进去的方式做网络请求。这个是最重要的,甚至可以新建一个页面,把网络请求作为query装进去,然后作为某个页面的传参,直接可以组织成这个页面。
3、由于该协议带来的动作行为封装, 在业务中也十分有用,我观察到绝大部分人有页面跳转封装的思想,但是动作封装的思想却比较少。动作封装其实更加重要,需要兼顾动作的头尾,兼顾整个过程,而页面的封装却十分片面。
缺点
我在喜马拉雅落地了这个,并且使用过程中的确发现了一些问题。
1、和后端需要明确落实,这涉及到一定的沟通向的问题
2、动作行为封装需要比较整合,比如触发的行为是购买,购买前必然要登陆,登陆过程只是这个行为的一次检验,并未确实触发这个行为,需要在登陆完继续完成这个行为
3、版本的升级需要明确哪个版本支持哪些动作,否则运营总是来问,这个最好技术端维护一下文档说明。
实现
1、route过程传入字符串,解析uri,判断协议是否相等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /** * 行为触发方法总体 */ public static void route(@Nullable String router, String traceFrom) { if (TextUtils.isEmpty(router)) { ToastUtils.showError("错误!无有效跳转路径", "UriRouter/route: router is null"); return; } Uri uriRouter = Uri.parse(router); if (TINGCAR_SCHEME.equals(uriRouter.getScheme())) { String path = uriRouter.getPath(); if (path == null) { ToastUtils.showError("错误!无跳转路径", "UriRouter/route: path is null"); return; } switch (path) { ... } } }
|
2、然后根据意图进行分发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| switch (path) { case OPEN_ALBUMFRAGMENT: openAlbumDetail(uriRouter, traceFrom); break; case RECEIVE_ACTIVITY: receiveActivity(uriRouter, traceFrom); break; case OPEN_PLAYLIVEFRAGMENT: openPlayLiveFragment(uriRouter, traceFrom); break; case OPEN_ALBUMLISTFRAGMENT: openAlbumListFragment(uriRouter, traceFrom); break; case OPEN_VIPFRAGMENT: openVipFragment(uriRouter, traceFrom); break; case OPEN_RECOMMENDFRAGMENT: openRecommendFragment(uriRouter, traceFrom); break; case OPEN_COUPONLISTFRAGMENT: openCouponListFragment(uriRouter, traceFrom); break; case OPEN_PLAYRADIOFRAGMENT: // comment by YinPengcheng: 2020-06-16 4.3版本添加 openPlayRadioFragment(uriRouter, traceFrom); break; case OPEN_PLAYTRACKFRAGMENT: // comment by YinPengcheng: 2020-06-16 4.3版本添加 openPlayTrackFragment(uriRouter, traceFrom); break; case OPEN_H5FRAGMENT: // comment by YinPengcheng: 2020-06-17 4.3版本添加 openH5Fragment(uriRouter, traceFrom); break; case BUY_ALBUM: // comment by YinPengcheng: 2020-06-17 4.3版本添加 buyAlbum(uriRouter, traceFrom); break; case BUY_VIP: // comment by YinPengcheng: 2020-06-17 4.3版本添加 buyVip(uriRouter, traceFrom); break; case OPEN_BUY_VIP: openBuyVip(uriRouter, traceFrom);//4.6新增 非会员->点击弹出连会员购买弹框 已登录是会员->点击弹出会员购买弹框 break; case OPEN_MINEFRAGMENT: // comment by YinPengcheng: 2020-06-18 4.3版本添加 openMineFragment(uriRouter, traceFrom); break; default: // TODO: 2020-05-09 发现无法识别的跳转路径,此时最好上报一下服务器,告知版本号之类的 Log.i(TAG, "UriRouter,route: can not resolve " + router + CarOsUtil .getVersionSnapshot()); break; } }
|
3、分发到行为方法内部,根据方法版本号等参数,决定是否响应该次动作触发,以及埋点等。