You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现TypeScript npm包同时兼容Node与浏览器环境

TypeScript 同构 npm 包兼容 Node.js 与浏览器环境实现方案

问题背景

开发面向双端的 TypeScript npm 包时,若代码中存在 Node 内置模块(fs/path/url等)的顶层静态导入,会导致浏览器端打包工具静态解析时报错;传统双入口方案需要维护两份完整代码,冗余度高。需要实现以下目标:

  • Node 内置模块引用不触发打包工具解析报错
  • 无需包使用者修改打包器配置添加兼容 hack
  • 浏览器环境调用 Node 专属功能时抛出明确错误
  • 开发阶段可正常使用 @types/node 提供的类型提示

可行实现方案

方案1:运行时延迟加载(无需额外依赖,单入口无冗余代码)

核心逻辑是避免顶层静态导入 Node 内置模块:仅导入类型做开发时提示,真实模块加载逻辑放在 Node 环境专属分支内执行,绕过打包工具的静态依赖分析。
你之前设计的代理思路完全可行,只需要修正顶层静态导入的问题即可,以封装 fs 模块为例:

// safe-fs.ts
// 仅导入类型,TS编译后会完全擦除该行,不会产生实际的import语句
import type * as fsType from 'fs'

function createModuleProxy(moduleName: string): any {
  return new Proxy({}, {
    get(_, property) {
      return (...args: any[]) => {
        throw new Error(`方法 "${String(property)}" 来自模块 "${moduleName}",仅支持在Node.js环境下调用`)
      }
    }
  })
}

// 可靠的Node环境判断,避免误判类Node环境
const isNode = typeof process !== 'undefined' && !!process.versions?.node

let fsInstance: typeof fsType
if (isNode) {
  // 用Function构造函数包裹require,绕过webpack/rollup/vite等工具的静态依赖扫描
  fsInstance = Function('return require("fs")')() as typeof fsType
} else {
  fsInstance = createModuleProxy('fs') as typeof fsType
}

export default fsInstance

其他 Node 内置模块都可以参照这个模式封装成 safe-xxx.ts 文件,业务代码统一引用封装后的模块即可。

关键细节:必须用 import type 做类型导入,确保编译后无残留导入语句;Function 包裹的 require 不会被打包工具识别为静态依赖,不会触发模块解析逻辑,也不会把 Node 内置模块打包进浏览器产物。

方案2:条件导出(零运行时开销,冗余度极低)

如果不想引入运行时代理逻辑,可以使用所有现代打包工具和 Node.js 原生支持的 exports 条件导出能力,不需要维护两份完整代码,仅需拆分 Node 专属能力即可:

  1. 将所有依赖 Node 内置模块的代码单独放到 src/node-only 目录,双端通用逻辑放到公共目录
  2. 在 package.json 中配置条件导出,打包工具会自动根据运行环境选择对应入口:
{
  "main": "./lib/index.cjs",
  "module": "./lib/index.js",
  "types": "./lib/index.d.ts",
  "exports": {
    ".": {
      "node": "./lib/index.node.js",
      "browser": "./lib/index.browser.js",
      "default": "./lib/index.js"
    }
  }
}
  1. 两个入口文件仅需区分 Node 专属方法的实现,公共逻辑完全复用:
// index.node.js
export * from './public-api.js'
// 导出真实的Node端实现
export { readFile, resolvePath } from './node-only/fs.js'
// index.browser.js
export * from './public-api.js'
// 浏览器端仅导出抛错占位方法
export const readFile = () => { throw new Error('readFile 仅支持在Node.js环境下调用') }
export const resolvePath = () => { throw new Error('resolvePath 仅支持在Node.js环境下调用') }

该方案没有额外运行时开销,打包工具解析浏览器入口时完全不会接触到 Node 内置模块的导入语句,不需要用户侧做任何额外配置,且公共逻辑只需要维护一份,仅 Node 专属方法需要在浏览器端写几行抛错占位,代码冗余量可以忽略。

避坑说明

  • 绝对不要在文件顶层写针对 Node 内置模块的静态值导入(即不带 type 关键字的 import),只要存在顶层静态导入,打包工具就会尝试解析该模块,必然触发报错
  • 环境判断不要仅用 typeof window === 'undefined',部分 Web Worker、小程序环境可能不存在 window 但也不是 Node 环境,通过 process.versions.node 判断准确性最高
  • 如果使用 ESM 格式的动态 import() 加载 Node 模块,需要注意打包工具可能会把动态导入的模块单独打包成 chunk,建议还是用上述 Function 包裹 require 的方式最稳妥

内容的提问来源于stack exchange,提问作者Balázs Édes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 17:06:26