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 --- 所有格式化工具都是纯前端实现,上述方案在线上稳定运行。

相关推荐
晓得迷路了2 小时前
栗子前端技术周刊第 134 期 - React Router v8、TypeScript 7 RC、React Native 0.86...
前端·javascript·react.js
代码煮茶18 小时前
React 组件封装方法论 —— 以 Todo App 为例
javascript·react.js
猩猩程序员1 天前
零基础学习 React 19
react.js
spmcor1 天前
React 进阶指南:状态管理进化——从 Context 到 Redux Toolkit(第五篇)
react.js
spmcor1 天前
React 进阶指南:React Router v6 完全实战(第四篇)
react.js
YFF菲菲兔2 天前
调度系统和调和系统的桥梁
react.js
YFF菲菲兔2 天前
commitRoot 源码解析
react.js
光影少年3 天前
react批量更新、同步/异步更新场景
前端·react.js·掘金·金石计划