React、单页应用程序和浏览器的后退按钮

React, Single Page Apps, and the Browser's back button

我知道我的问题可能只是“这不能完成,这定义了 SPA 的目的”。但是...

我在我的 REACT 网络应用程序中导航到 mydomain.com。该页面从后端加载数据并填充精心设计的网格。加载和渲染大约需要 2 秒。

现在,我在那个精致的页面上单击 link,然后导航到 mydomain.com/otherPage。当我单击浏览器的 BACK 按钮从 return 到 mydomain.com 时,它是空白的,并且必须从头开始重建,因为 SPA 规定必须在每次页面更改时擦除并重建 DOM (至少它的特定于页面的动态部分,因为路由可以在 header/footer 等的固定布局内)。 我明白了...

除了迁移到 nextJS 和使用 SSR 之外....

在 REACT 中是否有任何神奇的解决方案以某种方式 'retain' 导航出页面时的 DOM 页面,以便当您浏览器返回页面时,该页面会立即显示并且不是从头开始渲染的?

API 个电话

您可以简单地将生成的需要密集计算的元素放在一个状态中,放在一个在更改页面时永远不会卸载的组件中。

这是一个示例,其中 Parent 组件包含 2 个子组件,5 秒后显示一些 JSX。当您单击您导航到子链接的链接时,当您单击浏览器的后退按钮时,您将返回 URL 路径。并且再次上 / 路径时,立即显示“密集”计算需要的元素。

import React, { useEffect, useState } from "react";
import { Route, Link, BrowserRouter as Router } from "react-router-dom";

function Parent() {
  const [intensiveElement, setIntensiveElement] = useState("");
  useEffect(() => {
    const intensiveCalculation = async () => {
      await new Promise((resolve) => setTimeout(resolve, 5000));
      return <p>Intensive paragraph</p>;
    };
    intensiveCalculation().then((element) => setIntensiveElement(element));
  }, []);
  return (
    <Router>
      <Link to="/child1">Go to child 1</Link>
      <Link to="/child2">Go to child 2</Link>
      <Route path="/" exact>
        {intensiveElement}
      </Route>
      <Route path="/child1" exact>
        <Child1 />
      </Route>
      <Route path="/child2" exact>
        <Child2 />
      </Route>
    </Router>
  );
}

function Child1() {
  return <p>Child 1</p>;
}

function Child2() {
  return <p>Child 2</p>;
}

关于快速重新显示DOM

我上面的解决方案适用于不像 API 调用那样做两次缓慢的事情。但是根据 Mordechai 的评论,我已经 an example repository 比较 DOM 使用浏览器后退按钮时 4 种解决方案的真正大 HTML 加载时间:

  1. 没有javascript的普通html(供参考)
  2. 根据我上面给出的代码示例做出反应
  3. Next.js 与下一个页面路由
  4. A CSS React 和 overflow: hidden; height: 0px; 的解决方案(比 display: none; 更有效并且元素不采用任何 space 与 visibility: hidden; 相反,opacity: 0; 等,但也许有更好的 CSS 方式)

每个示例加载 100 000 <span> 个元素的初始页面,并具有导航到小页面的链接,以便我们可以尝试浏览器。

您可以自己测试 static 版本的示例 on github pages here(这些页面在普通计算机上加载需要几秒钟,因此如果出现以下情况,请避免点击它们在移动设备上)。

我添加了一些 CSS 以使元素足够小以在屏幕上看到所有元素,并比较浏览器如何更新整个显示。

这是我的结果:

在 Firefox 上:

  1. 普通 HTML 加载约 2 秒,后退按钮显示页面约 1 秒
  2. 下一个应用在 ~2 秒内加载,后退按钮在 ~1 秒内显示页面
  3. CSS React 应用中的解决方案加载时间约为 2 秒,后退按钮显示页面的时间约为 1 秒
  4. React 应用加载时间约为 2.5 秒,后退按钮显示页面时间约为 2 秒

在 Chrome 上:

  1. CSS React 应用中的解决方案加载时间约为 2 秒,后退按钮显示页面的时间约为 1 秒
  2. React 应用加载时间约为 2.5 秒,后退按钮显示页面时间约为 2 秒
  3. 普通 HTML 在 ~8 秒内加载,后退按钮在 ~8 秒内显示页面
  4. 下一个应用加载时间约为 8 秒,后退按钮显示页面的时间约为 8 秒

还需要注意的重要事项:对于 Chrome 当 Next.js 或普通 HTML 需要 8 秒时,它们实际上是在页面上一点一点地加载元素,而我没有缓存使用后退按钮。

在 Firefox 上,我没有那种一点一点的显示,要么什么都没有显示,要么什么都显示(就像我在 Chrome 上使用的反应状态一样)。

我真的不知道我能得出什么结论,除了也许测试是有用的,有时会有惊喜...

我最初误读了这个问题。当用户转到另一个域 上的页面 .

时,我将保留最初的答案

更新答案

您在评论中写过

I was clear enough

嗯...从这里的讨论来看,情况并非如此。

这里有一些要考虑的问题,当你回答这些问题时,这应该是你问题的解决方案......不管它是什么:

  1. 你真的需要在组件的挂载上进行网络调用吗?对于 SPA,将您的状态和它的视觉表示(复数!)分离通常是个好主意。
  2. 显然,你需要缓存机制。但是,它应该是某种渲染节点的“缓存”(正如其他每个答案中所建议的那样)还是从网络接收的数据缓存,或两者兼而有之,取决于您。 SSR - 不是缓存机制。它的存在是出于其他原因。
  3. 你用router吗?如果是,那么是哪一个以及如何?因为其中一些 then 可以在内存中保留以前的路线,所以如果运气好的话,您永远不会遇到空白页问题。这就是答案。
  4. 但也许 mydomain.com/otherPage 不受 React or/and 的控制,也许它不是我们在这里谈论的真正的 SPA。进入这个页面的效果和进入另一个域的效果一样吗?那么我的初步答案成立。

简而言之:

  1. Is there any magic solution in REACT to somehow 'retain' the DOM for a page when navigating out of it.

    • ,如果通过导航出它一个页面 你的意思是在你的 SPA 中导航到 另一条路线 并且只是渲染一些其他组件, 没有 通过“标准”执行 GET 请求 <a>-单击、window.location.href 更改或类似的操作将导致浏览器启动 新页面 加载。

      为此,请阅读路由器的文档。

    • 如果您真的要离开 SPA。

      对于这种情况,我建议 serviceWorker。就我的口味而言,与使用 SSR 更改项目架构相比,这是一种更简单、更灵活的解决方案。

  2. as SPA dictates the DOM must be erased and re-built with every page change

    完全没有。 DOM 只有在组件状态或 props 发生变化时才会被擦除。但为了帮助您,我们需要查看代码。

初步回答

不太清楚你的问题是什么。您专注于防止 DOM 重建的想法,但同时您说瓶颈是 API 调用。他们是两个完全不同的事情。

您的问题的可能解决方案在很大程度上取决于您的代码架构。

如果您可以控制服务器端,则可以为您的调用设置缓存。如果没有,您可以在 PWA-style manner.

的客户端设置缓存

如果您有一个集中式商店,您可以在 <a> 单击时将其状态保存到 localStorage,并在用户返回到您的 localStorage 时从 localStorage 重新填充您的页面页。如果不是,您可以再次求助于 Service Worker API 来拦截 API 调用和 return 缓存响应。 (或者只是覆盖 fetch 或其他)

您甚至可以通过将 HTML 保存到 localStorage 并在用户返回时立即显示它来“模拟”SSR。 (但是该页面在几秒钟内不会完全正常运行,需要在您 API-调用完成时更换)

但是没有可行的方法来阻止 DOM 重建,因为虽然理论上可行,但缓存整个 React 内部状态可能是不切实际的。如果您的主要问题确实是 DOM 重建本身,那么您的代码可能需要认真优化。

使用开箱即用的路由,我会说:这是不可能的。

但是谁说一定要用路由呢?


解决方案一:

为什么不使用 Portals

如果您想 'retain' DOM 用于页面上的任何导航,这可能不起作用。但是如果你只想 'retain' 它在一个特定的页面上,那么你可以打开一个全屏 portal/modal/dialog (或者你想怎么称呼它)。


方案二:

如果你想'retain'所有导航DOM,那么你也可以自己写一个“路由器组件”。

您的组件逻辑可能如下所示:

首先你需要查找-table。当 url 被调用时,给每个 url 一个应该被渲染的相关组件。

  1. 检查目标 url 之前是否打开过
  2. 如果不是:创建一个新的 div 并在其中打开匹配的组件(来自查找)。将 div 置于最前面 (z-index)
  3. 如果是:将相关的(已经存在的)div放在前面(z-index)

编写这样的组件应该不会太难。我只看到两个问题:

  • 性能:如果您同时打开许多重叠组件,这可能会降低您的页面速度(取决于您有多少页面和内容)
  • 刷新时一切都丢失了

是的,很有可能在保持 DOM 呈现但隐藏的情况下切换路由! 如果您正在构建 SPA,使用客户端路由是个好主意。这使您的任务变得简单:

要隐藏,同时将组件保留在 DOM 中,请使用以下任一项 css:

  1. .hidden { visibility: hidden }只隐藏未使用的component/route,但仍保持其布局。

  2. .no-display { display: none } 隐藏未使用的 component/route,包括其布局。

对于路由,使用 react-router-dom,您可以在 Route 组件上使用 function children prop

children: func

Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children prop. It works exactly like render except that it gets called whether there is a match or not.The children render prop receives all the same route props as the component and render methods, except when a route fails to match the URL, then match is null. This allows you to dynamically adjust your UI based on whether or not the route matches.

在我们的例子中,如果路由不匹配,我将添加隐藏 css classes:

App.tsx:

export default function App() {
  return (
    <div className="App">
      <Router>
        <HiddenRoutes hiddenClass="hidden" />
        <HiddenRoutes hiddenClass="no-display" />
      </Router>
    </div>
  );
}

const HiddenRoutes: FC<{ hiddenClass: string }> = ({ hiddenClass }) => {
  return (
    <div>
      <nav>
        <NavLink to="/1">to 1</NavLink>
        <NavLink to="/2">to 2</NavLink>
        <NavLink to="/3">to 3</NavLink>
      </nav>
      <ol>
        <Route
          path="/1"
          children={({ match }) => (
            <li className={!!match ? "" : hiddenClass}>item 1</li>
          )}
        />
        <Route
          path="/2"
          children={({ match }) => (
            <li className={!!match ? "" : hiddenClass}>item 2</li>
          )}
        />
        <Route
          path="/3"
          children={({ match }) => (
            <li className={!!match ? "" : hiddenClass}>item 3</li>
          )}
        />
      </ol>
    </div>
  );
};

styles.css:

.hidden {
  visibility: hidden;
}
.no-display {
  display: none;
}

工作代码沙箱:https://codesandbox.io/s/hidden-routes-4mp6c?file=/src/App.tsx

比较 visibility: hiddendisplay: none 的不同行为。

请注意,在这两种情况下,所有组件仍安装到 DOM! 您可以使用浏览器的开发工具中的检查工具进行验证。

可重复使用的解决方案

对于可重用的解决方案,您可以创建一个可重用的 HiddenRoute 组件。

在下面的示例中,我使用钩子 useRouteMatch,类似于 children Route prop 的工作方式。基于匹配,我将隐藏的 class 提供给新组件 children:

import "./styles.css";
import {
  BrowserRouter as Router,
  NavLink,
  useRouteMatch,
  RouteProps
} from "react-router-dom";

// Reusable components that keeps it's children in the DOM
const HiddenRoute = (props: RouteProps) => {
  const match = useRouteMatch(props);
  return <span className={match ? "" : "no-display"}>{props.children}</span>;
};

export default function App() {
  return (
    <div className="App">
      <Router>
        <nav>
          <NavLink to="/1">to 1</NavLink>
          <NavLink to="/2">to 2</NavLink>
          <NavLink to="/3">to 3</NavLink>
        </nav>
        <ol>
          <HiddenRoute path="/1">
            <li>item 1</li>
          </HiddenRoute>
          <HiddenRoute path="/2">
            <li>item 2</li>
          </HiddenRoute>
          <HiddenRoute path="/3">
            <li>item 3</li>
          </HiddenRoute>
        </ol>
      </Router>
    </div>
  );
}  

可重用解决方案的工作 CodeSandbox:https://codesandbox.io/s/hidden-routes-2-3v22n?file=/src/App.tsx

我经常使用的一个解决方案是将数据保存在 location.state 中。然后在返回时,组件首先检查 location.state 中的数据,然后再尝试获取数据。

这允许页面立即呈现。

const Example = (props) => {
  const history = useHistory();
  const location = useLocation();

  const initialState = location.state;
  const [state, setState] = useState(initialState);


  useEffect(() => {
    const persistentState = state;
    history.replace({ pathname: location.pathname }, persistentState);
  },[state]);

  return ();
}