Node.js 库在浏览器里跑不了的教训

纯前端做格式化工具:Node.js 库在浏览器里跑不了的教训

做一个纯前端的在线格式化工具站,直觉上就是引入格式化库 → 在浏览器里调用。 但实际开发中发现,很多"格式化库"实际上只能在 Node.js 环境运行。

问题清单

📚 本文是 UtlKit 技术系列第 5 篇 --- 系列索引 →

开发过程中,三个常用库在浏览器里全部翻车:

用途 在浏览器里的表现
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 模块不存在。

虽然 terserpackage.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()
}

选型经验总结

浏览器兼容性检查清单

选库之前,按这个清单检查:

  1. package.json 有没有 browser 字段 --- 有 → 做了浏览器适配
  2. 依赖树里有没有 fspathnet 等 Node.js 核心模块 --- 有 → 不能用
  3. npm 包页面有没有 "Browser Support" 说明
  4. GitHub Issues 搜索 "browser"、"client-side" --- 看有没有类似报告
  5. 本地写个简单 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 篇 --- 系列索引 →


utlkit.com --- 所有格式化工具都是纯前端实现,上述方案在线上稳定运行。

相关推荐
烬羽5 小时前
React Router v7 完全指南:一个 Demo 吃透前端路由
javascript·react.js·前端框架
烬羽5 小时前
React Router 嵌套路由:一个 `Outlet` 引发的布局思考
react.js·面试·前端框架
kyriewen8 小时前
别再这样写条件渲染了——你的React组件里藏着这5种定时炸弹
前端·javascript·react.js
mONESY10 小时前
🎨 从零搭建 React + TypeScript + Vite 项目:以 Color Picker 为例,聊聊前端工程化的 model/api 分层架构
react.js
阿黎梨梨17 小时前
从“能用”到“好维护”:React + TypeScript 实战指南
react.js
无人生还17 小时前
从 Vue3 到 React · 快速上手系列第 11 篇:状态管理
前端·vue.js·react.js
GuWenyue17 小时前
90%前端写React TS都踩坑!一套父子组件+Hooks完整实战,彻底搞懂类型约束
前端·react.js
张元清17 小时前
React useMeasure Hook:用 ResizeObserver 测量 DOM 元素 (2026)
javascript·react.js
朝阳3919 小时前
react19【实战】配置化路由登录鉴权完整方案
前端·javascript·react.js
名字还没想好☜19 小时前
React 用 IntersectionObserver 实现图片懒加载与无限滚动:封装一个 useInView Hook
前端·javascript·vue.js·react.js·react