本文讨论可公开验证的通用设计方法,所有示例均为演示场景。
为什么“本地处理”值得当作架构承诺
开发者工具经常要处理敏感内容:一段含 token 的 JWT、一个带内网地址的配置 JSON、一条从日志里抠出来的报错。如果这类输入会被发送到服务端,工具的每一次粘贴都是一次信任消耗。
本地优先(local-first) 的承诺很简单:输入在浏览器里产生,在浏览器里处理,在浏览器里消失。它不是一句文案,而是一组可以逐条验证的工程约束。
约束清单
一个合格的本地工具页,至少要满足这五条:
- 没有上传路径。处理逻辑只存在于页面脚本中,页面不包含任何把用户输入写进
fetchbody 的代码。 - 持久化只针对偏好,不针对内容。字号、主题这类偏好可以进
localStorage;用户粘贴的内容最多进sessionStorage(关标签即失效),或者干脆只放在内存里。 - 日志脱敏。前端
console与埋点里不出现原始输入。最稳妥的做法是工具页根本没有埋点。 - 错误信息不携带数据。解析失败时提示“无法解析”而不是把输入原样打出来——错误提示本身也可能成为泄露面。
- 可以断网使用。这是验证前四条最直接的方式:拔掉网络,工具应该一切照常。
构建期索引:搜索也可以不出服务器
站内搜索通常需要一个“服务端”。但内容量在几百条以内的站点,完全可以在构建期把公开内容的标题、摘要、标签导出成静态 JSON:
// 构建脚本(Node):扫描内容集合 → 生成 search-index.json
const items = collections.flatMap((collection) =>
collection.entries
.filter((entry) => !entry.draft)
.map((entry) => ({
type: collection.name,
title: entry.title,
href: entry.href,
excerpt: entry.description,
tags: entry.tags,
})),
);
await writeFile('public/search-index.json', JSON.stringify({ items }));
页面端用模糊匹配(子序列打分即可)在内存里完成检索。这样做的额外收益:
- 草稿天然隔离:只导出非草稿内容,
draft: true的条目从源头就不会进入索引,避免了“草稿泄漏到搜索”这一整类问题。 - 无查询成本:没有搜索服务,也就没有搜索服务的限流、日志和故障。
- 索引即产物:索引随构建生成、随部署更新,“内容改了索引没更新”这一类陈旧问题被流水线消灭。
需要注意的边界:索引里只能放公开元数据(标题、摘要、标签),不要把正文全文塞进去——索引 JSON 是任何访客都能下载的静态文件。
用 DevTools 快速复核
发布前可以做一个快速自查,在 DevTools 的 Network 面板里:
- 打开工具页,粘贴一段测试内容,执行所有操作。
- 过滤 XHR/Fetch 请求:除页面自身资源外,不应该出现任何携带输入内容的请求。
- 检查
localStorage/sessionStorage:确认只有偏好键,没有内容键。 - 断网重测一遍核心功能。
四步都干净,“输入不出浏览器”才算成立。
小结
本地优先不是少写了后端,而是把信任模型改了:用户不需要相信服务器会删除数据,因为数据从未到达服务器。对个人开发者的工具站来说,这既是伦理选择,也是实打实的架构简化——没有上传路径,就没有为上传路径负责的成本。