关于 SSR 内容一致性的问题

最近我又双叒叕打算重写个人主页了,这次打算尝试一下 Gatsby,这是背景。

如果大家不了解 Gatsby 是什么,我这里简单介绍一下,它是一个基于 React 的静态页面构建工具。开发者通过编写页面模板(其实就是 React 组件)和配置文件,Gatsby 就能为指定的数据文件(可以是 Markdown 等)创建页面。

开发过程中我一直使用的是 serve 模式,这个模式就类似于 webpack dev server,所有的路由都会 rewrite 到 index.html,完全由客户端进行渲染。我在应用里添加了很多偏好设置,例如多语言和夜间模式之类的。就拿多语言举例,实现的大致思路就是写一个 Context 作为 scope,然后所有 scope 下的组件都可以通过 useContext 拿到有关多语言的上下文数据。

看一下代码:

import React, { createContext, useState, useContext } from 'react';

import { setPref, getPref } from './globalPrefs';

const ctx = createContext({});

export function I18NScope(props) {
  const [currentLang, setCurrentLang] = useState(getPref('lang') || 'en');

  function _setCurrentLang(lang) {
    setPref('lang', lang);
    setCurrentLang(lang);
  }

  return (
    <ctx.Provider
      value={{
        currentLang,
        setCurrentLang: _setCurrentLang,
        stringMap: props.stringMap }}>
      {props.children}
    </ctx.Provider>
  );
}

export function useI18N(key) {
  const { currentLang, setCurrentLang, stringMap } = useContext(ctx);
  if (key) {
    return ((stringMap || {})[currentLang] || {})[key] || key;
  }
  return { currentLang, setCurrentLang };
}
复制代码

使用的话也很简单:

function Post(props) {
  const { currentLang } = useI18N();
  const { currentStyle } = useTheme();

  const data = props.data;

  return (
    <>
      <div style={{ position: 'relative', paddingRight: '40px' }}>
        <Title text={data[currentLang].frontmatter.title} />
        <Paragraph>{data[currentLang].frontmatter.subtitle}</Paragraph>
        <Settings />
      </div>
      <div className={currentStyle.divider} />
      <div style={{ marginTop: '20px' }}>
        <article dangerouslySetInnerHTML={{ __html: data[currentLang].html }} />
      </div>
      <Links links={props.links} />
      <Footer />
    </>
  );
}
复制代码

用户设置语言后会同步到 LocalStorage 中,下一次应用启动时 context 的默认值就是 LocalStorage 中存储的值,这些都很简单。

到这里一切都没有问题。正当我写完一个版本打算 deploy 看一下效果时,我发现设置完语言再刷新页面,内容既有中文也有英文,英文正是默认语言(也就是 SSR 时输出的 HTML 的语言)。

有英文的部分是 article 标签下的文章内容,看上去是 dangerouslySetInnerHTML 属性在 Hydrate 过程中没被处理到。直觉告诉我这是 React 的 bug...

我迅速搜了一遍 GitHub 上的 issues,发现没有和我情况一样且与 dangerouslySetInnerHTML 相关的问题。后来我又发现,不仅仅是 dangerouslySetInnerHTML 不不一致,连 className 也不一致。于是我修改了关键字继续搜索,终于发现了 #14281 这个 issue,正符合我描述的现象。

其实这并不是一个 bug,而是 by design。简单来说 React SSR 以前是会重新渲染整个页面的,因此上述的问题并不存在,但是现在的版本中,React 会假设 SSR 的内容与 hydrate 后的内容一致。也就是说,我 SSR 出来的 HTML 是什么语言,运行出来以后就应该是什么语言。想要做到这一点也很容易,分别为英文和中文添加路由。语言还好说,那主题呢?如果以后再增加字号设置,我难道要为每一种组合都添加路由?显然是不行的。

当然,方法还是有的,就像 React 文档所说的,二次渲染就好。因为 SSR 过程是不会触发 componentDidMount()useEffect 的 effect 的。所以我们可以通过一个状态来识别当前的环境。一旦 componentDidMount() 或者 effect 被调用,就说明现在是客户端渲染,这时再应用 LocalStorage 里的设置重新渲染就可以了。

既然方法有了,剩下的事情就很简单了,直接修改我们的 context 组件就行了:

export function I18NScope(props) {
  const isClient = useClientEnv();  // 添加这个状态
  const [currentLang, setCurrentLang] = useState(getPref('lang') || 'en');

  function _setCurrentLang(lang) {
    setPref('lang', lang);
    setCurrentLang(lang);
  }

  return (
    <ctx.Provider
      value={{
        currentLang: isClient ? currentLang : 'en',
        setCurrentLang: _setCurrentLang,
        stringMap: props.stringMap }}>
      {props.children}
    </ctx.Provider>
  );
}
复制代码

其中 useClientEnv 就是一个自定义 hook:

import { useState, useEffect } from 'react';

export function useClientEnv() {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => {
    setIsClient(true);
  }, []);

  return isClient;
}
复制代码

重新 deploy,问题解决了。

TL;DR

SSR 和第一次客户端渲染的内容要保持一致,如果一定会有不一致,那就在第二次渲染时再渲染最新内容。

现在的 SSR 主要有两种目的,一种是为了减少首屏等待时间,那么对于这种目的,我们就可以在服务端渲染最少量的内容,例如只渲染出 skeleton。

另外一种是为了 SEO,那么服务端就需要渲染页面实际的内容,对于上面多语言的 case,其实最佳实践就是用路由控制显示的语言版本,这也有利于搜索引擎爬取内容,你一定不希望用户搜索出来的是中文,点进去却是英文吧。而主题、字号这类偏好设置,可以通过二次渲染来同步,不过这又引出了另外一个问题:页面闪烁。页面会在 JS 加载完的一瞬间重新渲染。即便 JS 被缓存,HTML 加载完成和 JS 加载完成并执行之间还是会有一定的时间间隔。这里可以做一个简单的优化:先将内容通过 CSS 隐藏起来,并在内联 script 标签中启动定时器,超时后显示内容以防首次 JS bundle 加载时间过长。后期就可以通过 Service Worker 等方式缓存 JS bundle 和相关资源,那么之后在进入页面时,由于 JS 资源被缓存,可以在短时间内加载并执行。

最后,来看一下效果吧:cyandevio.unixzii.now.sh

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它使JavaScript可以用于服务器端开发。React是一个用于构建用户界面的JavaScript库,它提供了组件化的开发方式和高性能的渲染能力。SSR(Server-Side Rendering)是指在服务器端将React组件渲染成HTML字符串后再返回给浏览器,以提高页面的渲染性能和SEO友好度。可以通过使用React.js和Node.js来实现SSR。 在使用React.js和Node.js实现SSR时,可以采用以下步骤: 1. 在Node.js中搭建一个服务器,并配置好路由,用于处理HTTP请求。 2. 在服务器端使用React.js渲染组件,并将渲染后的HTML字符串返回给浏览器。 3. 在浏览器端使用React.js重新渲染组件,并与服务器端渲染的HTML字符串进行比对,以确保两者一致。 4. 在浏览器端绑定事件和处理用户交互,以提供更丰富的用户体验。 使用Node.js和React.js实现SSR可以带来以下好处: 1. 提高页面的初始加载速度,因为服务器端渲染的HTML字符串可以更快地呈现给用户。 2. 改善SEO(Search Engine Optimization),因为搜索引擎可以直接抓取服务器端渲染的HTML内容。 3. 提高用户体验,因为在页面加载过程中,用户可以看到内容的逐渐呈现,而不是空白的加载界面。 需要注意的是,使用Node.js和React.js实现SSR需要一定的技术储备和对这两个技术的深入了解。同时,还需要考虑服务器的性能和可扩展性,以确保能够处理大量的并发请求。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [详解基于React.js和Node.js的SSR实现方案](https://download.csdn.net/download/weixin_38665162/12945005)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [JavaScript Applications with Node.js, React, React Native and MongoDB](https://download.csdn.net/download/weixin_43960172/10904948)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Java在小程序开发方面的优势及应用](https://download.csdn.net/download/milk416666/88250412)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值