作者:宣泽斌
编辑:邓艳琴
导读:鸿蒙应用与生态的布局是大势所趋,Web开发者如果想上手鸿蒙,是否只有直接使用鸿蒙原生框架开发应用这种成本高昂的方式?现在,拥有34000+star的开源跨端框架Taro给出了更为人性化的解决方案,也给出了国内Web技术栈适配鸿蒙的典型案例,本文将对其适配原理进行剖析。另外,本文作者Taro核心开发人员宣泽斌也将在4月18-20日进行演讲,2号举办的QCon全球软件开发大会北京站将分享性能优化方面的深度思路,敬请期待。
GitHub:
主题详情:
背景
鸿蒙是我国自主研发的操作系统,性能、安全性优秀,越来越多的互联网公司开始布局鸿蒙应用和生态,为开发者带来新的机会。
但直接使用鸿蒙原生框架开发应用是有一定门槛的,开发者需要学习全新的ArkUI框架,掌握ArkTS语言,这对于大部分只接触过Web开发的前端工程师来说是一个不小的挑战。
因此,Taro 给这些开发者提供了一种新的选择,让他们可以使用 Web 技术栈开发鸿蒙 APP 应用,并可以通过 Taro 直接转换成 H5 和小程序版本,减少二次开发的工作量。Taro 的适配代码直接桥接了鸿蒙的 ArkUI 层,中间环节带来的性能损耗相对较小。
本文将深入探究Taro适配鸿蒙ArkTS语言的工作原理,接下来我们来看一下适配的整体思路。
总体思路
在适配 ArkTS 的整体思路上,与小程序的适配类似,我们优先考虑面向运行时的适配方案,在运行时将 Taro 虚拟 DOM 树映射到对应的 ArkTS UI 组件上。
选择运行时偏向解决方案的原因
1. 前端框架 React/Vue 的 DSL 范式与 ArkTS 的 UI 范式存在较大差异
以 React 为例,我们只需在 React 和 ArkTS 上渲染一个 Button 组件,并赋予它一些简单的样式属性:
可以看到这只是一个简单的Button组件示例,两边的代码结构其实还是有很大区别的,更何况在实际项目中,我们还需要考虑各种循环体、表达式,如果在编译时对这些代码进行解析转换,必然会出现各种错漏,转换后代码的可读性也无法保证。
其实这也是 Taro 1/2 到 Taro 3 升级的一个缩影,运行时适配方案一方面可以带来更优质的用户开发体验,不限制开发者的语法规范,另一方面可以兼容更为广泛的 Web 生态,支持绝大多数现有开源工具及依赖包的运行。
2. 运行时适配方案可以继承部分小程序运行时和编译时逻辑
选择面向运行时的适配方案的另外一个原因是,Taro 目前采用的就是面向运行时的适配小程序的方式,因此如果也选择面向运行时的方案来适配鸿蒙 ArkTS,那么部分预编译的时序环节和运行时环节非常类似,甚至鸿蒙 ArkTS 的适配可以复用小程序的部分逻辑。
这种重用带来了两个明显的好处:
减少适配鸿蒙方舟的工作量,并且可以更早的上线Taro适配鸿蒙方舟的功能,供开发者体验使用。
后续针对小程序的性能优化或者功能添加,都可以同步或者以最低成本同步到HarmonyOS端,反之亦然。
部分运行时解决方案的设计思路
Runtime 的思路可以用一句话概括:模拟浏览器环境,让 React、Vue、Web 生态库直接运行在鸿蒙环境中
1. 在鸿蒙环境中模拟浏览器的BOM和DOM
为了支持使用 React、Vue 等前端框架开发鸿蒙应用,首先需要模拟一套浏览器 BOM 和 DOM,BOM 指的是浏览器自带的一些全局对象,比如 windows、history、document 等,DOM 指的是浏览器提供的一些文档节点对象模型,比如 div、img、button 等。
我们需要模拟一套能在鸿蒙环境下运行的BOM和DOM,提供给前端框架层使用,使得这些依赖浏览器环境的Taro代码能够在鸿蒙APP上运行起来。
class TaroElement extends TaroNode {
public _attrs: Record<string, string> = {}
private _innerHTML: string = ''
public readonly tagName: string
constructor (tagName: string) {
super()
this.tagName = tagName
}
public set id (value: string) {}
public get id (): string {}
public set className (value: string) {}
public get className (): string {}
public get classList (): ClassList {}
public get attributes (): NamedNodeMap {}
public get children (): TaroElement[] {}
public setAttribute (name: string, value: string): void {}
public getAttribute (name: string): string | null {}
public removeAttribute (name: string): void {}
}
2.通过 Reconciler 访问这些 BOM 和 DOM
和小程序类似,通过定义自定义的hostconfig,将终端侧相关的一些节点操作逻辑与框架层的核心逻辑分离。
以使用 React 框架开发鸿蒙应用为例,我们将一些关键的 React 操作(增删改查宿主环境节点)提取到一个 hostconfig 中,这个 hostconfig 中的逻辑和我们构建的虚拟 BOM 和 DOM 进行绑定。
绑定成功后,React 项目代码对目标节点的增删改查结果都会直接体现在我们创建的 Taro 虚拟 BOM 和 DOM 中。
3.实现虚拟BOM、DOM到ArkTS的桥接层
至此,执行完前端框架层代码之后,我们就可以得到由这些虚拟DOM组成的虚拟DOM树。
(1)页面渲染
之后我们会把这棵 DOM 树传递给鸿蒙应用的页面入口,绑定到其成员属性节点上,触发渲染,然后 ArkTS 会根据这棵节点树进行递归的渲染调用,生成对应的原生组件,从而渲染出具体的页面。
import { TaroElement } from "../../npm/@tarojs/runtime"
import { createNode } from "../../npm/@tarojs/components/render"
@Entry
@Component
struct Index {
@State node: TaroElement = new TaroElement("Block")
build() {
createNode(this.node)
}
}
这里的struct和前端里的Class的概念类似,而@Component表示当前的struct是一个自定义的组件,可以有自己的build方法,构建自己的UI,最后@Entry表示当前自定义组件是一个页面的入口组件,所有页面的第一个组件都应该以@Entry所在的组件开始。
(2)页面更新
相信大家对第一次渲染的流程已经有了基本的想法。那么Taro是如何实现应用页面节点的更新机制的呢?
在第一次渲染的时候,我们会在ArkTS UI组件中绑定相应的事件:
@Component
export default struct TaroVideoArkComponent {
@Objectlink node: TaroVideoElement
build() {
Video(this.getVideoData(this.node))
.onClick((e: ClickEvent) => eventHandler(e, 'click', this.node))
.props(this.getVideoProps())
.attrs(getNormalAttributes(this.node))
}
}
例如上面的Video组件会通过.onClick声明式地绑定点击事件,当用户在应用页面中点击Video组件时,就会触发此事件的回调,从而触发eventHandler的调用,eventHandler会获取Taro虚拟DOM中收集到的事件监听回调并执行。事件监听是在前端框架层进行的,以React为例,React会在Reconciler的commitUpdate钩子中解析出onClick等用户绑定的事件,然后通过node.addEventListener执行事件监听回调,从而监听事件。
回到 eventHandler,eventHandler 执行事件回调时,可能会导致前端框架层的数据状态发生更新,进而导致框架调用一些逻辑来修改 DOM 树节点、DOM 节点属性。由于这些 DOM 都是 Taro 自身模拟的,因此在创建时会绑定 Observed 装饰器,与 @objectlink 装饰器配合使用,用来监听 Taro DOM Node 属性的变化并触发组件的 update 方法的调用,达到更新的目的。
// TaroVideoElement
@Observed
class TaroVideoElement extends TaroElement {
constructor() {
super('Video')
}
async play() {
try {
this._instance.controller.start()
return Promise.resolve()
} catch (e) {
return Promise.reject(e)
}
}
// ...
}
// TaroVideoArkComponent
@Component
export default struct TaroVideoArkComponent {
@Objectlink node: TaroVideoElement
build() {
Video(this.getVideoData(this.node))
.defaultEvent()
.props(this.getVideoProps())
.attrs(getNormalAttributes(this.node))
}
}
(3)整体数据结构转换过程
整体的数据结构转换流程如下图所示,经历了三个阶段,第一阶段是前端框架层的代码,在Reconciler的作用下,转化为Taro创建的虚拟DOM节点,这些节点通过createNode递归调用,会变成对应的各种ArkTS UI组件。
4.使用ArkTS实现Taro组件所包含的内容及API标准
到此为止,相信你已经学会了如何使用 Taro 将前端框架代码转换成 ArkUI 代码。接下来我们需要在运行时处理两个问题:
前端框架层用到的组件在ArkTS中都需要有对应的实现,从上面的流程可以看出,每一种类型的React HostComponent组件最终都需要对应一个类型的TaroElement和一个类型的ArkComponent,所以我们需要为这些不同类型的TaroElement和ArkComponent分别提供一套完整的实现。
前端框架层所使用的 API 在 ArkTS 中也需要有对应的实现,除了组件之外,API 还需要遵循 Taro 目前的规范软件开发技术文档模板,利用鸿蒙环境提供的 API 模拟实现一套 Taro 规范的 API,并将这些 API 绑定到 Taro 的全局对象上并导出供用户使用。解决这两个问题的方案就是利用鸿蒙 ArkTS 提供的原生 API 和组件模拟实现一套对应前端框架层的 API 和组件。实现过程中涉及到复杂的代码细节,本文不再赘述,想要深入研究的朋友可以到源码仓库中阅读了解。
5.实现鸿蒙平台的工程逻辑
(1)实现鸿蒙平台插件
在 Taro 中,每个终端适配都有对应的平台,如微信小程序平台对应@tarojs/plugin-platform-weapp 插件,京东小程序平台对应@tarojs/plugin-platform-jd 插件。在适配鸿蒙 ArkTS 时,也会构建一个新的平台插件@tarojs/plugin-platform-harmony。用户使用 Taro 开发鸿蒙应用时,只需要引入此插件,并执行对应的打包命令即可。
各个端可以根据对应的平台来处理编译时和运行时逻辑,各个端平台插件需要处理的事情也大同小异。以小程序端平台为例:
调整编译时配置,启动编译过程,并加载相应的运行器。
在运行时添加或修改生命周期、组件名称、组件属性和 API 实现。
定制和修改小程序编译模板。
在鸿蒙平台中,由于组件和 API 都是原生重新实现的,所以所有实现的组件和 API 都会在编译时直接注入到输出目录中,而不是像小程序平台插件那样在运行时才注入到输出目录中。为了修改组件和 API,在鸿蒙平台插件中做了以下两件事:
调整编译时配置,启动编译过程,并加载相应的运行器。
存储已经实现的组件和API,等待编译时获取并注入。
(2)实现鸿蒙编译打包功能
上文提到,鸿蒙平台插件会触发鸿蒙的编译打包过程,在这个过程中,Taro 会利用 Vite(目前仅支持 Vite)对项目代码进行打包,最终输出一个可执行的鸿蒙项目源代码到鸿蒙项目目录下。
这里的封装逻辑主要分为以下五个部分:
判断用户是否在JSX文件中开启了compileMode模式,若开启则解析JSX模板,并生成对应的ETS模板,以优化运行时性能。
由于我们的方案在运行时会初始化很多自定义组件实例,所以我们方案的主要时间消耗就消耗在这个实例化逻辑上,因此在编译时我们会采用类似小程序半编译方案的方法,将一些可以提前分析的代码节点生成到对应的模板文件中,从而减少最终页面渲染时实例化的自定义组件数量。
我们需要利用打包工具将用户编写的JSX通过babel、swc等工具翻译成ArkTS能够理解的Typescript/Javascript语言,并分析这些文件引入的依赖,生成相应的chunk。
ArkTS 不支持 CSS 文件,所以我们还需要使用打包工具来处理样式文件。我们将在编译过程中分析所有引用 CSS 文件的 JSX 和 TSX 代码。
然后我们会用Rust开发一个工具来解析React组件和对应的CSS文件,并计算出每个React节点的最终样式,并运用到React Native、鸿蒙等不支持CSS书写的场景中(目前只支持类名选择器)。
最后我们会把引用 CSS 文件的代码交给样式解析工具,工具会把这些样式以 Style 属性的形式写到这些 JSX 节点上,并将最终处理好的 JSX 和 TSX 代码返回给后续的编译操作。
我们会根据用户的app.config.js配置生成对应的ets文件,并且会生成一个app.ets作为UIAbility组件。UIAbility组件是ArkTS中系统调度的基本单位,为应用程序绘制界面提供窗口。会根据配置中页面的配置生成一个或多个页面ets文件。这里需要注意的是,如果配置了tabbar,那么所有的tabbar页面会合并为一个taro_tabbar.ets文件。
这里的胶水代码指的是React与ArkTS页面之间的连接代码,与小程序类似,我们会生成两个对象app和page,app对象会在app.ets中初始化,用于接下来控制页面的注入与卸载;page对象会在对应页面的ets文件中初始化,用于加载对应的React页面组件,并在适当的时候触发其各种生命周期。
至于半编译模式和样式解析的具体实现,由于涉及的逻辑相对复杂,本文就不再赘述,我们会在下一篇鸿蒙系列文章中为大家介绍,小伙伴们可以耐心等待后续系列文章的发布。
使用案例
目前Taro仅支持React进行Harmony开发,下面是一个简单的代码示例:
import Taro from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import { IMG } from './constant'
import './index.scss'
export default function Index() {
return (
<View className='index'>
{}
<View compileMode>
<Text
className='index-world'
onClick={() => Taro.setBackgroundColor({ backgroundColor: 'red' })}
>
Hello,World
Text>
<Image src={IMG} className='index-img' />
View>
View>
)
}
另外我们还简单模仿了一个跨终端的电商demo,可以看到Taro在H5端、小程序端、转换之后的鸿蒙端的应用和页面效果基本一致。
并且在渲染1000个节点的时候,Taro对鸿蒙APP的渲染时间比原生多了300ms左右,而未来通过动态属性、节点映射优化等方式,这个性能差距将再次大幅缩小,预计降低到100ms-200ms左右。
使用限制
目前,虽然Taro与鸿蒙方舟TS的适配已经基本完成,但在适配过程中,我们也发现了一些暂时无法解决或计划后续解决的遗留问题。
样式解析存在一些限制
在 ArkTS 中,使用声明式 UI 来描述 UI 样式,因此不存在 sass、css 等样式文件,因此 Taro 在适配鸿蒙 ArkTS 时,会在编译时解析这些样式文件,并将样式以内联方式写入组件的 TS/JS 代码中。
普通样式基于 W3C 规范,存在类名级联和样式继承行为,由于各开发者的代码编写方式不同,Taro 在编译期没有办法获取准确的节点结构和节点类名信息,因此无法支持这两种行为。
另外,由于样式解析是基于组件文件的纬度,导致样式文件只能应用于其引用的组件文件,而不能跨文件应用,并且样式文件仅支持类选择器。
组件和 API 有使用限制
由于鸿蒙平台与小程序平台存在较大差异,部分小程序组件和API规范无法在鸿蒙平台上重新实现,比如登录和账号信息相关的API、live-player直播相关的组件。
总结与展望
本文深入分析了 Taro 框架如何适配华为鸿蒙操作系统下的新一代语言框架 ArkTS,并重点介绍了运行时适配策略,以减少编译过程中的转换错漏,优化开发者体验。浏览器环境中的 BOM 和 DOM 使 React 等前端框架能够运行在鸿蒙应用上,将 Taro 构建的虚拟 DOM 与 ArkTS 组件桥接起来,并充分利用工程中增加的半编译模式和样式解析能力。
后续规划
支持动态功能
目前软件开发技术文档模板,Taro 开发的鸿蒙应用只能随 Native 包的发布一同发布,并不像 RN 那样支持在运行 Native 时拉下 RN 包,因此就目前而言,Taro 也会动态地将这一功能指定为后续迭代的重点目标之一。
支持原生混合编译
在使用 Taro 开发小程序的项目中,除了基本的编译、打包功能外,最常用的功能就是原生混合功能,该功能允许开发者将 Taro 项目打包成原生的页面和组件,以便与原生项目混合。
在适配鸿蒙的过程中,我们也收到了很多业界小伙伴希望在鸿蒙环境中使用该功能的建议,因此Taro团队未来会更加优先考虑这个需求,争取在下一次迭代中加入该功能。
最后的想法
未来,Taro 团队会持续维护此适配方案,聆听社区反馈,不断提升应用性能和改善开发者体验,真正实现一套代码就能在多端运行的无缝体验。
关于作者
宣泽斌,京东零售前端技术专家,Taro 项目核心开发者,负责 Taro 在小程序端和鸿蒙端的适配开发,在小程序端主要负责 Taro 对小程序的日常适配维护及性能优化。在鸿蒙端主导了 Taro 适配鸿蒙 ArkTS 的核心工作,负责整体架构的设计、核心模块的开发以及性能问题的排查优化。在北京开发大会上,他将讲述 Taro 适配鸿蒙的框架原理及性能优化,分析 Taro 适配鸿蒙的主要性能瓶颈,如自定义组件过多、声明式属性绑定过多等,并分享具体的解决方案。
今日推荐文章
活动推荐
2024年4月18日至20日,QCon全球软件开发大会北京作为InfoQ大会首发地将隆重举办!今年QCon大会将启用全新主题——全面进化。今年我们还邀请到了国泰君安首席信息官俞锋博士、智普AI CEO张鹏博士、百度首席架构师(搜索与推荐)吴永炜、美的集团首席信息安全官兼软件工程研究院院长、欧洲科学院院士、IEEE Fellow刘向阳担任QCon大会联合主席,对内容进行顶层设计深挖细琢,并邀请多位业界知名专家担任专题制作人。
目前年会门票有3折优惠,仅限1月份,票务咨询可联系票务经理17310043226,点击“阅读原文”可查看专题详情,期待与各位开发者现场交流。