← 返回题目列表

什么是同构代码?编写同构代码要注意什么?

高频 中等 第 4 / 25 题 更新于 2026/07/28
SSR同构Node浏览器

简化版

同构代码是同一套代码既能在服务端运行,也能在客户端运行。编写时要注意运行环境差异:服务端没有 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 SSREdge 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 地址。

八、加强记忆

  1. 共享核心:领域计算和序列化尽量纯函数化。
  2. 能力边界:DOM、文件、secret、网络实现通过适配器注入。
  3. 导入安全:模块顶层也会执行,第三方包必须真正支持目标运行时。
  4. 请求隔离:用户 store、身份与 memo 每次请求新建。
  5. 输出确定:时间、随机和环境分支不得破坏首屏一致性。
  6. 产物审计:客户端 bundle 不包含服务端密钥、私有包和无用 polyfill。