纯前端做格式化工具:Node.js 库在浏览器里跑不了的教训
做一个纯前端的在线格式化工具站,直觉上就是引入格式化库 → 在浏览器里调用。 但实际开发中发现,很多"格式化库"实际上只能在 Node.js 环境运行。
问题清单
📚 本文是 UtlKit 技术系列第 5 篇 --- 系列索引 →
- 上一篇:Cloudflare 白页 →
- 下一篇:Sentry Source Map →
开发过程中,三个常用库在浏览器里全部翻车:
| 库 | 用途 | 在浏览器里的表现 |
|---|---|---|
terser |
JS 压缩 | ❌ fs is not defined |
html-minifier-terser |
HTML 压缩 | ❌ fs is not defined |
xml-formatter v3.7.0 |
XML 格式化 | ⚠️ ESM/CJS 互操作问题 |
terser:Node.js 的 fs 模块
问题复现
js
import { minify } from 'terser'
// 在浏览器组件里调用
const result = await minify(code, { compress: true, mangle: true })
构建时不报错。运行时:
javascript
Uncaught ReferenceError: fs is not defined
at Object.readSourceMap (terser/dist/bundle.min.js:1:xxxxx)
根因
terser 内部会调用 fs.readFileSync 来读取 source map 文件。在 Node.js 环境这没问题,在浏览器里 fs 模块不存在。
虽然 terser 的 package.json 里声明了 browser 字段做条件导出,但某些内部模块仍然引用了 fs,webpack/SWC 的 tree-shaking 没有完全清除。
解决方案:自写轻量级正则压缩
javascript
function minifyJs(code) {
return code
// 移除单行注释
.replace(/\/\/.*$/gm, '')
// 移除多行注释
.replace(/\/\*[\s\S]*?\*\//g, '')
// 移除不必要的空白
.replace(/\s+/g, ' ')
// 移除行首行尾空白
.trim()
// 移除逗号/分号前后的空格
.replace(/\s*([,;{}()\[\]])\s*/g, '$1')
// 移除多余空行
.replace(/\n\s*\n/g, '\n')
}
优势:
- 零依赖,纯 JS
- 浏览器/Node.js 双环境可用
- 代码量小(20 行)
- 对于工具类网站的 JS 压缩完全够用
劣势:
- 不如 Terser 压缩率高
- 不做 AST 级别优化(变量名混淆、死代码消除)
但对于"用户在浏览器里压缩一段代码看效果"这个场景,正则方案完全够了。
html-minifier-terser:同样死于 fs
问题
和 terser 一样的问题。html-minifier-terser 内部调用了 fs 来读取外部资源(<script src="..."> 的本地文件等)。
解决方案
javascript
function minifyHtml(html) {
return html
// 移除 HTML 注释
.replace(/<!--[\s\S]*?-->/g, '')
// 压缩空白
.replace(/>\s+</g, '><')
// 移除属性值前后的引号空白
.replace(/\s*=\s*/g, '=')
// 压缩多余空白
.replace(/ {2,}/g, ' ')
.trim()
}
xml-formatter:ESM/CJS 互操作问题
问题
这个库倒是支持浏览器环境。但 v3.7.0 的导出方式有问题:
js
// xml-formatter v3.7.0 的实际导出
export default function formatXml(...) { ... } // default export
export function minify(xml) { ... } // named export
但在 CJS 包裹层(Next.js 的 SWC 编译后),default export 变成了 exports.default,直接 import formatXml from 'xml-formatter' 在某些情况下拿到的不是函数。
排查过程
js
// ❌ 运行时 formatXml 是 undefined
import formatXml from 'xml-formatter'
import { minify } from 'xml-formatter' // ✅ 这个正常
// ✅ 正确方式
import _formatXml from 'xml-formatter'
const formatXml = _formatXml.default || _formatXml
最终方案
js
// 格式化用 xml-formatter(处理好的互操作)
import _formatXml from 'xml-formatter'
const formatXml = _formatXml.default || _formatXml
// 压缩用自写函数(xml-formatter 的 minify 在某些版本行为不一致)
function minifyXml(xml) {
return xml
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/\s+/g, ' ')
.replace(/>\s+</g, '><')
.trim()
}
选型经验总结
浏览器兼容性检查清单
选库之前,按这个清单检查:
package.json有没有browser字段 --- 有 → 做了浏览器适配- 依赖树里有没有
fs、path、net等 Node.js 核心模块 --- 有 → 不能用 - npm 包页面有没有 "Browser Support" 说明
- GitHub Issues 搜索 "browser"、"client-side" --- 看有没有类似报告
- 本地写个简单 HTML +
<script type="module">测试
纯前端工具推荐的库
| 功能 | 推荐库 | 原因 |
|---|---|---|
| JS/HTML/XML 格式化 | js-beautify |
明确支持 browser build |
| CSS 压缩 | csso |
纯 JS,无 Node.js 依赖 |
| JSON 格式化 | JSON.stringify/parse |
原生 API |
| SQL 格式化 | sql-formatter |
支持 browser |
| YAML 格式化 | js-yaml |
支持 browser |
核心经验
不要看库的名字和用途,要看它的运行时依赖。 名字里带 "js" 不代表能在浏览器跑,名字里带 "node" 也不一定不能在浏览器跑。
📚 本文是 UtlKit 技术系列第 5 篇 --- 系列索引 →
- 上一篇:Cloudflare 白页 →
- 下一篇:Sentry Source Map →
utlkit.com --- 所有格式化工具都是纯前端实现,上述方案在线上稳定运行。