混合开发最小接口

2020 年目前在做的最复杂的项目是将基于 Draft.js 深度定制开发的 web 编辑器适配到移动端,因为数据上牵涉到很多转换过程 native 端要从头开发的话成本过高,并且发现 web 并不能很正常地识别虚拟键盘弹出与否,于是将下图所示的工具栏通过 native 端单独进行开发与 web 集成,以获得更好的使用体验。

编辑器工具栏

简单介绍下 web – native 通讯方式,ark_editor_native 是 native webview 注入到 window 对象上的一系列方法简称 AENark_editor_web 则是 web 编辑器挂载成功后写入到 window 对象上供 native 调用的一系列方法简称 AEW,每次 editor 有任何更新,通过 AEN.syncState 传递给 native 一个 JSON 字符串,客户端需要自行反序列化成 JSON 对象,称为 syncState

syncState

名称
inline_styles { "BOLD": false, \n"CODE": false, "ITALIC": false, "STRIKETHROUGH": false },
block_type “unstyled”
alignment “left”
disabled_buttons [ “BOLD”, “unstyled”, “header-one”, …]
undo_disabled true
redo_disabled false

操作栏

行为 value disabled toggle insert update remove native 帮帮我 交给你了 web
加粗 ‘BOLD’ ‘BOLD’ in syncState.disabled_buttons AEW.toggleInlineStyle(‘BOLD’)
楷体 ‘ITALIC’ ‘ITALIC’ in syncState.disabled_buttons AEW.toggleInlineStyle(‘ITALIC’)
删除线 ‘STRIKETHROUGH’ ‘STRIKETHROUGH’ in syncState.disabled_buttons AEW.toggleInlineStyle(‘STRIKETHROUGH’)
行内代码 ‘CODE’ ‘CODE’ in syncState.disabled_buttons AEW.toggleInlineStyle(‘CODE’)
链接 ‘LINK’ ‘LINK’ in syncState.disabled_buttons AEW.toggleLink() AEW.insertLink(url) AEW.updateLink(entityKey, url) AEW.removeLink() AEN.showLinkEditor(entityKey, url)
注释 ‘FOOTNOTE’ ‘FOOTNOTE’ in syncState.disabled_buttons AEW.insertFootnote(content) AEW.updateFootnote(entityKey, content) AEN.showFootnoteEditor(entityKey, content)
标题( 总称,无实际意义) ‘headlines’ ‘headlines’ in syncState.disabled_buttons
一级标题 ‘header-one’ ‘header-one’ in syncState.disabled_buttons AEW.toggleBlockType(‘header-one’)
二级标题 ‘header-two’ ‘header-two’ in syncState.disabled_buttons AEW.toggleBlockType(‘header-two’)
三级标题 ‘header-three’ ‘header-three’ in syncState.disabled_buttons AEW.toggleBlockType(‘header-three’)
四级标题 ‘header-four’ ‘header-four’ in syncState.disabled_buttons AEW.toggleBlockType(‘header-four’)
五级标题 ‘header-five’ ‘header-five’ in syncState.disabled_buttons AEW.toggleBlockType(‘header-five’)
六级标题 ‘header-six’ ‘header-six’ in syncState.disabled_buttons AEW.toggleBlockType(‘header-six’)
默认文字块 ‘unstyled’ ‘unstyled’ in syncState.disabled_buttons AEW.toggleBlockType(‘unstyled’)
对齐 ‘alignments’ ‘alignments’ in syncState.disabled_buttons AEW.toggleAlignment(‘left’|’center’|’right’)
有序列表 ‘ordered-list-item’ ‘ordered-list-item’ in syncState.disabled_buttons AEW.toggleBlockType(‘ordered-list-item’)
无序列表 ‘unordered-list-item’ ‘unordered-list-item’ in syncState.disabled_buttons AEW.toggleBlockType(‘unordered-list-item’)
引用 ‘blockquote’ ‘blockquote’ in syncState.disabled_buttons AEW.toggleBlockType(‘blockquote’)
图片 ‘FIGURE’ ‘atomic’ in syncState.disabled_buttons AEW.selectImage()
代码块 ‘code-block’ ‘code-block’ in syncState.disabled_buttons AEW.insertCodeBlock()
分割线 ‘PAGEBREAK’ ‘ atomic’ in syncState.disabled_buttons AEW.insertPagebreak()
分行 ‘ atomic’ in syncState.disabled_buttons AEW.insertSoftNewLine()
撤销 syncState.undo_disabled AEW.undo()
重做 syncState.redo_disabled AEW.redo()
清除行内样式 AEW.removeFormat()

优先级原则:

  • 先判断是不是 disabled
  • toggle > native 帮我 > 交给你了 web > insert/update/remove

上面就是编辑器跨端通讯的基本形式了,简单来说 web 端每次 React componentDidUpdate 会把状态同步给 native 端,保证工具栏的及时性。当用户点击工具栏中可用按钮时,首先判断是不是需要和 web 进行状态判断,毕竟前一步只是判断能不能触发,触发以后的行为需要结合更具体的数据状态。然后是如果发现需要更多用户输入(比如弹窗输入框或勾选项等等)就需要唤起 native 的方法(native 帮帮我),如果没有这一步就可能是 web 专享操作,无关更多状态,比如上传图片或者撤销重做等等,这种情况就全权交给 web 了。最后则是 native 端二次调用 web 的方法,通常是关掉弹窗以后传递数据去改变 web 的状态。

体会:

  • 原本通过 React props 保证的数据实时性需要手动同步给 native 端
  • native 和 web 互相暴露方法需要考虑实现的最小集
  • 需要找到优雅的 debug 方式,出现过直接传 JS Object,native 端得到 undefined
  • 异步操作可能需要加锁

当然现在开发还在对接联调期,包括保存等功能还没有加上,与从零开始的搭建也不可同日而语。编辑器和阅读器都是电子阅读产品的重头戏,之前则是看了这篇《Visual Studio Code有哪些工程方面的亮点》,对编辑器的开发充满了崇敬的心情。VS Code 也是我们现在开发的主力工具,包括其丰富的插件体系、LSP 和宛如神器的 Remote 模式,都使我惊叹于其良好的实现。不说了,去下单《设计模式》了。

时滞效应

在股市的朋友一般都知道「K 线图」这个东西,简单说来就是包含四个数据,即开盘价、最高价、最低价、收盘价,所有的 K 线都是围绕这四个数据展开,反映大势的状况和价格信息。我的姨夫号称一代股神,开一次张可以吃三年,特别崇拜这个图。我个人是有些不以为然的,因为 T + 1 制度的存在杜绝了超短线投机。任何时刻的交易都是基于前一时刻的交易,如果这种方式是靠谱的,那也只是扒下了边际效益的薄薄一层皮而已。如果要做超短线就是期货了,曾有个笑话说炒期货的要尿个尿都要把手上的先抛掉才敢去尿。这就是时滞效应。

当前华夏大地处在一片新型肺炎疫情的阴霾笼罩之下,时隔 1 – 14 天不等的发病周期,让防疫工作困难重重。一旦某个小区发现感染病例,整个小区都会被隔离。我们可以看下下面这张疫情趋势图,图中可见疑似病例一开始是低于确诊病例的,但年初二之后突然呈井喷般上升趋势,但幸好当下已经逐渐趋于平稳。

全国疫情趋势图

没有疑似病例意味着什么,意味着毫无病例筛查。疑似病例井喷说明什么,说明堆积了足够多的样本上报。量变引发质变,足够多的病例将导致疾病向着失控的方向蔓延,而这一切在确诊病例上是延时的,不确切的。所以需要更多的筛查渠道和病毒检测盒,而现在更可怕的是病毒会“伪装”自己,在感染的前 10 天并无法检查出阳性,这样就给医学意义上的密切接触者数量更提升了一个数量级。

医生的真知灼见无法实施

而反观政府现在在做什么?隔离,对,隔离是必要的。区域自治,区域封锁,假期延长,将蔓延的机会降到最低。另外一点,隔离使得城市面临生活必需品保障短缺的问题,各级政府苦心经营的第三产业在疫情面前已经濒临瘫痪,第二产业随着机器人的大量应用其实已经实现了自治,但第一产业 —— 农业,依然是需要保障的重中之重,农产品需要从城市就近通过内部交通网络输送到各家各户,而这些都是都市人口已经很久很久没有考虑过的了。

居委会给隔离群众送菜

所以说,这次疫情之类最应当做的两个工作是:

  1. 深化医疗行业的规范改革,切实保障一线医务工作者的安全以及避免就诊患者交叉感染。
  2. 大力发展现代农牧业,例如屋顶大棚果蔬养殖和人工智能养猪等等,保障危急时刻群众吃饭问题。

都市人已经习惯了吃饭了叫个外卖,生病了挂个号慢慢排队的日子,这次疫情让人体会到时间的宝贵,不珍惜时间就会浪费更多的时间(假期)来弥补时间的亏空。在商业软件开发中也是一样,不尊重用户的日常需求,用户切实反馈的问题得不到解决,用户的怨言就会堆积起来,最终导致用户流失。而对于自身而言,每天投资自己一点点,自己变好一点点,就是增强一点点今后的抗风险能力。

再谈业务逻辑

我面试过一些岗位,也面试过很多人,基础的岗位问的最多的大概就是对框架的理解,对语言基础的掌握等等,但如果是高级岗位往往问到对市场大环境的感悟和对业务逻辑的理解,体现了工具的应用必然是为完成目的服务的标准。

实际开发中,由于前后端现在工种的分离,业务逻辑的归属是很大的问题。

后端面向数据库和第三方服务集成,数据库总是以最小业务模型为基准,考虑扩展性进行建模,而第三方服务与领域模型融合则需要对业务逻辑进行抽象化,比如支付接口需要考虑本身提供的商品种类和第三方支付的种类进行分别编码并抽取公共逻辑,常见的形式有 Adapter(适配器模式)等。

前端当下也很复杂,虽然有了一些基础组件,但随着业务的深入,组件上难免多出很多随着各种特殊使用场景分别配置的参数,甚至一个组件接受几十个参数来配置,可能也有同学一看这不行啊,感觉封装了几个配置好的组件供业务使用,但上面的过程多了以后,组件变得又深又长,深体现在封装层级上,长体现在单个组件的长度上,比如封装一个电子书的展示列表。

图片 -> 封面 -> 带标签的封面 -> 电子书 -> 书籍列表 -> 可无限加载的书籍列表

这时如果要把「图片的尺寸」或者「可点击与否」传递给最内部的组件,就需要一层层从上面传下来,React 有 context 方法来在组件间传递参数,但这毕竟是不可见的,需要自己去 useContext 来得到。ES6 中提供了 Reflect 对象,比如 Reflect.getMetadata('design:paramtypes', target) 可以得到在参数里定义的参数类型,Angular 的控制反转和依赖注入就是依靠实现的。这为我们提供了一些可以想象的思路,当然本质上它只能解决编码上的便利性,组件的层级和复杂度并没有因此降下来。

当前前后端通讯主要靠两种方式

  1. 首屏页面内 script 注入初始数据
  2. AJAX 接口

阅读站新的接口优先考虑 GraphQLGraphQL 是一种面向领域模型的非常好的查询语言,我们不再需要多次往返前后端通讯获得更多数据,或者是重复定义各种 build_xxx_entity,只需要一次定义,任何用到单个字段的地方都只用写字段名就可以使用,可以说极大地降低了边际成本,而且越用收益越高。当然有同学会问那如果大批量查询的问题呢?Facebook 给出了 DataLoader 的方案,但我们的 peewee 版本以及早期的 hack 写法限制了我们真正将 ORM <==> GraphQL 的过程,现在其实是手动 ORM <==> 字段,带缓存的字段 <==> GraphQL

当后端和接口都做到了尽量少耦合业务逻辑时,业务逻辑就大量堆积在表单 POST 接口和前端展示逻辑上,又因为表单校验因 js 和 python 没法互通,需要前后端分别实现,而展示逻辑则是大多硬编码在组件内。这点其实很吓人。表单方面有 JSON Schema 的方案,但因为 Python 这种胶水语言快读开发的节奏每每难以落地,只是可惜展示上没法脱离业务组件进行编码,之前有过在 App 上设计根据 type 来组合 widget 的展示形式,但一旦需求变动代码就变得非常臃肿。

一种现有的整理业务逻辑的形式是把 Business Logic 存到数据库中 …… 简单来说,如果数据库可以方便地表达因果关系,或许这是一种可行的方案。还有就是很通用的配置文件方式,也是我们搭建开发环境的方式,通过脚本配置在环境初始化时载入一系列的变量最终构建出符合业务需要的开发环境。

理想的情形可能是,前端只提供组件库和组件间的组合逻辑,后端只提供数据的存储和加工工具,业务逻辑由第三方提供配置,设置极限值和用户可选值,用户可以在享有最多自主权的情形下使用 web 产品,当然这也只是一种美好的愿望……毕竟当下都是想把产品做到尽善尽美,只给用户一种最好的选择,可这是不是用户想要的呢?做得越多也许越错呢?没有人真的会去想,大家都是假装想想罢了。