用 Wasp 构建 Trello 风格看板应用Waspello 全栈实现与实战解析【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/waspWaspello 是 Wasp 官方仓库中一个 Trello 风格的看板Kanban Board应用示例它演示了如何用极少的样板代码构建一个中等复杂度的多用户实时应用——认证、查询与操作大多只在一个声明式的main.wasp.ts文件中完成。本文将以 examples/waspello/README.md 为骨架结合仓库源码深入讲解 Waspello 的声明式配置、数据模型、操作实现、拖放排序原理、本地开发流程与端到端测试帮助你掌握用 Wasp 快速落地一个真实全栈应用的整体套路。项目概览Waspello 做了什么Waspello 是一个典型的 Trello 风格应用支持多用户使用核心能力包括邮箱 / 密码认证并额外配置了 Google 社交登录多个看板每个看板包含若干列表List与卡片Card跨用户实时更新——查询Query在数据被修改Mutation后自动失效并重新拉取列表与卡片的拖放重排卡片上的图片附件。官方演示环境将前端部署在 Netlify、后端部署在 Fly.io但本地开发完全基于 Wasp CLI 完成。值得注意的是整个应用的主要逻辑声明集中在一个文件 examples/waspello/main.wasp.ts 中这正是 Wasp“声明式全栈”理念的直接体现。声明式核心一个文件定义认证、路由与应用配置Waspello 使用 Wasp 的 TypeScript Specwasp.sh/spec来声明应用。打开 examples/waspello/main.wasp.ts可以看到整个应用的骨架import { app, page, route } from wasp.sh/spec; import { readFile } from fs/promises; import MainPage from ./src/cards/MainPage with { type: ref }; import Layout from ./src/Layout with { type: ref }; import { authSpec } from ./src/auth/auth.wasp; import { cardsSpec } from ./src/cards/cards.wasp; export default app({ name: waspello, wasp: { version: 0.26.0 }, title: (await readFile(appTitle.txt, utf-8)).trim(), auth: { userEntity: User, methods: { usernameAndPassword: {}, google: {}, }, onAuthFailedRedirectTo: /login, }, client: { rootComponent: Layout, }, spec: [ route(MainRoute, /, page(MainPage, { authRequired: true })), authSpec, cardsSpec, ], });逐项解读这份声明name与wasp.version定义应用名waspello以及声明所要求的最低 Wasp 版本0.26.0这保证了声明语法与生成器行为的兼容性title从项目根目录的appTitle.txt读取应用标题演示了在声明阶段读取本地文件的用法auth声明认证配置——用户实体为User启用usernameAndPassword用户名 密码与googleGoogle OAuth两种方式并设置认证失败时重定向到/loginclient.rootComponent将 examples/waspello/src/Layout.tsx 指定为客户端根组件用于包裹所有页面例如渲染导航栏spec注册路由与页面route(MainRoute, /, page(MainPage, { authRequired: true }))表示首页路径/对应MainPage且该页面要求登录后才能访问。认证相关的登录、注册路由被拆分到了 examples/waspello/src/auth/auth.wasp.tsimport { page, route, type Spec } from wasp.sh/spec; import LoginPage from ./LoginPage with { type: ref }; import SignupPage from ./SignupPage with { type: ref }; export const authSpec: Spec [ route(SignupRoute, /signup, page(SignupPage)), route(LoginRoute, /login, page(LoginPage)), ];而看板业务相关的 Query / Action 则集中在 examples/waspello/src/cards/cards.wasp.ts两者通过spec数组合并到主文件中形成清晰的分模块组织方式。登录页与注册页实现分别位于 examples/waspello/src/auth/LoginPage.tsx 和 examples/waspello/src/auth/SignupPage.tsx配合EmailAndPassForm.jsx、GoogleAuthButton.jsx等组件完成 UI。数据模型User、List、Card 三实体关系Wasp 直接使用 Prisma 定义数据模型Waspello 的实体定义在 examples/waspello/schema.prisma 中datasource db { provider postgresql url env(DATABASE_URL) } generator client { provider prisma-client-js } model User { id Int id default(autoincrement()) lists List[] cards Card[] } model List { id Int id default(autoincrement()) name String pos Float user User relation(fields: [userId], references: [id]) userId Int cards Card[] } model Card { id Int id default(autoincrement()) title String pos Float list List relation(fields: [listId], references: [id]) listId Int author User relation(fields: [authorId], references: [id]) authorId Int }从模型可以看出数据源固定为 PostgreSQLDATABASE_URL通过环境变量注入List属于某个User一对多Card属于某个List且记录创建者authorList与Card都带有一个pos: Float字段用于在拖放排序时表示相对顺序详见下文“位置计算”一节。这套模型正是声明式 Query / Action 中entities声明与权限判断的基础。Query 与 Action声明实体依赖实现多租户隔离Waspello 的看板业务层只有两类文件声明文件 examples/waspello/src/cards/cards.wasp.ts 与实现文件 examples/waspello/src/cards/lists.js、examples/waspello/src/cards/cards.js。声明部分非常直观import { action, query, type Spec } from wasp.sh/spec; import { createCard, updateCard } from ./cards with { type: ref }; import { createList, createListCopy, deleteList, getListsAndCards, updateList, } from ./lists with { type: ref }; export const cardsSpec: Spec [ query(getListsAndCards, { entities: [List, Card] }), action(createList, { entities: [List] }), action(updateList, { entities: [List] }), action(deleteList, { entities: [List, Card] }), action(createListCopy, { entities: [List, Card] }), action(createCard, { entities: [Card] }), action(updateCard, { entities: [Card] }), ];每个query/action通过entities声明它依赖哪些实体Wasp 生成器据此自动注入context.entities并为对应实体生成客户端调用函数useQuery、createCard等。这也是“跨用户实时更新”的实现基础Wasp 会在客户端发起的 Mutation 成功后自动失效受影响的 Query从而触发重新拉取让不同用户看到的数据保持同步。实现端最值得学习的是“多租户隔离”的写法。以 examples/waspello/src/cards/lists.js 中的查询为例import { HttpError } from wasp/server; export const getListsAndCards async (args, context) { if (!context.user) { throw new HttpError(403); } return context.entities.List.findMany({ // We want to make sure user can get only his own info. where: { user: { id: context.user.id } }, include: { cards: true }, }); };要点在于每个操作都必须校验context.user存在否则抛出 403并在查询条件中把context.user.id作为过滤条件确保用户只能读到自己的数据。写入类操作同样严格例如updateList使用updateMany({ where: { id: listId, user: { id: context.user.id } } })让“非本人列表”的更新影响行数为 0deleteList则先findUnique校验list.userId context.user.id再级联删除该列表下的所有卡片createListCopy在复制列表时还会用Promise.all逐张复制其中的卡片。卡片的操作实现见 examples/waspello/src/cards/cards.jsupdateCard同样先校验卡片归属再执行更新。拖放排序的实现原理浮点位置与二分插值Waspello 的拖放体验基于hello-pangea/dnd详见 examples/waspello/package.json 中的依赖前端交互逻辑在 examples/waspello/src/cards/MainPage.jsx 中。核心技巧则是 examples/waspello/src/cards/PositionContext.jsx 实现的“位置计算器”新元素插入末尾时位置取当前最大pos加上固定间隔DND_ITEM_POS_SPACING2 ** 16 65536元素在列表内移动时新位置取相邻两个元素pos的平均值二分插值保证不改变其他元素的位置值空列表的初始位置为DND_ITEM_POS_SPACING - 1。由于pos是Float类型这种策略可以在不重写整列位置的前提下完成任意次插入与移动这也是看板类应用常见的排序方案。PositionProvider通过 React Context 把getPosOfNewItem、getPosOfItemMovedWithinList、getPosOfItemInsertedInAnotherListAfter等计算函数提供给列表与卡片组件。拖放完成后前端调用由 Wasp 生成的客户端操作updateList/updateCard从wasp/client/operations导入把新的pos和listId提交到后端后端校验归属后更新数据库随后 Query 失效触发重新拉取整个流程闭环。MainPage.jsx中针对卡片跨列表移动与同列表内移动分别调用calcNewPosOfDndItemInsertedInAnotherList和calcNewPosOfDndItemMovedWithinList并区分了BOARD列表级与CARD卡片级两种拖放类型。本地开发从数据库到运行的五步流程按照 examples/waspello/README.md 的说明本地跑起 Waspello 需要以下步骤安装 Wasp 依赖先执行wasp install让 Wasp CLI 准备好项目所需的工具链与依赖。启动数据库Waspello 使用 PostgreSQL最简单的方式是用 Docker 在本地拉起一个 Postgreswasp start db执行数据库迁移在另一个终端运行wasp db migrate-dev这会根据 examples/waspello/schema.prisma 生成 Prisma Client 并应用迁移历史迁移文件位于 examples/waspello/migrations。配置环境变量将env.server复制为.env.server并填入实际值。其中必须包含DATABASE_URL指向wasp start db启动的本地 Postgres若启用 Google 登录还需配置对应的 OAuth 凭据服务端配置方式可参考 web/docs/project/env-vars.md。启动开发服务器wasp start命令会同时启动客户端与服务器并自动运行 Vite 与 Node 服务打开浏览器即可注册账号、创建列表与卡片。如果遇到依赖或环境问题可参考仓库根目录的 README.md 了解 Wasp 的整体安装与使用方式。端到端测试Playwright 覆盖核心用户路径Waspello 用 Playwright 做端到端测试测试套件位于 examples/waspello/e2e-tests/tests包含helpers.ts封装注册、登录、随机凭据生成等工具与simple.spec.ts。按 README 说明运行全部 e2e 测试只需npm run test该命令定义于 examples/waspello/package.json会先执行playwright install --with-deps安装浏览器依赖再以DEBUGpw:webserver playwright test --config e2e-tests/运行测试。examples/waspello/e2e-tests/tests/simple.spec.ts 覆盖了认证与基础用法的完整链路注册新用户后应跳转到首页/点击退出按钮回到/login使用错误密码登录应看到 “Invalid credentials” 提示正确登录后依次创建列表Add a list→ 输入标题 →Add list、创建多张卡片Add a card→ 输入标题 →Add card、再创建第二个列表并逐一断言页面元素正确渲染。这套测试与 CI 集成在每个 PR 上运行保证了看板核心交互在迭代中不回归。小结从示例到实战的启示Waspello 用极少的样板代码展示了一条完整的“声明式全栈”路径main.wasp.ts声明应用与认证、schema.prisma定义数据模型、.wasp.ts声明 Query / Action 并自动获得类型安全与客户端调用、HttpErrorcontext.user完成权限隔离、浮点位置算法配合拖放库实现实时排序、Playwright 守护端到端质量。如果你正在规划自己的看板、项目管理或任何多用户协作类应用这个示例的声明结构与权限写法是可直接复用的最佳起点。【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考