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

相关推荐
光影少年11 小时前
RN 样式特点:Flex 布局默认、没有像素单位、样式不能级联
react native·react.js·掘金·金石计划
小蚂蚁i14 小时前
React Hooks 原理深度解析:从会用到真正懂
前端·react.js
索西引擎15 小时前
【React】虚拟 DOM:原理、机制与核心价值分析
前端·react.js·前端框架
光影少年18 小时前
react的View/Text/Image/ScrollView 常用组件注意事项
开发语言·前端·javascript·react.js·前端框架
索西引擎19 小时前
【React】React Fiber 架构:设计动机、核心机制与调度策略分析
前端·react.js·架构
索西引擎1 天前
【React】JSX:JavaScript 语法扩展的本质、编译机制与表达能力分析
前端·javascript·react.js
hxy06011 天前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Patrick_Wilson2 天前
从一次 Safari 白屏,聊聊 CDN 外置 React 的坑与前端监控盲区
前端·react.js·cdn
weixin_471383032 天前
Next.js - 04 - 深入理解next.js渲染原理
前端·javascript·next.js