作者 | 当轩
点击查看视频
大家好,我是来自阿里巴巴集团 ICBU 互动端技术团队的当轩,很荣幸能在第十五届 D2 大会上和大家做一次分享 ,我此次分享的主题是《跨端的另一种思路》。
我在开发端应用的时候,常常怀念 Web 的天然跨平台能力和开发效率。然而在开发 Web 应用时又会苦恼没有端应用那么好的性能和体验。所以就常常在想,有没有一种技术方案,像 Web 一样可以便捷的开发、在所有平台都能运行,同时又有很好的性能和体验。
当时最有名的一句宣传语我相信大家都有印象 -- Write Once, Run EveryWhere
,一次编写,到处运行。
然而理想很饱满,现实很骨感,不同的跨端方案层出不穷,但是都难以达到我们最终理想中的效果。
举例来说:
-
Web 作为天然的跨端方案,在研发效率上非常不错,然后性能和体验的优化难度都非常高
-
React-Native 类的方案,性能上要比 Web 好了不少,然后多端一致性和研发效率则较低
-
Flutter 在 App 上的性能和一致性不错,但是在差异比较大的 Web 端性能就非常糟糕了,也无法满足 Web SEO 等诉求
之所以理想和现实存在这种差距,是因为跨端的方案本身在带来收益的同时也意味着在某些方面会增加一定的成本。
例如我们期望用同一套代码覆盖所有平台的同时,又希望研发的*度和表达能力不要受到限制,然而这种方案只能使用不同平台表达力的子集。最典型的场景就是小程序的跨端方案,静态转译的小程序跨端方案往往通过限制开发者的表达能力来实现一套代码跨多端。
既然现实中的方案在带来收益的同时同样带来成本,我们就不能一味的追求 Write Once, Run EveryWhere
我们的实际场景做出取舍。于是我们就提出另外一种成本更低的思路:逻辑跨端。
今天以我们 Alibaba.com 的交易场景为例,分为 Android/iOS/M 站/PC 网页几个端。
几端的 UI 交互并不完全一致,但具有相同的业务逻辑,同时 APP 端对于性能和体验上有更高的要求。我们在效率上最直观的问题在于,一个哪怕非常简单的业务需求的上线,都需要经过多端开发同学的资源协调和相互依赖,消耗在沟通上的成本非常高。
由于 Android/iOS 端对于性能体验的要求高,加上这两端的 UI 和交互基本一致,我们考虑用 Flutter 来作为主要的技术选型。然而在 Native 和 Web 之间,Flutter 并没有成熟的跨端方案,前面我们提到过 Flutter for Web 的性能基本是不可用状态。
通过 JavaScript 容器 + Yoga 做类似于 React-Native 的方案我们也考虑过,然而其带来的建设成本和抽象代价在这种场景下是否合适是存疑的,因为 PC 和 App 的交互完全不同,通过引入 Yoga 类的方案反而会让两端的开发都束手束脚。除此之外,我们也担心 JavaScript 容器的引入在端上带来更高的优化成本。
于是我们就提出了另外一种思路:有没有可能仅针对业务逻辑做跨端共享,这样我们不需要对容器侧进行太大的改造,也不用担心引入新的优化成本,同时也能做到跨端共享代码。从而让前端同学去单独 hold 业务侧的需求成为一个可能的方案。
那么对于这样一种方案我们会有几个问题:
-
采取什么语言
-
如何分离逻辑和 UI
-
差异逻辑怎么写
第一个问题是采取什么语言,其实 Dart 就是一个现成的选项,因为 Dart 从一开始就是为 Web 设计的语言,甚至在 Flutter 出现前曾经一度在 Chrome 中有默认 VM 实现。虽然后来放弃了在浏览器中的发展,但是 Dart => JavaScript 仍然是一个成熟的技术。
同时因为我们的前端同学其实对于 Dart 仍然不是那么的熟悉,另外 Dart 的类型系统在很多时候并不能满足动态类型的诉求,以至于到处都是 dynamic。例如说基础的联合类型:string | number
这样的类型都无法直接支持,所以我们也在探索从 TypeScript 转译到 Dart 的方案。
通过 TypeScript 提供的能力,我们可以直接把一份 TS 的代码从源码解析到 AST,而后通过遍历 AST 生成对应的 Dart 代码。同时其中通过 getTypeChecker.getTypeAtLocation
等 API 获取到 AST 对应的 TS 类型。然后通过把 TS 类型转换成对应的 Dart 类型。对于不支持的类型降级到 dynamic
,把原有的完整类型信息输出到对应的注释里。
第二个问题是如何有效的分离逻辑和 UI,其实我们都知道如果只是单纯的把纯函数作为一个独立模块给抽离出来并不困难。然而逻辑中必然不仅仅是单纯的输入得到输出的纯模块,还有很多涉及到组件生命周期、渲染、状态变化等副作用(side effect)。如果我们需要在 Flutter 和 Web 间复用逻辑,我们就需要定义一份类似的接口。
我们可以先看看 Flutter 和 Web(我们这里采用的是 React)究竟存在多大差异,这里是 Flutter 和 React 构建组件的一个简单对比。其实我们知道,在代码组织方式上,Flutter 和 React 是非常相似的,事实上他们背后的的状态更新 => 触发 Diff => 重新渲染的逻辑也基本一致。所以说最理想的情况下,我们能直接用同样的逻辑抽象方式共同来书写逻辑,这样可以避免在不同场景下的接口对接方式和心智负担。
那么,有什么合适的方案可以用于分离逻辑和 UI 呢。其实对于很多前端同学来说,可能都了解 React 16 推出的 React Hooks,我们可以在这里看到相比 React 15 的 Class API 的一个区别。
看上去好像除了代码量变小外似乎问题不大?但其实如果我们把这个组件拆成两个函数,就能明显看出差别了。
没错,React Hooks 最大的作用不在于单纯的少写代码,而是让我们可以以非常低的成本把逻辑从 UI 或者其他逻辑中抽离出来,并且进行再组合。也就是说其实 Hooks 就是这么一个现成的逻辑拆分方案,大家也广泛接受其理念和 API。那么,有没有可能让 Hooks 的逻辑在 Flutter 上也能使用呢?
前面我们提到,Flutter 构建组件的方式以及运行原理都和 React 十分的相似,那么我们其实只要理解了 React 中 Hooks 工作的原理,就能在 Flutter 中再实现一遍。Hooks 可以简单理解为 闭包 + 数组(实际上在 React 中是链表)。以 useState
为例,Hooks 和普通函数最大的差异在于其可以在多次 render
调用中保持状态,实际上就是通过在闭包中保留状态,同时通过每次重新 render
时重置计数,从而依赖执行顺序还原出具体的状态。
那么到了 Flutter 中,我们就可以实现一个 HooksWidget
,在触发渲染时重置计数,同时把当前组件存储到闭包中,从而让 Hooks
能够根据计数找到对应的状态,并且知道应该去触发那个组件的重渲染。
最后一点就是对于差异化逻辑的书写,Dart 从 x.x 版本就引入了 condtional import
的能力,让我们可以根据不同的环境(Flutter 和 Web 引入不同的包)。于是我们可以在不同的包中书写接口相同,但底层逻辑不同的类,从而实现差异化逻辑。
同时在 Web 端,我们可以通过 Dart 官方提供的 js
包,轻松的和浏览器中的 JavaScript 原有能力进行交互。于是我们可以通过这样的一层胶水层,让我们在 Flutter 端的逻辑走我们上面写的 Hooks
能力,而在 Web 端则直接调用 React
的 Hooks。
最后我们能实现的一个效果,就是用同一份逻辑代码来表达 Flutter 和 Web 端的业务逻辑,并且作为我们非常熟悉的 Hooks,引入到 UI 上并且直接绑定。
同时借助 SourceMap,我们在 Devtools 里也能进行调试。
由于我们在渲染上并不强制差异很大的 Web 和 Flutter 保持统一,所以在性能上可以针对不同平台的特性做出优化,比 Flutter for Web 的性能表现更好。
点击查看视频
这个 DEMO 仅代表这个场景下的一个性能对比,并不代表所有场景下的方案性能都如上所示,事实上针对不同的方案我们也可以再采取不同的优化措施去改进。之所以放这个 DEMO,主要是为了解释这种方案在不经过太大投入的情况下,也能达到理想的性能。
最后我们再做一个大概的总结:
-
Write Once 是一个理想目标,Write Logic Once 在部分场景下能更好的解决我们的问题。
-
介绍的方案仅适合重逻辑的场景,对于重 UI 表现场景不适用。我们需要根据场景做出对应的选择取舍。
-
对于写一份逻辑,无论是 Dart 还是 TS,仍然存在一定的抽象泄露问题,对于一些复杂问题的排查仍依赖开发者的经验
????第十五届 D2 前端技术论坛 PPT 集合已放出,马上获取
关注「Alibaba F2E」
回复 「PPT」一键获取大会完整PPT