React Router 如何在客户端与服务端使用同一接口的不同日志实现?
可以继续使用统一的 ILogger 接口,同时在模块转换阶段隔离服务端实现。仅用 typeof window 判断,无法保证客户端构建时不解析 nodeLogger.server。在使用 React Router Framework Mode 和 Vite 的项目中,可以借助 vite-env-only 的 serverOnly$ 宏移除客户端中的服务端引用,同步选择对应的 logger。
为什么条件导入仍然报错?
运行时是否执行一段代码,不影响构建工具分析它的依赖。动态 import() 仍是模块依赖的入口,即使放在运行时条件判断中,也不会自动从客户端依赖图中消失。
React Router 会根据 .server 约定检查服务端模块是否进入客户端模块图。如果存在这样的引用,构建就会失败。这个后缀用于声明和检查模块边界,不会自动为共享模块选择另一套实现。React Router 服务端模块文档
原代码还有一个独立问题:
(async () => {
const serverSideLogs = await import("~/util/nodeLogger.server");
logger = serverSideLogs.nodeLogger;
});
这段代码只创建了一个异步函数,并没有调用它。即使在末尾加上 (),logger 在动态导入完成前仍是 consoleLogger,所以服务端早期产生的日志可能会走默认实现。
使用构建宏选择日志实现
下面的方案假设两个现有模块都同步导出符合 ILogger 的日志对象,且服务端运行环境支持 nodeLogger 使用的 Node.js 依赖。
1. 安装并配置插件
npm install -D vite-env-only
在现有 vite.config.ts 中加入 envOnlyMacros(),同时保留已有的路径别名和其他插件。相关配置如下:
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import { envOnlyMacros } from "vite-env-only";
export default defineConfig({
plugins: [
envOnlyMacros(),
reactRouter(),
],
});
serverOnly$ 需要插件转换,只导入宏而不配置 Vite 无法生效。导出路径也要与安装的插件版本一致。vite-env-only 配置说明
2. 建立统一的日志入口
将共享入口写为普通模块,例如 app/util/logger.ts:
import type { ILogger } from "~/types/logger";
import { consoleLogger } from "~/util/consoleLogger";
import { nodeLogger } from "~/util/nodeLogger.server";
import { serverOnly$ } from "vite-env-only/macros";
export const logger: ILogger =
serverOnly$(nodeLogger) ?? consoleLogger;
业务代码统一导入:
import { logger } from "~/util/logger";
之后按现有 ILogger 的方法签名调用 logger.warn、logger.information 即可。
serverOnly$(nodeLogger) 在服务端保留 nodeLogger,在客户端则被替换为 undefined。插件会清理因此失去引用的标识符和导入。因此,nodeLogger 只能用于这个服务端表达式,不能在共享模块的其他位置继续引用。vite-env-only 宏与依赖清理说明
两端都能同步获得一个 ILogger,不必等待动态导入完成。保留 nodeLogger.server.ts 的命名,还能继续使用 React Router 的模块边界检查。
3. 检查默认实现是否适合共享导入
上面的共享入口仍会导入 consoleLogger,所以需要确保它能在服务端安全加载。例如,模块顶层不能直接访问 window 或 document。
如果 consoleLogger 依赖浏览器专用 API,就需要进一步隔离该实现,或改由客户端、服务端各自的入口注入 logger。不要直接把共享入口改名为 .client.ts:React Router 的 .client 模块导出值在服务端会是 undefined,无法满足始终提供日志对象的要求。React Router 客户端模块文档
能否只改成 import.meta.env.SSR?
Vite 提供 import.meta.env.SSR,并会在生产构建时静态替换它,以便移除无用分支。Vite SSR 条件逻辑文档
对于这里已经触发的 .server 模块检查,仅替换条件还不能确定问题已经解决,也要考虑依赖解析和框架插件的处理过程。构建宏负责环境表达式替换及相关依赖清理,更适合需要在共享模块中保留同步日志接口的场景。
如果不想增加插件,也可以让共享业务函数接收 ILogger 参数:服务端入口传入 nodeLogger,客户端入口传入 consoleLogger。这样共享模块就不再导入服务端实现,但需要调整现有调用方式。
如何确认修复生效?
使用与构建服务器一致的锁文件和生产构建命令进行验证,并分别确认:
- 客户端产物没有包含
nodeLogger.server及其 Node.js 专用依赖。 - 浏览器中的日志使用
consoleLogger。 - 服务端从首次调用开始就使用
nodeLogger,没有异步切换窗口。
现有材料不足以确定本地与构建服务器行为不同的具体原因。如果仍有差异,应对比依赖版本、构建命令和 Vite 配置,不能把本地构建成功当作模块隔离已经正确的证明。
备注:内容仅供参考。
