ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

civitai SvelteKit 应用标准:Svelte 5 Runes、shadcn-svelte 与 Kysely 的工程化实践指南

civitai SvelteKit 应用标准:Svelte 5 Runes、shadcn-svelte 与 Kysely 的工程化实践指南 civitai SvelteKit 应用标准Svelte 5 Runes、shadcn-svelte 与 Kysely 的工程化实践指南【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文以仓库根目录 docs/svelte-app-standard.md 为骨架围绕apps/moderator、apps/auth、apps/creator-studio三个 SvelteKit 应用SvelteKit 5 Kysely shadcn-svelte Tailwind v4展开从 Svelte 5 runes 的状态与异步数据模型到civitai/ui组件库的使用约束、样式规范、服务端分层、单元测试与多 Agent 评审流程。读完你将掌握这套标准中每一个用真金白银踩过坑的约定以及它们对应的源码级证据。这套标准从哪来、约束谁仓库根目录的 CLAUDE.md 描述的是主Next.js 应用Mantine、tRPC、Prisma而 SvelteKit 应用是另一条技术栈SvelteKit 5 Kysely shadcn-svelte Tailwind v4。为了避免两套约定互相污染docs/svelte-app-standard.md成为所有 SvelteKit 应用的唯一共享标准三个应用的CLAUDE.md都只记录自己真正不一样的地方apps/moderator/CLAUDE.md本文件未在仓库快照中列出但apps/auth、apps/creator-studio均指向该标准——审核工作台apps/auth/CLAUDE.md——登录、OAuth、会话与会话注册中心明确标注predates most of the standard即它早于大部分标准而存在未遵循之处属于待补的 gap而不是豁免apps/creator-studio/CLAUDE.md——创作者工作室额外声明This app formats itself它用独立的 Prettier 3 prettier-plugin-svelte自管格式pnpm -F civitai/creator-studio-app format因为根格式化器是 Prettier 2.8.8两个大版本对 TypeScript 的处理不一致混跑会互相覆盖。标准本身也承诺某个应用暂时没遵循某条约定那是下次动到该文件时要补齐的缺口而不是该应用的例外。Svelte 5只用 runes所有组件一律使用 runes没有export let、没有$:、没有为组件局部状态使用 store。数据流是$props()接收、$state()承载、$derived()派生let { userId, form }: { userId: number; form: FormResult } $props(); let expanded $state(false); const visible $derived(expanded ? rows : rows.slice(0, 5));异步数据派生 promise绝不往$state里赋值这是三个应用里最高频复现的 bug在$effect里 fetch 并把结果赋给$state会得到卡死的 spinner、重跑循环或旧响应落到新查询上。正确姿势是把 promise 放进$derived让模板用{#await}消费!-- Do -- const signals $derived( browser ? fetch(/api/user-signals/${userId}).then((r): PromiseSignals { if (!r.ok) throw new Error(String(r.status)); return r.json(); }) : null ); {#await signals} p classtext-sm text-dark-2Checking…/p {:then result} … {:catch} p classtext-sm text-red-300Could not load security signals./p {/await}要点拆解新的userId会产生新 promise模板自动重新 await因此不存在会变旧的 statebrowser守卫让 SSR 不发请求避免服务端渲染时发出 HTTP 请求每个{#await}都必须有{:catch}——没有 catchrejection 会被静默吞掉面板永远不会填充内容写入之后要重取用版本计数器?v${version}触发重建——它属于 derived 表达式的一部分promise 会随之重建。数据不是来自load时不要用invalidateAll()。$effect的正确用途是与 Svelte 之外的东西同步订阅、命令式 API、prop 变化时重置本地镜像它不是数据获取 hook也不是计算值。需要用 prop 初始化$state时用untrack()。key 是正确性问题不是 lint 规则{#each rows as row (row.id)}中的 key 一旦缺失或重复循环会复用错误的 DOM 节点导致一行上的操作按钮被接错到另一行。自然键不唯一时就组合出唯一键{#each accounts as acct (${acct.userId}:${acct.ip}:${acct.type})}更优的做法是在查询里直接选中一个真实主键而不是在模板里拼一个。单向传入的$bindableprop 会闩锁latchshadcn 包装组件把交互状态——checked、indeterminate、value、open——声明为$bindable底层原语在交互时会回写它。如果当作普通 prop 单向传入那次回写就成了子组件局部覆盖而 Svelte 只有在父组件表达式产出的值与上次推入的不同时才会丢弃它。结果任何交互后状态不变的场景控件会一直渲染成与你的数据相反的样子——穿过 re-render、穿过 reset 按钮。经典案例是三态复选框点击一个未选中off的框让它进入mixed全程checked保持false于是复选框本地闩锁在true与缓冲数据、变更集、服务端全部不一致。父组件拥有状态时一律使用函数绑定Checkbox bind:checked{() state on, () toggle(row)} bind:indeterminate{() state mixed, () {}} /setter 可以忽略参数——而且往往必须忽略因为原语会把点击 indeterminate 复选框解析为true照单全收就会变成总是授权而不是切换。该问题的源码证据见 checkbox.sveltechecked与indeterminate均为$bindable(false)且第 23-24 行对底层原语做了bind:checked/bind:indeterminate回写。⚠️svelte-check看不见这个问题只看 diff 的 review 也看不见——单向版本类型检查通过、读起来也正常。它当初是在apps/moderator的/admin页面上手动点击页面发现的2026-08-14。凡是三态或原语持有状态的控件收尾前必须亲自交互一遍。表单form actions use:enhance服务端变更一律用form actions并以use:enhance渐进增强——不用fetch JSON。自定义enhance回调会替换默认处理包括applyAction。必须自己调用它否则每个fail()都会被丢弃被拒绝的操作看起来和成功一模一样const afterAction () async ({ result }: { result: ActionResult }) { await applyAction(result); if (result.type success) { … } };同页多个面板提交到同一路由时共享同一个form对象因此每个失败都要打上 scope 标签每个面板只渲染属于自己的那部分——页面上的每个 action 失败都必须可见乐观更新必须在失败时回滚如果点击先把某行置灰或标记为已处理服务端答复不是 success 就要撤销否则操作者自己的操作记录就是错的而他们跳过的正是失败的那一项。其他约定用{#key}包裹任何持有局部状态、且必须在主体切换时重置的东西——打开着的确认弹窗不能因为搜索换了主体还残留着用 Snippets{#snippet}替代重复标记用 children 而非 slots事件绑定写onclick不写on:click。UI 组件以civitai/uishadcn-svelte原语为唯一来源统一使用 civitai/uishadcn-svelte原语。动手自造组件前先检查packages/civitai-ui/src/lib/components/shadcn 原语在ui/子目录手写共享组件与它平级如selection/。缺的组件要加到那个包里绝不加进某个 app原语npx shadcn-sveltelatest add name在packages/civitai-ui包内运行--overwrite会重新生成ui/所以ui/下的文件永远不要手改手写组件放在ui/的平级目录仓库中现有示例 selection-checkbox.svelte。包内现有原语非常齐全button、checkbox、dialog、select、dropdown-menu、tabs、table、tooltip、popover、sidebar、date-picker、range-calendar 等六十余个见 components 目录。消费方式import { Button } from civitai/ui/components/ui/button/index.js; import * as Dialog from civitai/ui/components/ui/dialog/index.js;配套约束用Select不用NativeSelectnative-select在包里存在但不是默认选择——它不吃主题旁边全是主题控件时它看起来就是一个浏览器原生控件其实现见 native-select.svelte大量硬编码的 Tailwind 类而非主题 token裸button/input只用于真正无样式的内联操作如行内的 revoke 链接任何读起来像控件的元素都用原语不用 Mantine不用clsx——类名合并用civitai/ui/utils.js导出的cn。样式Tailwind v4纯暗色标准要求 Tailwind v4 仅暗色主题。调色板 token 定义在 theme.cssToken色值用途text-dark-0#c1c2c5主要数值text-dark-1#a6a7ab——text-dark-2#8c8fa3正文与次级文本text-dark-3#5c5f66仅边框与禁用态对bg-dark-6对比度不足text-dark-4#373a40边框text-dark-5#2c2e33——text-dark-6#25262b面板背景要点正文用text-dark-2本能会去抓的text-dark-3在bg-dark-6上对比度不过关只当边框和禁用态用标题用text-white复用页面上已有的形状别发明间距面板就是rounded-xl border border-dark-4 bg-dark-6 p-5别给 button 加cursor-pointerTailwind v4 的 preflight 会把button的指针光标去掉civitai/ui的theme.css已经统一加回来了。源码证据见 theme.csslayer base中对button:not(:disabled)、[rolebutton]:not([aria-disabledtrue])、label:has(...)、summary统一恢复cursor: pointer禁用态给cursor: not-allowed。逐元素覆盖只会和它分道扬镳。组件放置页面级组件是page.svelte的兄弟文件放在路由目录里——这是默认$lib/components/只放被超过一个路由使用的东西——第二个消费者出现时才移过去不要预判页面局部 helper 和类型也放在兄弟模块里page.svelte超过约 150 行或容纳了不止一个面板的标记就该拆分。服务端分层page.server.ts的load负责读formactions负责写业务逻辑放$lib/server/Kysely builder 优先裸sql只用于 builder 表达不了的地方位掩码索引匹配、PG 函数、jsonb/LATERAL以及 Prisma schema 未建模的表外部 HTTP、以及任何未缓存但较慢的读都走/api/*由面板 fetch避免卡住首屏便宜的数据都放loadClickHouse 聚合读取是长期豁免项Creator Studio 的 analytics 标签页在load里读全部聚合——Redis 缓存$lib/server/cache、一次Promise.all并行发出、每个都带.catch(() null)一个面板挂了拖不垮整页。缓存命中是毫秒级读取SSR 的价值大于延迟。当读取已缓存且可按面板降级时照这个形状做两者都不满足时走/api/*每个 action 的输入都用 zod 校验每次变更按 owner 和 id 双重限定WHERE id ? AND userId ?0 行受影响按失败处理不按成功处理——对零写入报成功等于给一件没发生的事写了审计行action 的鉴权路径必须与页面本身一致组节点的授权是子节点的并集在父级上做 gating 会静默扩大可操作者范围。注释只做防破坏护栏按根 CLAUDE.md 的更严格版执行这些应用里的注释只有作为防破坏护栏breakage guard才有存在价值——一个不变量、一次类型断言、一个顺序要求、一处未来编辑会踩进去的坑。不要写叙述、不要写来源、不要写 ported from X、不要向 reviewer 解释你的工作——那些话放到 PR 里说。验证typecheck不 checkbuild 不是检查用typecheck绝不用check——两者都会跑svelte-kit sync与 dev server 的文件监听冲突曾因此把编辑器冻住一整天svelte-check的WARNING 行也要读state_referenced_locally是真 bug而且不会出现在任何别的地方绝不在.svelte文件里的函数签名中写可选参数n?: numberSvelte 5 的 TS 剥离会擦掉类型注解但保留?于是 rollup 收到非法 JS——只有build会失败typecheck是干净的、dev 能正常出页面、每个 review 都通过。用默认值n 0或显式联合e: SubmitEvent | null null代替。类型内部的?{ reset: (id?: string) void }没问题整个注解都会被擦除。正是这个不对称性要求交付前跑一次build——即便它不在编辑→验证循环里。只跑一次别当诊断循环用。曾有两个这样的 bug 进到生产环境没人发现因为本可能抓住它们的循环恰恰是被禁止跑的那个。测试每个应用都有自己的vitest.config.ts声明name: app:slug根配置按配置文件glob所以没有配置文件的应用会被静默排除。运行方式# 单个应用 pnpm --filter civitai/app test # 全部应用对应 CI 的 App unit tests job pnpm run test:apps:run根 package.json 中该脚本实现为vitest run --project app:*。app:前缀是承重墙每个应用同时也以civitai/*名义发布去掉name会让这套用例滑进 packages job。根 vitest.config.mts 的注释明确解释了这一点scripts/ci/assert-workspace-suites-ran.mjs会在应用 job 中检测这种回退。以 apps/moderator/vitest.config.ts 为实例environment: node、name: app:moderator、手动 alias$lib与$env/dynamic/*。这些是node 环境、纯模块的单元测试——没有 SvelteKit 管线所以$lib和$env虚拟模块在各自 app 的 config 里被 alias而一个模块只要碰了未 alias 的$app/*就根本没法被导入。路由逻辑可达从page.server.ts导入load/actions用它们读到的 event 切片直接调用。组件行为不可达——没有哪个 SvelteKit 应用有浏览器测试项目依赖use:enhance、bindings 或生命周期的东西靠 review 和打开页面来验证而不是靠测试。测试套件绝不能连接DATABASE_URL指向的任何东西要 mock 应用的 db 模块。确实需要真实 schema 时只 plan 不执行用 Kysely 的DummyDriver编译后发EXPLAIN不带ANALYZE校验列、join 和类型而不真正执行对写操作同样安全。用describe.skipIf(!hasDb)门控让没有数据库的检出环境也能跑其余用例。工作示例apps/moderator/src/test/explain-harness.ts原版在packages/civitai-db-queries。永远不要往不是你自己创建的 URL 写 fixtures。评审三段收尾前必跑一个 segment 的 diff 上跑三个 AgentAgent关注点svelte-correctness-review逻辑、数据形状、鉴权范围、失败路径svelte-idiom-reviewSvelte 5 惯用法 上述 UI/样式约定svelte-abstraction-review重复、缺失组件、放置位置每个都以应用目录为 scope并读该应用的CLAUDE.md了解局部差异。修完一个非平凡 bug、或抽取/改动共享组件之后再跑svelte-recurrence-sweep它拿一个已知缺陷跨三个应用找出所有同形状的地方。三个 review 都只盯着眼前的 segment一个页面里修好的 bug 会一直活在它的兄弟页面里直到有人恰好想起来——这正是apps/moderator里两个$effectbug 在隔壁修好后又存活数天的原因。有未决 findings 的 segment 不算完成只过了 typecheck 也不算——要看页面。typecheck 和 build 在大量渲染空白的页面上同样能通过。这三个评审是拿代码和它自己比看不见你没写过的东西因此缺失的能力能同时通过三者。从别处Retool、主应用移植时要加第四道拿构建产物与源比对——那是唯一能抓住某功能整个缺席的一关。小结docs/svelte-app-standard.md本质上是这个 monorepo 用真实故障换来的工程经验清单derive-the-promise 消灭陈旧数据与卡死 spinnerkeyed each、带{:catch}的 await、会闩锁的$bindable处理的是正确性而非风格civitai/ui Tailwind v4 暗色 token 保证三个应用视觉一致Kysely 优先 EXPLAIN 不执行的测试策略让读真实 schema不再危险而typecheck/build的不对称陷阱与app:前缀测试分组则把看似绿实则坏的验证盲区逐一堵上。对任何要在这三个应用里动代码的人来说这份标准不是建议而是验收条件。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表