拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Valdi 样式系统完全指南:用 `Style<>` 对象实现高效、可复用、零冗余的批量样式管理

Valdi 样式系统完全指南用Style对象实现高效、可复用、零冗余的批量样式管理【免费下载链接】ValdiValdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.项目地址: https://gitcode.com/gh_mirrors/val/ValdiValdi 是一款跨平台 UI 框架其样式的核心思路是属性即样式——任何应用于原生视图的属性如color、backgroundColor、padding本质上都在样式化该视图。本指南以 Valdi 的StyleT对象为主线讲解如何将大量静态属性打包成可复用的样式对象、通过merge/extend组合样式、理解内联属性覆盖样式的优先级规则以及利用 Style Interning 机制写出高性能的渲染代码。读完本文你将掌握 Valdi 中从逐个属性堆砌到样式对象化、组件化的完整进阶路径。从属性即样式说起回顾 Valdi 的绝大多数示例你会发现我们一直在用属性来影响具体组件的渲染label colorblack // 通过 color 属性改变 label 的渲染 valueHelloWorld /在 Valdi 中任何应用于原生视图的属性实际上都是在样式化该原生视图。color、width、backgroundColor、borderRadius……这些属性直接决定了一个视图在屏幕上的样子它们构成了 Valdi 样式体系的最小单元。这种设计简单直观但随着业务功能复杂度的增长问题也随之而来。问题属性爆炸与重复声明当一个视图需要表达复杂的视觉效果时属性列表会迅速膨胀view backgroundColorwhite flexDirectionrow justifyContentcenter alignItemscenter padding{10} margin{10} borderRadius{10} boxShadow0 8 11 rgba(0, 0, 0, 0.1) // 这已经是很多属性了 // 实际场景中甚至更多 /view更常见的痛点则是属性组的重复声明渲染树中不同分支的视图明明共享一组视觉属性却因为内容不同而被迫逐一声明view // 这个视图有一组属性 backgroundColorlightblue boxShadow0 8 11 rgba(0, 0, 0, 0.1) padding{10} margin{10} borderRadius{10} image /* ... *// /view view // 这个视图位于渲染树的不同分支 // 内容也不同 // 所以不得不重新声明所有属性 backgroundColorlightred // 尽管它和其他视图共享大量属性 boxShadow0 8 11 rgba(0, 0, 0, 0.1) padding{10} margin{10} borderRadius{10} label /* ... *// /view属性一多模板就变得臃肿、难以维护属性一重复改一处样式就要在多处同步修改。这正是 Valdi 引入StyleT对象的初衷。Style对象一次应用一组属性StyleT对象允许我们在一个视图上一次应用多个属性。例如const myStyle new StyleView({ backgroundColor: lightblue, width: 100, height: 100, }); view style{myStyle}/这段代码与下面这种逐属性写法完全等价view backgroundColorlightblue width{100} height{100} /也就是说StyleT只是把散落在 JSX 标签上的属性收拢到了一个可复用的对象里。它的价值在于当需要为大量元素应用相同样式、或在不同渲染分支间复用同一组属性时样式对象是唯一高效且整洁的载体。从源码看StyleT的本质打开 Valdi 源码中Style类的实现src/valdi_modules/src/valdi/valdi_core/src/Style.ts可以看到它的核心结构非常精简export type NativeStyle number; export class StyleT implements IStyleT, ConsoleRepresentable { readonly attributes: OmitT, style; private native: NativeStyle | undefined; constructor(attributes: T) { this.attributes attributes; this.native undefined; } toNative(convertFunc: StyleToNativeFunc): NativeStyle { let native this.native; if (native undefined) { native convertFunc(this.attributes); this.native native; } return native; } // ... }几个值得注意的设计点attributes: OmitT, style样式对象内部就是一组普通属性排除style属性本身避免嵌套。native字段是一个缓存的原生标识toNative()第一次调用时会把整包属性序列化到原生运行时并拿到一个整数 ID之后再次调用直接返回缓存的 ID不再重复序列化。这正是文档中提到的Style Interning机制的实现基础详见下文性能章节。与之对应的类型契约定义在 src/valdi_modules/src/valdi/valdi_tsx/src/IStyle.d.tsIStyleT接口声明了attributes与toNative()。关于转换只发生一次的行为仓库的单元测试做了直接验证src/valdi_modules/src/valdi/valdi_test/test/Style.spec.tsit(convert to native only once, () { const style new Style({ hello: world, nice: 42 }); const native styleToNative(style); const native2 styleToNative(style); expect(native).toBeTruthy(); expect(native).toBe(native2); // 第二次转换返回同一个缓存结果 });在渲染器侧src/valdi_modules/src/valdi/valdi_core/src/JSXRendererDelegate.ts 第 213-215 行、第 269-270 行无论是元素属性还是style属性传入的Style实例最终都会通过style.toNative(runtime.createCSSRule)得到对应的原生 CSS Rule 标识从而把整组样式一次性交付给原生层。Style实战示例下面是一个完整的实战片段它演示了样式对象在真实组件中的典型用法——声明、组合、复用// 按需导入各元素类型的属性接口 import { Layout, View } from NativeTemplateElements; import { Style } from valdi_core/src/Style; // 声明我们已知会一起使用的属性组 const styles { // 这是 StyleView只能应用在 view 上 container: new StyleView({ backgroundColor: lightgrey, flexDirection: row, flexWrap: wrap, }), // 这是 StyleLayout只能应用在 layout 上 // 注意view 本身也是一个 layout square: new StyleLayout({ height: 100, width: 100, }), // 可以制作简单样式之后再组合它们 withMargin: new StyleLayout({ margin: 10, }), withPadding: new StyleLayout({ padding: 10, }), withRounded: new StyleView({ borderRadius: 10, }), }; // 注意Style 是可以组合的 // 使用 Style.merge 组合 const itemStyle Style.merge(styles.square, styles.withMargin, styles.withPadding, styles.withRounded); // 使用 Style.extend 扩展 const imageStyle styles.withRounded.extend({ height: 100%, width: 100%, }); // 构建一个使用这些属性组称为 Style(s)的组件 export class HelloWorld extends Component { onRender() { // 在同一模板中把样式复用到大量元素上 view style{styles.container} view style{itemStyle} backgroundColorlightgreen image style{imageStyle} srchttps://placedog.net/500 / /view view style{itemStyle} backgroundColorlightpink image style{imageStyle} srchttps://placedog.net/500 / /view view style{itemStyle} backgroundColorlightblue image style{imageStyle} srchttps://placedog.net/500 / /view /view; } }上面这段示例正是本文开头那张效果图的来源三个卡片共享itemStyle正方形 边距 内边距 圆角仅通过内联的backgroundColor区分彼此。样式对象负责共性内联属性负责差异两者配合即可写出既整洁又灵活的模板。类型系统如何约束样式StyleT是类型化的泛型参数T决定了该样式对象能应用到哪种元素上StyleView只能应用在view上StyleLayout只能应用在layout上因为view继承自layout所以StyleLayout也可以用在view上反过来StyleView用在layout上虽然不报类型错误但其中 View 专属的属性如backgroundColor不会生效。样式遵循 TypeScript 的类型继承体系const viewStyle new StyleView({ backgroundColor: red, width: 100, }); // ✅ 合法 —— View 继承自 Layout view style{viewStyle} / // ✅ 合法 —— 可用于 layout但 backgroundColor 不会生效 layout style{viewStyle} / // ❌ 类型错误 —— Label 并不直接继承 View const labelStyle new StyleLabel({ /* ... */ }); view style{labelStyle} /这套类型约束把错误的样式用错地方这类问题拦截在了编译期。组合样式merge 与 extend现实中的样式往往由多个可复用的碎片拼装而成。StyleT提供了两种组合方式。Style.merge(...)合并多个样式把多个样式对象合并成一个后传入的样式覆盖先传入的const baseStyle new StyleView({ width: 100, height: 100, backgroundColor: red, }); const roundedStyle new StyleView({ borderRadius: 8, }); const shadowStyle new StyleView({ boxShadow: 0 2 10 rgba(0,0,0,0.1), }); // 合并三者 const cardStyle Style.merge(baseStyle, roundedStyle, shadowStyle); // 结果width: 100, height: 100, backgroundColor: red, // borderRadius: 8, boxShadow: 0 2 10 rgba(0,0,0,0.1)从源码看merge的实现就是对每个样式的attributes做对象展开合并返回一个新的Style实例src/valdi_modules/src/valdi/valdi_core/src/Style.tsstatic merge(...styles: Styleany[]): Styleany { const attributes styles.reduce((c, style) { return { ...c, ...style.attributes }; }, {}); return new Style(attributes); }.extend(...)基于现有样式派生在现有样式基础上覆盖或追加属性生成新样式const baseStyle new StyleView({ width: 100, height: 100, backgroundColor: red, }); const blueStyle baseStyle.extend({ backgroundColor: blue, // 覆盖 borderRadius: 8, // 新增 }); // 结果width: 100, height: 100, backgroundColor: blue, borderRadius: 8extend的实现同样直观——把新属性展开合并到原属性之上extendT2(attributes: T2): StyleOmitT, style T2 { if (attributes instanceof Style) { return this.extend(attributes.attributes); } const newAttributes { ...this.attributes, ...attributes }; return new Style(newAttributes); }注意extend也接受另一个Style实例作为参数此时会展开其attributes再合并这让样式之间可以互相派生。仓库测试对merge/extend的行为做了完整覆盖src/valdi_modules/src/valdi/valdi_test/test/Style.spec.tsextend既能新增属性也能覆盖同名属性merge支持 2 个乃至更多样式源码里为 2-4 个参数提供了类型安全的重载签名运行时则支持任意数量的组合。条件样式按状态切换样式对象天然适合表达状态驱动的外观。预先创建好各种状态的样式渲染时按状态选择即可const styles { normal: new StyleView({ backgroundColor: #ffffff, borderColor: #dddddd, }), selected: new StyleView({ backgroundColor: #007AFF, borderColor: #0051D5, }), disabled: new StyleView({ backgroundColor: #f5f5f5, borderColor: #cccccc, opacity: 0.5, }), }; // 渲染时 view style{ this.state.disabled ? styles.disabled : this.state.selected ? styles.selected : styles.normal } /由于所有候选样式都在初始化时创建完毕渲染期只是做一次三目运算选择没有任何新的样式分配因此既高效又清晰。优先级内联属性永远覆盖 StyleStyleT对象的一个重要特性是元素上直接设置的属性永远会覆盖样式对象中同名的属性const myStyle new StyleView({ height: 100, width: 100, backgroundColor: red, }); // height: 100, width: 100, backgroundColor: red view style{myStyle}/; // height: 100, width: 100, backgroundColor: blue view style{myStyle} backgroundColorblue;这条规则给了我们极具弹性的开发模式把不可变静态的值放进 Style把可变动态的值用普通属性在元素上覆盖。例如const staticStyle new StyleView({ width: 100, height: 100, borderRadius: 8, }); class MyComponent extends Component { onRender() { // 样式是静态的动态值以内联属性呈现 view style{staticStyle} backgroundColor{this.state.isActive ? blue : gray} / } }在仓库的真实应用中这种样式 内联覆盖的组合随处可见。以 apps/text_attributes_example/TextAttributesExample.tsx 为例模块级定义了styles对象全部在初始化期创建组件渲染时对每个view/label传入style{styles.xxx}再按需叠加内联属性如backgroundColor{this.viewModel.color}、value{...}等动态内容。性能考量Style Interning 与创建时机Valdi 官方明确建议样式是静态、不变属性的最佳载体但应避免在渲染期实例化样式。原因有二一是创建新对象会带来内存分配开销二是更重要的——Style Interning机制决定了复用同一对象才是性能最优路径。什么是 Style InterningStyle Interning样式实例第一次被发送到原生运行时通过toNative()时会被分配一个唯一的整数标识符CSS Rule ID。之后只要再次使用同一个样式对象就只需传递这个 ID从而极大降低序列化marshalling开销。换句话说原生层为每个样式对象建立了身份证首次见面登记建档此后报个号即可。这也正是应该复用样式对象、而不是每次现场创建的原因——每次new Style(...)都会产生一个全新的、需要重新登记的对象。这条机制的实现证据就在Style.ts的toNative()缓存逻辑里native字段一旦通过convertFunc渲染器中即runtime.createCSSRule见 src/valdi_modules/src/valdi/valdi_core/src/JSXRendererDelegate.ts生成就会永久缓存后续调用零成本返回。好与坏的写法对比// 好 —— 初始化期创建 const myCheapStyle new StyleView({ width: 100, height: 100, backgroundColor: red, }); // 同样好 —— 初始化期派生 const myCheapStyle2 myCheapStyle.extend({}) const myCheapStyle3 Style.merge(myCheapStyle, myCheapStyle2); /** * 以下是应避免的写法 */ class MySlowComponent { onRender() { // 坏 —— 每次渲染都是一次昂贵的调用含内存分配等开销 const myExpensiveStyle new StyleView({ width: 100, height: 100, backgroundColor: blue, }); // 坏 —— merge / extend 同样等价于实例化一个新 Style const myExpensiveStyle2 myExpensiveStyle.extend({}) const myExpensiveStyle3 Style.merge(myExpensiveStyle, myExpensiveStyle2); // height: 100, width: 100, backgroundColor: red view style{myCheapStyle}/; // height: 100, width: 100, backgroundColor: blue view style{myExpensiveStyle}/; } } /** * 正确的做法 */ class MyFastComponent { onRender() { // height: 100, width: 100, backgroundColor: red view style{myCheapStyle}/; // height: 100, width: 100, backgroundColor: blue view style{myCheapStyle} backgroundColorblue/; } }注意只要不创建新的样式对象对已有样式的管理与操作都是廉价的。例如在初始化期静态地准备好一组变体渲染期仅做选择// 静态阶段在初始化时创建所有需要的样式 const item new StyleView({ width: 100, height: 100, }); const styles { itemSelected: item.extendView({ backgroundColor: blue }), itemDeselected: item.extendView({ backgroundColor: white }), } // 动态阶段只使用已有的样式 interface State { selected: boolean; } class MyFastComponentWithLogic extends StatefulComponentState { onRender() { // 这里很快没有样式创建 view style{this.state.selected ? styles.itemSelected : styles.itemDeselected}/ } }另一种高性能变体组合是基础样式 预创建变体模式const baseButton new StyleView({ padding: 12, borderRadius: 8, }); const styles { primary: baseButton.extend({ backgroundColor: #007AFF }), secondary: baseButton.extend({ backgroundColor: #5856D6 }), danger: baseButton.extend({ backgroundColor: #FF3B30 }), }; // 渲染期零样式创建 view style{this.props.variant primary ? styles.primary : styles.secondary} /哪些内容不能放进 Style并非所有属性都适合放进样式对象。以下内容必须以内联方式使用docs/api/api-style-attributes.md回调/函数onChange、onTap、onLayout等复杂对象path、filter、fontProvider等动态内容value、src、placeholder多数情况下引用ref属性程序化属性focused、contentOffsetX等。Style 属性参考总览StyleT可以包含元素支持的任何属性style属性本身除外。完整的逐属性参考见 docs/api/api-style-attributes.md这里给出分类速查。布局与定位所有元素通用尺寸width、height、minWidth、maxWidth、minHeight、maxHeight、aspectRatio位置position、top、right、bottom、left间距margin*、padding*含margin/padding简写及10 20式双向简写FlexboxflexDirection、justifyContent、alignItems、alignContent、alignSelf、flexGrow、flexShrink、flexBasis、flexWrap、display、overflow、zIndex。Valdi 的布局系统由 Facebook 的Yogaflexbox 布局引擎驱动其行为与所用 Yoga 版本强相关本仓库锁定在 third-party/yoga/版本细节见 third-party/yoga/README.snap。调试布局问题时务必以该版本行为为准因为部分默认值与标准 CSS flexbox 不同例如flexDirection默认是columnCSS 默认row。尺寸、间距、定位、flexBasis支持数值点、百分比、auto三种取值百分比始终相对父元素的对应维度计算横向相对父宽、纵向相对父高。而以下属性不支持百分比borderWidth、borderRadius、flexGrow/flexShrink无量纲数字、aspectRatio数字比值、scaleX/scaleY/rotation/translationX/translationY、touchAreaExtension*。外观View 及以上背景background渐变字符串如linear-gradient(#ff0000, #0000ff)、backgroundColor、opacity0.0-1.0边框border简写2 solid #000000、borderWidth、borderColor、borderRadius数字或8 8 0 0分角写法阴影boxShadow字符串格式x y blur color裁剪slowClipping。手势View 及以上touchEnabled、touchAreaExtension*点击区域扩展、onTapDisabled/onDoubleTapDisabled/onLongPressDisabled/onDragDisabled/onPinchDisabled/onRotateDisabled手势开关、onTouchDelayDuration、longPressDuration。变换View 及以上scaleX、scaleY、rotation弧度、translationX/translationY、transformOrigin。文本Label / TextField / TextView字体font如Montserrat-Bold 16 unscaled 20、color、textGradient、textShadow布局numberOfLines0不限、textAlign、textDecoration、lineHeight、letterSpacing、textOverflow自适应adjustsFontSizeToFitWidth、minimumScaleFactor输入placeholderColor、tintColor、contentType、returnKeyText、autocapitalization、autocorrection、keyboardAppearance等。无障碍accessibilityCategory、accessibilityNavigation、accessibilityPriority、accessibilityLabel、accessibilityHint、accessibilityValue、accessibilityStateDisabled/accessibilityStateSelected/accessibilityStateLiveRegion、accessibilityId测试用。性能与视口lazy、lazyLayout、limitToViewport、ignoreParentViewport、extendViewportWithChildren、estimatedWidth、estimatedHeight、animationsEnabled。元素专属样式每个元素类型都有专属的样式属性集同时也明确限制了哪些属性不能进样式StyleLayout仅布局类属性尺寸、位置、间距、flexbox、无障碍、生命周期不能含backgroundColor、borderRadius等 View 专属属性StyleView全部 Layout 属性 外观、手势、变换、遮罩、平台专属属性StyleScrollView全部 View 属性 滚动行为horizontal、bounces*、pagingEnabled、showsVerticalScrollIndicator等。注意不能使用flexDirection横向滚动请用horizontalcontentOffsetX/Y等程序化属性也不可入样式StyleImageView全部 View 属性 objectFit、tint、flipOnRtl、contentScaleX/Y、contentRotationsrc、filter、回调不可入样式StyleVideoView全部 View 属性 volume、playbackRatesrc、seekToTime、回调不可入样式StyleLabel全部 View 属性 文本样式/布局/自适应属性value、selection、回调不可入样式StyleTextField全部 Label 属性 键盘配置与编辑行为contentType、returnKeyText、characterLimit等value、placeholder、selection、focused、回调不可入样式StyleTextView全部 TextField 属性 returnType、textGravity、customUnderlineStyle、backgroundEffect*等同样排除动态内容与回调StyleBlurView全部 Layout 有限 View 属性 blurStyleiOS 系统毛玻璃风格如systemMaterial、systemThinMaterial等StyleSpinnerView全部 View 属性 colorStyleShapeView全部 View 属性 描边/填充strokeWidth、strokeColor、strokeCap、strokeJoin、strokeStart/strokeEnd、fillColorpath为复杂对象需内联StyleAnimatedImage全部 View 属性 loop、advanceRate、objectFit、currentTime、animationStartTime/animationEndTimesrc、fontProvider、回调不可入样式。完整的逐元素属性清单与示例代码请直接查阅 Style Attributes Referencedocs/api/api-style-attributes.md该文档还包含大量百分比布局实战示例。实战百分比布局与常见误区样式对象的百分比能力非常适合做响应式布局。以下示例摘自属性参考文档的实战章节docs/api/api-style-attributes.mdconst styles { // 通栏容器 container: new StyleView({ width: 100%, // 占满父容器宽度 padding: 5%, // 四周 5% 内边距 backgroundColor: #f5f5f5, }), // 双栏布局并排 column: new StyleView({ width: 48%, // 略小于 50%为间距留出空间 margin: 1%, // 在列之间制造间隔 backgroundColor: #ffffff, }), // 限宽居中的内容区 content: new StyleLayout({ width: 90%, // 父容器宽度的 90% maxWidth: 600, // 但绝不超过 600pt marginLeft: 5%, // 配合右外边距实现居中 marginRight: 5%, }), // 绝对定位的遮罩层 overlay: new StyleView({ position: absolute, top: 10%, // 距顶部 10% left: 10%, // 距左侧 10% width: 80%, height: 80%, backgroundColor: rgba(0,0,0,0.5), }), };百分比计算的三个关键规则横向取值left/right、marginLeft/marginRight、paddingLeft/paddingRight相对父容器宽度纵向取值top/bottom、marginTop/marginBottom、paddingTop/paddingBottom相对父容器高度简写margin: 5%/padding: 5%四周统一百分比横向按父宽、纵向按父高计算。常见误区这些写法会导致错误或意外行为// ❌ 错误 —— 这些属性不支持百分比 const wrongStyles new StyleView({ borderRadius: 50%, // ❌ borderRadius 只接受数值点 opacity: 50%, // ❌ opacity 是 0.0-1.0 标量不是百分比 scaleX: 150%, // ❌ 缩放是数值倍率 rotation: 90%, // ❌ 旋转使用弧度 aspectRatio: 16/9, // ❌ aspectRatio 是数字 16/9不是字符串 }); // ✅ 正确 const correctStyles new StyleView({ borderRadius: 8, // ✅ 仅点值 opacity: 0.5, // ✅ 0.0-1.0 scaleX: 1.5, // ✅ 数值倍率150% 1.5 rotation: Math.PI / 2, // ✅ 弧度90° π/2 aspectRatio: 16/9, // ✅ 数字比值 });最佳实践速查TLDR✅用 Style 打包静态属性组把不变的外观/布局属性收拢为样式对象提升复用性与可读性✅使用 Style 性能极佳快于逐属性传递——得益于Style Interning样式实例首次送入原生运行时即被分配唯一整数 ID后续复用同一对象只传 ID序列化开销趋近于零这正是必须复用样式对象、杜绝现场创建的根本原因❌不要在渲染期实例化/合并/扩展 Stylenew、merge、extend都产生新的样式对象成本高昂✅实例化/合并/扩展放在初始化或懒加载阶段模块级const styles {...}或组件类属性是推荐位置可参考 apps/text_attributes_example/TextAttributesExample.tsx 的styles定义✅动态值使用内联属性样式负责静态部分渲染期的动态值通过普通属性覆盖内联属性优先级永远高于 Style✅按需切换预建样式用this.state.selected ? styles.selected : styles.normal之类的方式实现有限粒度的动态样式切换✅遵守类型约束StyleView与StyleLayout有明确的适用范围编译期即可发现误用。延伸阅读Style Attributes Reference完整样式属性表逐元素类型、逐属性组的完整参考含百分比布局实战Core Flexbox布局基础理解 Yoga flexbox 布局行为Complete API Reference元素 API 全解API Quick Reference常用属性速查。【免费下载链接】ValdiValdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.项目地址: https://gitcode.com/gh_mirrors/val/Valdi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门