NextJS 水合错误(文本内容与服务器渲染的 HTML 不匹配。)

Kar*_*rel 5 javascript typescript reactjs next.js

仅在部署应用程序时出现两个错误,而不是在开发模式或本地构建中,这完全是另一个问题。

第一个错误:文本内容与服务器呈现的 HTML 不匹配。

第二个错误:补水时出现错误。由于错误发生在 Suspense 边界之外,因此整个根将切换到客户端渲染。

我发现这些错误是由我从 Unix 时间戳格式 (current.dt) 的 API 获取的时间格式引起的,但我不知道如何处理此问题。我应该在服务器上格式化日期,即在 getServerSideProps 中吗?或者这些错误是由其他原因引起的?

编辑:有错误的实时应用程序:https ://weather-test-mu.vercel.app/

天气显示

import type { Current, Location } from '../../types/typeWeatherApi';

const WeatherDisplay = ({
    location,
    current,
}: {
    location: Location;
    current: Current;
}) => {
    // This causes the errors:
    const hour = ('0' + new Date(current.dt * 1000).getHours()).slice(-2);
    const minutes = ('0' + new Date(current.dt * 1000).getMinutes()).slice(-2);
    const currentTime = `${hour}:${minutes}`;

    return (
        <article>
            <h1>{location.name}</h1>
            <p>{location.country}</p>
            <p>{current.feels_like}</p>
            {/* This causes the error: */}
            <p>{currentTime}</p>
        </article>
    );
};

export default WeatherDisplay;
Run Code Online (Sandbox Code Playgroud)

带有 getServerSideProps 的索引页

import axios from 'axios';
import { useState } from 'react';

import type { NextPage, GetServerSideProps } from 'next';
import type { Data, Location } from '../types/typeWeatherApi';

import { getCurrentWeather } from './api/currentWeather';
import { getCurrentLocation } from './api/currentLocation';

import WeatherDisplay from '../components/weather-display/weather-display';

const Home: NextPage = ({ initialWeather, initialLocation }: any) => {
    const [location, setLocation] = useState<Location>(initialLocation);
    const [weatherData, setWeatherData] = useState<Data>(initialWeather);
    const [units, setUnits] = useState('metric');
    const [lang, setLang] = useState('en');

    const getGeolocationData = () => {
        navigator.geolocation.getCurrentPosition(
            (position) => {
                axios
                    .get(
                        `/api/currentLocation?lon=${position.coords.longitude}&lat=${position.coords.latitude}`
                    )
                    .then((response) => setLocation(response.data[0]));
            },
            (error) => {
                console.warn(`ERROR(${error.code}): ${error.message}`);
            },
            {
                timeout: 10000,
                maximumAge: 0,
            }
        );
    };

    const getCurrentWeather = async () => {
        await axios
            .get(
                `/api/currentWeather?lon=${location.lon}&lat=${location.lat}&units=${units}&lang=${lang}`
            )
            .then((response) => setWeatherData(response.data))
            .catch((error) => console.error(error));
    };

    return (
        <>
            <section>
                <div>
                    <p>Latitude: {location.lat}</p>
                    <p>Longitude: {location.lon}</p>
                </div>
                <button onClick={getGeolocationData}>Get Current Location</button>

                <button onClick={getCurrentWeather}>Get Current Weather</button>
            </section>
            <section className="current-weather">
                <WeatherDisplay location={location} current={weatherData.current} />
            </section>
        </>
    );
};

export const getServerSideProps: GetServerSideProps = async () => {
    const defaultWeatherQuery = {
        lat: '51.5072',
        lon: '0.1276',
        exclude: '',
        units: 'metric',
        lang: 'en',
    };

    const defaultLocationQuery = {
        lat: defaultWeatherQuery.lat,
        lon: defaultWeatherQuery.lon,
    };

    const defaultWeather = await getCurrentWeather(defaultWeatherQuery);
    const defaultLocation = await getCurrentLocation(defaultLocationQuery);

    return {
        props: {
            initialWeather: defaultWeather,
            initialLocation: defaultLocation[0],
        },
    };
};

export default Home;

Run Code Online (Sandbox Code Playgroud)

Kar*_*rel 3

我从GitHub 上关于 next.js 的讨论中得到了答案,我将把它粘贴到这里,供任何与我面临同样问题的人使用:

引用冰冷的约瑟夫

你好,

是的,这两个错误结合在一起了。

因为客户端在第一帧上需要看到与服务器发送过来的相同的 HTML,以便放置事件侦听器并正确放置同级和子级,如果在执行此操作时出现错误,React 会记录错误。据我所知,这种情况早在 React 17 就已经发生过。

第二个问题是由新的渲染根开始的,它将此视为渲染错误,这对并发功能不友好,因此水合作用失败并将整个事情抛到了窗外。

至少我是这样解释第二个错误的。在 React 17 发布的整整 2 年里,很多人都忽略了第 1 号错误,并不是说你忽略了,但很多人使用了这样做的库,而其他人只是忽略了它们。可能的解决方法

时间和随机性是最常产生这种情况的两个因素。

我会这样解决这个问题:

  1. 让服务器呈现时间,但采用 UTC 格式
  2. (可选)放置 CSS 使其隐藏,但仍显示在布局上(不显示任何内容)
  3. 从客户端,更新到正确的时区,也使时间可见
import { CSSProperties, useEffect, useState } from "react";
import { Example } from "../components/Example";

const TimeDisplay = ({ time }: { time: number }) => {
  const [currentTime, setCurrentTime] = useState(() => {
    const hour = ("0" + new Date(time * 1000).getUTCHours()).slice(-2);
    const minutes = ("0" + new Date(time * 1000).getUTCMinutes()).slice(-2);

    return `${hour}:${minutes} UTC`;
  });

  useEffect(() => {
    setCurrentTime(() => {
      const hour = ("0" + new Date(time * 1000).getHours()).slice(-2);
      const minutes = ("0" + new Date(time * 1000).getMinutes()).slice(-2);

      return `${hour}:${minutes}`;
    });
  }, [time]);

  // optionally make this content take space, but remain invisible, to avoid layout shifts
  // it's better to use a CSS class instead
  const style: CSSProperties = {
    visibility: currentTime.includes("UTC") ? "hidden" : "visible",
  };

  return (
    <article>
      <p style={style}>{currentTime}</p>
    </article>
  );
};
Run Code Online (Sandbox Code Playgroud)

我建议还混合使用 DateTimeFormat API, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat,它可以为您显示时区:

const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0, 200));

options = {
  hour: 'numeric', minute: 'numeric', second: 'numeric', 
  timeZoneName: 'short' 
};

console.log(new Intl.DateTimeFormat('sv-SE', options).format(date));
Run Code Online (Sandbox Code Playgroud)

如果我在repl上运行上面的代码,我会得到 3:00:00 AM UTC (这将是您的服务器发送的内容),但如果我在浏览器上运行它,我会得到 4:00:00 CET。

您页面的源代码包含服务器看到的时间。

由于用户来自不同的时区,因此您必须考虑显示 UTC 的框架,然后显示用户时区的时间。将其与 CSS 可见性相结合是很好的,因为您可以避免布局变化等,时间将取代它,但它不会是可见的。

机器人、爬虫仍然可以看到时间,对于禁用 JS 的用户,您可以添加一个标签,使日期再次可见。