关于跨端方案的调研

前言

近几年来,跨端方案这个概念很火热,解决方案也不少。熟悉 React 的同学,就算没使用过 RN,也听说过 React Native 吧?同时还有大量的解决方案,比如小程序(包含快应用等轻量级 app)、Weex、Flutter 等。同时字节跳动研发团队出了一个 Lynx 的跨端方案。在开始之前,有几个问题想问大家:

传统的 Web 本身就是跨端的,为啥要构思跨端方案?

说到跨端方案,Web 都流行了十几二十年了,天然的跨端,为何还要考虑跨端方案呢?其实,这个概念不一定是从纯前端的角度来说的,可能是从客户端的角度。比如,需要开发一款 app,是否需要开发 iOS 和 Android 两套代码呢?当然不一定!如果没有跨端方案,当然是 iOS 一套代码,Android 一套代码;如果有跨端方案,写一份中间代码,分别转译成 iOS、Android 代码,这不就极大节省了人力吗? —- 节省人力

问题又来了,都说了 Web 天然的跨端,为什么不直接搞 Web,还要搞 Native?这个就更简单了:Native 拥有对设备的深入控制,只要是开发人员想的到的功能,基本都能实现。但 Web 就不一样了:它只是给你提供了一个展示内容的画板,以及一些符合潮流的 api(W3C 制定的)。如果在 Web 上想搞点什么骚操作,得先问问容器(一般是浏览器)答不答应。 —- 复杂能力支持

同时,跨端方案不仅仅局限于如何将一份代码转为不同平台的 Native 代码,甚至还想转译到 Web、小程序、快应用、XXXX 上去。这是所有开发人员的追求,而不仅仅是前端开发!试想,一份代码,能转译成 Web,运行在普通的浏览器;转译成 iOS 程序,运行在 iOS 手机上;转换成 java 程序,运行在安卓手机上;转换成 dmg 程序,运行在 Mac 上。除此之外,还有其他还多平台呢。 —- 跨平台扩展性

当然了,由于 Web 的 JS 是解释型语言,避免不了的运行时编译,降低了程序的运行速度。虽然当前 Web 提出了 WebAssembly 的方案,但由于被提出还不久,支持度还不够高,还没引起足够的重视。相比较而言,Objective-C/JAVA 等语言,虽然也是高级语言,但属于编译型语言,最终运行时都是执行的二进制代码,效率比 JS 肯定还是高很多的。因此在考虑到性能表现时,Web 的表现往往不如 Native 来的好。 —- 性能表现

其他对比,等待大家的补充。

如果需要跨端方案,怎么选择?

首先要明确的是,你需要跨端方案吗(参考上一个问题)?其次,考虑使用哪种方案。不同方案间有不同的适用场景,这块就根据经验,或者一些解决方案它自身说明的适用场景。

今天笔者要说的,也只有 Flutter 和 Lynx。我会从很多方面来对比这两者。

方案对比(Flutter 与 Lynx)

整体架构

Flutter 是一款采用了自绘引擎的 UI 框架。什么概念呢?就是别的跨端方案可能还是让 Native 来绘制,要绘制一个按钮,就告诉 Native,绘制一个按钮。但 Flutter 自己基于 skia 平面绘制引擎实现了自己的绘制引擎。相当于所有的绘图操作就是在 Dart 层代码解决,减少了与 Native 的频繁通信。这一点,对比于 React Native,由于 RN 的通信实现是基于桥的,导致当有复杂的绘制、交互时,频繁的与 Native 通信,成为了性能瓶颈。

Dart 层代码实现了绘制、交互等基础功能,向 C++ 注册了一些系统级服务。从这张图,可以看出 Flutter 是极其重视绘图的,这是它的优点。不过,由于 Flutter 的重心在绘制层,导致它提供的 Native 能力看似就比较弱了。在 Flutter 中要使用 Native 能力,需要用到 Flutter 定义的 plugin。plugin 对 Dart 代码提供调用接口,同时将所有调用通过 MethodChannel 送到 Native,由 iOS、Android 接收并处理。这是 Flutter 的整体架构简介。

Lynx 是字节跳动前端团队开发的一款基于小程序的跨双端(iOS、Android)解决方案。
基于小程序的语法,将模板编译成页面,动态直出,加速首屏渲染;增加 DOM diff 功能,同时将 diff 的工作放在 Native,进一步加速 diff。组件是小程序组件的子集。Lynx release 版本目前采用 Native View,设计中的基于 skia 的自绘引擎处于 demo 状态中。

接入成本

Flutter 可以是完整单独的 app 开发,也可以是以 module 的形式嵌入到已有 Native app 中。完整独立的 app,不用谈论接入成本,因为不算接入;迁入已有 app,需要在 Native 依赖中增加 Flutter 等依赖包。以 module 形式接入,iOS 包体积增量在 18M 左右,Android 可以采用动态下发,接入成本基本为零。

Lynx,作为 module 迁入到已有 app 中,接入方式与 Flutter 一样,添加对应的 Flutter 相关依赖。Lynx 接入体积增量大概不到 1M。接入方式参考相关文章。

作为业务方,可以不关心接入这一步骤。

开发体验

Flutter:

  1. Flutter 使用 Dart 语言开发。Dart 语言是强类型语言,编译时会解决类型错误;
  2. VSCode 上有 Flutter 提供的插件,极大地方便了 Flutter 的使用。同时,Flutter 提供了可视化的树状结构,类似 Web 的 inspect 功能;
  3. 断点可以直接打在编辑器里,不用像常规的前端项目,需要使用 debugger 关键字来声明断点;
  4. 独立 app 使用 Flutter,在不接触非常规 Native 调用时,开发人员无需接触双端 Native 代码。比如,如果需要振动的功能,直接在 Flutter 依赖中添加对应的 plugin 即可(部分插件需要在 Native 代码中初始化)。

Lynx:

  1. 需要使用单独的 IDE,需要手动安装脚手架;
  2. IDE 上编写代码,IDE 预览,真机预览。目前双端都已接入,直接扫码 IDE 真机调试的二维码即可;
  3. 前端同学非常熟悉的调试方式。

调试成本

Flutter 独立 app 调试极其简单:在 VSCode 环境运行 Flutter 程序时,可以直接打开 Dart devTools,可以查看 Widget Tree、RenderObject Tree、绘制性能等常规调试面板;当 Flutter 运行于已有 app 时,调试成本就不低了。拿头条来说:安卓调试,需要拉取 ttmain 工程,修改相关本地配置,本地打包,Flutter attach;iOS 调试,拉取 tt_ios_app 工程,修改相关本地配置,本地打包,Flutter attach。说着简单,但要让一个前端同学把这两个工程 run 起来,可能要不断练习一周。说不定过一段时间,app 的构建发生了变化,还得找对应客户端同学咨询打包方式。这是 Flutter 混合工程调试的麻烦。

Lynx,本身就处于混合工程,不过由于调试器已经迁入了内测版本中,在支持 Lynx 的内测版本可以轻松调试。

总得来说,Lynx 在混合工程下,调试成本要小得多

上线流程

Flutter 需要先修改业务方自己的 Flutter 工程,比如 tt_novel_Flutter,然后发 pub 包;更新 tt_flutter_module 中的依赖版本,然后发对应的 Android 插件、iOS pod 库;最后更新双端工程中对应的依赖版本即可。整体流程很长(在未接入动态化能力条件下),还是比较麻烦的。

目前 Lynx 主要是客户端接入,然后可以让前端同学维护卡片代码。经过常规的前端项目上线流程:scm 打包 -> Gecko/Goofy 发布即可。

可见,Lynx 上线流程,简单、快速

跨平台能力

Flutter 初衷是跨 iOS、Android 双端,目前 For Web 已经合入主仓库,有望尽快应用于 release。同时,Flutter 也在朝着跨桌面平台发展。

Lynx 作为移动端跨双端解决方案,在跨平台能力不及 Flutter。

性能对比

由于两者本身定位不同,而且渲染方式也不同:Flutter 是自绘引擎,Lynx 是原生渲染 (实现了另一套渲染算法,同时基于 skia 的自绘引擎处于 demo 状态)。这一块对比意义不大。

组件支持

Flutter 的组件是在 Dart 层实现的。所有组件继承 Widget 类,除了官方实现的大量 UI 组件外,可以在 Dart 层任意实现自定义 UI、交互、功能组件等。

Lynx 组件是小程序组件的子集。目前可用的组件不多,粗略看了一下,不到 20 个(应该是没有更新完全)。因为还没实际使用过,不清楚这些组件是否可以满足大部分需求。同时,如果需要扩展 / 自定义组件,就更麻烦了,需要编写双端 Native 代码。不过对于通用组件,可以先联系 Lynx 支持。

这可谓是 Flutter 完胜。毕竟 Flutter 本身定位就是 UI 框架。

底层支持

Flutter 的系统级 api 调用建立在 MethodChannel 之上,可以获取所有 Native 的能力。不过同样需要编写双端代码,成为一个 plugin,供 Dart 层调用。

Lync 本身基于小程序,可以调用 tt 的一部分 api,比如 request。由于官方文档未描述支持 api,这块只能靠尝试。对于特殊的需求,可以联系接入方客户端同学,注入相关 Bridge 以供调用。

客服支持

使用 Flutter,业务方能寻求 Flutter onCall 的帮助。但对于 Native 相关的问题,或者具体到某一 case 时,往往不好交流、解决。自己去学习的成本太高。但若是纯 Flutter 开发,不涉及到 Native 的改动,那么 Flutter 开发过程中,可能就某些字节系插件接入需要客服支持,这时沟通效率往往都很高。

使用 Lynx,由于组件限制、api 限制等,在开发过程中,往往是遇到某个组件的使用姿势,或者深入一点的,怎么构建一个自定义组件,这块问题一般都在客服容易理解的范围内,一般能得到较好的解答。

适用场景

Flutter 适合整页、独立 app 的开发;Lynx 适合放入 Native 的长列表中作为某个 cell 卡片,目前暂不支持嵌入 h5 页中。

参考

Flutter for web developers
为什么移动端跨平台开发不靠谱?