news 2026/9/21 15:21:46

Valdi Polyglot 模块开发指南:用 BUILD.bazel 与 TSX 集成 Android / iOS / macOS / Web 四端原生实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Valdi Polyglot 模块开发指南:用 BUILD.bazel 与 TSX 集成 Android / iOS / macOS / Web 四端原生实现
  • 跨平台
  • UI组件
  • 前端
  • 移动开发

【免费下载链接】Valdi

Valdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.

项目地址:https://gitcode.com/gh_mirrors/val/Valdi
点击查看免费下载

导读

Valdi 是一个跨平台 UI 框架,其"Polyglot(多语言)模块"机制允许开发者在一个模块内同时维护 Valdi TSX 组件与各平台的原生实现:Android(Kotlin/Java)、iOS(Objective-C)、macOS(AppKit)以及浏览器端 Web(TypeScript)。本文以 ai-skills/skills/valdi-polyglot-module/skill.md 为核心,完整讲解 Polyglot 模块的目录结构、BUILD.bazel接线模式、Web 入口的自动注册机制、macOS 原生视图的属性回调桥接,并结合仓库源码剖析底层实现。读完本文,你将能够:搭建一个四端共享的 Polyglot 模块、用ts_project正确编译 Web 实现、让webPolyglotViews导出自动注册到WebViewClassRegistry,以及在 macOS 上用SCValdiMacOSFunction把 AppKit 事件回调给 TSX 组件。

一、Polyglot 模块的目录结构与职责

Polyglot 模块与普通 Valdi 模块的区别在于:它在 Valdi TSX 源码之外,还携带多个平台的"原生实现目录"。文档给出的标准结构如下:

my_module/ src/ # Valdi TSX components (compiled by Valdi compiler) android/ # Kotlin/Java native implementation ios/ # Objective-C native implementation macos/ # Objective-C native implementation (macOS desktop) web/ # TypeScript web implementation (compiled by ts_project, NOT Valdi compiler) strings/ # Localization module.yaml tsconfig.json BUILD.bazel

各目录的核心职责:

目录实现语言编译方式说明
src/TS / TSXValdi 编译器模块的 UI 主体,所有平台共享的组件与业务逻辑
android/Kotlin / Javakt_android_libraryAndroid 原生视图实现
ios/Objective-Cobjc_libraryiOS 原生视图实现
macos/Objective-Cobjc_library(AppKit)macOS 桌面端原生视图实现
web/TypeScriptts_project(tsc)浏览器端实现,不使用Valdi 编译器
strings/JSON(strings-*.jsonValdi 编译流程本地化字符串资源,通过valdi_modulestrings_dir参数接入

关键分界点是web/:它运行在浏览器而非 Valdi runtime 中,因此必须走标准 TypeScript 工具链(ts_project+tsc),而src/则交给 Valdi 编译器生成各平台产物。这条分界贯穿整篇指南。

二、BUILD.bazel 模式:把四端实现接入 valdi_module

文档给出的BUILD.bazel模板是 Polyglot 模块的接线核心,其中web_deps必须是ts_project,永远不要对 Web 代码使用filegroup

load("@aspect_rules_ts//ts:defs.bzl", "ts_project") # web_deps MUST be a ts_project — never use filegroup for web code. ts_project( name = "my_module_web", srcs = glob( [ "web/**/*.ts", "src/**/*.d.ts", # include if web code imports module type declarations ], exclude = ["web/**/*.d.ts"], # exclude to avoid TS5055 output collision with composite ), allow_js = True, composite = True, transpiler = "tsc", # required — aspect_rules_ts does not default it tsconfig = "web/tsconfig.json", visibility = ["//visibility:public"], ) valdi_module( name = "my_module", srcs = glob(["src/**/*.ts", "src/**/*.tsx"]) + ["tsconfig.json"], android_deps = [":android_impl"], ios_deps = [":ios_impl"], macos_deps = [":macos_impl"], web_deps = [":my_module_web"], deps = [...], )

*_deps参数在构建系统中如何被消费

从源码看,valdi_module宏(bzl/valdi/valdi_module.bzl)正是按平台把这些依赖"接线"到对应产物上的:

  • android_deps:在_setup_android_target中通过exports = [":{}_api_kt".format(name)] + android_deps(valdi_module.bzl)把原生实现随{name}_kt一起导出,保证 Android 侧能拿到原生类;
  • ios_deps:在_setup_ios_target中拼入deps = objc_deps + [... ] + ios_deps + [...](valdi_module.bzl),成为{name}_objc的依赖;
  • macos_deps:在_setup_native_target中通过select按平台条件挂载(valdi_module.bzl),即"Obj-C/C++ deps for the module's _desktop native target on macOS only(例如 NSOpenPanel),在 Linux 上被忽略"(宏参数 doc,见 valdi_module.bzl);
  • web_deps:传给valdi_compiled(valdi_module.bzl),再由_setup_web_target把传递性的 web 产物、资源、字符串、protodecl 收拢成{name}_web_srcs_filegroup等目标(valdi_module.bzl),最终进入 Web bundle。

自动注册:web_deps 与 WebViewClassRegistry

web_deps的一个关键特性是:web_deps中任何导出了webPolyglotViews的文件,都会在打包时自动注册到WebViewClassRegistry,无需额外配置。注册机制的实现见 src/valdi_modules/src/valdi/web_renderer/src/WebPolyglotRegistry.ts,其中registerWebPolyglotViewClass会调用addRegistrationCallback把类名→工厂的映射挂到共享注册表上。

修改 module.yaml 后重新生成 BUILD

文档强调:修改module.yaml的 deps 之后,需要重新生成模块的 BUILD.bazel 文件,对应命令为./scripts/regenerate_valdi_modules_build_bazel_files.sh(该命令出自上游工程脚手架;若你的工程根目录未内置此脚本,需以自身工程提供的生成流程为准)。另外,对于手工维护 BUILD 文件的模块,必须在module.yaml中设置bazel_build_file_generation_disabled: true,否则重新生成脚本会覆盖手工 BUILD 文件并丢掉macos_deps/ios_deps

三、Web Polyglot 入口:webPolyglotViews 与自动注册

入口文件范式

Web 端入口文件导出一组"视图工厂"(view factories),打包时自动注册到WebViewClassRegistry。文档要求使用带AttributeHandler接口的强类型 TypeScript:

interface AttributeHandler { changeAttribute(name: string, value: unknown): void; } type ViewFactory = (container: HTMLElement) => AttributeHandler; function createMyViewFactory(): ViewFactory { return (container: HTMLElement): AttributeHandler => { const element = document.createElement('div'); container.appendChild(element); return { changeAttribute(name: string, value: unknown): void { if (name === 'myAttribute' && typeof value === 'number') { element.textContent = String(value); } }, }; }; } // Keys must match the webClass attribute in <custom-view webClass="MyCustomViewClass"> export const webPolyglotViews: Record<string, ViewFactory> = { MyCustomViewClass: createMyViewFactory(), };

两个要点:

  1. 键名必须与 TSX 中<custom-view webClass="...">的属性值完全一致。渲染器正是以webClass的值为 key 去注册表中查工厂;
  2. 工厂返回的 handler 中的changeAttribute(name, value)负责接收后续属性更新,这与原生平台"属性绑定"的语义对齐。

源码级原理:注册表与渲染器如何协作

仓库中的实现印证了文档描述(见 src/valdi_modules/src/valdi/web_renderer/src/WebViewClassRegistry.ts):

  • 注册表与待执行回调都存放在globalThis上(__valdiWebViewClassRegistry__valdiWebViewClassRegistryCallbacks,见 WebViewClassRegistry.ts),目的是"所有 chunk 共享同一个注册表";
  • addRegistrationCallback(WebViewClassRegistry.ts)在注册表已存在时立即执行回调,否则把回调压入 pending 队列;
  • getRegistry(WebViewClassRegistry.ts)在首次被调用(例如ValdiWebRendererDelegate构造时)才创建注册表并一次性 flush 所有 pending 回调——这保证了模块代码无论以何种加载顺序执行,都能在渲染器真正查询之前完成注册。

渲染侧的WebValdiCustomView(src/valdi_modules/src/valdi/web_renderer/src/views/WebValdiCustomView.ts)处理<custom-view>webClass属性:

  • 收到webClass后通过getWebViewClassFactory(attributeValue)查表并调用工厂填充 DOM(WebValdiCustomView.ts);
  • androidClass/iosClass/macosClass在 Web 渲染器中被直接忽略(WebValdiCustomView.ts),这正是"一份 TSX、按平台分发到不同实现"的落点;
  • webClass尚未到达时其他属性先到,会被缓冲进_pendingAttributes,待工厂执行后统一 flush(WebValdiCustomView.ts);
  • 查表失败时渲染一个占位符,并保证容器至少 80px 的最小高度(minHeight = '80px'),避免 0 尺寸容器导致布局塌陷。

四、Web 端 tsconfig.json 与 web/ 目录约束

web/tsconfig.json必须是独立配置,不要 extend 模块级 tsconfig(后者带有 Valdi 的路径映射,不适用于 Web 构建)。文档给出的标准配置:

{ "compilerOptions": { "target": "ES2016", "module": "commonjs", "strict": true, "composite": true, "allowJs": true, "lib": ["dom", "ES2019"] } }

web/目录文件的硬性约束(务必逐条核对):

  • tsc(经ts_project)编译,而不是 Valdi 编译器
  • 不要使用 Valdi 导入valdi_core/valdi_tsx/)——Web 文件运行在浏览器里,没有 Valdi runtime;
  • 不要手动调用globalThis.moduleLoader.resolveRequire()——构建系统已处理注册,手动调用会破坏加载流程;
  • web/tsconfig.json必须独立,且带lib: ["dom", "ES2019"]module: "commonjs"
  • ts_project规则中始终显式指定transpiler = "tsc"——aspect_rules_ts 不会默认选择 transpiler;
  • srcsglob 中纳入src/**/*.d.ts(当 Web 代码导入模块类型声明时)并用exclude = ["web/**/*.d.ts"]排除,以避免 composite 模式下的 TS5055 输出冲突。

五、macOS 原生视图与属性绑定:把 AppKit 事件桥接回 TSX

macOS 的<custom-view>实现可以通过SCValdiMacOSFunction从 TSX 接收回调属性——这是把 AppKit 视图事件(例如键盘输入)桥接回 Valdi 组件的标准途径。其底层机制由两个类提供(源码见 valdi/src/valdi/macos/SCValdiMacOSAttributesBinder.h 与 valdi/src/valdi/macos/SCValdiMacOSFunction.h):

  • SCValdiMacOSAttributesBinder:负责把 TSX 属性名绑定到原生 selector,提供bindUntypedAttribute:invalidateLayoutOnChange:selector:bindColorAttribute:...bindAccessibilityAttributes等 API;
  • SCValdiMacOSFunction:封装指向 Valdi/JS 函数的桥接对象,核心方法为performWithParameters:performWithParametersAndReturnValue:(支持返回值),其SCValdiMacOSFunctionBlock类型签名是(NSArray<id>* parameters) -> id(见 SCValdiMacOSFunction.h)。

原生侧(Objective-C)

// macos/SCMyView.h #import <AppKit/AppKit.h> @class SCValdiMacOSAttributesBinder; @interface SCMyView : NSView + (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder; @end // macos/SCMyView.m #import "SCMyView.h" #import "valdi/macos/SCValdiMacOSAttributesBinder.h" #import "valdi/macos/SCValdiMacOSFunction.h" @interface SCMyView () { SCValdiMacOSFunction *_onEvent; } @end @implementation SCMyView - (void)valdi_setOnEvent:(id)value { _onEvent = value; } // Call the Valdi callback with a dictionary parameter: // [_onEvent performWithParameters:@[@{@"key": @"ArrowUp"}]]; + (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder { [attributesBinder bindUntypedAttribute:@"onEvent" invalidateLayoutOnChange:NO selector:@selector(valdi_setOnEvent:)]; } @end

TSX 侧

<custom-view macosClass='SCMyView' onEvent={this.handleEvent} width={200} height={200} > {/* child elements render inside the custom-view */} </custom-view>

BUILD.bazel

objc_library( name = "macos_impl", srcs = glob(["macos/**/*.m"]), hdrs = glob(["macos/**/*.h"]), copts = ["-I."], sdk_frameworks = ["AppKit"], visibility = ["//visibility:public"], deps = ["@valdi//valdi:valdi_macos_desktop_lib"], )

关键行为约定

  • bindUntypedAttribute:中的属性名必须与 TSX 属性名完全一致(例如"onEvent"onEvent={...});
  • 回调值以SCValdiMacOSFunction对象到达——调用[callback performWithParameters:@[...]]即可调用 Valdi/JS 函数;
  • 参数以NSArray传入,元素可为NSDictionary/NSString/NSNumber,会被自动转换为 JS 对象;
  • custom-view 必须具有非零尺寸才能参与 responder chain(键盘焦点等依赖于此);
  • 键盘输入处理:覆写acceptsFirstResponder返回YES、实现keyDown:,并在viewDidMoveToWindow中调用[self.window makeFirstResponder:self]
  • 手工维护 BUILD 文件时,在module.yaml中设置bazel_build_file_generation_disabled: true,防止重新生成脚本覆盖手工内容。

相关底层实现可继续阅读 valdi/src/valdi/macos/SCValdiMacOSFunction.mm、valdi/src/valdi/macos/SCValdiMacOSAttributesBinder.mm 与 valdi/src/valdi/macos/SCValdiMacOSViewManager.mm;测试可参考 valdi/test/macos/SCValdiMacOSViewManagerTests.mm。iOS / Android 侧<custom-view>的通用语义可参见 docs/docs/native-customviews.md 与 ai-skills/skills/valdi-custom-view/skill.md。

六、常见错误与排查清单

  • web/文件放进srcs:Web 代码必须走ts_project+web_deps,不能交给 Valdi 编译器;
  • 遗漏平台_deps:每个平台实现都要通过android_depsios_depsmacos_deps接线,漏接会导致该平台产物缺失;
  • web/中使用 Valdi 导入valdi_core/valdi_tsx/在浏览器 bundle 中不存在;
  • 创建 0×0 的 custom-view 用于键盘输入:macOS 不会授予其 first responder 状态——应在内部包裹可见内容,保证非零尺寸;
  • 忘记在module.yaml中设置bazel_build_file_generation_disabled: true:重新生成脚本会覆盖手工维护的 BUILD 文件,丢掉macos_deps/ios_deps

七、速查:一个 Polyglot 模块的完整落地路径

  1. 按第一节结构创建src/android/ios/macos/web/strings/目录及module.yamltsconfig.json
  2. web/编写入口文件,导出webPolyglotViews(键名与webClass对齐),并放置独立的web/tsconfig.json
  3. 编写BUILD.bazelts_project产出_web目标,valdi_module用四个*_deps接入各平台实现;
  4. src/的 TSX 中使用<custom-view webClass="..." androidClass="..." iosClass="..." macosClass="...">分发到各平台;
  5. macOS 原生侧用SCValdiMacOSAttributesBinder绑定属性、用SCValdiMacOSFunction回传事件;
  6. 修改module.yaml后重新生成 BUILD 文件;若手工维护 BUILD,务必设置bazel_build_file_generation_disabled: true

以上流程覆盖了文档定义的全部规则与源码中的实现细节,你可以对照仓库中的valdi_module宏定义与WebViewClassRegistry实现逐项验证。

  • 跨平台
  • UI组件
  • 前端
  • 移动开发

【免费下载链接】Valdi

Valdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.

项目地址:https://gitcode.com/gh_mirrors/val/Valdi
点击查看免费下载

相关推荐

上一篇:Qwen3VL文本编码器与Qwen Image VAE:Krea-2背后的AI视觉技术揭秘
下一篇:零基础上手r0capture:10分钟学会安卓应用抓包

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 15:21:06

web3.js web3-eth-accounts 使用指南:Ethereum 账户管理与交易签名

web3.js web3-eth-accounts 使用指南&#xff1a;Ethereum 账户管理与交易签名 【免费下载链接】web3.js Collection of comprehensive TypeScript libraries for Interaction with the Ethereum JSON RPC API and utility functions. 项目地址: https://gitcode.com/gh_mirr…

作者头像 李华
网站建设 2026/9/21 15:10:36

Ubuntu 20.04离线安装Realtek b852无线网卡驱动全攻略

1. 一块网卡引发的折腾&#xff1a;为什么离线装驱动比想象中麻烦Realtek b852 这块无线网卡&#xff0c;最近两年在不少轻薄本和迷你主机上出现得挺频繁。它本身是 RTL8852BE 系列的衍生型号&#xff0c;支持 Wi-Fi 6 和蓝牙 5.2&#xff0c;纸面参数不差。但问题在于&#xf…

作者头像 李华