最近在做一个内部工具集时,需要给团队提供一个在线代码格式化功能。需求很简单:输入 HTML/JS/CSS,一键压缩或美化,结果直接复制或下载。
本来想直接找个现成的 npm 包塞进项目里,但考虑到这只是个轻量工具,不想为了它引入整个构建链路,而且团队成员经常需要临时处理一段代码,打开浏览器就能用才是最优解。于是决定写一个单文件 HTML 工具,所有逻辑都跑在浏览器本地。
第一版:直接用 js-beautify,结果被 CDN 坑了
最开始的思路很直接------用 js-beautify 这个老牌库。它同时支持 HTML、JS、CSS 三种语言的格式化,API 也简单:
javascript
const result = html_beautify(code, { indent_size: 2 });
但问题出在 CDN 上。我一开始用的是 jsdelivr,结果国内网络环境下经常加载失败,页面直接白屏。后来换成 staticfile 才稳定下来。
这里有个细节值得注意:CDN 加载失败不能静默处理。我在 script 标签上加了 onerror 标记,这样即使库加载失败,用户也能看到明确的错误提示,而不是不明不白地发现按钮没反应。
html
<script src="https://cdn.staticfile.org/js-beautify/1.15.1/beautifier.min.js" onerror="window.__beautifierFailed=true"></script>
第二版:压缩功能比想象中复杂
美化功能交给 js-beautify 就够了,但压缩(minify)才是真正的坑。
HTML 压缩:不能无脑删空白
我最初的实现是直接 replace(/\s+/g, ' '),把多余空白全折叠掉。结果发现 <pre> 标签里的代码被压成了一行,<script> 里的字符串也被破坏了。
解决方案是先把需要保护的块提取出来,压缩完再放回去:
javascript
const blocks = [];
const protected = code.replace(/<(script|style|pre)\b[\s\S]*?<\/\1>/gi, m => {
blocks.push(m);
return '\x00BLOCK' + (blocks.length - 1) + '\x00';
});
// 压缩处理后再把 blocks 还原
JS 压缩:ASI 的坑
JS 压缩是最容易出问题的。一开始我想的是把换行也去掉,全部压成一行。但很快发现这样会破坏 ASI(自动分号插入)规则。
比如这段代码:
javascript
return
{ a: 1 }
如果压成一行变成 return { a: 1 },语义就完全变了------前者返回 undefined,后者返回对象。
还有 a\n--b 这种写法,压成一行会变成 a--b,变成了自减运算。
所以 JS 压缩我做了个保守方案:保留换行,只压缩行内的多余空格和注释:
javascript
result = noComments
.split('\n')
.map(l => l.trim().replace(/[ \t]{2,}/g, ' '))
.filter(l => l !== '')
.join('\n');
这样虽然压缩率不如真正的 minifier,但至少不会破坏代码语义。
CSS 压缩:相对简单但也有细节
CSS 压缩相对简单,核心就是删注释、折叠空白、压缩运算符周围的空格:
javascript
result = code
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\s+/g, ' ')
.replace(/\s*([{}:;,])\s*/g, '$1')
.trim();
注意 replace(/\s+/g, ' ') 要放在 replace(/\s*([{}:;,])\s*/g, '$1') 之前,否则第二部操作会把空格删掉后,第一步又加回来。
第三版:AI 辅助开发的实际体验
这个工具的核心逻辑是我用 AI 辅助写的。整个过程不是一次成型,而是来回调了好几轮。
第一轮 prompt :我描述了功能需求,AI 生成了完整的单文件 HTML。代码结构清晰,i18n 机制也写得不错,但压缩逻辑过于简单------直接全局折叠空白,根本没考虑 <pre> 标签和 ASI 的坑。
第二轮对话 :我指出问题后,AI 给出了保护块的方案,但它的正则写错了,匹配 <script> 标签时用了 <script\b[\s\S]*?<\/script>,这样会把 <script src="..."> 也匹配进去。我手动修正后才跑通。
第三轮调整:JS 压缩的换行处理是我自己改的。AI 一开始的方案是全部压成一行,我试了几个边界案例发现会破坏 ASI,于是改成保留换行的保守策略。
我的感受:AI 适合生成骨架和常规逻辑,但对边界情况的处理需要开发者自己把关。尤其是压缩这类对语义敏感的操作,不能完全信任 AI 生成的代码。
实现细节与设计决策
国际化
i18n 我做了个轻量方案------一个包含中英文的字典对象,通过 t('key') 函数取值。语言检测优先级:URL 参数 ?lang=en > 浏览器语言 > 默认中文。
javascript
const I18N = {
zh: { beautify: "美化", minify: "压缩" },
en: { beautify: "Beautify", minify: "Minify" }
};
const t = (k) => I18N[lang][k] || k;
字节数统计
用 new Blob([code]).size 来计算字节数,比 code.length 更准确------后者统计的是字符数,中文等多字节字符会算错。
深色模式
通过 CSS 变量配合 prefers-color-scheme 实现,不用 JS 切换,浏览器自动适配。
适用场景
这个工具适合以下场景:
- 从压缩后的线上代码恢复可读性
- 提交代码前压缩减小体积
- 临时处理一段代码,不想打开 IDE
不适合的场景:
- 生产环境的正式构建(应该用 terser 等专业工具)
- 需要 sourcemap 的场景
最后
如果你也有类似需求,可以直接在线使用这个工具:craftvo.app/zh/tool/cod...
代码逻辑都在浏览器本地跑,不会上传你的代码。压缩功能不依赖外部库,即使 CDN 挂了也能用。