尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpacetimeDB 客户端绑定生成实战:从 `spacetime generate` 到类型安全客户端代码

SpacetimeDB 客户端绑定生成实战:从 `spacetime generate` 到类型安全客户端代码 SpacetimeDB 客户端绑定生成实战从spacetime generate到类型安全客户端代码【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本文是 SpacetimeDB 客户端开发的核心指南围绕spacetime generate命令展开系统讲解如何为 TypeScript、C#、Rust、Unreal C 四种客户端生成模块绑定Module Bindings以及生成代码的结构、重新生成机制与源码级实现原理。读完本文你将能够独立完成从模块到客户端绑定的完整生成流程理解表、Reducer、Procedure、View 在每种语言中的生成形态并掌握排查生成问题的正确姿势。什么是 Module Bindings模块绑定在客户端应用能够与 SpacetimeDB 数据库交互之前必须先为你的模块生成客户端绑定。所谓模块绑定就是一段自动生成的代码它镜像了模块的 schema 与函数签名为客户端提供了类型安全的访问层。绑定代码提供的核心能力包括类型定义Type definitions与模块中表、类型一一对应的客户端类型结构可调用函数Callable functions用于调用 Reducer 与 Procedure 的客户端封装查询接口Query interfaces用于订阅subscription与本地缓存访问local cache access的接口回调注册Callback registration用于监听数据库变更插入、更新、删除的回调机制。绑定保证了客户端与服务端代码在编译期的类型安全——在模块中把某列声明为u64生成到客户端后就不会被当作String使用从而在运行时之前捕获大量错误。使用spacetime generate生成绑定spacetime generate是 SpacetimeDB CLI 中负责客户端代码生成的子命令。从 CLI 的源码定义crates/cli/src/subcommands/generate.rs可以看出该命令的完整用法为spacetime generate [DATABASE] --lang LANG [--module-path DIR | --bin-path PATH | --js-path PATH] [--out-dir DIR | --uproject-dir DIR] [--unreal-module-name MODULE_NAME] [OPTIONS]下面按语言分别介绍推荐用法。TypeScriptmkdir -p src/module_bindings spacetime generate --lang typescript --out-dir src/module_bindings --module-path PATH-TO-MODULE-DIRECTORY生成的 TypeScript 文件位于src/module_bindings/在客户端中通过命名空间导入使用import * as moduleBindings from ./module_bindings;其中PATH-TO-MODULE-DIRECTORY指向模块所在目录——对于 TypeScript 模块而言即包含模块package.json的目录仓库模板 templates/basic-ts/spacetimedb/package.json 展示了典型结构其build脚本为spacetime build。C#mkdir -p module_bindings spacetime generate --lang csharp --out-dir module_bindings --module-path PATH-TO-MODULE-DIRECTORY生成的 C# 文件位于module_bindings/会被自动包含进你的 .NET 工程。PATH-TO-MODULE-DIRECTORY指向包含模块.csproj的目录。C# 生成还支持--namespace参数自定义命名空间默认值为SpacetimeDB.Types见 generate.rs 源码 与prepare_generate_run_configs中的默认逻辑。Rustmkdir -p src/module_bindings spacetime generate --lang rust --out-dir src/module_bindings --module-path PATH-TO-MODULE-DIRECTORY生成的 Rust 文件位于src/module_bindings/在客户端中通过模块声明引入mod module_bindings;PATH-TO-MODULE-DIRECTORY指向包含模块Cargo.toml的目录。UnrealCspacetime generate --lang unrealcpp --uproject-dir PATH-TO-UPROJECT --module-path PATH-TO-MODULE-DIRECTORY --unreal-module-name YOUR_MODULE_NAME生成的 Unreal C 文件位于工程的ModuleBindings目录会被自动纳入 Unreal 工程编译。三个占位参数分别说明如下PATH-TO-UPROJECTUnreal 工程目录包含.uproject文件的目录等价于--out-dir的 Unreal 专用替代PATH-TO-MODULE-DIRECTORYSpacetimeDB 模块目录YOUR_MODULE_NAMEUnreal 模块名通常就是工程名。该名字被用于生成 DLL 导出宏如YOUR_MODULE_NAME_API是 unrealcpp 模式下的必填参数。命令参数速查综合 generate.rs 的参数定义spacetime generate的完整参数列表如下参数简写含义默认值 / 约束--lang-l生成语言rust、csharp、typescript、unrealcpp必填可用别名rs/ts/cs/uecpp等--module-path-p模块工程目录缺省先找spacetimedb/子目录再找当前目录--bin-path-b直接指定已编译的 wasm 二进制路径跳过构建与--module-path、--build-options互斥--js-path-j直接指定已打包的 JS 文件路径跳过构建与--module-path、--build-options互斥--out-dir-o输出目录Rust/TS 默认src/module_bindingsC# 默认module_bindings--uproject-dir—Unreal 工程目录仅--lang unrealcpp使用unrealcpp 必填--namespace—生成代码命名空间仅 C#SpacetimeDB.Types--unreal-module-name—Unreal 模块名DLL 导出宏用unrealcpp 必填别名--module-name--module-prefix—生成类型的前缀仅 unrealcpp空--build-options—透传给构建命令的选项空字符串--dotnet-version—使用的 .NET SDK 主版本视环境检测--include-private—将私有表/函数也生成进代码类型始终包含false--no-config—忽略spacetime.json配置关闭--env—配置文件分层环境名如dev、staging无值得注意的是源码中实现了客户端语言自动检测当未指定--lang时CLI 会根据客户端工程中的文件推断语言——存在package.json判定为 TypeScript存在Cargo.toml判定为 Rust存在.csproj判定为 C#见 detect_default_language 及对应单元测试test_detect_typescript_language_from_client_project。生成流程的底层原理理解spacetime generate的内部流程有助于在出错时快速定位问题。结合 run_prepared_generate_configs 的实现一次生成过程大致分为五步获取模块定义ModuleDef默认情况下CLI 先调用spacetime build编译模块携带--build-options拿到 wasm/js 产物若传了--bin-path或--js-path则跳过构建直接读取也可以通过--module-def从 JSON 编码的 ModuleDef 生成。提取 schema通过spacetimedb-standalone extract-schema wasm_file从产物中解析出模块的完整 schema见 extract_descriptions。过滤可见性默认只生成公共表与公共函数私有表会被跳过并打印提示只有加--include-private才会纳入对应 CodegenVisibility。逐文件生成核心的 generate() 函数会依次遍历公共表 → View → 子模块公共表 → 子模块 View → 自定义类型 → Reducer → Procedure → 子模块 Reducer/Procedure → 全局文件每种实体交给目标语言的Lang后端生成OutputFile文件名 代码。落盘与清理为每个输出文件创建目录、写入内容同时扫描输出目录中带有自动生成标记// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB见 AUTO_GENERATED_PREFIX的过期旧文件并提示删除需要交互确认或--yes强制最后用rustfmtRust、dotnet formatC#等工具对生成代码做格式化。排序方面代码生成对表、Reducer、Procedure、View、类型均按名称排序见 util.rs 中的 iter_* 函数保证生成结果确定可复现。此外spacetime generate支持通过spacetime.json配置多个生成目标按数据库名 glob 过滤、父子目标继承module-path、按(module_path, generate_entry)去重这部分在 get_filtered_generate_configs 及其单元测试中有完整覆盖。会生成什么四类核心产物表Tables模块中的每张表都会生成三样东西类型/类定义为每一列生成对应的属性表访问器挂在DbConnection上用于查询客户端本地缓存回调注册方法用于监听插入、更新、删除事件。例如模块中的user表含id、name、email_address三列TypeScript// Generated type export default __t.row({ id: __t.u64(), name: __t.string(), emailAddress: __t.string(), }); // Access via DbConnection conn.db.userC#// Generated type public partial class User { public ulong Id; public string Name; public string EmailAddress; } // Access via DbConnection conn.Db.UserRust// Generated type pub struct User { pub id: u64, pub name: String, pub email_address: String, } // Access via DbConnection conn.db().user()Unreal// Generated type USTRUCT(BlueprintType) struct FUser { GENERATED_BODY() UPROPERTY(BlueprintReadWrite) int64 Id; UPROPERTY(BlueprintReadWrite) FString Name; UPROPERTY(BlueprintReadWrite) FString EmailAddress; }; // Access via DbConnection Context.Db-User注意生成后的名称遵循各语言的命名惯例。模块中声明的email_address列在 TypeScript 中变成emailAddress在 C# 中变成EmailAddress在 Rust 中保持email_address。这一点在 crates/codegen/src/util.rs 的collect_case中通过convert_case库统一实现PascalCase / camelCase 转换而 Unreal 后端还支持通过--module-prefix为类型名加前缀见 unrealcpp.rs。Reducer模块中的每个 Reducer 都会生成客户端可调函数向服务端发送 reducer 调用请求回调注册方法监听该 reducer 的执行类型安全参数与 reducer 签名严格对应。例如create_userreducerTypeScript// Call the reducer await conn.reducers.createUser({ name, email }); // Register a callback to observe reducer invocations conn.reducers.onCreateUser((ctx, { name, email }) { console.log(User created: ${name}); });C#// Call the reducer conn.Reducers.CreateUser(name, email); // Register a callback to observe reducer invocations conn.Reducers.OnCreateUser (ctx, name, email) { Console.WriteLine($User created: {name}); };Rust// Call the reducer conn.reducers().create_user(name, email); // Register a callback to observe reducer invocations conn.reducers().on_create_user(|ctx, name, email| { println!(User created: {}, name); });Unreal// Call the reducer Context.Reducers-CreateUser(TEXT(Alice), TEXT(aliceexample.com)); // Register a callback to observe reducer invocations Context.Reducers-OnCreateUser.AddDynamic(this, AMyActor::OnCreateUser); // Callback function (must be UFUNCTION) UFUNCTION() void OnCreateUser(const FReducerEventContext Ctx, const FString Name, const FString Email) { UE_LOG(LogTemp, Log, TEXT(User created: %s), *Name); }从 util.rs 的 iter_reducers 可以看出一个细节生命周期类 reducer如init以及标记为Internal可见性的 reducer不会被生成到客户端——它们本就不应被客户端直接调用。Procedure模块中的每个 Procedure 都会生成客户端可调函数调用该 procedure返回值处理处理 procedure 的返回结果类型安全参数与 procedure 签名对应。例如fetch_external_dataprocedureTypeScript// Call the procedure conn.procedures .fetchExternalData(url) .then(result console.log(Got result: ${result})) .catch(error console.error(Error: ${error}));C#// Call the procedure without a callback conn.Procedures.FetchExternalData(url); // Call the procedure with a callback for the result conn.Procedures.FetchExternalData(url, (ctx, result) { if (result.IsSuccess) { Console.WriteLine($Got result: {result.Value!}); } else { Console.WriteLine($Error: {result.Error!}); } });Rust// Call the procedure without a callback conn.procedures().fetch_external_data(url); // Call the procedure with a callback for the result conn.procedures().fetch_external_data_then(url, |ctx, result| { match result { Ok(data) println!(Got result: {:?}, data), Err(error) eprintln!(Error: {:?}, error), } });Unreal// Call the procedure without a callback Context.Procedures-FetchExternalData(url, {}); // Call the procedure with a callback for the result FOnFetchExternalDataComplete Callback; BIND_DELEGATE_SAFE(Callback, this, AMyActor, OnFetchComplete); Context.Procedures-FetchExternalData(url, Callback); // Callback function (must be UFUNCTION) UFUNCTION() void OnFetchComplete(const FProcedureEventContext Ctx, const FString Result, bool bSuccess) { if (bSuccess) { UE_LOG(LogTemp, Log, TEXT(Got result: %s), *Result); } else { UE_LOG(LogTemp, Error, TEXT(Error)); } }注意 C# 与 Rust 版本都提供了无回调调用与带回调调用两种形态Unreal 则通过委托delegate绑定完成结果回调回调函数必须是UFUNCTION。View视图模块中的每个 View 都会生成类型定义对应视图返回行类型订阅接口用于订阅视图结果查询方法访问缓存的视图结果更新回调当视图拥有已知主键时生成对应更新回调on_update/OnUpdate/onUpdate。View 是建立在数据之上的可订阅、可计算的查询。由于 View 在客户端视角与表几乎一致代码生成时 View 被转换为TableDef再走表的生成路径见 lib.rs 的 generate_view_file这也是为什么 View 能像表一样挂在DbConnection上、支持更新回调的原因。重新生成绑定每当修改模块的 schema 或函数签名后都需要重新运行spacetime generate。该命令会覆盖输出目录中已存在的生成文件。由于生成文件带有固定的自动生成标记前缀CLI 还能识别出上一轮生成、但本轮不再生成的过期文件并询问是否删除源码中对应 run_prepared_generate_configs 的文件清理逻辑。需要特别强调生成代码不会随模块变更自动更新。如果正在积极开发迭代建议把spacetime generate纳入构建或开发工作流例如 TypeScript 模板的package.json中把spacetime build/spacetime publish写成 npm scripts可仿照此模式加入 generate 脚本避免客户端拿到过期绑定。使用生成代码生成绑定之后就可以连接数据库并开始交互。后续路线连接 SpacetimeDB建立客户端连接SDK API 参考使用生成绑定的完整 API语言专属参考Rust、C#、TypeScript、Unreal。仓库中还提供了大量可直接运行的示例templates/目录下的basic-ts、basic-rs、basic-cs、basic-cpp、chat-react-ts等模板以及modules/目录下的sdk-test-*系列模块如 sdk-test-view-pk、sdk-test-procedure、sdk-test-connect-disconnect它们都演示了模块 生成绑定 客户端调用的完整闭环是学习绑定用法的绝佳素材。疑难解答找不到模块目录Missing module directory如果spacetime generate找不到你的模块请确认--module-path指向的是包含模块工程文件Cargo.toml、.csproj或package.json的目录。源码在找不到模块时会明确报错并给出三种补救建议见 prepare_generate_run_configs 中的校验传入正确的--module-path在spacetime.json中配置module-path改用--bin-path/--js-path直接指向已构建产物跳过源码构建。绑定过期Outdated bindings如果客户端看不到新增的表或 reducer请确认在更新模块之后重新运行了spacetime generate。如前文所述生成代码不会在模块变化时自动更新此外还需留意 schema 迁移对旧客户端的影响——自动迁移文档明确指出客户端不会自动感知 schema 变化删除表等破坏性变更要求客户端使用重新生成的新绑定后再重连见 自动迁移文档。私有表没有生成默认情况下只有公共表/公共函数会进入生成结果这是刻意的可见性控制对应 CodegenVisibility::OnlyPublic。如果确实需要可加--include-private让私有表也参与生成类型本身始终包含但请评估在客户端暴露内部结构的风险。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表