用纯前端实现一个代码压缩美化工具,我踩了这三个坑

最近在做一个内部工具集时,需要给团队提供一个在线代码格式化功能。需求很简单:输入 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 挂了也能用。

相关推荐
随便写个软件9 分钟前
【新版本】EasyExplorer资源管理器 3.12.0——地址栏重要更新
运维·服务器·前端·everything·资源管理器
Jack N12 分钟前
【Strapi 系列之二】Collection / Single / Component 怎么选?Strapi 字段大全一篇讲透
前端·javascript·strapi
hiahiahia12320 分钟前
Agent Tool 执行失败怎么办
服务器·前端
Java后端的Ai之路20 分钟前
09-React组件生命周期
前端·react.js·前端框架
IT_陈寒27 分钟前
SpringBoot开发实战:从入门到精通
前端·人工智能·后端
乘风gg27 分钟前
一个人一个月合并 2500 PRs 的秘密是什么?
前端·ai编程·claude
郑州光合科技余经理37 分钟前
本地生活服务系统:预约状态和进度通知接口怎么拆
java·开发语言·前端·javascript·后端·php·生活
Peter-Code1 小时前
Nginx 常用操作整理(前端)
服务器·前端·nginx
Java后端的Ai之路11 小时前
06-React-Props
前端·react.js·前端框架
Flynt11 小时前
开源"间谍卫星"说数据全是真的,我把它的免key数据源挨个试了一遍
前端·开源·数据可视化