【译文】构建 HTML 工具的实用设计模式

HTML 工具HTML 工具

原文链接:Useful patterns for building HTML tools

作者:Simon Willison

我开始使用”HTML 工具”这个术语来指代自己开发的 HTML 应用程序——这些应用将 HTML、JavaScript 和 CSS 整合到单个文件中,从而提供实用功能。过去两年间我已构建了超过 150 个此类工具,其中几乎全部由大型语言模型(LLMs)编写。本文将分享我在实践中总结的一系列实用模式。

首先,举几个例子来说明我所指的内容:

  • svg-render 将 SVG 代码渲染为可下载的 JPEG 或 PNG 图片。
  • pypi-changelog 让你生成(并复制到剪贴板)不同 PyPI 包版本之间的差异对比 (diffs)。
  • bluesky-thread 为 Bluesky 上的讨论串提供嵌套视图。

这些是我最近最喜欢的几款。我还有几十个类似的,经常使用。

你可以在 tools.simonwillison.net 上浏览我的收藏——按月查看功能便于浏览整个收藏库。

若需查看代码和提示词,本文中几乎所有示例的页脚都包含指向 GitHub 的”查看源代码”链接。GitHub 提交记录通常包含提示词本身,或指向用于创建该工具的脚本文件的链接。

解构 HTML 工具

以下是在构建此类工具时,我发现最高效的特征和模式:

  1. 保持单个文件:将 JavaScript 和 CSS 内联在同一个 HTML 文件中,意味着在托管或分发它们时遇到的麻烦最少。至关重要的是,这意味着你可以直接从大语言模型(LLM)的回复中将它们复制粘贴出来。
  2. 避免使用 React,或者任何需要构建步骤的东西。React 的问题在于 JSX 需要构建步骤,这使得一切都变得极其不方便。我通常会在提示词中加上 “no react”(不要用 React),直接跳过这整个复杂体系。
  3. 从 CDN 加载依赖。依赖越少越好,但如果有成熟的库能解决问题,我会很乐意从 CDNjs、jsDelivr 等 CDN 加载它。
  4. 保持工具小规模。几百行代码意味着可维护性不再是大问题:任何一个靠谱的 LLM 都能读懂它们的逻辑,而在 LLM 的帮助下从头重写一个工具只需要几分钟。

最终结果就是,几百行代码,可以干净地复制粘贴进 GitHub 仓库。

使用 Artifacts 或 Canvas 进行原型开发

构建这类工具最简单的方法,是直接从 ChatGPT、Claude 或 Gemini 开始。这三者都支持直接生成并展示一个 HTML + JavaScript 应用。

Claude 把这个功能叫 Artifacts, ChatGPT 和 Gemini 把它叫 Canvas。Claude 默认启用该功能,而 ChatGPT 和 Gemini 可能需要在工具菜单中手动开启。

你可以在 Gemini 或 ChatGPT 中试试这个提示词:

构建一个 canvas,让我粘贴 JSON,并把它转换为 YAML。不使用 React。

在 Claude 中可以这样写:

构建一个 artifact,让我粘贴 JSON,并把它转换为 YAML。不使用 React。

我总是在提示词里加上 “No React”,否则它们往往会用 React 构建,导致文件更难复制粘贴、也更难在别处使用。我发现使用 React 的尝试通常:加载时间更长(因为还要跑构建步骤),更容易出现崩溃性 bug,尤其是在 ChatGPT 中。

使用编码代理(coding Agent) 处理更复杂的项目

像 Claude Code、Codex CLI 这样的编码代理有一个明显优势:它们可以在编写过程中 自己测试代码,比如通过 Playwright。当我在做更复杂的项目时(例如前面提到的 Bluesky 线程查看器),我通常会升级使用这些工具。

我也经常使用异步编码代理(比如 Claude Code for web)来修改已有工具。我曾在《Building a tool to copy-paste share terminal sessions using Claude Code for web》中分享过相关视频。

Claude Code for web 和 Codex Cloud 可以直接操作我的 simonw/tools 仓库,这意味着它们可以通过 Pull Request 发布或升级工具,我甚至不需要自己复制粘贴任何代码。

从 CDN 加载依赖

只要工具里需要额外的 JavaScript 库,我都会选择从 CDN 加载。

三大 LLM 平台在 Artifacts / Canvas 功能中都支持特定 CDN,所以如果你说 “使用 PDF.js”,它们通常能自动生成可用的 CDN URL。有时你需要自己去 cdnjs 或 jsDelivr 查找 URL,然后手动贴进对话。

这些 CDN 已经存在很多年了,只要 URL 中明确包含版本号,我对它们的可靠性是非常放心的。相较之下,使用 npm 并引入构建步骤会显著降低我快速试验工具的效率,也让自托管变得更复杂。

把工具托管在别的地方

我不喜欢把 HTML 工具留在 LLM 平台上托管,原因有几个:首先,LLM 平台通常在非常严格的沙盒中运行工具:这样往往无法加载外部 URL 的数据或图片,有时甚至连跳转外部链接都被禁用。其次,最终用户体验也通常不佳:新用户会看到各种警告,加载速度慢,平台还会频繁展示自身的推广信息。此外,它们的可靠性也不如传统静态托管。如果 ChatGPT 或 Claude 出现故障,我仍然希望能访问我之前构建的工具。

正因如此,我才坚持 不使用 React + 使用 CDN:没有构建步骤,就意味着可以非常轻松地拷贝粘贴进行自托管。

我最常用的是 GitHub Pages:只需要把 HTML 粘贴到 GitHub 上的一个文件中,几秒钟后就能通过一个永久 URL 访问。我的大多数工具都放在 simonw/tools 仓库中,并通过 tools.simonwillison.net 提供静态访问。

充分利用复制与粘贴

复制与粘贴是 HTML 工具中极其重要的输入 / 输出方式。

我经常构建这样的工具:接收粘贴的内容,对其进行转换,再让用户复制结果到剪贴板。

在手机上操作复制粘贴比较麻烦,所以我经常加一个 “复制到剪贴板” 按钮,一键完成操作。

大多数操作系统的剪贴板可以同时保存多种格式的数据。这就是为什么你粘贴数据到 Word 里,数据能保持格式,而粘贴到文本编辑器时只会得到纯文本。JavaScript 的粘贴事件同样可以访问这些富数据格式,
这为 HTML 工具打开了大量可能性。

一些示例:

构建调试工具

构建有趣 HTML 工具的关键,在于理解浏览器到底能做什么。而自制调试工具,是探索这些能力的最佳方式。

clipboard-viewer 是我最常用的工具之一:你可以向它粘贴任何内容(文本、富文本、图片、文件),它会逐一展示剪贴板中所有可用的数据类型。

这个工具帮助我发现了大量“不可见”的数据,并成为我构建许多其他工具的基础。

其他调试示例:

  • keyboard-debug:显示当前按下的按键及 KeyCode
  • cors-fetch:检查某个 URL 是否支持 CORS
  • exif:显示选中图片的 EXIF 信息

将状态持久化到 URL

HTML 工具通常无法使用服务器数据库,但实际上你可以直接把大量状态存储在 URL 中。

这非常适合需要书签或分享的工具。

icon-editor:这是我做的一个 24×24 图标编辑器,它会把正在编辑的图标状态完整存储在 URL 中,方便你保存或分享。


使用 localStorage 存储密钥或更大状态

浏览器的 localStorage API 允许 HTML 工具在用户设备上持久保存数据,而无需暴露给服务器。

我通常用它来存储:不适合放在 URL 中的大型状态,API Key 等敏感信息。即便是静态托管,服务器日志也可能不是我能完全控制的,所以我绝不会把密钥放到服务器上。

  • word-counter:写作时自动保存进度,防止意外关闭页面丢失内容
  • render-markdown:我经常用它写博客,不希望内容丢失
  • haiku:让用户输入 Claude API Key,并存储在 localStorage 中,用来生成关于摄像头画面的俳句

收集支持 CORS 的 API

CORS(跨域资源共享)决定了一个网站上的 JavaScript是否可以请求另一个域名上的 API。

开放 CORS 的 API 是 HTML 工具的金矿。非常值得长期积累。

我常用的一些:

  • iNaturalist:获取动物目击记录和照片
  • PyPI:获取 Python 包信息
  • GitHub:公共仓库内容可以通过 raw.githubusercontent.com 访问,自带 CDN 缓存,几乎不用担心限流
  • Bluesky
  • Mastodon(如 phanpy.social 使用的接口)

我尤其喜欢 GitHub Gists:它们允许你通过跨域 API 调用,把状态持久化到一个永久 Gist 中。


通过 CORS 直接调用 LLM

OpenAI、Anthropic 和 Gemini 都提供了可通过 CORS 直接调用的 JSON API。

问题在于:你仍然需要 API Key,而如果把 Key 写在 HTML 里,任何人都可以偷走并产生费用。

因此我会结合 localStorage 存储密钥 的模式来使用。体验确实不够好(用户需要自己去申请 Key 再粘贴进来),但它是可行的。


不要害怕打开本地文件

你不需要把文件上传到服务器,就可以通过 <input type="file"> 使用本地文件。JavaScript 可以直接读取文件内容,这解锁了大量功能。

  • ocr:使用 PDF.js + Tesseract.js 在浏览器中对 PDF 和图片进行 OCR
  • social-media-cropper:打开或粘贴图片,并裁剪为各平台所需尺寸
  • ffmpeg-crop:在浏览器中预览视频,拖动裁剪框,生成 ffmpeg 命令

提供可下载文件

HTML 工具可以在不依赖服务器的情况下生成可下载文件。JavaScript 生态中有大量库可生成各种格式的文件。


Pyodide:在浏览器中运行 Python

Pyodide 是一个将 Python 编译为 WebAssembly、并直接在浏览器中运行的发行版。这是一个工程奇迹,也是 Python 世界中最被低估的项目之一。

它可以从 CDN 加载,这意味着在 HTML 工具中使用它几乎没有理由拒绝。

更棒的是,Pyodide 自带 micropip,可以通过 CORS 从 PyPI 加载纯 Python 包。

  • pyodide-bar-chart: 演示了如何在浏览器中直接运行 Pyodide、Pandas 和 matplotlib 来绘制条形图。
  • numpy-pyodide-lab:是针对 Numpy 的实验性交互式教程。
  • apsw-query:演示了在浏览器中运行的 APSW SQLite 库,利用该库展示 SQLite 查询的 EXPLAIN QUERY 执行计划。

WebAssembly 打开更多可能性

Pyodide 的存在本身就得益于 WebAssembly。这意味着大量原本用其他语言编写的软件,现在都可以在 HTML 工具中使用。

Squoosh.app 是我见过的首个令我深信此模式强大之处的实例。它让多个业界顶尖的图像压缩库直接在浏览器中即可使用。

我自己的工具中也使用了 WebAssembly:

  • ocr:Tesseract.js 和 Tesseract OCR 引擎。
  • sloccount:将 Perl / C 工具移植到浏览器
  • micropython:更轻量级的 Python 运行方案

重组你以前的工具

拥有一个包含 100+ 工具的公共集合,最大的优势在于:LLM 可以轻松重组它们。

有时我会将之前的工具复制粘贴到当前上下文中,但当我与编码助手协作时,可以直接通过名称调用它们——或者在助手开始工作前,指示它搜索相关示例。

任何可运行的工具源代码,同时也是实现特定功能的清晰文档,其中包含使用编辑库的模式。当大型语言模型(LLM)的上下文中存在一两个现有工具时,它生成可运行代码的可能性会大幅提高。


记录提示词和对话记录

我喜欢保留并公开我和 LLM 的所有交互记录,以便持续提升自己使用它们的能力。对于 Claude Code、Codex CLI 或其他编码工具,我会将终端中的完整对话记录复制粘贴到终端转 HTML 工具中,然后通过 Gist 分享该内容。

无论哪种情况,当我将完成的工具保存到我的代码库时,都会在提交信息中附上这些记录的链接。您可以在我的 tools.simonwillison.net 网站查看这些链接。


去构建吧

在过去一年半里,我用这种方式探索 LLM 的能力,并从中获得了巨大的乐趣。这种构建方式不仅帮助我理解了 HTML 的潜力,也让我更深入理解了 LLM 的能力边界。

如果你也想开始自己的工具集合,我强烈推荐你这样做。你只需要一个免费的 GitHub 仓库,并启用 GitHub Pages,就可以开始复制粘贴你生成的 .html 文件了。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注