用 Wasp 构建 Trello 风格看板应用:Waspello 全栈实现与实战解析
2026/9/13 2:38:32 网站建设 项目流程

用 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/wasp

Waspello 是 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 Spec(@wasp.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, ], });

逐项解读这份声明:

  • namewasp.version:定义应用名(waspello)以及声明所要求的最低 Wasp 版本(0.26.0),这保证了声明语法与生成器行为的兼容性;
  • title:从项目根目录的appTitle.txt读取应用标题,演示了在声明阶段读取本地文件的用法;
  • auth:声明认证配置——用户实体为User,启用usernameAndPassword(用户名 + 密码)与google(Google OAuth)两种方式,并设置认证失败时重定向到/login
  • client.rootComponent:将 examples/waspello/src/Layout.tsx 指定为客户端根组件,用于包裹所有页面(例如渲染导航栏);
  • spec:注册路由与页面,route("MainRoute", "/", page(MainPage, { authRequired: true }))表示首页路径/对应MainPage,且该页面要求登录后才能访问。

认证相关的登录、注册路由被拆分到了 examples/waspello/src/auth/auth.wasp.ts:

import { 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.jsxGoogleAuthButton.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且记录创建者author
  • ListCard都带有一个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,并为对应实体生成客户端调用函数(useQuerycreateCard等)。这也是“跨用户实时更新”的实现基础: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 } } }),让“非本人列表”的更新影响行数为 0;deleteList则先findUnique校验list.userId === context.user.id,再级联删除该列表下的所有卡片;createListCopy在复制列表时还会用Promise.all逐张复制其中的卡片。卡片的操作实现见 examples/waspello/src/cards/cards.js,updateCard同样先校验卡片归属再执行更新。

拖放排序的实现原理:浮点位置与二分插值

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

由于posFloat类型,这种策略可以在不重写整列位置的前提下完成任意次插入与移动,这也是看板类应用常见的排序方案。PositionProvider通过 React Context 把getPosOfNewItemgetPosOfItemMovedWithinListgetPosOfItemInsertedInAnotherListAfter等计算函数提供给列表与卡片组件。

拖放完成后,前端调用由 Wasp 生成的客户端操作(updateList/updateCard,从wasp/client/operations导入)把新的poslistId提交到后端,后端校验归属后更新数据库,随后 Query 失效触发重新拉取,整个流程闭环。MainPage.jsx中针对卡片跨列表移动与同列表内移动分别调用calcNewPosOfDndItemInsertedInAnotherListcalcNewPosOfDndItemMovedWithinList,并区分了BOARD(列表级)与CARD(卡片级)两种拖放类型。

本地开发:从数据库到运行的五步流程

按照 examples/waspello/README.md 的说明,本地跑起 Waspello 需要以下步骤:

  1. 安装 Wasp 依赖:先执行wasp install,让 Wasp CLI 准备好项目所需的工具链与依赖。
  2. 启动数据库:Waspello 使用 PostgreSQL,最简单的方式是用 Docker 在本地拉起一个 Postgres:
    wasp start db
  3. 执行数据库迁移:在另一个终端运行:
    wasp db migrate-dev

    这会根据 examples/waspello/schema.prisma 生成 Prisma Client 并应用迁移(历史迁移文件位于 examples/waspello/migrations)。

  4. 配置环境变量:将env.server复制为.env.server并填入实际值。其中必须包含DATABASE_URL(指向wasp start db启动的本地 Postgres),若启用 Google 登录还需配置对应的 OAuth 凭据(服务端配置方式可参考 web/docs/project/env-vars.md)。
  5. 启动开发服务器
    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安装浏览器依赖,再以DEBUG=pw: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 并自动获得类型安全与客户端调用、HttpError+context.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询