文章

对通用平台项目的思考

对通用平台项目的思考

本文记录了对于近期项目设计方式, 实现方式的思考

前言

我司项目以 iOS 小组件为核心, 主要包含组件详情/编辑 & 桌面小组件, 是一款通用的小组件平台. 欢迎体验我司产品 万象小组件

如何设计一款通用,可扩展的通用平台就成了最核心的方向性问题. 本文则记录了主要的设计思路和逻辑问题

基本的业务流程设计

简单来说通用平台就是要支持所有类型的小组件, 首先就是要分析小组件的 『UI 元素 + 数据 + 功能』, 并将其抽象成可用于信息交换的协议. 然后由服务器提供小组件资源包『UI资源 + 协议』, 客户端解析协议并绘制小组件 UI.

这一切似乎理所当然的成了最初的设计方案. 由此我们的协议定义如下

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<largewidget>
    <text frame="x,y,w,h" title="the title to show"/>
    <text frame="x,y,w,h" title="the title to show"/>
    <image frame="x,y,w,h" src="img.png" action="actionName"/>
    <image frame="x,y,w,h" src="img.png" action="actionName"/>
</largewidget>

交互流程如下:

我的最初想法即如上所示:

  1. 协议作为小组件的描述文件, 类似于编程语言中的 class, 渲染出来的小组件本身即为 instance.
  2. App 在详情/编辑页面解析, 编辑, 序列化, 保存小组件信息, 存储于主工程与小组件的共享内存.
  3. Widget 在桌面加载时, 解析共享内存来渲染 UI, 并响应用户事件, 回调 App 执行响应.

由此, 小组件的基本的展示 + 用户事件响应流程就逻辑上自洽了. 接下来就是小组件的编辑逻辑

编辑功能设计

通用小组件设计的难点: 以最小的设计支持全部已知类型, 所以必须高度抽象化, 高度抽象的结果就是匿名化. 匿名化就存在信息缺失, 就必须以其他的补充机制来定位具体信息. 这点在编辑时候尤其必要. 例如, 需要增加 id 值, 它仅用于编辑定位, 本身也是没有意义的值, 唯一需要保证的就是唯一性.

UI 渲染协议

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<largewidget>
    <text frame="x,y,w,h" title="the title to show" id="id1"/>
    <text frame="x,y,w,h" title="the title to show" id="id2"/>
    <image frame="x,y,w,h" src="img.png" action="actionName" id="id3"/>
    <image frame="x,y,w,h" src="img.png" action="actionName" id="id4"/>
</largewidget>

编辑协议

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<edit>
    <text title="the title to show" targetId="id1"/>
    <text title="the title to show" targetId="id2"/>
    <image src="img.png" action="actionName" targetId="id3"/>
    <image src="img.png" action="actionName" targetId="id4"/>
<edit>

UI 页面的设计

组件详情/编辑功能实际在同一个页面之中, 结构如下:

1
2
3
4
5
6
7
var body: some View {
    VStack {
        WidgetView() // 通过解析组件协议渲染
        EditView()  // 通过解析编辑协议渲染
        SaveBtn() // 编辑完成, 保存按钮
    }
}

协议解析即上面的配置文件, 我们仅能将其解析为一个数组, 让每个 NODE 来自定义渲染自己的 View, 这些都可以通过让其遵守 protocol 来完成统一规则.

编辑功能的实现

编辑功能即上述 EditView, 可以通过交互 EditView 的某个子 View 修改内容值, 并同步映射到 WidgetView 的显示中. 这里的技术实现基于 SwiftUI 响应式框架, 需要有个 dataModel: ObserveableObject 保存数据并监听变化.

具体来说: 『编辑协议』读取到内存中, 『组件协议』也读取到内存. 均保存到 dataModel 中. 用户触发编辑操作, 通过 id 映射直接修改内存中的『组件协议』. 由此映射 UI 同步预览.

当用户点击保存按钮: 用户点击保存直接拿到内存中的『组件协议』反序列化成 XML 文件, 并替换原本资源中的 XML 文件.

到此: 整个编辑逻辑自洽, 上文提到的 App/Widget 整体交互逻辑自洽.

其他自定义数据

每个组件除了通用协议之外, 还有其个性化的部分, 比如: 倒计时/计数器/音频/天气, 它们需要根据自己特定的类型来保存自己个性化数据. 理论上我们需要知道小组件具体是哪个 type, 来保存其具体个性化数据.

这里面的数据包含两类: 理论上都可由 App 处理

  1. 权限类型信息
    • 如运动信息组件, 依赖健康权限, App 端负责请求权限
    • 如天气组件, 依赖地理位置权限, 通过 GPS 定位获取城市id, 再由城市 id 获取具体天气.此类型属于既需要权限信息, 又需要自定义信息.
  2. 自定义信息
    • 自定义信息是高度个性化, 可以放到其『组件协议』xml 文件中, 定义 <customData name="custom data"/> 标签来匿名化type 定义

方向打通后下一步要做的事

代码结构调整

  1. 以 XML 的方式调整 protocol 规则, 如增加 type / id / targetID 字段
  2. 『编辑协议』与『组件协议』中 targetId / id 映射函数编写. 重点是变量的修改与存储, 因为每个 model 最初设计都是 struct, 这里需要重点注意

组件节点反 XML 序列化规则设计

支持解析到内存的 XML 数据反序列化为 XML, 如增加反序列化规则

总结:

  1. 自己惯性思维局限性
  2. 多讨论, 多交流, 学习和借鉴不同思路. 打破局限, 拓宽视野.

THE END.

PS

编辑协议实现部分的插曲:

受限使用 JSON 的经验, 并且我最初设计思路是将 App 解析的内容作为可序列化数据存储到磁盘, 小组件直接反序列化使用具体数据即可.

在通用编辑(背景/Font/FontColor)部分完全满足我的设想, 到个性化编辑就发现 XML 携带的信息是信息丢失的, 我这里没有对应的 key 来保存个性化编辑产生的中间变量,比如设置照片,音频实际产生的中间值放在哪(url & data 本身)

对此我也主动想到修改组件 xml 文件, 将其作为存储结构, 但始终坚持最初的 JSON, 导致这里我一度将前期支持的个性化编辑当作特例来处理.

第一版发完, 及时和同事沟通, 借鉴他提供的前端思维方式, 以遍历查找 targetID/id 的方式定位修改原 xml, 点破了我对 JSON(key:value) 格式的执着.

总之: 及时沟通拓展了我的局限, 收益颇多.

聊一下 XML / JSON 对比优缺点

以下对比仅是我现有认知水平之下的总结:

JSON

  1. JSON 数据量小, 轻便易读, 节省数据, 是当前开发常用的数据交互格式
  2. JSON (key: value) 的结构定义, 具有唯一性, 能让代码非常明确的实现特性需求
  3. 缺点: 所有数据都是具名化的, 需要定义大量唯一 key, 不适用做通用组件平台的场景

XML

  1. XML 相对复杂一些, 其本身是前端协议, 很多标签在前端开发中有特定作用. 数据量也更加大, 在移动端开发中极少使用
  2. XML 数据的读取只能以数组的形式存储, 其内部标签代表一个个对象, 缺少唯一 key, 无法存储为 hash 表, 不适用于 JSON(key: value) 形式的的序列化规则. 如果想支持序列化与反序列化, 需要自行定制一套规则.
  3. 优点: 其内部节点是匿名化的, 匿名之后就摆脱了每个信息都要有 key 的束缚, 极大释放了其信息容量,特别适用于具体内容不确定的通用平台.

最后一张图来总结一下这两者的区别:

以上图中理论信息量来看的话:

  • JSON 的信息量好比全排列 A₉¹
  • XML 信息量好比全排列 A₉⁹

THE END.

本文由作者按照 CC BY 4.0 进行授权