最近开发者社区里有一个很典型的讨论:AI 已经可以很快生成一个 HTML 小工具,甚至可以把它放到手机里运行。这个方向很有意思,因为它把「写代码」的门槛降得很低,但也把一个老问题重新摆到了台面上:Demo 能跑,不等于工具能长期用。
这篇文章不讨论某个具体产品,也不承诺某种方案在所有 iOS、Android、桌面浏览器里都无差异运行。我们只讨论一个后端程序员更关心的问题:当 AI 帮你写出一个单页 HTML 以后,怎样补齐需求、结构、数据、安全、测试和交付这几层工程能力,把它从一次性玩具变成一个真正能维护的本地小工具。
本文讨论的是无后端或轻后端的个人本地工具,不覆盖支付、医疗、身份认证等高风险生产系统。
先定义:什么叫「本地应用」
很多问题一开始就混在一起了。
你让 AI 写了一个 index.html,双击能打开,这是「单文件网页」。你希望断网也能访问,这是「离线 Web 应用」。你希望它能加到桌面、像 App 一样启动,这可能是 PWA。你希望它访问系统能力、进入应用商店、和原生能力深度集成,那可能要走 WebView 容器或原生实现。
这四件事不是同一件事:
| 形态 | 适合场景 | 典型风险 |
|---|---|---|
| 单文件 HTML | 验证需求、自用小工具 | 修改困难、数据易丢、兼容性靠感觉 |
| 离线 Web | 需要断网可用 | 缓存策略、资源依赖、更新机制 |
| PWA | 需要安装入口和离线体验 | 浏览器支持差异、调试成本 |
| WebView/原生容器 | 需要系统能力或分发 | 打包、权限、审核、长期维护 |
所以第一条原则是:不要一上来就问「怎么打包成 App」。先问它到底要解决什么问题,使用者是谁,数据放在哪,失败后应该怎样恢复。
第一层:把提示词变成规格
AI 很擅长根据一句话生成界面,但真实工具需要规格。规格不是复杂 PRD,而是一张最小需求卡。
可以先写成这样:
工具名称:本地 JSON 清洗器
输入:粘贴一段 JSON 字符串
输出:格式化、压缩、按字段排序、错误定位
数据规模:常见 100KB 内,极端 2MB
数据保存:默认不保存内容,只保存用户偏好
离线要求:必须能离线打开
设备范围:桌面浏览器优先,移动端只保证可查看
失败行为:JSON 错误时显示行列号,不清空原输入
隐私边界:不上传用户输入,不引入不必要的第三方脚本
这张卡能帮你判断:它需要数据库吗?需要登录吗?需要后端吗?需要上架吗?如果只是给自己用的 JSON 工具,答案大概率是不需要。它更需要的是稳定的输入处理、清晰的错误提示、可靠的数据边界。
很多 AI Demo 的问题不是功能少,而是没有边界。没有边界的工具,一旦功能变多,就会从「一个文件」变成「一团文件」。
第二层:把单文件拆出职责
单文件不是原罪。一个 200 行以内的小工具,完全可以用单文件完成。但如果代码开始出现这些信号,就该拆职责了:
- UI 事件和业务逻辑缠在一起
- 存储读写散落在多个函数里
- 同一个 DOM 选择器被复制很多次
- 错误提示靠
alert - 样式靠内联属性堆出来
- 修改一个按钮会影响三个功能
最低限度可以拆成四块:界面层、业务层、存储层、样式层。即使仍然放在一个 HTML 文件里,也可以在代码组织上先分层。
示例:
<script>
const storage = {
loadSettings() {
try {
return JSON.parse(localStorage.getItem("tool.settings") || "{}");
} catch {
return {};
}
},
saveSettings(settings) {
localStorage.setItem("tool.settings", JSON.stringify(settings));
}
};
function formatJson(input) {
const value = JSON.parse(input);
return JSON.stringify(value, null, 2);
}
function renderResult(text) {
document.querySelector("#result").textContent = text;
}
function renderError(error) {
document.querySelector("#error").textContent = error.message;
}
document.querySelector("#format").addEventListener("click", () => {
const input = document.querySelector("#input").value;
try {
renderResult(formatJson(input));
renderError({ message: "" });
} catch (error) {
renderError(error);
}
});
</script>
这段代码不复杂,但它体现了一个关键点:用户输入进入页面时,用 textContent 这类安全文本 API,而不是随手拼 innerHTML。AI 生成的代码经常为了省事把字符串直接塞进 HTML,这在处理不可信输入时会埋安全坑。
第三层:认真处理数据
本地工具最容易被低估的是数据。
如果数据只存在内存里,刷新就没了。如果存在 localStorage,它适合少量、非敏感、结构简单的数据,比如主题、最近选项、开关状态。如果数据量更大、结构更复杂,可能要考虑 IndexedDB。如果涉及敏感信息,浏览器端存储就必须格外谨慎。
这里有几条底线:
- 不把长期 API Key 写进前端代码。
- 不把敏感数据默认塞进
localStorage。 - 保存的数据要带版本号。
- 给用户导入、导出和清空数据的入口。
- 旧版本数据迁移失败时,不要静默覆盖。
一个简单的数据结构可以这样设计:
const CURRENT_VERSION = 1;
function createExportPayload(items, settings) {
return {
version: CURRENT_VERSION,
exportedAt: new Date().toISOString(),
items,
settings
};
}
function validateImportPayload(payload) {
if (!payload || payload.version !== CURRENT_VERSION) {
throw new Error("暂不支持这个数据版本");
}
if (!Array.isArray(payload.items)) {
throw new Error("导入数据缺少 items");
}
return payload;
}
别小看这几行。它们决定了工具半年后还能不能升级,决定了用户数据出问题时有没有退路。
第四层:补齐安全和隐私边界
AI Demo 常见的安全问题并不玄学,通常很朴素:
- 为了好看引入一堆 CDN,却没想过离线和供应链风险
- 直接把用户输入拼进 HTML
- 把 API Key 写在前端
- 默认把所有输入内容存到浏览器
- 错误日志里带出敏感信息
- 没有区分本地文件模式、localhost、线上 HTTPS 的差异
个人工具不等于可以忽略安全。越是「自己用」,越容易放松警惕,把真实数据、真实密钥、真实工作内容丢进去。
安全边界可以写成一段很明确的说明:
这个工具默认不上传用户输入。
这个工具不内置任何长期密钥。
用户可以手动导出和清空本地数据。
如果需要调用第三方 API,请通过后端代理或临时令牌方案处理。
这不是形式主义。它会反过来约束你的代码设计。
第五层:让失败可见
能跑的 Demo 往往只覆盖 happy path。真正的工具要回答这些问题:
- 用户输入空内容怎么办?
- 输入特别大怎么办?
- JSON 格式错了怎么办?
- 本地存储满了怎么办?
- 导入了旧版本数据怎么办?
- 断网时页面还能不能打开?
- 移动端按钮会不会点不到?
最小验证清单可以放在仓库里:
- [ ] 刷新页面后设置仍在
- [ ] 断网后核心功能仍可使用
- [ ] 输入空内容有明确提示
- [ ] 输入错误 JSON 不会清空原文
- [ ] 导入错误文件不会覆盖旧数据
- [ ] 2MB 输入不会卡死主流程
- [ ] 移动端竖屏下按钮和文本不重叠
- [ ] 清空数据前有二次确认
这里的重点不是追求复杂测试框架,而是不要靠「我刚才点了一下没问题」来判断一个工具能长期使用。
第六层:选择交付路线
到了最后,才该问交付方式。
只给自己用,可以是本地 HTML、浏览器书签、GitHub Pages 私有替代方案或内网静态服务。需要离线和桌面入口,可以评估 PWA,但要认真处理 manifest、service worker、缓存更新和浏览器差异。需要调用摄像头、文件系统、通知、后台任务等系统能力时,再考虑 WebView 容器或原生实现。
一个简单决策表:
| 需求 | 建议路线 |
|---|---|
| 自用、无敏感数据、功能很轻 | 单文件或静态页面 |
| 多设备访问、持续更新 | 静态站点 + 版本管理 |
| 离线可用、可安装入口 | PWA 评估 |
| 需要系统权限或商店分发 | WebView/原生容器 |
| 多用户、账号、同步、权限 | 重新设计后端架构 |
不要为了「像 App」而提前打包。很多工具真正需要的不是壳,而是规格、边界、数据和验证。
什么时候应该停下来重做
如果一个 HTML 小工具开始出现多人协作、账号体系、权限控制、敏感数据、复杂同步、支付或关键业务流程,就不要继续给 Demo 打补丁了。那时问题已经不是「AI 生成的代码怎么整理」,而是「这个系统该怎么设计」。
AI 让原型变快,这是好事。但工程的价值也因此更明显:把一个能跑的东西,变成一个可理解、可修改、可恢复、可交付的东西。
我的建议很简单:下次 AI 给你生成一个 HTML 小工具以后,先别急着打包。先用这 6 层检查一遍:
- 需求是否清楚
- 职责是否分开
- 数据是否可迁移
- 安全边界是否明确
- 失败是否可见
- 交付路线是否匹配真实需求
能过这 6 关,它才真正从 Demo 走向了工具。