对通用平台项目的思考
对通用平台项目的思考
本文记录了对于近期项目设计方式, 实现方式的思考
前言
我司项目以 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>
交互流程如下:
我的最初想法即如上所示:
- 协议作为小组件的描述文件, 类似于编程语言中的 class, 渲染出来的小组件本身即为 instance.
- App 在详情/编辑页面解析, 编辑, 序列化, 保存小组件信息, 存储于主工程与小组件的共享内存.
- 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 处理
- 权限类型信息
- 如运动信息组件, 依赖健康权限, App 端负责请求权限
- 如天气组件, 依赖地理位置权限, 通过 GPS 定位获取城市id, 再由城市 id 获取具体天气.此类型属于既需要权限信息, 又需要自定义信息.
- 自定义信息
- 自定义信息是高度个性化, 可以放到其『组件协议』xml 文件中, 定义
<customData name="custom data"/>
标签来匿名化type 定义
- 自定义信息是高度个性化, 可以放到其『组件协议』xml 文件中, 定义
方向打通后下一步要做的事
代码结构调整
- 以 XML 的方式调整 protocol 规则, 如增加 type / id / targetID 字段
- 『编辑协议』与『组件协议』中 targetId / id 映射函数编写. 重点是变量的修改与存储, 因为每个 model 最初设计都是 struct, 这里需要重点注意
组件节点反 XML 序列化规则设计
支持解析到内存的 XML 数据反序列化为 XML, 如增加反序列化规则
总结:
- 自己惯性思维局限性
- 多讨论, 多交流, 学习和借鉴不同思路. 打破局限, 拓宽视野.
THE END.
PS
编辑协议实现部分的插曲:
受限使用 JSON 的经验, 并且我最初设计思路是将 App 解析的内容作为可序列化数据存储到磁盘, 小组件直接反序列化使用具体数据即可.
在通用编辑(背景/Font/FontColor)部分完全满足我的设想, 到个性化编辑就发现 XML 携带的信息是信息丢失的, 我这里没有对应的 key 来保存个性化编辑产生的中间变量,比如设置照片,音频实际产生的中间值放在哪(url & data 本身)
对此我也主动想到修改组件 xml 文件, 将其作为存储结构, 但始终坚持最初的 JSON, 导致这里我一度将前期支持的个性化编辑当作特例来处理.
第一版发完, 及时和同事沟通, 借鉴他提供的前端思维方式, 以遍历查找 targetID/id 的方式定位修改原 xml, 点破了我对 JSON(key:value) 格式的执着.
总之: 及时沟通拓展了我的局限, 收益颇多.
聊一下 XML / JSON 对比优缺点
以下对比仅是我现有认知水平之下的总结:
JSON
- JSON 数据量小, 轻便易读, 节省数据, 是当前开发常用的数据交互格式
- JSON (key: value) 的结构定义, 具有唯一性, 能让代码非常明确的实现特性需求
- 缺点: 所有数据都是具名化的, 需要定义大量唯一 key, 不适用做通用组件平台的场景
XML
- XML 相对复杂一些, 其本身是前端协议, 很多标签在前端开发中有特定作用. 数据量也更加大, 在移动端开发中极少使用
- XML 数据的读取只能以数组的形式存储, 其内部标签代表一个个对象, 缺少唯一 key, 无法存储为 hash 表, 不适用于 JSON(key: value) 形式的的序列化规则. 如果想支持序列化与反序列化, 需要自行定制一套规则.
- 优点: 其内部节点是匿名化的, 匿名之后就摆脱了每个信息都要有 key 的束缚, 极大释放了其信息容量,特别适用于具体内容不确定的通用平台.
以上图中理论信息量来看的话:
- JSON 的信息量好比全排列 A₉¹
- XML 信息量好比全排列 A₉⁹
THE END.