从意图到架构
规划的本质
大部分技术文档是倒着写的。它们展示最终方案,隐藏思考过程。这让规划看起来像是突然出现的,像是某种天赋。
但真实的过程不是这样的。好的方案不是想出来的,而是从问题域中自然流出的。
规划是一个场。信息进入,意图凝结,策略环绕,路径收敛。整个过程类似行星形成:从星云到恒星,从可能到必然。
差距即设计空间
有了 Want(我想要什么)和 Have(我有什么),差距就显现了。这个差距不是障碍,而是设计的全部空间。
想要的:一个评论系统。用户可以在文章任意位置留下想法,可以在文末进行讨论。
拥有的:一个静态生成的博客。内容在构建时确定,页面在部署时就已经存在。
差距:静态内容与动态交互之间的鸿沟。
这个差距揭示了真正的问题:如何在构建时和运行时之间建立桥梁。
💡 Click the maximize icon to view in fullscreen
差距的形状决定了方案的形状。
意图井:问题的凝结
所有信息都进入这个场。功能需求、技术约束、用户期待、性能指标。它们在这里碰撞、混合、沉淀。
噪音在此消散。"评论需要富文本编辑器吗?""是否需要实时更新?""管理员审核功能的优先级?"这些问题暂时悬置。
真正的问题在此凝结:
核心挑战一:身份系统。评论需要知道"谁在说话"。 核心挑战二:数据持久化。评论需要被存储和检索。 核心挑战三:文本定位。行间评论需要准确标记在原文中的位置。
这三个挑战不是并列的。它们有轻重。
身份系统可以外包(Clerk)。数据持久化有成熟方案。但文本定位是独特的,是这个系统特有的复杂度。
意图核心形成:构建一个在静态页面上运行的、能准确定位文本片段的评论系统。
当你能用一句话说清楚"真正要解决的问题",意图就凝结了。
这句话不是功能列表,不是技术栈,而是问题的本质。
策略云:可能性的展开
围绕意图核心,可能性开始旋转。每条路径都是一种势能分布。
路径 A:全客户端
认证在客户端。评论存储在第三方服务。所有交互在浏览器完成。
这条路径的势能:低。实现简单,快速上线。 这条路径的代价:SEO 弱,首屏慢。 这条路径的边界:评论永远不会成为内容的一部分。
路径 B:混合渲染
认证在客户端。评论存储在自己的数据库。服务端 API 处理 CRUD。客户端组件处理交互。
这条路径的势能:中等。需要建立 API 层,需要数据库。 这条路径的代价:开发周期增加,基础设施复杂度上升。 这条路径的边界:评论可以被预加载,可以被索引,可以逐步优化。
路径 C:完全集成
Prisma ORM,tRPC 类型安全,WebSocket 实时更新,Redis 缓存。
这条路径的势能:高。企业级架构,可扩展性强。 这条路径的代价:开发周期长,维护成本高,过度工程风险。 这条路径的边界:适合大规模应用,对小博客来说是负担。
💡 Click the maximize icon to view in fullscreen
三条路径共存。此刻不急于坍缩,让势能场充分展开。
选择漏斗:势能的倾斜
场开始倾斜。不是人为选择,而是约束条件形成了势能差。
时间约束:7-12 天开发周期。路径 C 不现实。 资源约束:个人博客,没有运维团队。路径 C 的维护成本过高。 扩展性需求:未来可能增加功能。路径 A 的天花板太低。
势能场自然倾向路径 B。
混合渲染是平衡点。它不是最简单的,也不是最强大的,但它在当前约束下提供了最好的势能分布。
最优解不是选出来的,是场自然流向的。
当一个方案自然地解决了大部分问题,同时避开了大部分陷阱,它就是当下的最优解。
不需要完美,只需要合适。
理解边界
方案确定,设计开始。第一步是理解边界。
构建时边界:
- MDX 文章内容在此确定
- 静态 HTML 在此生成
- 路由结构在此固定
运行时边界:
- 用户登录状态在此确认
- 评论数据在此加载
- 交互逻辑在此执行
评论系统必须跨越这条边界。但跨越的方式有讲究。
不能在构建时处理评论数据(数据是动态的)。不能在运行时重新生成整个页面(会破坏静态生成的优势)。
正确的方式是:静态内容先渲染,动态评论后注入。两个世界通过 hydration 连接。
定义数据
边界清晰后,数据结构自然显现。
// 评论的本质
interface Comment {
id: string
userId: string // 来自 Clerk
postSlug: string // 关联到文章
content: string // 评论内容
createdAt: Date
parentId?: string // 支持回复
}
// 行间评论的特殊性
interface InlineComment extends Comment {
anchor: {
selectedText: string // 用户选中的文本
prefix: string // 前后上下文
suffix: string
offsetY: number // 视觉位置
}
}
数据结构不是凭空设计的,而是从问题域中提取的。
评论需要知道"谁说的"(userId),"在哪说的"(postSlug),"说了什么"(content)。行间评论额外需要知道"在哪个位置说的"(anchor)。
每个字段都对应一个真实需求。没有多余,没有遗漏。
行间评论最难的部分是 anchor。
文章内容可能更新,DOM 结构可能变化,用户的浏览器可能不同。如何在这些变化中依然准确定位?
答案是多重定位策略:
- 精确匹配(selectedText + prefix + suffix)
- 模糊匹配(允许轻微变化)
- CSS 路径回退
- 视觉位置估算
不依赖单一方法,而是建立定位的鲁棒性。
设计函数
数据确定后,操作数据的方式也确定了。
// 评论的生命周期
async function createComment(data: CreateCommentInput): Promise<Comment>
async function getComments(postSlug: string): Promise<Comment[]>
async function updateComment(id: string, content: string): Promise<Comment>
async function deleteComment(id: string): Promise<void>
// 行间评论的特殊操作
function createTextAnchor(selection: Selection): Anchor
function locateTextAnchor(anchor: Anchor): Position
function highlightText(range: Range, commentId: string): void
函数的接口不是随意设计的。它们直接对应数据的状态转换。
创建评论需要什么信息?CreateCommentInput。获取评论需要什么条件?文章的 postSlug。定位文本需要什么输入?用户的 Selection。
每个函数都是一个纯粹的意图:做一件事,把它做好。
建立抽象
最后一步是隐藏复杂度。
┌─────────────────────────────────────┐
│ UI 层 │
│ CommentSection, CommentForm │
│ 只关心"显示"和"交互" │
└─────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────┐
│ Hook 层 │
│ useComments(), useTextSelection() │
│ 管理状态和副作用 │
└─────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────┐
│ Service 层 │
│ CommentService, TextAnchorService │
│ 处理业务逻辑 │
└─────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────┐
│ API 层 │
│ fetch('/api/comments') │
│ 与后端通信 │
└─────────────────────────────────────┘
每一层都有清晰的职责。UI 层不知道数据如何存储,Service 层不知道数据如何渲染。
抽象的目的不是炫技,而是让每一层都能独立演化。未来换数据库?只改 API 层。未来改 UI 框架?只改 UI 层。
好的抽象是看不见的。它不增加理解成本,只减少耦合。
涌现的本质
回顾整个过程:
- 差距显现设计空间
- 意图凝结核心问题
- 策略展开可能路径
- 场域倾斜自然选择
- 边界约束设计形状
- 数据提取问题本质
- 函数定义状态转换
- 抽象隐藏实现细节
没有一步是凭空出现的。每一步都从前一步流出。
这不是线性过程,而是势能的流动。信息进入场域,意图成为引力源,策略围绕旋转,最优解自然涌现。
技术规划的本质不是计划,而是理解问题域的势能分布,然后让方案沿着最自然的路径流动。
试着不要马上跳到方案。
先让问题充分展开。 让所有可能性旋转一会儿。 看看势能场自然倾向哪里。
答案会自己出现。
Related Posts
Articles you might also find interesting
依赖注入
依赖注入不是关于框架或工具,而是关于控制权的转移。理解这个转移,就理解了软件设计的核心原则。
编码前的思考
软件设计不是从代码开始的。在动手之前,有一套思维框架值得遵循:理解边界、定义数据、设计函数、建立抽象。
代码的边界:从项目到包的重构实践
将博客系统打包为 npm 包的完整实践,聚焦边界定义、抽象层建立和务实的取舍决策
适配器模式:对现实的妥协
当 PayPro 要求 IP 白名单而 Stripe 不需要,当一个按秒计费另一个按请求计费,架构设计不是消除约束——而是管理约束。适配器模式不是优雅设计,而是对现实混乱的务实投降。
API 测试各种边界情况
边界情况是系统最脆弱的地方,也是最容易被忽略的地方。测试边界情况不是为了追求完美,而是为了理解系统的真实边界。
文档标准是成本计算的前提
API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。
调用链路追踪法:从断点到根因
功能失效的背后,是一条完整的调用链路。追踪这条链路,定位断点,才能从根本上解决问题。
离屏渲染:照片捕获为什么需要独立的 canvas
实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。
集中式配置:让 Reddit 组件脱离重复泥潭
当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。
约束驱动设计:为何选择内存追踪
在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。
CRUD 操作
四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑
让文档跟着代码走
文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。
双重导出管道的架构选择
在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。
端到端 Postback 模拟测试
真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。
错误隔离
失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。
Purikura的页面系统
通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。
重复数据的迁移实践:从 N 个文件到 1 个真相源
当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
实现幂等性处理,忽略已处理的任务
在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解
引入懒加载模式
懒加载不是优化技巧,而是关于时机的选择。何时创建,决定了系统的效率和复杂度。
多厂商 AI 调度:统一混乱的供应商生态
当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。
分布式 Workers 的解耦设计
通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。
队列、可靠性与系统边界
探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡
Studio 系统架构:从状态机到端到端流程
深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。