Better Auth 服务端与客户端导入边界解析:客户端代码为何不能 import "better-auth"
【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth
本篇文章基于 Better Auth 仓库中的事故复盘文档 .postmortem/client-side-import-server.md 展开,深入剖析一个反复出现的高频问题:用户在客户端(React/Vue/Solid 等)代码中误导入服务端包better-auth,导致构建失败、运行时崩溃与包体膨胀。读完本文,你将掌握 Better Auth 服务端包与客户端包的完整导入边界、package.json导出映射的底层设计,以及一套可复用的排错与预防方案。
事故背景:一次典型的客户端误导入
Better Auth 官方在复盘文档中记录了一个持续困扰社区的问题:用户直接在客户端代码中导入服务端的better-auth包,从而引发构建与运行时错误。这并非某个版本引入的回归,而是一个"反复冒头"的常见用户错误。
复盘文档将问题定性得很明确——这是 100% 的用户使用错误,而不是库的缺陷:
loginPage (React/Vue/Solid 组件) ↓ auth-client.ts ↓ import { ... } from "better-auth" ← 错误!根因分析:错误的导入依赖链
复盘文档给出了问题的完整链路。用户在客户端代码中构建了这样一条错误的依赖链:页面组件 →auth-client.ts→import { ... } from "better-auth"。
正确的做法是区分两个完全不同的入口:
// 错误 - 在客户端代码中导入服务端包 import { ... } from "better-auth" // 正确 - 客户端代码应导入客户端包 import { createAuthClient } from "better-auth/client"服务端包better-auth与客户端包better-auth/client虽然同属一个 npm 包名,但它们是两个截然不同的构建产物,面向完全不同的运行环境。
为什么会发生:四个诱因
复盘文档归纳了用户反复踩坑的四类原因:
- 包命名带来的混淆——用户天然假设
better-auth是"唯一的主入口",所有功能都应该从这里导入; - IDE 自动导入建议——编辑器的自动补全常常把用户引导到错误的导入路径;
- 从示例中复制粘贴——用户把服务端示例代码直接粘进客户端代码;
- 缺乏清晰的报错信息——构建错误没有明确指出"你越过了服务端/客户端的导入边界"。
从源码层面看,这种混淆有客观基础:在 packages/better-auth/src/index.ts 中,服务端入口会export * from "@better-auth/core"、导出betterAuth工厂函数、APIError、createTelemetry等大量服务端能力;而客户端入口 packages/better-auth/src/client/index.ts 则完全不同,它导出的是createAuthClient、InferPlugin、InferAuth以及会话刷新、焦点管理等浏览器端运行时能力。两者名字相似,内容天差地别。
真实影响:当服务端代码进入客户端包
一旦用户在客户端代码中导入better-auth,复盘文档列出了四层连锁后果:
- Node.js 模块被打包进客户端——服务端专属依赖(如
node:sqlite)会被 bundle 进浏览器产物; - 构建失败——打包器无法解析
node:内置模块(如node:sqlite、node:crypto); - 运行时错误——即便构建侥幸通过,代码在浏览器中运行也会崩溃;
- 包体膨胀——无关的服务端代码被无谓地发送到客户端。
这一条在仓库中有直接证据:demo/nextjs/lib/auth.ts 第 1 行就是import { DatabaseSync } from "node:sqlite";,随后用NodeSqliteDialect构建数据库连接。这是典型只能运行在 Node.js 服务端的代码,一旦被客户端打包器解析,node:前缀模块立刻触发解析失败。
澄清误判:PR #4360 为何被错误指责
复盘文档专门澄清了一起"错误甩锅"事件。此前有 PR(#4360)通过在代码中加入typeof window === "undefined"判断来规避node:sqlite的报错,社区一度将其视为问题根源。
复盘文档明确指出:那是一个 workaround(临时绕过),而不是原因。真正的病根始终是"用户在客户端代码中导入了服务端包"。打补丁掩盖症状,只会让真正的边界问题继续潜伏——这也是本次复盘最重要的方法论结论之一:workaround 会掩盖根因。
正确用法:服务端与客户端各自的导入姿势
复盘文档给出了一套明确的"用户行动清单":
// 在任何客户端文件中(React、Vue、Solid 等) // 永远不要这样做: import { anything } from "better-auth" // 始终这样做: import { createAuthClient } from "better-auth/client" import type { Session, User } from "better-auth/types"在仓库的 packages/better-auth/package.json 的exports字段中,可以看到这套子路径导出的完整设计:./client、./client/plugins、./react、./vue、./solid、./svelte、./lynx、./types、./plugins、./minimal、./next-js、./adapters/*等等。每个子路径都有独立的构建产物(dist/**/*.mjs与dist/**/*.d.mts),这就是"导入边界"在工程层面的物理体现。
从代码实现看,createAuthClient定义在 packages/better-auth/src/client/vanilla.ts,它接收BetterAuthClientOptions,返回AuthClient类型——一个集成了InferClientAPI、InferActions、InferResolvedHooks的交叉类型对象,同时提供useSessionatom、$fetch、$store、hydrateSession等能力。客户端入口 packages/better-auth/src/client/index.ts 再把它与session-refresh、parser、query、broadcast-channel等模块一起重新导出。
仓库示例中的规范用法
Better Auth 自己的演示应用就是"正确用法"的最佳范本,服务端与客户端代码被严格分离到不同文件:
服务端配置(demo/nextjs/lib/auth.ts)——导入betterAuth、APIError等,并注册organization、twoFactor、passkey、stripe、sso等服务端插件:
import { APIError, betterAuth } from "better-auth"; import { nextCookies } from "better-auth/next-js"; // ... 服务端插件与数据库方言 export const auth = betterAuth({ database, emailAndPassword: { enabled: true }, plugins: [organization(), twoFactor(), passkey(), /* ... */], });客户端配置(demo/nextjs/lib/auth-client.ts)——从better-auth/react导入createAuthClient,从better-auth/client/plugins导入插件,并用import type { auth } from "./auth"仅以类型方式引用服务端实例:
import { createAuthClient } from "better-auth/react"; import { adminClient, organizationClient, twoFactorClient, // ... } from "better-auth/client/plugins"; import type { auth } from "./auth"; export const authClient = createAuthClient({ plugins: [organizationClient(), twoFactorClient(), /* ... */], });注意一个细节:import type { auth } from "./auth"用的是import type,类型导入在编译后被完全擦除,不会把 demo/nextjs/lib/auth.ts 中的node:sqlite等运行时依赖带进客户端产物。
服务端中的客户端实例(demo/nextjs/lib/server-client.ts)——即使要在服务端调用客户端 API,也是从better-auth/client导入createAuthClient,而不是从根包导入。这说明better-auth/client是一个"可移植的 API 客户端",它既能在浏览器运行,也能在服务端上下文使用,唯独根包better-auth不能被带进浏览器。
库层面的改进方向
复盘文档为 Better Auth 库本身提出了三个改进方向,并给出了示例实现:
- 更好的报错信息——检测客户端环境并在服务端包被导入时抛出清晰错误。复盘文档给出了在
better-auth/index.ts中加入运行时检测的示例:
// In better-auth/index.ts if (typeof window !== "undefined") { throw new Error( "You are importing 'better-auth' in client code. " + "Use 'better-auth/client' instead." ); }构建期检测——通过
package.json的exports字段设计来约束错误导入。这一点在仓库中已经落地:packages/better-auth/package.json 的exports与typesVersions为每个子路径显式声明了独立产物,任何超出声明路径的导入都会在解析期被工具链拦截。文档强化——把服务端/客户端的区别讲得足够清楚。
从当前仓库的实际状态看,exports字段已经包含了./client、./react、./vue、./solid等全套子路径,这正是"构建期检测"与"自动导入引导"的工程基础——IDE 和打包器都会优先遵循exports声明来解析与提示。
PR #7532 带来的变化
复盘文档记录了 PR #7532 的修复过程:在修复测试基础设施时,真实问题浮出水面——测试暴露了"服务端代码在客户端上下文中被导入"这一边界问题。该 PR 包含两项内容:
- 测试清理——修复了 window 的 stubbing(次要问题);
- 导入边界暴露——测试揭示了服务端代码被带入客户端上下文的真实问题。
这也印证了复盘的核心观点:测试基础设施的修复只是引子,真正的价值在于让导入边界问题暴露出来。
经验教训与预防措施
复盘文档总结了四条经验教训:
- 用户一定会犯这个错——命名本身具有迷惑性;
- workaround 会掩盖根因——加
window检查并不能阻止用户导入错误的包; - 清晰的边界是必须的——服务端包与客户端包必须严格分离;
- 这不是回归缺陷——这是一个持续的用户教育问题。
预防措施分为两个层面:
立即行动:
- 添加运行时检测(上述
typeof window !== "undefined"抛错方案); - 更新文档,加入醒目的导入警告;
- 通过
package.json配置修正 IDE 的自动导入建议。
长期方案:
- 构建期校验——开发 ESLint 插件拦截错误导入;
- 更好的示例——将服务端与客户端代码在示例中明确分开。
排查清单:遇到 node:* 构建错误怎么办
最后,复盘文档给出一份可直接照做的行动清单。如果你在构建中看到node:*模块相关的错误:
- 检查你的导入——你很可能在客户端代码中导入了
better-auth; - 修正导入——改为
better-auth/client(或框架对应的better-auth/react、better-auth/vue、better-auth/solid); - 牢记原则——永远不要在客户端代码中导入服务端包。
这并非 Better Auth 的缺陷,而是不正确的使用方式。理解并尊重服务端/客户端的导入边界,是每个 Better Auth 使用者应该掌握的第一课。
【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考