从意图到架构
规划的本质
大部分技术文档是倒着写的。它们展示最终方案,隐藏思考过程。这让规划看起来像是突然出现的,像是某种天赋。
但真实的过程不是这样的。好的方案不是想出来的,而是从问题域中自然流出的。
规划是一个场。信息进入,意图凝结,策略环绕,路径收敛。整个过程类似行星形成:从星云到恒星,从可能到必然。
差距即设计空间
有了 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
编码前的思考
软件设计不是从代码开始的。在动手之前,有一套思维框架值得遵循:理解边界、定义数据、设计函数、建立抽象。
调用链路追踪法:从断点到根因
功能失效的背后,是一条完整的调用链路。追踪这条链路,定位断点,才能从根本上解决问题。
依赖注入
依赖注入不是关于框架或工具,而是关于控制权的转移。理解这个转移,就理解了软件设计的核心原则。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
引入懒加载模式
懒加载不是优化技巧,而是关于时机的选择。何时创建,决定了系统的效率和复杂度。
队列、可靠性与系统边界
探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。