Union App2 前端应用开发指南:Nix 构建、资源命名规范与图标管理
本篇指南以 Union 仓库中 app2/README.md 为骨架,系统讲解 Union 桥协议 Web 前端应用(app2)的本地开发启动、生产构建、前后端资源的分层命名规范以及图标组件的添加流程。读完本文后,你可以直接使用 Nix 命令拉起开发服务器与生产构建产物,并按照仓库既定的 Schema → Query → Store → Component 分层约定在项目中组织新资源,理解每个约定在 app2/src 目录中的真实落点。
项目定位:app2 是什么
从 app2/package.json 可以确认,app2 是一个基于 SvelteKit + Svelte 5 的 Web 应用,核心工程栈包括:
- 状态与错误处理:
effect系列依赖(@effect/experimental、@effect/opentelemetry、@effect/platform),以及仓库内的 workspace 包@unionlabs/effect-svelte; - 链交互 SDK:workspace 依赖
@unionlabs/sdk、@unionlabs/sdk-cosmos、@unionlabs/sdk-evm、@unionlabs/sdk-sui,配合@cosmjs/*、viem、@mysten/sui、starknet、@wagmi/core等实现多链接入; - 钱包与账户:
@keplr-wallet/types、@leapwallet/types、@safe-global/safe-apps-sdk等; - 数据接口:
gql.tada+graphql+graphql-request,生成的类型位于app2/src/generated/下(1 个.ts与 1 个.graphql文件)。
从 app2/src/lib/queries 与 app2/src/lib/stores 中现有的 chains、channels、clients、packets、transfers、statistics、tokens 等资源命名可以推断,app2 承载的是 Union 协议的转账、桥通道/客户端状态、统计与质押(stake)等前端视图。项目的代码约定细节另见 app2/CONVENTIONS.md(Effect 错误处理、Svelte 5 语法、Tailwind 样式等规范)。
使用 Nix 启动开发服务器
README 给出的开发入口是:
nix run .#app2-dev-server -L
这条命令对应的定义在 app2/app2.nix 中的 apps.app2-dev-server 属性里。该 Nix 属性通过 pkgs.writeShellApplication 生成一个 shell 应用,其执行逻辑按顺序为:
${ensureAtRepositoryRoot}:断言当前处于仓库根目录;cd app2/进入应用目录;- 导出一组
PUBLIC_*环境变量(详见下表); pnpm install安装依赖;pnpm run dev --host,即调用 app2/package.json 中定义的vite dev并开启主机访问。
构建/开发共用的环境变量(同样定义在 app2/app2.nix 中):
| 环境变量 | 取值来源 | 作用 |
|---|---|---|
PUBLIC_LOG_TOKEN |
Nix 内硬编码的公共 token | OpenTelemetry 日志上报鉴权 |
PUBLIC_LOG_ENDPOINT |
https://logs.union.build/app(见 Nix 源码注释用途) |
前端遥测上报端点 |
PUBLIC_GIT_REV |
gitShortRev(仓库当前短提交号) |
标识构建自哪个 revision |
PUBLIC_LAST_MODIFIED_DATE / PUBLIC_LAST_MODIFIED_EPOCH |
lastModifiedDate / lastModified |
标识构建时间 |
PUBLIC_SUPABASE_URL |
https://api.dashboard.union.build |
Supabase(dashboard)API 地址 |
PUBLIC_SUPABASE_ANON_KEY |
Nix 内嵌的 anon JWT | Supabase 匿名访问凭证 |
PUBLIC_DEPLOYMENTS_JSON |
builtins.readFile ../deployments/deployments.json |
内嵌部署清单,供前端识别各网络环境 |
开发环境的依赖集合(deps)定义在 app2/app2.nix:python3、stdenv.cc、pkg-config、nodePackages_latest.nodejs、pnpm_10 与 imagemagick。
除了 dev-server,app2.nix 还注册了几个与前端工程密切相关的辅助命令:
app2-check-watch(app2/app2.nix):pnpm run check --watch --threshold error,即持续运行svelte-kit sync && svelte-check(见 app2/package.json),用于开发时实时做 TypeScript/Svelte 类型检查;app2-grafana(app2/app2.nix):通过 Docker 运行grafana/otel-lgtm镜像(映射3001:3000、4317:4317、4318:4318),用于本地调试前端 OpenTelemetry 遥测数据;app2-sync-logo(app2/app2.nix):用 Inkscape 把site/public/u.svg渲染为 192/512 尺寸的app2/static/web-app-manifest-*.png;app2-fetch-schema(app2/app2.nix):通过gql.tada从 GraphQL 服务端拉取 schema 并生成app2/src/generated/schema.graphql与app2/src/generated/graphql-env.d.ts,是gql.tada类型化查询的刷新入口。
适用前提:以上命令都依赖 Nix flake 环境(仓库根目录的 flake.nix 提供了
ensureAtRepositoryRoot、buildPnpmPackage等共享定义),-L参数用于查看构建日志。
生产构建:nix build .#app2
README 给出的生产构建命令:
nix build .#app2 -L
对应的 packages.app2 定义在 app2/app2.nix,基于 buildPnpmPackage 派生,关键配置如下:
- workspace 边界:
extraSrcs将app2、effect-svelte、ts-sdk、ts-sdk-cosmos、ts-sdk-evm、ts-sdk-sui六个目录纳入构建源;pnpmWorkspaces则声明了app2与@unionlabs/effect-svelte、@unionlabs/sdk(-cosmos/-evm/-sui)这 6 个 pnpm workspace 成员。也就是说,app2 的构建总是连同仓库内的四个 TypeScript SDK 一起安装、编译,保证前端与 SDK 版本严格同步。 buildPhase:先导出上表所列的全部PUBLIC_*环境变量,然后执行pnpm --filter=app2 --filter=effect-svelte prepare(触发 package.json 中svelte-kit sync)与pnpm --filter=app2 build(vite build)。checkPhase:doCheck = true,在构建后运行pnpm --filter=app2 check,即svelte-check --tsconfig ./tsconfig.json,类型检查不通过则构建失败(Nix 源码中留有 TODO,计划把 warning 也纳入失败判定)。installPhase:mkdir -p $out后把./app2/build/*拷入输出目录,nix build产物即为 Vite/SvelteKit 的静态构建输出。- 构建使用固定哈希
hash = "sha256-lAmG8o1FH/cBu5Wqqb8Dxm34cRuxKfwk1w3jRP7WGDk="锁定 pnpm store,保证可复现构建。
不依赖 Nix 时,也可以在 app2/ 目录内直接使用 package.json 的脚本:pnpm run dev(vite dev)、pnpm run build(vite build)、pnpm run check(svelte-check)、pnpm run lint(eslint)、pnpm test(vitest)、pnpm run knip(死代码检测)。但注意:直接调用这些脚本时需要自行准备上表中的 PUBLIC_* 环境变量,否则前端在运行期拿不到部署清单与遥测配置。
资源命名规范:Schema → Query → Store → Component
这是 README 中最核心的工程约定。以「从 API 拉取一个 Block 资源、需要全局存储并展示」为例,README 规定各层的命名与落盘位置如下:
| 层 | 命名 | 约定路径 |
|---|---|---|
| 数据 Schema(类型定义) | Block |
src/lib/schemas/block.ts |
| 查询函数 | blockQuery |
src/lib/queries/block.ts |
| 状态 Store 类 | BlockStore |
src/lib/stores/block.svelte.ts |
| Store 的具体实例 | block |
与类同文件导出 |
| 展示组件 | BlockComponent |
src/lib/components/data/block-component.svelte |
用仓库现状核对这套分层
这套命名与 app2/src/lib/stores/block.svelte.ts 的实际实现完全吻合,全文仅 10 行,是理解约定的最佳样本:
import type { Block } from "@unionlabs/sdk/schema"
class BlockStore {
data: Option.Option<typeof Block.Type> = $state(Option.none())
error: Option.Option<FetchDecodeError> = $state(Option.none())
}
export const block = new BlockStore()
可以看到三个约定细节:
BlockStore类 + 小写单例block:与 README 的「类叫BlockStore,实例叫block」逐字对应;- Store 字段一律用 Effect 的
Option建模:data与error都是Option.Option<...>,配合$state()变成 Svelte 5 响应式状态——这与 app2/CONVENTIONS.md 中「用Option<T>代替string | null | undefined」的类型安全要求一致; - Store 文件名带
.svelte.ts后缀:因为文件内使用了$state()等 Svelte runes,需要 Vite 的 Svelte 编译管线处理。app2/src/lib/stores 下的balances、wallets、packets、transfers、statistics等 store 均遵循同一命名模式。
对应地,app2/src/lib/queries 目录下存在 chains.svelte.ts、channels.svelte.ts、clients.svelte.ts、statistics.svelte.ts、tokens.svelte.ts、transfer-list.svelte.ts、packet-details.svelte.ts 等查询模块,以及 fragments/ 下的 packet-list-item.ts、transfer-list-item.ts 两个 GraphQL 片段,验证了「一个资源对应一个 query 模块、公共字段抽 fragment」的查询层组织方式。
一处现状演进说明:README 示例中的
src/lib/schemas/block.ts路径在当前仓库中已不存在该目录;从 block.svelte.ts 的import type { Block } from "@unionlabs/sdk/schema"可以推断,资源的 Schema 类型已下沉到 workspace 包@unionlabs/sdk中由 store 直接引用。新增资源时仍按 README 的分层思路组织 Query/Store/Component,Schema 优先复用@unionlabs/sdk导出的类型。
UI 组件命名规范
README 约定:UI 基础组件统一放入 src/lib/components/ui/ 目录,例如 Button 放在 src/lib/components/ui/button/index.svelte。
对照 app2/src/lib/components/ui 的当前文件结构,实际采用的是扁平化文件命名:Button.svelte、Card.svelte、Modal.svelte、Tabs.svelte、Tooltip.svelte、Skeleton.svelte、Label.svelte、DateTimeComponent.svelte、ProgressBar.svelte、Input.svelte、Switch.svelte 等。从源码结构看,组件目录由 README 描述的「每组件一个目录 + index.svelte」演化为「一组件一 PascalCase 文件」,但「基础 UI 组件集中在 components/ui/ 并按组件名命名」这一核心约定保持不变;新增 UI 组件时应以目录现状(components/ui/XxxName.svelte)为准。
组件层面的书写约定在 app2/CONVENTIONS.md 中有明确要求,与命名规范配合使用:
- 所有 UI 组件必须接受
classprop 以便外部样式定制,并用cn()工具(clsx+tailwind-merge,见 package.json 依赖)合并类名,标准写法为const { children, class: className = "", ...rest }: Props = $props(); - 展示错误统一用
components/model/ErrorComponent.svelte,展示日期时间用components/ui/DateTimeComponent.svelte,卡片/标签/骨架屏复用Card/Label/Skeleton,不要自造; - 使用 Tailwind
zinc色板代替gray,用flex gap-*代替space-x-*。
如何添加一个图标
README 给出的图标添加流程(以 Icones 图标的 Material Sharp 集合为来源):
- 在 Icones 图标平台搜索 sharp 集合(
collection/ic?s=sharp); - 点选目标图标;
- 点击 Components > Svelte 生成 Svelte 组件代码;
- 保存为独立 Svelte 文件放入 app2/src/lib/components/icons,命名规则:
ic:sharp-banana→SharpBananaIcon.svelte,即把ic:sharp-前缀转为Sharp、把短横线分段转为 PascalCase,并以Icon.svelte结尾。
app2/src/lib/components/icons 目录现状印证了这一规则:其中存在大量按此规则命名的文件,如 SharpTransferIcon.svelte、SharpWalletIcon.svelte、SharpStakeIcon.svelte、SharpPacketsIcon.svelte、SharpChannelsIcon.svelte、SharpClientsIcon.svelte、SharpDashboardIcon.svelte,以及项目自有图标(AirdropIcon.svelte、EscherLogo.svelte 等)。新增图标后按 Svelte 常规方式 import { SharpBananaIcon } from "$lib/components/icons/SharpBananaIcon.svelte" 使用即可。
关键文件索引
| 内容 | 路径 |
|---|---|
| 本文对应的原始文档 | app2/README.md |
| Nix 包与应用定义(dev-server、check-watch、grafana、fetch-schema) | app2/app2.nix |
| 脚本与依赖声明 | app2/package.json |
| 代码约定(Effect 错误处理、Svelte 5 语法、样式) | app2/CONVENTIONS.md |
命名规范样本:BlockStore 类与 block 单例 |
app2/src/lib/stores/block.svelte.ts |
| 查询层 | app2/src/lib/queries |
| Store 层 | app2/src/lib/stores |
| 基础 UI 组件 | app2/src/lib/components/ui |
| 图标组件 | app2/src/lib/components/icons |
| gql.tada 生成物目录 | app2/src/generated |
要点回顾:app2 的开发与构建都以 Nix flake 属性为统一入口(nix run .#app2-dev-server -L / nix build .#app2 -L),构建时会把四个 workspace SDK 一起纳入编译并强制通过 svelte-check;新增一个「资源」时,按 blockQuery(src/lib/queries/)→ BlockStore/block(src/lib/stores/xxx.svelte.ts,字段用 Option + $state() 建模)→ 展示组件(src/lib/components/)的分层命名组织代码;新增图标则按 ic:sharp-xxx → SharpXxxIcon.svelte 的规则放入 src/lib/components/icons。掌握这三条主线,即可在 app2 中按仓库既有惯例安全地扩展前端功能。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00