什么是同构代码?编写同构代码要注意什么?
简化版
同构代码是同一套代码既能在服务端运行,也能在客户端运行。编写时要注意运行环境差异:服务端没有 window、document、localStorage,客户端没有 Node 私有能力。浏览器专属逻辑应放到客户端生命周期中执行。
详细版
同构应用中,组件可能在服务端渲染一次,又在客户端水合一次。
注意点:
- 不在顶层直接访问 window/document。
- 不在服务端使用 localStorage、navigator。
- 不把请求级用户数据放全局变量。
- 注意时区、随机数、时间导致水合不一致。
- 网络请求要区分服务端和客户端 baseURL。
- 敏感密钥不能打进客户端包。
同构代码要求开发者明确每段代码运行在哪里。
完整版教学
一、同构的含义
同构不是“完全一样的环境”,而是“一套业务代码尽量复用在两端”。服务端负责生成 HTML,客户端负责接管交互。
组件、路由、数据模型可以共享,但运行环境差异必须处理。
二、环境差异
服务端是 Node 或边缘运行时,没有浏览器 DOM。客户端是浏览器,没有 Node 的文件系统和服务端私密环境变量。
因此这类代码会在 SSR 中报错:
const width = window.innerWidth
因为服务端没有 window。
三、如何写安全的同构代码
浏览器 API 放到客户端生命周期中,例如 mounted、useEffect。
需要判断环境时,可以使用框架提供的运行环境变量,或判断 typeof window !== 'undefined'。
请求相关逻辑要注意服务端转发 Cookie、Header,客户端使用浏览器相对路径或公开 API 地址。
四、面试追问与工程落地
面试官可能问:“为什么不能把用户信息放模块全局变量?”
SSR 服务是多请求共享进程。模块全局变量也会被多个用户共享,可能造成数据串号。用户信息必须放请求上下文中。
工程中同构代码最重要的习惯是先问一句:这段代码会不会在服务端执行?
五、能力检测比“服务端/客户端”二分更可靠
服务端运行时也不只有 Node:Node 可能有文件系统和长进程,Edge runtime 常限制原生模块、TCP 和执行时长;浏览器又有 DOM、Storage 和用户权限 API。
| 能力 | 浏览器 | Node SSR | Edge runtime |
|---|---|---|---|
| DOM/window | 有 | 无 | 无 |
| 文件系统 | 无 | 视部署可用 | 通常不可用或受限 |
| Web 标准 fetch | 有 | 现代版本有 | 通常有 |
| 长生命周期全局状态 | 页面生命周期 | 进程可能复用 | 实例可能频繁启停 |
| 服务端 secret | 不应有 | 可有 | 可有但由平台管理 |
因此库设计应注入 storage、fetch、clock 等能力,而不是内部到处判断 window。纯函数接收参数最容易复用,平台适配放在边界层。
同构不是让每行代码在所有环境都跑,而是共享纯业务核心,并把平台副作用隔离成明确适配器。
六、模块副作用、请求状态和打包安全
模块顶层代码在 import 时就执行。即使函数内部判断了客户端,第三方包在顶层访问 document 仍会让 SSR 导入阶段崩溃;此类包要放客户端入口动态加载,或选择真正支持 SSR 的版本。
shared/core:校验、格式化、领域计算(无平台副作用)
server adapter:Cookie、数据库、secret、文件系统
client adapter:DOM、Storage、浏览器事件
模块单例在 100 个并发 SSR 请求间共享。如果把 currentUser 存进去,后写入的请求可能污染先请求;每次请求创建 context/store,并只把明确公共、无用户维度的数据放跨请求缓存。
构建时检查客户端 bundle,确认服务端环境变量和依赖没有被静态引用带入。变量名不带“public”不自动安全,是否泄露取决于框架暴露规则、代码依赖图和最终产物。
七、常见误区与追问
- 误区:
typeof window !== 'undefined'能解决所有同构问题。 它只避免部分报错,仍可能造成首屏分支不同和水合不一致。 - 误区:服务端代码一定拥有完整 Node API。 Edge 等运行时可能只有部分 Web API 和严格资源限制。
- 误区:模块单例只在当前用户请求内存在。 SSR 进程会复用模块,用户状态必须请求隔离。
- 追问:第三方库顶层访问 document 怎么办? 客户端动态导入、替换库或在客户端边界包装,不能仅在调用处判断。
- 追问:为什么推荐依赖注入平台能力? 业务核心可测试、可移植,也减少散落环境分支。
- 追问:服务端 secret 如何防止进 bundle? 使用框架服务端模块边界,并检查构建产物和依赖图。
- 追问:同构网络请求 baseURL 如何处理? 服务端使用可信内部地址并转发必要请求上下文,浏览器使用公开同源/API 地址。
八、加强记忆
- 共享核心:领域计算和序列化尽量纯函数化。
- 能力边界:DOM、文件、secret、网络实现通过适配器注入。
- 导入安全:模块顶层也会执行,第三方包必须真正支持目标运行时。
- 请求隔离:用户 store、身份与 memo 每次请求新建。
- 输出确定:时间、随机和环境分支不得破坏首屏一致性。
- 产物审计:客户端 bundle 不包含服务端密钥、私有包和无用 polyfill。