要让ES Module同时在Node.js和浏览器中运行,需以ESM为源码规范,通过构建工具抹平差异:统一ESM语法、避开环境敏感API、显式路径导入、静态导出、动态导入隔离副作用,并依赖Vite/Webpack等工具完成解析与产物生成。

要让一个 ES Module 同时在 Node.js 和浏览器中运行,关键不是“写一套代码就直接跑通”,而是用统一语法 + 分层适配 + 构建介入。纯原生 ESM 在两端存在根本性差异(比如裸模块、路径规则、全局对象),硬写“兼容”反而容易出错。真正可行的路径是:以 ESM 为源码规范,靠构建工具抹平差异。
统一用 ESM 语法,但避开环境敏感操作
所有源文件都用 import/export,不混用 require 或 module.exports。但要注意:
- 不直接访问
window、document、localStorage等浏览器专属 API;需包裹判断:if (typeof window !== 'undefined') { ... } - 避免在顶层使用
__dirname、__filename—— Node.js ESM 中不可用,改用import { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); - HTTP 请求等副作用逻辑,封装成函数,在调用时再判断环境,或由上层框架(如 React/Vue SSR)控制执行时机
导入路径必须显式、可解析
浏览器不支持裸模块(如 import React from 'react'),Node.js 却能自动查 node_modules。解决办法:
- 开发阶段:所有
import都用相对路径或完整 URL,例如import { debounce } from './utils/debounce.js';第三方库暂时不用裸名 - 生产打包:交由 Vite/Webpack 处理——它们会把
import React from 'react'自动映射到node_modules/react/index.js,再转成浏览器可加载的路径或内联代码 - 若想跳过打包(极少数场景),可用 import maps 声明裸模块映射,但需浏览器支持(Chrome 89+,Firefox 105+),且 Node.js 不支持
导出方式保持静态、明确
ESM 的静态特性是跨平台稳定的基础:
- 命名导出统一用
export const foo = ...或export { foo, bar },导入严格对应import { foo } from './x.js' - 默认导出只用一次:
export default function render() { ... },导入用import render from './x.js' - 避免
export * from './y.js'混淆来源;如需聚合,显式重导出:export { foo as myFoo } from './y.js' - 动态导入
const mod = await import('./browser-only.js')可用于延迟加载仅浏览器需要的模块,Node.js 也能执行该语句(只要路径存在)
构建配置决定能否真正同构
没有构建工具,所谓“同构”只是理论可行。实际必须依赖打包器:
- Vite:开箱支持
import react from 'react',自动处理 node_modules 解析、SSR 入口、服务端预渲染,并生成两套产物(client + server) - Webpack:配合
target: ['web', 'node']或分别配置多入口,用resolve.alias和externals控制不同环境的模块行为 - 注意 package.json 中设
"type": "module",确保 Node.js 环境按 ESM 解析;同时禁用 CommonJS 混用,避免require()在浏览器报错
不复杂但容易忽略:同构不是让代码在两个环境“一样地执行”,而是让逻辑可复用、副作用可隔离、构建可收敛。写的时候用 ESM,跑的时候靠工具桥接——这才是当前最稳的实践。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











