最近开发者社区里有一个很典型的讨论: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。如果涉及敏感信息,浏览器端存储就必须格外谨慎。

这里有几条底线:

  1. 不把长期 API Key 写进前端代码。
  2. 不把敏感数据默认塞进 localStorage
  3. 保存的数据要带版本号。
  4. 给用户导入、导出和清空数据的入口。
  5. 旧版本数据迁移失败时,不要静默覆盖。

一个简单的数据结构可以这样设计:

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 层检查一遍:

  1. 需求是否清楚
  2. 职责是否分开
  3. 数据是否可迁移
  4. 安全边界是否明确
  5. 失败是否可见
  6. 交付路线是否匹配真实需求

能过这 6 关,它才真正从 Demo 走向了工具。