最近在做一个内部项目,需要频繁地把用户输入的内容转成 HTML 实体,再塞进模板里渲染。一开始我都是打开浏览器控制台,手写几行 replace 搞定,但用着用着发现几个痛点:
- 特殊字符一多,正则写得跟天书似的,回头自己都看不懂
- 解码的时候用
innerHTML直接塞,遇到</textarea>这种字面量直接翻车 - 每次都要打开 DevTools,复制粘贴,效率属实拉胯
于是想着,干脆做一个在线小工具,一劳永逸。反正就是个单页面,纯前端,不涉及后端,正好也试试最近很火的 AI 辅助编程到底靠不靠谱。
需求拆解:别一上来就写代码
跟 AI 协作的第一步,不是让它写代码,而是把需求想清楚。我花了几分钟把功能列了个清单:
- 编码:
<、>、&、"、'转成<这种实体 - 解码:反向操作,把实体还原成字符
- 实时转换:输入即输出,不用点按钮
- 全量编码:把非 ASCII 字符全转成
&#xxx;数字实体 - 辅助功能:复制、交换、清空、下载
这个清单看着简单,但里面有几个坑,后面跟 AI 对线的时候全踩了一遍。
第一轮对话:AI 的第一版代码,问题不大但全是细节
我把需求用自然语言描述给 Claude,让它生成一个单文件 HTML。第一版代码框架是对的,UI 布局、事件绑定、i18n 机制都有了,但细看之下有几个问题:
问题一:解码逻辑用 innerHTML 整段塞
AI 第一版是这么写的:
function decodeHTML(str) {
const el = document.createElement('div');
el.innerHTML = str;
return el.textContent;
}
看着挺简洁,但有个致命 bug:如果输入是 </textarea>,浏览器会把它解析成真正的 </textarea>,直接提前结束 RCDATA 元素,后面的内容全丢了。这在实际使用中太常见了——用户粘贴的代码片段里经常有这种字面量。
我让 AI 改成逐个实体解析,用正则匹配 &...; 再单独解码。AI 改完的版本是这样的:
function decodeHTML(str) {
return str.replace(/&(#\d+|#[xX][0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body) => {
if (body[0] === '#') {
const cp = (body[1] === 'x' || body[1] === 'X') ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
if (!Number.isFinite(cp) || cp < 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return m;
try { return String.fromCodePoint(cp); } catch (e) { return m; }
}
return decodeNamedEntity(body);
});
}
这个版本就好多了,String.fromCodePoint 处理增补平面字符,try/catch 兜底非法码点,0xD800-0xDFFF 过滤代理项。这些细节 AI 自己就考虑到了,省了我不少事。
问题二:全量编码的字符范围搞错了
AI 第一版用的是 /[^\x00-\xFF]/g,把所有非 Latin-1 字符都转义了。但需求里说的是"非 ASCII",应该是 /[^\x00-\x7F]/g。就一个字节的差别,行为完全不同——前者会把 é、ü 这种也转掉,后者只处理真正的非 ASCII。
我指出来之后,AI 秒懂,改成了 [^\x00-\x7F],还顺手加了个 /u 标志,让字符类按码点匹配,避免 emoji 被拆成两个非法代理项实体。这个细节我一开始都没意识到,AI 主动加的。
第二轮:i18n 和深色模式,AI 的模板功力
工具要发布到线上,面向的可能有海外用户,所以 i18n 和深色模式是硬需求。这块 AI 的模板能力很强,我只需要说"支持中英文切换,URL 参数 ?lang=en 优先,其次浏览器语言",它就给出了完整的方案:
const I18N = {
zh: { title:"HTML 编码/解码", encode:"编码", decode:"解码", ... },
en: { title:"HTML Encode/Decode", encode:"Encode", decode:"Decode", ... }
};
function detectLang() {
const p = new URLSearchParams(location.search);
if (p.get('lang')==='en') return 'en';
if (p.get('lang')==='zh') return 'zh';
return navigator.language.startsWith('zh')?'zh':'en';
}
深色模式更简单,CSS 变量 + prefers-color-scheme 媒体查询,几行代码就搞定。AI 在这种标准化场景下几乎不用返工,一次过。
第三轮:踩坑记录,AI 教我的那些细节
跟 AI 来回对线了四五轮,有几个坑值得记录一下:
坑一:textContent 和 innerHTML 的边界
编码方向我用的是 replace + 映射表,不涉及 DOM 操作,安全。但解码方向如果图省事用 innerHTML 整段塞,就会踩 RCDATA 的坑。AI 给的逐个解析方案虽然代码多了几行,但稳。
坑二:URL.createObjectURL 要配对释放
下载功能里,AI 写了 setTimeout(()=>URL.revokeObjectURL(url), 0),我当时还问它为什么不能直接 revoke。它解释:如果在 click() 之后立即 revoke,某些浏览器(尤其 Safari)会来不及触发下载。加个 setTimeout 延迟到当前事件循环结束,就稳了。这个细节我服。
坑三:复制功能的降级处理
navigator.clipboard 在非 HTTPS 环境下不可用,AI 加了个降级逻辑:
if(!navigator.clipboard) { setStatus('error', t('copyFail')); return; }
虽然没实现 document.execCommand('copy') 的完整降级,但至少在不可用的时候给了明确提示,不会让用户干点没反应。
最终效果
工具做出来之后,我实际用了一段时间,体验比 DevTools 手敲好太多了。实时转换 + 全量编码 + 一键复制,基本上是无缝的。而且纯前端,数据不出浏览器,隐私上也没负担。
代码量不大,单文件 HTML,CSS 和 JS 全内联,没有外部依赖。整个文件不到 10KB,加载基本是瞬时的。
对 AI 辅助编程的一些感想
这次实践下来,我的感受是:AI 编程不是银弹,但确实能把重复劳动压缩到一个很低的水平。对于这种"需求明确、技术栈固定、没有历史包袱"的小工具,AI 的产出质量相当高,我只需要在关键决策点(比如解码逻辑用哪种方案)把把关。
但 AI 也不是万能的——它第一次给出的解码方案就有 RCDATA 的 bug,全量编码的字符范围也搞错过。所以我的工作流是:
- 自己先想清楚需求和边界条件
- 用自然语言把需求描述给 AI,让它生成初版
- 逐条对照需求检查代码,发现问题就反馈给 AI 让它改
- 对关键逻辑(安全、边界)自己过一遍,不依赖 AI 的"自觉"
这个流程跑下来,效率确实比纯手写高,质量也有保障。
在线体验
如果你也有类似的 HTML 实体转换需求,可以直接用这个工具:craftvo.app/zh/tool/htm…
支持中英文、深色模式、实时转换、全量编码,所有操作都在浏览器本地完成。源码就是个单文件,想魔改也很方便。