Taro 鸿蒙技术内幕系列(二):如何让 W3C 标准的 CSS跑在鸿蒙上
基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕。本篇为该系列第二篇:《如何让 W3C 标准的 CSS跑在鸿蒙上》。
样式书写方式不一致
以几个我们日常会使用到的属性为例,下面的分别是 CSS 的写法和 ArkUI CAPI 的写法。
对比可以看出,CSS 样式和鸿蒙样式在单位系统和数据表示方式上存在显著差异。CSS 提供多样化的尺寸和颜色单位,而 ArkUI 的 CAPI 接口采用更统一的表示方式。
ArkUI 的 CAPI 接口将所有尺寸统一为 vp 单位,颜色采用 0xAARRGGBB 格式的 uint32 类型,对于渐变和 transform 等复杂样式属性,更是需要转换为颜色停止点和角度值列表和矩阵运算,这样的接口简洁但需要调用者根据具体场景完成必要的单位转换。
一个页面通常会包含非常多的样式规则,如果所有的单位转换都放在运行时候完成,必定会造成明显的性能问题。因此,我们选择提前完成部分转换。HTML 节点的样式主要来源于 CSS 和 Style 属性。CSS 样式通常是静态的,可以在编译阶段进行转换。为此,我们基于 lightningCSS 开发了一个 Rust 插件。该插件通过遍历项目 CSS 的抽象语法树(AST),将其转换为 ArkUI 的 CAPI 接口可直接使用的数据结构。
布局存在差异
除了书写方式的差异,ArkUI 有很多布局属性的行为在细节上也和 W3C 的布局属性存在着不小的差异,比如鸿蒙的绝对定位相对父级定位,Web 的绝对定位相对最近的已定位祖先元素定位,并且鸿蒙的定位不支持 right 和 bottom(早期)。
web可以通过margin的auto实现居中,鸿蒙能通过flex实现居中。
在具体实现中,我们需要在构建 Taro 节点树的同时,构建结构一致的 Yoga 节点树。
然后把原本直接设置到鸿蒙节点上的样式属性(如宽高、margin、padding、display 和 position)设置到 Yoga 节点上。经 Yoga 计算后,我们再从 Yoga 节点上读取计算后的 width、height、x 和 y 值设置到鸿蒙节点上,从而实现鸿蒙端和 web 端的布局一致性。
通过使用 Yoga 作为布局引擎,我们不仅解决了鸿蒙系统与 Web 布局之间的差异,还提高了跨平台一致性。这种方法使开发者可以使用熟悉的 Web 布局概念,同时确保在鸿蒙平台上获得预期的布局效果。
介绍完 Taro 适配 ArkUI 的 CAPI 样式过程中遇到的问题和对应的解决策略之后,我们就可以来看看基于这些策略,鸿蒙样式的整个工作流程是怎么样的。
样式初始化
首先,项目启动后,编译器处理后的样式文件将第一个被加载到运行时环境。样式处理逻辑会根据各个选择器(selector)生成相应的样式规则(StyleRule),即 CSS 属性的键值对集合。
根据 ClassName 匹配 StyleRule
React 在构建每个节点的同时,会通过 Reconciler 把 React 节点的 ClassName 和 Style 设置到相应的 Taro 节点上,这个时候我们就开始进入节点的样式匹配环节。样式处理会执行以下步骤:首先,从 CSSStylesheet 这个样式集合里识别出与 className 相关的所有 StyleRule;然后,根据选择器的优先级合并这些 StyleRule;最后,将合并结果与由 Style 生成的 StyleRule 合并,从而得出最终的样式配置。
这里顺带提一下,CSS 除了可以书写样式之外,还可以书写伪元素和关键帧动画,这两者在都没办法直接设置到鸿蒙的样式里,在处理某个节点时,如果匹配到这个节点的样式里包含伪元素,就会把这个伪元素转换成 一次 insertBefore api 的调用,用这个新 insert 进去的子元素来承载伪元素的 StyleRule,举一个例子,下面的 F 节点的 CSS 样式里带有一个 ::after 的伪类,那么当 F 节点匹配到这个样式的时候,就会被插入一个子节点用来承载 ::after 对应的样式。
样式的应用
确定了节点对应的样式表后,我们就到了把样式应用到节点上这个环节了。这个环节我们会调用节点的 SetStyle 方法,遍历 StyleRule 中的所有样式。对于布局相关的属性(如 display、position、float、flex、width、height、margin、padding),如需更新,会被设置到节点对应的 Yoga 节点上,同时为节点本身添加 layout_dirty 标记。接着,判断是否有绘制相关的属性需要更新,如果有,则设置到节点对象的临时属性上,并为节点添加 draw_dirty 标记。这些被标记的节点并不会立刻被处理,而是会被纳入下一帧的样式处理队列中,这样能避免同一帧多次设置同一个结点的相同属性,确保样式更新的高效性,同时也能保证布局属性和绘制属性设置到鸿蒙节点时的前后时序。
在标记完所有需要更新的节点后,下一帧的样式处理流程就会对这些节点进行处理。首先,系统会调用 Yoga 的 calcYGLayout 函数,让 Yoga 从根节点开始对所有的 Yoga 节点进行测算。在此过程中,布局信息发生改变的 Yoga 节点会被打上 has_new_layout 的标记,节点上的信息也会被更新。
测算完成后,我们遍历Yoga节点树,找出标记为has_new_layout的节点,并将其width、height、x、y值更新到对应的鸿蒙节点上。这样,所有节点的布局信息就更新完毕了。
布局更新完成后,我们再把前一帧中添加到样式处理队列的节点拿出来。将存储在节点临时对象中的绘制属性转移到鸿蒙节点上。在这个环节里大多数绘制属性可以直接设置,少量依赖节点布局信息的属性(如百分比形式的 background-size)也可以利用新计算出的布局信息来准确确定这些属性的值。
样式的更新
当元素的 ClassName 更新时,我们需要执行以下步骤来确保样式正确应用:
1.识别包含新 ClassName 的所有选择器规则。
2.根据 ClassName 在规则中的位置,确定需要重新进行样式匹配的元素:
◦目标元素选择器:更新当前节点
◦直接后代选择器:更新直接子节点
举一个例子,当样式规则和元素结构如下时:
.E .G {}.E .H {}.I > .J {}.I {}
为蓝色的节点添加className I,为红色的节点添加className E,那么需要要被更新的节点就有 F G H I J
在实际应用中,我们还需考虑性能问题。对于大型应用或复杂的元素结构,频繁的样式重计算可能会影响性能。因此,我们采取了一种优化策略:找出需要更新的节点后,不会立即进行样式重匹配,而是将这些节点标记为"脏"并放入更新队列中。然后,我们在下一帧统一完成所有样式重匹配的工作。这种方法可以有效减少重复计算,提高整体性能。
推荐阅读
Taro 鸿蒙技术内幕系列(一):如何将 React 代码跑在 ArkUI 上