前端开发:如何正确地跨端?

前端开发:如何正确地跨端?

作者 | 当轩
来源 | 阿里技术公众号

跨端

Write once, run everywhere。

我们都听说过这句经典的宣传用语,后来我们都知道,没有什么东西是可以真正 run everywhere 的,充其量也只能做到 debug everywhere。

而当我们谈论一次编写多端运行时,显然不可能真的指跨一切所有端,大多数情况下你不会需要在电脑和手环上同步开发一个功能。

  • 跨 PC 和无线端。
  • 跨多 Native 平台:例如跨 Android 和 iOS,甚至跨 Windows。
  • 跨投放 APP:随着超级 APP 越来越多,很多业务需要在多个 APP 中投放同一个页面。
  • 跨 Web 和 APP:Web 在很多情况下仍然是不可避免的,我们的页面可能需要分享、SEO 或者投放到 M 站上等等,这时候就需要同时能在 Web 和 APP 内运行。
  • 跨 Web、多小程序、QuickApp 等:其实本来类似跨 APP,但是奈何小程序本身是各家控制的封闭生态,故而有了开发一次适配到多种封闭生态的诉求。
  • 其他端的跨端诉求:例如跨 POS 机,手表等。

与我们多种多样的跨端诉求相对应的,是百花齐放的跨端方案。

前端开发:如何正确地跨端?

百花齐放的跨端方案

以 Web 为基础的 H5 Hybrid 方案

这类方案最为直接,简单来说就是用网页来跨端。由于我们绝大多数端上(甚至包括封闭的小程序生态)都支持 Webview,所以只要开发网页然后投放到多个端即可,在桌面端对应的方案就是 Electron。

为什么不直接全用 Web?

从开发成本低、标准统一、生态繁荣上来说,H5 Hybrid 方案基本是不二之选。然而这种方案难以避免在性能和体验上存在差距。Web 的生态繁荣来自于其良好的历史兼容性,也意味着沉重的历史包袱。

  • W3C 标准作为开放技术标准,历史包袱多,逻辑复杂。
  • Web 标准在设计上不是 Design for Performance 的,导致很多地方难以进一步改善,例如 JS 执行和 Layout、渲染互斥无法并行,导致过长的 JS 执行任务会执行正常的渲染导致卡顿。
  • Web 的标准化在推进上也比较慢,新的能力可能要比较长的时间才能使用。

React-Native/Weex 类方案

在移动平台上尤其是早期 WebView 的性能体验非常糟糕,前面我们也提到这种差距主要来自于 Web 生态本身沉重的历史负担。

而 React-Native/Weex 这类方案通过尽可能的取长补短,通过结合 Web 的生态和 Native 的组件,让 JS 执行代码后用 Native 的组件进行渲染。由于抛弃了 Web 的历史包袱,这类方案可以做一些大刀阔斧的改动。

例如 RN 就如下图中,把 JS 执行、布局(Yoga)和渲染(Native 组件)放在三个进程分开执行,避免了 JS 执行复杂任务时界面卡顿。通过抛弃 CSS 中的大量标准,只支持部分 flex 布局能力来减少布局和渲染的复杂度。

前端开发:如何正确地跨端?

这种方案同样存在一些缺陷:

  • iOS/Android 双端本身不一致的组件和布局机制,让双端一致性难以得到保障。
  • 依赖于 Native 机制也让一些 CSS 属性实现起来比较困难,例如老大难的 z-index 问题。

而最麻烦的一点在于,这套方案意味着非常高的维护支持成本。

  • 借用了 Web 的生态但并不完全是 Web 生态,很多地方不一致,最常见的吐槽就是惯用的 CSS 布局方式无法使用。
  • 相比于浏览器新增一个传感器 API 都要配套完善的 devtool,这类方案大部分情况下的开发体验保障可以说是刀耕火种(下图为 Chrome 的方向传感器 API 的 devtool)。

前端开发:如何正确地跨端?

在 WebView 性能差距逐渐缩小的今天,维护这一套复杂方案的 ROI 是否值得,需要根据我们场景的具体诉求考量。

Flutter

Flutter 要解决的问题和上面的方案不同,完全不打算继续在 Web 生态上借力,从设计之初也并没有把 Web 生态考虑进去。相比于 RN 依赖 Native View 渲染,Flutter 则是自绘的组件,直接通过 Skia 绘制到屏幕上。

由于可以完全发挥 GPU 的能力,也不需要去 Native 绕一圈。Flutter 理论上能做到更好的性能和两端一致性,这一意味着理论上未来可能基于 Flutter 的 JS 动态化方案能够在样式上支持的比 WEEX 更好。

从前端的视角看仍然更像是一个 Native 开发方案而非跨端方案(虽然其实是跨 Android/iOS 的)。目前最主要的问题是 Flutter for Web 从技术原理上来说离生产可用可能还非常遥远。除此之外动态化能力的确实也会让部分场景不适用。

研发框架 for 小程序

小程序是被创造出来的问题,各家小程序出于商业上的考量主动在 Web 生态的基础上构造了相对封闭的生态。导致和 Web 生态格格不入。然而有多端小程序投放,或者同时投放小程序和 Web 端的场景难以接受使用。

由于小程序的端封闭且不受控,要解决小程序的跨端问题往往只能从研发框架层面出发。

编译时方案

比较知名的编译时方案是 Taro,大致的原理可以解释为将 JSX 编译到小程序的 WXML/WXSS/JS 上,而这类框架的实现原理其实并非真的是一个 React 或者类 React 框架,而是把看起来像是 JSX 的模板通过静态编译的方式翻译成小程序自身的模板。

这样做的限制非常明显,那就是 JSX 是 JavaScript 的拓展语言(React Blog 写的是 is a syntax extension to JavaScript),而小程序所采用的 WXML 却是一个表达能力非常受限的模板语言,我们不可能完成从一个通用编程语言到模板语言的编译。

而静态编译类框架为了做到这一点,采取的方式就是限制开发者的写法,这也是为什么 taro 对 JSX 的写法做出了诸多限制。这一点直接导致了无穷尽的维护成本和严重受损的开发体验,而后 taro/next 也转向了运行时方案 + 静态编译优化的结合。

运行时方案

不谦虚的说,针对小程序的运行时方案应该是最早我在写下 remax 第一个 issue 时[1]提出的。

通过 React Reconciler(类似于 Rax Driver)我们可以让运行在小程序容器中的 React 不去直接操作 DOM,而是把操作的数据通过 setData 传递给小程序的 View 层映射到最后的界面上。

虽然 Remax、Rax 运行时、Taro Next 等几种方案不尽相同,但是思路大同小异,就是利用小程序模板一定程度上的动态化能力 + 类 React 框架的 VirtualDOM 来进行渲染。当然这种做法相对于小程序原生的渲染方式存在一定的性能损耗。

前端开发:如何正确地跨端?

remax 的支付宝端性能测试

在部分场景下,这种损耗是值得的。这些运行时框架也都在陆续通过允许关掉编译产生的模板中的不用的属性、部分静态编译、虚拟列表等方式来改进性能。

当然了,最后内嵌 Webview 仍然是一个方案。

作为业务技术团队,我们该做什么

上面介绍的都是针对某些具体的场景的一些解决方案,然而对于业务技术团队来说,跨端的本质是提效。针对新的变化提出新的方案是一方面,更重要的如何让这种提效真的长治久安,让我们的提效不会变成从一个新方案跳到另外一个新方案。

让我们重新看上面这张图,可以确定的是,跨端的诉求和与之对应的方案仍然会处于频繁的变化中,也不会出现一个解决所有跨端问题的方案。而其中相对不变的部分是值得我们为了长治久安必须要投入的。

前端开发:如何正确地跨端?

WebView & H5 Hybrid

WebView 可能是众多容器中最为特殊的一个,虽然很难满足部分场景对于性能和体验的极致要求,但是会是最稳定、长期存在且得到支持的方案。

从开发效率和未来长期的维护演变来看,在能够满足性能体验要求的前提下,Web 方案仍然是最优先应该考虑的。

同时,在 APP 的 WebView 容器上我们能做更多的工作,例如通过容器来提供一些端内的能力,结合 Native 能力实现的并行数据加载,页面保活等等。

基础建设

无论采用何种跨端方案,在哪个容器中,性能、稳定性、效能都是绕不开的三驾马车。

性能

对于不同的方案往往存在不同的性能方案,上面我们也提到在小程序的运行时方案中就会有减少编译模板产出的字段这样的优化。然而,其实除了这种特定方案的优化外,大部分优化手段是大同小异的:离线缓存、数据预取、快照、SSR、NSR 等等方案。

对于不同的端和容器,对于性能问题的度量和发现也应该是一致的,我们需要对页面在不同端的性能究竟如何有明确的感知和横向对比。

性能的端侧建设(端能力、具体到某一个端的性能测算方案、性能打点等)可能需要根据不同的端、不同的跨端方案而不同。但性能的基础建设(首屏标准、数据分析、基础优化能力)在跨端中应该是相对稳定的。

在端侧能力方面,ICBU 早期在 WEEX 性能优化时引入了并行加载的能力,通过 wh_prefetch 协议来使用容器的并行加载能力。而后在新的容器(WebView、浏览器)中,虽然底层能力存在差异,但仍然识别相同的协议。

前端开发:如何正确地跨端?

在数据采集和分析方面,我们通过统一跨端基础库,让不同端不同技术方案可以在同样的标准下分析、度量和对比。

前端开发:如何正确地跨端?

稳定性建设

在无线端我们常常把性能 & 稳定性并称为 “高可用”,稳定性主要涵盖的范围包括灰度能力、业务监控、报警、错误监控、白屏检测等等。

这些能力相比起来对具体端和跨端方案的依赖更少,除了在端侧的数据采集逻辑稍有区别外其他建设部分相对也是比较稳定的。集团针对这些场景也存在一些跨端可用的方案、例如 iTrace 等。

工程基建

对于不同场景的跨端,虽然在方案上存在一定的差异,但是我们的工程基建是可以保持统一

  • 容器层提供统一的 API 和文档能力
  • 统一的研发流程
  • 研发工具提供统一的抓包、debug 能力等
  • 一致的工具库等等

这样,当有新的容器或者方案出现时,我们只需要按照相应的能力进行对齐,就能让我们上层的业务代码和开发体验维持相对稳定的状态。

前端开发:如何正确地跨端?

业务逻辑跨端

相对来说,我们会发现在多种跨端方案的演化中,如何渲染、如何布局等 UI 层面的变化要远远大于业务逻辑层面。甚至是小程序和 Flutter,其大致的开发范式都没有发生太大的改变。例如 Flutter 开发范式和 React 非常相似,同样是声明式 UI,同样存在 VirtualDOM。

考虑到 SEO 和性能等问题和 Flutter 本身基于 Skia 的渲染模式,Flutter for Web 在相当长一段时间内可能都不会是一个生产环境可用的方案。

而在统一了业务逻辑代码的组织方式后,我们可以通过 Hooks for ALL 的方案让 Flutter 和 Web 端可以共享一份基于 Hooks 的业务逻辑代码。

前端开发:如何正确地跨端?

有时候你不需要真的 run everywhere,能够提高效能、保持一致就已经达到目的了。

视图层

目前来看视图层的跨端仍然充满了变数,在我们的业务逻辑层跨端做的足够原子化后,也许我们部分交互逻辑不是特别重的视图层能够通过 DX + 绑定原子化逻辑 + 数据参数的方式覆盖更多的跨端场景。从而同时满足性能、效能方面的诉求。

然而对于通用场景的视图跨端,仍然没有银弹。

总结

总体来说,跨端处于且将长期处于多方案并存且不断变化的状态。除了针对新的变化新的场景选择或创造合适的方案,我们更要做好这种动态变化中长治久安的部分:

  • H5 Hybrid。
  • 性能、稳定性、效能三驾马车的统一性和延续性。
  • 在不强求 write once 的场景下,考虑比 UI 跨端更简单的业务逻辑跨端。
参考资料
[1]https://github.com/remaxjs/remax/issues/1
上一篇:CSS选择器的权重与优先规则


下一篇:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(8)-MVC与EasyUI DataGrid 分页