React,单页应用和浏览器的后退按钮

9

我知道我的问题可能只能回答“这是无法做到的,这就是SPA的目的”。但是...

我在我的REACT Web应用程序中导航到mydomain.com。此页面从后端加载数据并填充详细的网格。它需要大约2秒钟来加载和渲染。

现在我在那个详细页面上单击一个链接并导航到mydomain.com/otherPage。当我单击浏览器的“返回”按钮返回到mydomain.com时,它是空白的,并且必须从头开始重建,因为SPA指定DOM必须在每次页面更改时擦除并重新构建(至少是特定于页面的动态部分,因为路由可以在固定的页眉/页脚布局内)。我明白了...

除了迁移到NextJS并使用SSR之外...

在REACT中是否有任何神奇的解决方案,可以在导航出页面时“保留”页面的DOM,以便当您浏览器返回到该页面时,该页面立即显示,而不是从头开始渲染?


如果我在一个页面上工作,从后端加载数据......然后转到另一个页面,并点击返回,我不想再次渲染整个页面。这可能在纯React中是不可能的,我必须迁移到NextJS。我的问题不涉及加载时间,而是关于需要重新构建页面而不是像任何经典的SSR页面一样从内存中显示它的需求。 - JasonGenX
对于 API 调用,我建议使用我的(现已删除的)答案。正如我所说,除非你覆盖它(就像某些对话框实现那样),否则无法保留 DOM。也许一个自定义渲染器可以解决这个问题。 - Mordechai
2
我正在开发一个应用程序,其中页面之间有一些密集的API调用,但由于我使用redux来存储我的数据,一旦调用完成并且我返回到该页面,它会加载得非常快,因为我不必进行这些调用。当使用后退按钮时,至少在Chrome中,Chrome将缓存API调用,并且它们比完全刷新要快,但肯定需要更多时间。如果经常使用后退按钮,也许您可以在页面上提供一个“返回”链接,以鼓励用户使用它而不是浏览器的后退按钮?这只是一个建议。 - Brett East
顺便说一句,转换到 next.js 并不难。它只需要一些时间,但确实值得。 (我知道您需要纯 React 的解决方案,但添加一点 JavaScript 调味料并不会毁掉世界) - TheMisir
如果你正在使用React Router,是否可以选择使用HashRouterMemoryRouter - Slbox
显示剩余7条评论
5个回答

8
是的,完全可以在保持DOM渲染但隐藏的情况下切换路由!如果您正在构建SPA,则最好使用客户端路由。这使得您的任务变得容易:
为了隐藏组件,同时保持其在DOM中,可以使用以下任一CSS:
1. `.hidden { visibility: hidden }` 仅隐藏未使用的组件/路由,但仍保留其布局。 2. `.no-display { display: none }` 隐藏未使用的组件/路由,包括其布局。
对于路由,使用 `react-router-dom`,您可以在 `Route` 组件上使用 function children prop

children: func

有时候你需要渲染路径是否与位置匹配。在这种情况下,你可以使用函数 children prop。它的工作方式与render完全相同,只是无论是否匹配都会被调用。当路由未能匹配URL时,children render prop接收所有相同的路由props作为组件和渲染方法,除非match为空。这使您能够根据路由是否匹配动态调整UI。

在我们的案例中,如果路由不匹配,我将添加隐藏的CSS类:

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;
}

使用CodeSandbox: https://codesandbox.io/s/hidden-routes-4mp6c?file=/src/App.tsx进行工作。

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

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

可重用解决方案

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

在以下示例中,我使用了钩子useRouteMatch,类似于children Route属性的工作方式。根据匹配结果,我向新组件的子级提供了隐藏类:

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


3

对于API调用

您可以将需要进行大量计算的生成元素简单地放置在状态(state)中,放在一个组件中,该组件永远不会在更改页面时卸载。

这里有一个示例,展示了一个名为Parent组件持有2个子组件和一些JSX内容,并在5秒后显示。当您单击链接以导航到子组件时,当您单击浏览器的返回按钮时,您会回到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的评论,我创建了一个示例存储库,以比较4种解决方案在使用浏览器后退按钮时加载大型HTML的DOM时间:

  1. 没有JavaScript的纯HTML(作为参考)
  2. 使用上面我提供的代码示例的React
  3. 使用Next.js的页面路由
  4. 一种CSS解决方案,与React和overflow:hidden;height:0px;(比display:none;更高效且元素不占用任何空间,与visibility:hidden;opacity:0;等不同,但可能存在更好的CSS方式)

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

您可以在此处测试自己的静态示例(这些页面在普通计算机上需要几秒钟才能加载,因此如果在移动设备上,请避免单击它们)。

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

以下是我的结果:

在Firefox上:

  1. 纯HTML大约需要2秒钟,后退按钮在约1秒钟内显示页面
  2. Next应用程序大约需要2秒钟,后退按钮在约1秒钟内显示页面
  3. React应用程序中的CSS解决方案大约需要2秒钟,后退按钮在约1秒钟内显示页面
  4. React应用程序大约需要2.5秒钟,后退按钮在约2秒钟内显示页面

在Chrome上:

  1. React应用程序中的CSS解决方案大约需要2秒钟,后退按钮在约1秒钟内显示页面
  2. React应用程序大约需要2.5秒钟,后退按钮在约2秒钟内显示页面
  3. 纯HTML大约需要8秒钟,后退按钮在约8秒钟内显示页面
  4. Next应用程序大约需要8秒钟,后退按钮在约8秒钟内显示页面

还要注意的一件重要事情:对于Chrome,当Next.js或纯HTML需要8秒钟时,它们实际上会逐渐在页面上加载元素,并且我没有使用后退按钮缓存。

在Firefox中,我没有那种逐渐显示的效果,要么什么都没有,要么一切都显示出来(就像我在Chrome中使用React状态时的效果)。

除了测试的东西有时会有惊喜之外,我不太清楚我能得出什么结论...


这将缓存vDOM元素(通常在每次渲染时生成),与卸载无关。问题实际上是关于实际的DOM元素。 - Mordechai
1
这是我阅读的内容: "由于需要服务器调用,直到收到响应之前,视觉元素都是空白的"。 - Roman Mkrtchian
https://dev59.com/eL_qa4cB1Zd3GeqPJoiG#66342938?noredirect=1#comment117286575_66248219 - Mordechai
https://dev59.com/eL_qa4cB1Zd3GeqPJoiG?noredirect=1#comment117286792_66248219 - Roman Mkrtchian
根据评论中的进一步阅读,OP似乎建议缓存DOM是他们正在寻找的。无论如何,尽管不太清楚,您的解决方案似乎并没有帮助。 - Mordechai
显示剩余5条评论

2

我经常使用的一个解决方案是将数据持久化在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 ();
}

这也会在页面重新加载后加载先前的状态,但重新加载页面应该确实重新加载页面,而不是恢复旧状态。 - Serguei A

2

我最初误读了问题。如果用户进入另一个域名的页面,则会保留最初的答案。

更新的答案

您在评论中写道:

我的表述很清楚

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

以下是需要考虑的一些要点,当您回答它们时,这应该是解决您问题的方案...无论问题是什么:

  1. 您是否真的需要在组件挂载时进行网络调用?对于SPA,将状态和其视觉表示(复数!)分离通常是一个好主意。
  2. 显然,您需要一些缓存机制。但是它应该是某种呈现节点的“缓存”(如其他答案中所建议的那样),还是从网络接收到的数据的缓存,或者二者兼而有之,由您决定。 SSR不是缓存机制。它存在的原因是其他的。
  3. 您使用任何router吗?如果是,那么是哪一个以及如何使用?因为其中一些可以在内存中保留先前的路由,所以您可能永远都不会遇到空白页面的问题。这可能就是答案。
  4. 但是,也许 mydomain.com / otherPage 不受React的控制,或者/和这不是真正的SPA,我们在这里谈论的是去到这个页面的影响与去到另一个域名相同?那么我的最初的答案仍然适用。

简而言之:

  1. 是否有魔术解决方案可以在导航出页面时“保留”该页面的DOM。

    • 是的,如果通过您的SPA导航到另一个路由,并只渲染其他组件,而不是通过“标准”的<a>-click执行GET请求,window.location.href更改或类似的东西,这将导致浏览器启动新页面加载。

      对于此,请查阅您的路由器文档。

    • 如果您实际上离开了您的SPA。

      对于这种情况,我建议使用serviceWorker。对我来说,它比改变项目架构以进行SSR的解决方案要简单得多且更灵活。

  2. 因为SPA规定每次页面更改都必须擦除DOM并重新构建

    根本不是这样的。只有在组件的状态或props更改时才会擦除DOM。但是,要帮助您解决此问题,我们需要查看代码。

初步回答

您的问题并不是很明确。您关注于防止DOM重建的想法,但同时又说瓶颈在API调用上。这两件事情需要处理的方式完全不同。

解决您的问题可能取决于您代码的架构。

如果您对服务器端有控制权,则可以为您的调用设置缓存。如果没有,则可以按照PWA-样式的方式在客户端设置缓存。

如果您有一个集中式存储,则可以在单击<a>时将其状态保存到localStorage中,并在用户返回您的页面时从localStorage重新填充页面。如果没有,则可以再次使用Service Worker API拦截API调用并返回缓存的响应。(或仅覆盖fetch或其他函数)

您甚至可以通过将HTML保存到localStorage中并在用户返回时立即显示来“模拟”SSR。(但是页面在几秒钟内无法完全正常工作,并且需要在完成API调用时替换)

但是,没有可行的方法可以防止DOM重建,因为尽管从理论上讲可能,但缓存整个React内部状态可能是不切实际的。如果您的主要问题确实是DOM重建本身,则很可能需要对代码进行严格优化。


我已经表达得很清楚了。答案可能是:不,React无法避免在页面内容更改时销毁DOM并重新构建它。无论加载页面需要2毫秒还是20秒,我都在寻找一种避免这种破坏/重建的方法,而不需要升级到NextJS。看起来除了采用SSR路线之外别无选择。 - JasonGenX
是的,“React无法避免销毁DOM并重新构建它”。但是,如果瓶颈在API调用中,PWA可能会是一种更简单、更好的解决方案。 - x00
而且需要明确的是:销毁页面不是React的职责,而是浏览器的决定。 - x00
没错,这正是我想的。接下来就是NextJS/SSR了... - JasonGenX

1
使用现成的路由,我会说:这是不可能的。
但是谁说我们需要使用路由呢?

解决方案1:

为什么不使用门户

如果您想在页面上进行任何导航时“保留”DOM,则这可能不起作用。但是,如果您只想在一个特定页面上“保留”它,则可以打开全屏门户/模态/对话框(或者您想称之为什么)。


解决方案2:

如果您想要在所有导航中“保留”DOM,则还可以自己编写“路由器组件”。

您的组件逻辑可能如下:

首先,您需要一个查找表。为每个URL提供一个相关组件,当调用该URL时应呈现该组件。

  1. 检查目标URL是否已经打开
  2. 如果没有:创建一个新的div,并在其中打开匹配的组件(从查找表中)。将该div置于最前面(z-index)
  3. 如果是:将相关(已存在的)div置于最前面(z-index)

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

  • 性能:如果您同时打开许多重叠的组件,这可能会减慢页面速度(取决于您拥有的页面和内容数量)
  • 刷新后所有内容都会丢失

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接