【问题标题】:DOMException: Failed to execute 'replaceState' on 'History': A history state object with URLDOMException:无法在“历史”上执行“replaceState”:具有 URL 的历史状态对象
【发布时间】:2020-11-16 18:34:07
【问题描述】:

在 Google 上打开网页的缓存版本时,React 应用程序出现以下错误。

DOMException:无法在“历史”上执行“replaceState”:历史 无法在 URL 中创建具有 URL 'https://projecturl' 的状态对象 具有源“https://webcache.googleusercontent.com”和 URL 的文档 'https://webcache.googleusercontent.com/search?q=cache:X4dz2ukZZAYJ:https://projecturl/+&cd=1&hl=en&ct=clnk&gl=in'

在我们的应用程序中,我们使用 react-router-dom 并实现了服务器端渲染。在谷歌搜索中打开带有缓存选项的页面时,它首先加载页面几秒钟,然后在控制台中显示带有上述错误的空白页面。

在寻找解决方案时,我发现了https://github.com/ReactTraining/react-router/issues/5801 与我的问题有关,但没有解决方案。

更新 1:

here 提出了同样的问题,但针对 Angular。 虽然我无法理解答案中的解释以及它与我的问题有何关系。

我们正在使用React Loadable SSR Add-on 为我们的 React 应用程序进行服务器端渲染。

更新 2:

在用于服务器端渲染的 npm 包的 Git repo 上打开了相同的问题。 Issue opened

更新 3:

当我通过禁用安全性在谷歌浏览器中打开它时,该页面工作正常。因此它不应该与我的代码有关。

此外,在必应搜索引擎缓存版本中打开时会出现不同的错误:

脚本资源位于重定向后面,这是不允许的。

在 bing 和 yahoo 搜索引擎上,404 页面以缓存版本出现。

更新 4:

这是路由文件的外观:

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Loadable from 'react-loadable';
import {
  OTPNew,
  LoginNewFb,
  OnboardingWrapper,
  PageNotFound,
  SignUpSpecialty,
  SignUpDetails,
  Feed,
} from './lazy';

const RootComponent = Loadable({
  loader: () =>
    import(/* webpackChunkName: "rootcomp" */ '../components/RootComponent'),
  loading: () => null,
  modules: ['../components/RootComponent'],
  webpack: () => [require.resolveWeak('../components/RootComponent')],
});

const signupRoutes = [
  {
    path: '/login/otp',
    component: OTPNew,
  },
  {
    path: '/login',
    component: LoginNewFb,
  },
  {
    path: '/signup/details',
    component: SignUpDetails,
  },
  {
    path: '/signup',
    component: SignUpSpecialty,
  },
];

const Routes = () => {
  return (
    <Switch>
      {signupRoutes.map(sRoute => (
        <Route
          key={sRoute.path}
          path={sRoute.path}
          render={routeProps => (
            <OnboardingWrapper>
              <sRoute.component {...routeProps} />
            </OnboardingWrapper>
          )}
        />
      ))}
      <Route path="/feed" component={Feed} />
      <Route path="/" component={RootComponent} />
      <Route path="*" component={PageNotFound} />
    </Switch>
  );
};

export default Routes;

RootComponent.js

    import React from 'react';
    import { Switch, Route } from 'react-router-dom';
    import { useSelector } from 'react-redux';
    import MainComponent from './MainComponent';
    import { DiscussionComponent, HomePage, PageNotFound } from '../routes/lazy';
    import { useIsMobile } from '../actions/VerifyMobileAction';
    import rootRoutes from '../routes/rootRoutes';
    import quizRoutes from '../routes/quizRoutes';
    import { parseQueryParameter, getAPIHost } from '../helpers/helperFunctions';
    
    function cacheQueryParser(query, projectCanonnicalAddr) {
      return query
        .split(projectCanonnicalAddr)
        .pop()
        .split('+')[0];
    }
    
    function getPageOrNotFound(location) {
      const queryObject = parseQueryParameter(location.search);
      const projectCanonnicalAddr = getAPIHost();
    
      if (
        location.pathname === '/search' &&
        'q' in queryObject &&
        queryObject.q.indexOf('cache') === 0 &&
        queryObject.q.indexOf(projectCanonnicalAddr) > -1
      ) {
        const replacer = cacheQueryParser(queryObject.q, projectCanonnicalAddr);
        return {
          ComponentRender: null,
          replacer,
        };
      }
      return {
        ComponentRender: PageNotFound,
        replacer: null,
      };
    }

const RootComponent = () => {
  const { OtpVerified } = useSelector(store => store.authenticationReducer);
  const isMobileViewport = useIsMobile();

  function logicForHomeRoute() {
    if (OtpVerified) {
      return {
        component: DiscussionComponent,
      };
    }
    return {
      renderHeaderDesktop: false,
      renderHeaderMobile: false,
      renderFooter: false,
      renderSideProfile: false,
      component: HomePage,
    };
  }

  const typeOfAppClassName = `${
    isMobileViewport ? 'mobile' : 'desktop'
  }-viewport-app`;

  return (
    <div className={typeOfAppClassName}>
      <Switch>
        <Route
          exact
          path="/"
          render={() => <MainComponent {...logicForHomeRoute()} />}
        />
        {[...quizRoutes, ...rootRoutes].map(sRoute => (
          <Route
            key={sRoute.path}
            path={sRoute.path}
            render={props => {
              const { location, history } = props;
              if (sRoute.path === '/:alternate_username') {
                if (location.pathname.startsWith('/dr') === false) {
                  const { replacer, ComponentRender } = getPageOrNotFound(
                    location
                  );
                  if (ComponentRender) {
                    return <ComponentRender />;
                  }
                  history.replace(replacer);
                  return null;
                }
              }
              return <MainComponent {...props} {...sRoute} />;
            }}
          />
        ))}
      </Switch>
    </div>
  );
};

export default RootComponent;

更新 5

控制台显示另一个错误:

获取脚本时收到错误的 HTTP 响应代码 (404)。

【问题讨论】:

  • 您介意分享您的代码吗?
  • 似乎您正试图将完整的网址推送到历史记录。
  • @SonuBamniya,我不确定我应该分享哪一部分代码来调试这个问题

标签: reactjs react-router react-router-dom server-side-rendering


【解决方案1】:

由于您在“空白页面”之前看到您的正常页面,我的猜测是,首先,您会看到网站的 SSR 版本,然后在加载脚本后,您会看到“空白页面”,因为 CORS 限制.

作为一种解决方案,您可以只为您的域加载脚本,而为 google/bing/yahoo 保留 SSR 版本,而没有机会加载这些脚本并破坏网站。

答案基于react-loadable-ssr-addon 的示例,这里的想法是检查window.location.origin 与您的域的来源。否则,完全跳过这些文件。

原例:

res.send(`
  <!doctype html>
  <html lang="en">
    <head>...</head>
    ${styles.map(style => {
      return `<link href="/dist/${style.file}" rel="stylesheet" />`;
    }).join('\n')}
    <body>
      <div id="app">${html}</div>
      ${scripts.map(script => {
        return `<script src="/dist/${script.file}"></script>`
      }).join('\n')}
  </html>
`);

这是我的动态加载示例:

res.send(`
  <!doctype html>
  <html lang="en">
    <head>...</head>
    ${styles.map(style => {
      return `<link href="/dist/${style.file}" rel="stylesheet" />`;
    }).join('\n')}
    <body>
      <div id="app">${html}</div>
      <script>
        function loadScript(url) {
          var s = document.createElement("script");
          s.src = url; document.body.appendChild(s);
        }

        if (
          window.location.origin === "https://example.com" ||
          window.location.origin === "http://localhost" // for development purpose
        ) {
          ${scripts.map(script => {
            return `loadScript("/dist/${script.file}");`
          }).join('\n')}
        }
      </script>
  </html>
`);

当然,你的应用程序中有你自己的代码,所以我不能给你一个完整的解决方案。你应该改变你的代码来遵循这个想法。

至于其他错误。部分地,它们也基于 CORS 限制,其中之一来自https://connect.facebook.net/en_US/fbevents.js。与您的服务工作者文件相关的 404 错误,由 google 的内部“webcache”算法引起,我不确定您是否可以对此采取措施。

无论如何,这些错误不应影响缓存站点的正确显示,因为它们不是您看到空白页面的原因。在我看来,当然,这是基于截图。

【讨论】:

  • artanik,非常感谢您提供这个详细的答案。你能解释一下为什么我的脚本出现 CORS 错误吗?
  • Harsimer,因为“缓存”版本具有不同的域(准确地说是webcache.googleusercontent.com,这违反了COSR的规则。
  • 好的,知道了。那么最终用户将无法与网页交互,并且没有任何脚本将加载到缓存版本中?我已经使用 cors npm 包 (app.use(cors())) 在 express 中启用了 CORS,但这可能是针对 http 请求而不是脚本。
  • 好吧,您的浏览器会为您发出 HTTP 请求,以获取 js 文件。您可以允许某些服务获取这些文件并执行它们,方法是将它们的 URL 添加到“允许列表”,如 here in the docs 所示。
  • 对,这是快递,试试req.hostname
猜你喜欢
  • 1970-01-01
  • 2016-02-04
  • 1970-01-01
  • 2018-02-05
  • 1970-01-01
  • 2018-08-03
  • 2018-09-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多