我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境

作者:戈德斯文日期:2026/6/6

最近,我做了一个叫「摸鱼表格」的小项目。

它看起来像表格,却没有表头、公式、筛选和工作任务。这里有的,只是一面可以无限拖动、缩放和探索的公共格子墙。

任何人都能选一个空格,写下一句话、一个突然冒出的想法,或者把它当成临时树洞。

项目已经可以直接体验:

moyu-table.tangyuan.art

这篇文章不只展示成品。我想完整复盘一次:一个看似简单的互动想法,如何逐步变成拥有数据库、登录、部署、备份和公网入口的产品。

一、它像表格,但这里没有标准答案

传统表格围绕效率组织信息:表头定义含义,行列约束结构,公式负责计算。

我想保留的不是 Excel 外观,而是共享表格里偶尔出现的另一种体验:大家会在空白区域留言、接龙、拼字,甚至自然形成一些主题区域。

这种玩法最有趣的地方,是空间本身也成为内容。

坐标不只是数据库字段。一个人在 (0, 0) 留下欢迎语,另一个人在很远的位置写下近况,两句话之间的距离也会产生想象。

因此,产品从一开始就明确了几个边界:

  • 不复刻 Excel,不做公式、筛选和复杂表头。
  • 不做强实时协同编辑器。
  • 每个坐标最多只能写入一个格子。
  • 内容写入后永久锁定,不允许修改。
  • 匿名用户也能参与,降低第一步门槛。

这些限制看似减少功能,实际上是在保护产品最核心的体验:选择一个位置,然后留下某个时刻的自己。

二、核心交互:把坐标变成可探索的空间

项目的主体验不是传统 DOM 列表,而是一块 Canvas 画布。

Canvas 是浏览器提供的绘图画布。相比为每个格子创建一个 DOM 元素,它更适合绘制大面积网格、透视效果和大量动态元素。

项目中的坐标系统分成三层:

1单元格坐标:用户理解的整数坐标,例如 x: 3, y: -2
2世界坐标:Canvas 场景中的位置
3屏幕坐标:浏览器视口内的像素位置
4

单元格坐标的 y 轴向上递增,而屏幕坐标的 y 轴向下递增。两者不能直接混用,必须通过明确的转换函数连接。

1/**
2 * 把单元格 y 坐标转换为 Canvas 世界坐标。
3 */
4function cellToWorldY(cellY: number): number {
5  return -cellY
6}
7

这种看似基础的约束非常重要。如果坐标语义没有集中管理,拖拽、跳转、小地图和范围查询很快就会各自形成一套方向规则。

为了让空旷的格子墙不至于“能逛,但不知道去哪”,我后来增加了几种探索入口:

  • 随机发现:跳转到一个已有格子。
  • 最新写入:看看公共空间刚刚发生了什么。
  • 近邻发现:从当前位置继续向附近探索。
  • 坐标跳转:输入明确坐标,直接移动相机。
  • 小地图:观察内容密度和当前位置。

这些能力没有试图做推荐算法。它们只是为用户提供几个继续走动的理由,让一面空旷的墙逐渐产生现场感。

三、写入体验:每次发布都应当有一点重量

格子的写入流程并不复杂:选择内容类型、输入正文、选择色调,然后发布。

但因为格子写入后无法修改,编辑窗口必须帮助用户在发布前确认结果。

编辑区采用左右分栏:

  • 左侧输入 Markdown 源文本。
  • 右侧始终展示实时预览。
  • 工具栏支持加粗、引用、列表和分隔线。
  • 色板用于选择格子的视觉色调。

Markdown 是一种使用普通字符表达格式的轻量标记方式。例如,**文字** 表示加粗,- 文字 表示列表。

项目只支持少量格式,没有引入完整富文本编辑器。这是一个刻意的选择:格子应该适合留下一句话,而不是逐渐变成一篇排版复杂的文档。

每个格子的 Markdown 源文本最多存储 10,000 个字符。这个限制同时存在于前端输入、API 校验、领域规则和 PostgreSQL CHECK 约束中。

数据库约束是最后一道保护。即使未来某个接口绕过了前端校验,数据库仍会拒绝超限内容。

四、技术架构:让产品规则有明确归属

项目使用的主要技术栈是:

1Next.js App Router
2React + TypeScript
3Canvas
4PostgreSQL
5Prisma
6Vitest
7Tailwind CSS
8

Next.js 同时承载网页和 API。Prisma 是 ORM,也就是对象关系映射工具,它让 TypeScript 代码可以通过类型安全的接口读写 PostgreSQL。

代码按职责分成几个区域:

1src/
2├── domain/cells/   # 格子规则、坐标、内容、格式与探索算法
3├── data/           # Prisma 仓库、API 客户端与数据适配
4├── features/wall/  # Canvas、面板、交互与页面状态
5└── lib/            # 通用基础设施
6

领域层不依赖 React 或浏览器 API。像坐标转换、写入准备、内容限制和探索计算,都可以独立测试。

这种分层让 UI 不必承担所有规则,也让数据库仓库不需要理解画布交互。

坐标唯一性必须由数据库保证

用户点击空格时,前端会先检查该位置是否已经被占用。

但前端检查只能改善体验,不能保证最终正确。两个用户可能同时看到同一个空格,并在极短时间内一起提交。

因此,数据库通过唯一约束保证同一坐标只能有一个格子:

1model Cell {
2  id      String @id
3  x       Int
4  y       Int
5  content String
6
7  @@unique([x, y])
8}
9

最终写入冲突会由数据库明确拒绝。前端负责友好,数据库负责权威。

匿名写入也需要准入规则

项目允许匿名用户写入,但匿名不等于没有限制。

服务端会基于可信代理传来的客户端 IP,通过 HMAC 生成不可逆摘要,并按时间窗口限制写入次数。

HMAC 是使用服务端密钥生成摘要的方法。数据库只保存摘要,不直接保存用户原始 IP。

登录用户与匿名用户使用不同的写入额度。这样既能保留低门槛参与,也能减少公开写入接口被滥用的风险。

五、从“本机能跑”到真正可访问

开发环境跑通以后,我遇到一个很现实的问题:这个项目应该部署在哪里?

最后选择了一套有点特别、但很适合当前阶段的方案:

1正式域名
2   ECS 上的 OpenResty
3   ECS 上的 FRPS
4   本机 OrbStack 中的 FRPC
5   本机生产应用
6   独立生产 PostgreSQL
7

FRP 是一套内网穿透工具。FRPC 是客户端,运行在本机;FRPS 是服务端,运行在云服务器。FRPC 主动连接 FRPS,让公网域名可以访问本机服务。

本机同时存在开发环境和生产环境,但两者完全隔离:

1开发应用:localhost:3000
2开发数据库:localhost:5433
3
4生产应用:127.0.0.1:3007
5生产数据库:独立 Docker 网络,不暴露宿主机端口
6FRPS 出口:ECS 127.0.0.1:3008
7

生产环境由 Docker Compose 管理。Docker Compose 是用于统一定义和运行多个容器的工具。

生产栈包括三个长期运行的容器:

  • app:Next.js standalone 生产应用。
  • postgres:独立生产数据库。
  • frpc:连接 ECS 的内网穿透客户端。

所有生产容器使用 restart: unless-stopped。OrbStack 或电脑重启后,它们会自动恢复;手动停止后则保持停止。

为什么坚持独立生产数据库

开发数据库经常需要重置、写入测试数据、验证 migration。如果生产和开发共用数据库,一次普通调试就可能影响真实数据。

因此,生产 PostgreSQL 使用独立数据卷,且不映射宿主机端口。日常开发工具无法误连,应用只能通过 Compose 内部网络访问它。

每次生产部署前,脚本会自动执行 pg_dump 创建备份,并保留最近 14 份。

为什么使用标签部署

生产部署脚本不接受当前工作区,也不直接部署 main 分支,只接受已经推送到 GitHub 的 Git 标签。

标签是指向固定提交的版本标记。这样可以避免未提交代码意外进入生产,也确保当前运行版本可以重新构建。

部署流程大致如下:

1验证标签已推送
2 导出独立源码快照
3 启动并检查生产数据库
4 创建部署前备份
5 构建生产镜像
6 应用 migration
7 重启应用和 FRPC
8 验证本机入口与代理状态
9

六、开发过程中几个值得记录的坑

1. 数据库已有表,但没有 migration 历史

早期本地数据库已经存在部分表结构,但 Prisma 的 migration 历史表为空。

直接执行 prisma migrate deploy 会收到 P3005:数据库 schema 不为空,无法直接应用初始 migration。

解决方式不是重置数据库,而是先比较现有结构与目标结构,将已经真实存在的历史 migration 标记为已应用,再部署剩余 migration。

这个过程叫 baseline,也就是为已有数据库补齐迁移基线。

2. 面板定位尺寸不等于真实内容高度

格子阅读面板最初按固定高度定位。短内容正常,但登录提示出现后,真实内容高度超过定位高度,内容便从面板底部溢出。

最终方案是把面板拆成三部分:

1固定顶部:类型、标题、关闭按钮
2滚动中部:正文
3固定底部:坐标、时间、收藏与分享操作
4

面板宽度调整为 380px,最大高度根据视口动态计算。短内容自然收缩,长内容才出现滚动。

3. OpenResty 与 FRPS 的网络关系必须确认

1Panel 中的 OpenResty 与 FRPS 都使用 host 网络模式,因此 OpenResty 可以反向代理到 127.0.0.1:3008

如果 OpenResty 使用普通 Docker bridge 网络,容器里的 127.0.0.1 只代表容器自身,此时同样的代理配置就不会工作。

部署不能只看配置长什么样,还要确认进程究竟运行在哪个网络空间里。

4. 真实客户端 IP 不能盲目信任

应用使用 X-Real-IP 进行匿名写入限流。

OpenResty 必须覆盖写入该请求头,而不是原样信任客户端传来的值。否则访问者可以伪造 IP,绕过写入限制。

同时,FRPS 的 3008 端口不应该直接向公网开放。用户请求应该只经过域名的 80/443,再由 OpenResty 从 ECS 本机访问 FRPS。

七、测试不是最后补上的清单

项目目前使用 Vitest 编写自动化测试,覆盖领域规则、数据仓库、API 路由、面板行为和架构约束。

最近修复阅读面板溢出时,我使用了红-绿-重构的 TDD 循环:

1RED:先写“关闭按钮能取消选中”的失败测试
2GREEN:实现最小关闭行为
3
4RED:写“长正文与固定操作区分离”的失败测试
5GREEN:拆分滚动正文区
6
7RED:写“380px 宽度且不越过视口”的失败测试
8GREEN:调整定位模型
9

TDD 是测试驱动开发,即先用失败测试描述用户可观察行为,再写最少代码使测试通过。

它的价值不只是增加覆盖率。更重要的是,测试迫使我们先说清楚:用户最终应该看到什么,而不是直接沉入 CSS 细节。

八、现在,这面墙刚刚开始

我在 (0, 0) 写下了第一句话:

欢迎来到摸鱼表格。
这里没有表头,也没有标准答案。
选一个空格,留下一句话,让这面墙慢慢长出来。

现在整面墙依然很空。

但我期待有一天打开它时,会看到陌生人在很远的坐标留下近况。大家也许会自然发明留言、接龙、拼字和主题区域。

如果你想体验,可以访问:

moyu-table.tangyuan.art/

如果是你,会在这面墙的第一个格子里写什么?



我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境》 是转载文章,点击查看原文


相关推荐


Gateway 鉴权场景:网关统一鉴权 + 业务应用决定放行规则
超梦dasgg2026/5/30

目录 一、核心设计原则(一句话总结) 二、具体实现方案(最主流、最推荐) 1. 整体流程 2. 具体技术实现(Spring Cloud Gateway 为例) (1)业务服务:定义注解 + 扫描接口路由 (2)业务服务:启动时自动上报路由权限规则 (3)Gateway:拉取规则 + 全局过滤器统一鉴权 三、更轻量的简化方案(中小型项目常用) 四、为什么要这么设计?(面试加分回答) 五、面试标准回答(你可以直接背) 总结 这是微服务架构中最标准、最常用的鉴权方案:G


保姆级教程:零成本在本地跑AI大模型_Ollama
凤年徐2026/5/8

保姆级教程:零成本在本地跑 AI 大模型——Ollama 从安装到实战 手把手教你,用自己的电脑跑起来满血版 Qwen/DeepSeek/Llama,不需要 API Key,不需要云服务器 预计完成时间: 2-3 小时 所需技能: 会用命令行(3条命令够了) 适合人群: 想玩 AI 大模型但不想花钱、担心隐私泄露、喜欢折腾的同学 前言:为什么要在本地跑大模型? 用过 ChatGPT、DeepSeek 的同学应该知道,每次调用都是要花钱的——DeepSeek-V3 每次 A


我让 AI 当了回老师,把 Claude Code 从头到尾盘了一遍 🔥
LinDaiDai_霖呆呆2026/4/29

前言 你盼世界,我盼望你无bug。Hello 大家好,我是霖呆呆! 最近尝试在用 Claude Code 写项目,建仓库、修 bug、代码审查什么的都用它。但说实话,用是用了,总感觉自己就是个"面向弹窗编程"选手 —— 它弹窗我就点确认,它问我就说好,至于它到底是怎么运作的?权限模式有几种?Hooks 能干嘛?emmm...😅 直到我发现三元写了个 skill(名为 sigma) ,安装完后在 claudeCode 里使用 /sigma 你想学习的知识 命令,它就能变身 AI 1v1 家教,


拒绝低效!这款神器,让你的终端效率起飞 | 深度解析 fzf 终极指南
GetcharZp2026/4/20

还在手动敲 cd 和 ls?还在繁琐的 history 中翻找命令?是时候换个方式工作了。一篇文章带你彻底掌握命令行模糊找回神器 fzf,从安装到进阶玩法,助你效率翻倍! 身为开发者,我们每天大部分的时间都花在了终端(Terminal)里。不论是切换目录、搜索文件,还是翻阅历史命令,这些细碎的操作如果效率低下,积少成多便会吞噬掉大量专注力。 你是否也曾经历过: 想找一个深层目录下的文件,却记不清完整路径,只能不断 ls 确认? 按 Ctrl+R 搜索历史命令,结果搜出来的不是自己想要的?


OpenClaw实操指南13|用AI接管飞书多维表格:自动建表、写数据、做分析,一条指令搞定
Rubin智造社2026/4/12

飞书多维表格是很多团队的数据中枢——项目管理、内容选题、客户跟进、数据分析,全在里面。 但维护它是个体力活:手动建字段、逐条录数据、定期整理……重复劳动大量消耗精力。 这篇教程教你用OpenClaw的lark全套技能,把飞书多维表格的常见操作全部交给AI。 一条指令建表,一条指令批量写数据,一条指令做分析汇总。你只需要告诉AI你要什么,剩下的它来做。 核心概要 这篇解决什么问题? 安装并配置lark全套技能,实现飞书多维表格的AI自动化操作:建表、字段管理、数据读写、视图


别再把 LangChain 当成 API 胶水:Runnable 才是把 AI 流程工程化的关键接口
swipe2026/4/4

很多人第一次接触 LangChain,会把它理解成一组“帮你调模型”的工具类:PromptTemplate 负责拼 prompt,ChatOpenAI 负责调模型,OutputParser 负责解析结果。这样理解没错,但只对了一半。 真正到了工程里,问题很快就不是“怎么调一次模型”,而是“怎么把一条会持续演化的 AI 流程组织好”。 比如一个看起来简单的企业问答助手,往往很快就会长成这样: 先清洗用户问题 再决定这是闲聊、任务型问题,还是知识问答 不同类型走不同 prompt 有的分支要结构化


【35天从0开始备战蓝桥杯 -- Day6】
小年糕是糕手2026/3/26

🫧个人主页:小年糕是糕手 💫个人专栏:《C++》《Linux》《数据结构》《C语言》 🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来! 目录 一、进制转换 1.1、二进制转十进制 1.2、十进制转二进制 1.3、二进制转八进制 1.4、二进制转十六进制 1.5、原码、反码、补码 练习 1°10 进制转 x 进制 2°x 进制转 10 进制 3°进制转换1 4°进制转换2 二、位运算操作符 2.1、左移操作符 2.2、右移操


【养虾日记】Openclaw操作浏览器自动化发文
卷福同学2026/3/18

用QClaw操作本地浏览器,登录自媒体平台,实现自动发文 1.更新 Openclaw在3.13版本更新后,加入了Chrome DevTools MCP 官方支持,就是可以控制浏览器了。 Openclaw虽然自带内置浏览器,但是打开后完全没有登录信息和安装的扩展插件,这次升级后就能用上我们自己的浏览器了,比较方便。 这里小卷用QClaw做演示,如何让它操作浏览器干活 2.打开浏览器调试 我们需要用到Chrome浏览器 打开chrome://inspect/#remote-debugging


OpenClaw 卸载教程,一篇讲透
不惑_2026/3/10

有些朋友尝鲜体验后,觉得不太适合自己,想要完全卸载却不知道如何操作。以下是完整的卸载步骤: 1. 打开终端,输入以下命令: openclaw uninstall 2. 使用鼠标上下移动光标,按空格键勾选所有选项,然后按回车键确认。 3. 选择 yes 并按回车,此命令会自动删除 OpenClaw 的工作目录。 4. 卸载 npm 包: 如果使用 npm 安装:npm rm -g openclaw 如果使用 pnpm 安装:pnpm remove -g openclaw 如果使用 bun


弃用html2pdf.js,这个html转pdf方案能力是它的几十倍
刘发财2026/3/2

欢迎转载文章 在前端开发中,“把网页变成 PDF”是个老生常谈的需求。无论是生成发票、报告还是简历,用户总希望点一下按钮就能带走一份格式完美的文档。 目前主流的前端html转pdf方案是通过html2canvas将网页渲染成canvas,再通过jsPDF将canvas转换为pdf。代表方案就是 html2pdf.js,npm包周下载量达到了80万,为广大开发者所接受。但是因为它基于html2canvas和jsPDF,会有一些无法解决的问题,比如: 生成速度慢 生成的pdf文件体积大 生成的pd

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读