创见博客
在 SSG 页面中通过根节点状态属性控制 DOM 显隐
七崽爱吃小饼干2026/08/17阅读 1

在静态站点生成(SSG)场景中,构建阶段无法获取用户的运行时状态,例如登录态、AB 实验分组等。如果为了这些状态改用整页客户端渲染,会引入首屏闪烁、布局偏移和 Hydration 不一致等问题。

对于只影响页面展示、且允许多个版本同时存在于静态 HTML 中的场景,可以在页面首次绘制前读取运行时变量,并把状态写入 <html> 根节点的 data-* 属性,再由 CSS 控制对应 DOM 的显隐。

实现思路

整体流程如下:

  1. SSG 产物同时输出登录态和未登录态的展示节点。
  2. 服务端或页面容器向 window.__globalVars__ 注入运行时信息。
  3. 在 <head> 中同步执行一段轻量脚本。
  4. 脚本读取 loginStatus,在 <html> 上写入 data-login-state。
  5. 首屏 CSS 根据该属性隐藏不匹配的 DOM。

最终 HTML 根节点会呈现为以下两种状态之一:

html
<html data-login-state="logged-in">
html
<html data-login-state="logged-out">

相比添加 is-logged-in、is-logged-out 两个 class,单个 data-login-state 属性更适合表达互斥的业务状态,也能避免两个冲突 class 同时出现。

Head 脚本

脚本必须放在 window.__globalVars__ 完成注入之后,并尽量位于首屏样式和页面内容渲染之前,也就是放在Body之前,就可以在页面渲染之前实现属性的注入,之后页面渲染时,就会按照指定的样式分支取渲染:

html
<script>
  (() => {
    let globalVars = window.__globalVars__;

    // 兼容注入结果是 JSON 字符串的情况
    if (typeof globalVars === 'string') {
      try {
        globalVars = JSON.parse(globalVars);
      } catch {
        globalVars = {};
      }
    }

    const isLoggedIn = Boolean(globalVars?.loginStatus);

    document.documentElement.setAttribute(
      'data-login-state',
      isLoggedIn ? 'logged-in' : 'logged-out'
    );
  })();
</script>

CSS 显隐规则

页面节点使用语义明确的 class:

html
<section class="logged-in-content">
  登录用户看到的内容
</section>

<section class="logged-out-content">
  未登录用户看到的内容
</section>

对应 CSS:

css
html[data-login-state='logged-in'] .logged-out-content,
html[data-login-state='logged-out'] .logged-in-content {
  display: none;
}

为了避免运行时变量异常或脚本尚未执行时同时展示两份内容,可以增加保守的默认规则:

css
html:not([data-login-state]) .logged-in-content {
  display: none;
}

这样脚本异常时页面默认按未登录态展示。

在 React SSG 中使用

组件本身不再读取 window,而是稳定地输出两份静态结构:

tsx
export function HomePage() {
  return (
    <>
      <div className="logged-in-content">
        <LoggedInHome />
      </div>

      <div className="logged-out-content">
        <PublicHome />
      </div>
    </>
  );
}

服务端生成的 HTML 和 React Hydration 看到的 DOM 结构保持一致,状态只影响 CSS,不需要在首次渲染时调用 setState,因此不会产生 Hydration mismatch。

扩展到 AB 实验

同样的模式也可以表达 AB 实验分组:

js
document.documentElement.dataset.abVariant = 'aiV2';
css
[data-ab-content] {
  display: none;
}

html[data-ab-variant='control'] [data-ab-content='control'],
html[data-ab-variant='aiV2'] [data-ab-content='aiV2'] {
  display: block;
}

根节点可以同时保存多个互不干扰的运行时状态:

html
<html data-login-state="logged-in" data-ab-variant="aiV2">

适用范围

该方案适合:

  • 登录前后文案、按钮和普通模块的切换。
  • 小范围 AB 文案或样式实验。
  • SSG 页面中需要在首次绘制前确定的展示状态。
  • 不包含敏感数据的纯展示内容。

不建议用于:

  • 登录后才有权限访问的敏感数据。
  • 两个版本都包含大量图片、视频或复杂交互组件的整页实验。
  • 需要保证隐藏版本完全不下载资源的场景。

CSS 隐藏不等于权限隔离。隐藏节点仍然存在于 HTML 和 DOM 中,用户可以通过开发者工具查看。敏感信息必须由接口鉴权,并且只在鉴权成功后请求和渲染。

另外,即使图片节点被 display: none 隐藏,浏览器仍可能下载其资源。对于较重的隐藏模块,应在运行时只挂载命中的版本,或者采用多份静态产物配合 Edge 重写。

结论

对于 SSG 页面中的轻量登录态展示切换,将状态同步写入 <html> 的 data-* 属性,再由首屏 CSS 控制 DOM 显隐,是一种实现简单、不会破坏 Hydration、且基本无首屏闪烁的方案。

它解决的是展示分流,不是身份认证或权限控制。只要明确这个边界,就可以用于登录态 UI 和小型 AB 实验。

评论
0/100