首页
/ Union App2 前端应用开发指南:Nix 构建、资源命名规范与图标管理

Union App2 前端应用开发指南:Nix 构建、资源命名规范与图标管理

2026-09-05 13:23:32作者:何将鹤

本篇指南以 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/suistarknet@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/queriesapp2/src/lib/stores 中现有的 chainschannelsclientspacketstransfersstatisticstokens 等资源命名可以推断,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 应用,其执行逻辑按顺序为:

  1. ${ensureAtRepositoryRoot}:断言当前处于仓库根目录;
  2. cd app2/ 进入应用目录;
  3. 导出一组 PUBLIC_* 环境变量(详见下表);
  4. pnpm install 安装依赖;
  5. 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.nixpython3stdenv.ccpkg-confignodePackages_latest.nodejspnpm_10imagemagick

除了 dev-server,app2.nix 还注册了几个与前端工程密切相关的辅助命令:

  • app2-check-watchapp2/app2.nix):pnpm run check --watch --threshold error,即持续运行 svelte-kit sync && svelte-check(见 app2/package.json),用于开发时实时做 TypeScript/Svelte 类型检查;
  • app2-grafanaapp2/app2.nix):通过 Docker 运行 grafana/otel-lgtm 镜像(映射 3001:30004317:43174318:4318),用于本地调试前端 OpenTelemetry 遥测数据;
  • app2-sync-logoapp2/app2.nix):用 Inkscape 把 site/public/u.svg 渲染为 192/512 尺寸的 app2/static/web-app-manifest-*.png
  • app2-fetch-schemaapp2/app2.nix):通过 gql.tada 从 GraphQL 服务端拉取 schema 并生成 app2/src/generated/schema.graphqlapp2/src/generated/graphql-env.d.ts,是 gql.tada 类型化查询的刷新入口。

适用前提:以上命令都依赖 Nix flake 环境(仓库根目录的 flake.nix 提供了 ensureAtRepositoryRootbuildPnpmPackage 等共享定义),-L 参数用于查看构建日志。

生产构建:nix build .#app2

README 给出的生产构建命令:

nix build .#app2 -L

对应的 packages.app2 定义在 app2/app2.nix,基于 buildPnpmPackage 派生,关键配置如下:

  • workspace 边界extraSrcsapp2effect-sveltets-sdkts-sdk-cosmosts-sdk-evmts-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.jsonsvelte-kit sync)与 pnpm --filter=app2 buildvite build)。
  • checkPhasedoCheck = true,在构建后运行 pnpm --filter=app2 check,即 svelte-check --tsconfig ./tsconfig.json,类型检查不通过则构建失败(Nix 源码中留有 TODO,计划把 warning 也纳入失败判定)。
  • installPhasemkdir -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()

可以看到三个约定细节:

  1. BlockStore 类 + 小写单例 block:与 README 的「类叫 BlockStore,实例叫 block」逐字对应;
  2. Store 字段一律用 Effect 的 Option 建模dataerror 都是 Option.Option<...>,配合 $state() 变成 Svelte 5 响应式状态——这与 app2/CONVENTIONS.md 中「用 Option<T> 代替 string | null | undefined」的类型安全要求一致;
  3. Store 文件名带 .svelte.ts 后缀:因为文件内使用了 $state() 等 Svelte runes,需要 Vite 的 Svelte 编译管线处理。app2/src/lib/stores 下的 balanceswalletspacketstransfersstatistics 等 store 均遵循同一命名模式。

对应地,app2/src/lib/queries 目录下存在 chains.svelte.tschannels.svelte.tsclients.svelte.tsstatistics.svelte.tstokens.svelte.tstransfer-list.svelte.tspacket-details.svelte.ts 等查询模块,以及 fragments/ 下的 packet-list-item.tstransfer-list-item.ts 两个 GraphQL 片段,验证了「一个资源对应一个 query 模块、公共字段抽 fragment」的查询层组织方式。

一处现状演进说明:README 示例中的 src/lib/schemas/block.ts 路径在当前仓库中已不存在该目录;从 block.svelte.tsimport 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.svelteCard.svelteModal.svelteTabs.svelteTooltip.svelteSkeleton.svelteLabel.svelteDateTimeComponent.svelteProgressBar.svelteInput.svelteSwitch.svelte 等。从源码结构看,组件目录由 README 描述的「每组件一个目录 + index.svelte」演化为「一组件一 PascalCase 文件」,但「基础 UI 组件集中在 components/ui/ 并按组件名命名」这一核心约定保持不变;新增 UI 组件时应以目录现状(components/ui/XxxName.svelte)为准。

组件层面的书写约定在 app2/CONVENTIONS.md 中有明确要求,与命名规范配合使用:

  • 所有 UI 组件必须接受 class prop 以便外部样式定制,并用 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 集合为来源):

  1. 在 Icones 图标平台搜索 sharp 集合(collection/ic?s=sharp);
  2. 点选目标图标;
  3. 点击 Components > Svelte 生成 Svelte 组件代码;
  4. 保存为独立 Svelte 文件放入 app2/src/lib/components/icons,命名规则:ic:sharp-bananaSharpBananaIcon.svelte,即把 ic:sharp- 前缀转为 Sharp、把短横线分段转为 PascalCase,并以 Icon.svelte 结尾。

app2/src/lib/components/icons 目录现状印证了这一规则:其中存在大量按此规则命名的文件,如 SharpTransferIcon.svelteSharpWalletIcon.svelteSharpStakeIcon.svelteSharpPacketsIcon.svelteSharpChannelsIcon.svelteSharpClientsIcon.svelteSharpDashboardIcon.svelte,以及项目自有图标(AirdropIcon.svelteEscherLogo.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;新增一个「资源」时,按 blockQuerysrc/lib/queries/)→ BlockStore/blocksrc/lib/stores/xxx.svelte.ts,字段用 Option + $state() 建模)→ 展示组件(src/lib/components/)的分层命名组织代码;新增图标则按 ic:sharp-xxxSharpXxxIcon.svelte 的规则放入 src/lib/components/icons。掌握这三条主线,即可在 app2 中按仓库既有惯例安全地扩展前端功能。

登录后查看全文
热门项目推荐
相关项目推荐