React-router在刷新或手动输入网址时无法工作。

1243

我正在使用React-router,当我点击链接按钮时它能正常工作,但是当我刷新网页时,它不能加载我想要的内容。

例如,我在localhost/joblist页面,一切都很好,因为我是通过点击链接到达此处的。但是,如果我刷新网页,我会得到:

Cannot GET /joblist

默认情况下,它不是这样工作的。最初我的URL为localhost/#/localhost/#/joblist,它们完美地运行。但我不喜欢这种类型的URL,所以试图删除那个#,我写了:

Router.run(routes, Router.HistoryLocation, function (Handler) {
 React.render(<Handler/>, document.body);
});

这个问题在localhost/上不会出现,它总是返回我想要的结果。

这个应用是单页面应用程序,因此/joblist不需要向任何服务器请求任何内容。

我的整个路由器。

var routes = (
    <Route name="app" path="/" handler={App}>
        <Route name="joblist" path="/joblist" handler={JobList}/>
        <DefaultRoute handler={Dashboard}/>
        <NotFoundRoute handler={NotFound}/>
    </Route>
);

Router.run(routes, Router.HistoryLocation, function (Handler) {
  React.render(<Handler/>, document.body);
});

除非您使用htaccess加载主要的旅游页面并告诉路由器使用location.pathname,否则它将无法正常工作。 - CJT3
你是怎么删除那个 # 符号的?谢谢! - SudoPlz
13
如果您将React应用托管在S3存储桶中,只需将错误文档设置为index.html即可。这将确保无论如何都会访问到index.html - Trevor Hutto
在我的情况下,它在Windows中表现良好,但在Linux中不行。 - Ejaz Karim
这是帮助我解决问题的参考资料:https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#serving-apps-with-client-side-routing - jimbotron
60个回答

1805

服务器端 vs 客户端

首先要明白的是,现在有两个地方解释URL,而在“旧日子”里只有一个。过去,当生活还很简单时,某个用户向服务器发送了对 http://example.com/about 的请求,服务器检查了URL的路径部分,确定用户请求的是关于页面,然后将该页面返回。

使用客户端路由,这也是React Router提供的功能,事情变得不那么简单了。起初,客户端没有加载任何JavaScript代码。因此,第一个请求将始终发送到服务器。然后,服务器会返回包含所需脚本标记以加载React和React Router等内容的页面。只有在这些脚本加载完毕后才开始第二阶段。在第二阶段中,例如,当用户点击“关于我们”导航链接时,URL仅在本地更改为http://example.com/about(由History API实现),但不会向服务器发出任何请求。相反,React Router在客户端执行其操作,确定要呈现哪个React视图,并呈现它。假设您的关于页面不需要进行任何REST调用,则已经完成。您已从主页转换到关于我们,而无需触发任何服务器请求。
基本上,当您单击链接时,会运行一些JavaScript来操作地址栏中的URL,而不会导致页面刷新,这反过来会使React Router在客户端执行页面转换。但是现在考虑一下,如果您复制并粘贴地址栏中的URL并将其发送给朋友会发生什么。您的朋友尚未加载您的网站。换句话说,她仍处于第一阶段。此时,她的计算机上没有运行React Router。因此,她的浏览器将向服务器发出请求http://example.com/about
这就是问题所在。直到现在,您可以通过在服务器的Web根目录中放置静态HTML来摆脱这个问题。但是,对于所有其他URL,当从服务器请求时,它们会产生404错误。这些相同的URL在客户端上运行良好,因为React Router正在为您进行路由,但是除非您让服务器理解它们,否则它们在服务器端会失败。
结合服务器和客户端路由
如果你想让 http://example.com/about 的URL在服务器端和客户端都能使用,你需要在服务器端和客户端都设置路由。这很有道理,对吧?
然后你就需要做出选择了。解决方案从完全绕过问题(通过返回引导HTML的通用路由),到完全同构的方法(即服务器和客户端运行相同的JavaScript代码)不等。
绕过问题:哈希历史记录
使用 哈希历史记录,而不是 浏览器历史记录,关于页面的URL将看起来像这样: http://example.com/#/about 在 URL 中,井号(#)符号后面的部分不会发送到服务器。因此服务器只看到 http://example.com/ 并按预期发送索引页面。React Router 将获取 #/about 部分并显示正确的页面。 缺点:
  • URL 不美观
  • 无法使用这种方法进行服务器端渲染。就 搜索引擎优化(SEO)而言,您的网站只包含一个几乎没有任何内容的页面。

全捕获

使用这种方法,您确实使用了浏览器历史记录,但只需在服务器上设置全捕获,将 /* 发送到 index.html,实际上给您提供了与哈希历史记录类似的情况。但是,您具有干净的 URL,并且稍后可以改进此方案,而无需使所有用户的收藏夹无效。

缺点:
  • 设置更加复杂
  • 仍然没有良好的 SEO

混合

在混合方法中,您可以根据特定路由添加特定脚本,以扩展通用情况。您可以编写一些简单的PHP脚本来返回包含内容的站点最重要的页面,以便Googlebot至少可以看到页面上的内容。 缺点
  • 设置更加复杂
  • 只有为特定路由提供特殊处理时,才能获得良好的SEO
  • 在服务器和客户端上都需要重复渲染内容的代码

同构

如果我们使用Node.js作为服务器,这样我们就可以在两端运行相同的JavaScript代码了。现在,我们在单个react-router配置中定义了所有路由,并且不需要复制我们的渲染代码。这是所谓的“圣杯”。服务器发送的标记与如果页面转换发生在客户端上将得到的完全相同。从SEO方面来看,此解决方案是最佳的。 缺点:
  • 服务器必须能够运行JavaScript。我已经尝试过使用Java和Nashorn一起使用,但对我来说不起作用。实际上,这主要意味着您必须使用基于Node.js的服务器。
  • 许多棘手的环境问题(在服务器端使用window等)
  • 陡峭的学习曲线

我应该选择哪一个?

选择您可以摆脱的那个。就我个人而言,我认为通用解决方案足够简单,因此这将是我的最低要求。这种设置允许您随着时间的推移改进事物。如果您已经将Node.js作为服务器平台,则应该调查执行同构应用程序。是的,一开始很难,但一旦掌握了它,它实际上是解决问题的非常优雅的解决方案。

因此,对我来说,这将是决定性因素。如果我的服务器运行在Node.js上,我会选择同构;否则,我会选择通用解决方案,并随着时间的推移(混合解决方案)扩展它以满足SEO要求。

如果您想了解React中的同构(也称为“通用”)渲染,有一些关于该主题的好教程: 此外,为了帮助您入门,我建议您查看一些入门套件。选择一个与您的技术堆栈匹配的套件(记住,React只是MVC中的V,您需要更多的东西来构建完整的应用程序)。首先看看Facebook自己发布的那个。

或者选择社区中众多的项目之一。现在有一个很不错的网站,试图索引所有这些项目:

我曾经从这些项目开始:

目前,我正在使用一种自己创造的、受到上述两个起始包启发的通用渲染版本,但它们现在已经过时了。

祝你好运!


4
很棒的帖子,Stijn!您是否建议使用起始套件来创建同构 React 应用程序?如果是这样,请给出您喜欢的一个示例。 - Chris
2
@Paulos3000 这取决于你使用的服务器。基本上,你需要为 /* 定义一个路由,并让它响应你的 HTML 页面。这里的棘手问题是要确保不要用这个路由拦截 .js 和 .css 文件的请求。 - Stijn de Witt
2
@Stijn de Witt 这是一个很好的澄清,并且对这个话题回答得很好。然而,当您的应用程序可能在子目录中提供服务时(例如由iis提供),有哪些选项呢?即:http://domain/appName/routeName或http://domain/appName/subfolder/routeName。 - Sagiv b.g
6
自从这个回答被写出来之后,React Router已经改变了他们的实现方式。现在他们为不同的历史记录有不同的Router实例,并且他们在后台进行历史记录的配置。基本原则仍然相同。 - Stijn de Witt
3
@peter-mortensen 你能否停止编辑我的答案?你只是在改变风格,而我并不同意你的所有选择。例如,当我链接到文章“在ReactJS中创建同构应用程序的痛苦和快乐”时,我是有意使用那个标题的。这是作者为他们的作品选择的标题。我认为尊重这一点很重要。你已经两次编辑了它以适应你喜欢的风格,而我已经两次撤销了那个编辑。你能不能现在就离开它呢?你所做的事情相当于开始了一场编辑战争。 - Stijn de Witt
显示剩余35条评论

244
如果您使用Apache作为Web服务器,您可以将以下内容插入到.htaccess文件中:
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_FILENAME} !-l
  RewriteRule . /index.html [L]
</IfModule>

我使用的是react: "^16.12.0"react-router: "^5.1.2"。这个方法是Catch-all,可能是让你开始的最简单的方法。


9
不要忘记将RewriteEngine On作为第一行。 - Kai Qing
41
请注意,本回答涉及Apache服务器上的配置。这不是React应用程序的一部分。 - user1847
我不理解上面的答案。没有任何教程说我的链接根本无法在实际环境中工作。然而,这个简单的htaccess文件起了作用。谢谢。 - Thomas Williams
1
有人可以帮我吗?这个htaccess文件应该放在哪里?添加完文件后需要运行npm run build吗? - Muteshi
1
@TayyabFerozi 你必须确保RewriteBase后面的路径和最后一个RewriteRule后面的路径与应用程序所在的文件夹匹配(如果它是子文件夹)。 - Jason Ellis
显示剩余10条评论

198

这里的答案都非常有帮助。调整Webpack服务器以接受路由对我很有帮助。

devServer: {
   historyApiFallback: true,
   contentBase: './',
   hot: true
},

历史API回退对我解决了这个问题。现在路由工作正常,我可以刷新页面或直接输入URL,无需担心Node.js服务器的解决方法。显然,此答案仅适用于使用Webpack的情况。请参见我的答案React-router 2.0 browserHistory doesn't work when refreshing以获取更详细的原因说明。

38
请注意,Webpack团队建议不要在生产环境中使用开发服务器(dev server),具体内容请参考链接:https://www.npmjs.com/package/webpack-dev-server。 - Stijn de Witt
6
对于通用开发目的而言,这是最好的解决方案。historyApiFallback足以满足需求。至于其他所有选项,也可以通过CLI使用标志--history-api-fallback进行设置。 - Marco Lazzeri
3
@Kunok,这并不起作用。这只是一种快速开发的临时解决方法,但你仍然需要找出生产环境的解决方案。 - Stijn de Witt
contentBase: ':/' 是因为您的应用程序文件可以从URL访问。 - Nezih
我在服务器和本地都遇到了这个问题。这个问题已经在本地解决了。在服务器上,.htaccess 已经修复。 - Izabela Furdzik
1
适用于React 17。谢谢。 - prathameshk73

101

对于React Router V4用户:

如果您尝试使用其他答案中提到的哈希历史技术来解决这个问题,请注意:

<Router history={hashHistory} >

在V4中不起作用,请改用HashRouter代替:

import { HashRouter } from 'react-router-dom'

<HashRouter>
  <App/>
</HashRouter>

参考: HashRouter


你能详细解释一下为什么HashRouter可以解决这个问题吗?你提供的链接对我来说并没有解释清楚。另外,有没有办法在路径中隐藏哈希值?我之前一直在使用BrowserRouter,但是遇到了404错误。 - Ian Smith
我之前使用的是浏览器路由,但在刷新时会导致404错误。后来我将浏览器路由替换为哈希路由,现在一切正常了。谢谢。 - Nabeel Hussain Shah
不希望在我的URL中出现哈希。 - Philip Rego

84

我刚刚使用Create React App创建了一个网站,遇到了这里提到的相同问题。

我使用来自react-router-dom包的BrowserRouting。我在Nginx服务器上运行,并将以下内容添加到/etc/nginx/yourconfig.conf中,它对我有用:

location / {
  if (!-e $request_filename){
    rewrite ^(.*)$ /index.html break;
  }
}

在运行Apache的情况下,这相当于将以下内容添加到.htaccess中:

Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]

这似乎也是Facebook自己建议的解决方案,可以在这里找到。


4
哇,这对于nginx来说是一个简单的解决方案;对于一个新的nginx用户来说,它非常好用。只需复制粘贴即可使用。+1 链接到官方fb文档。继续努力。 - user3773048
1
你救了我。我昨天一直在尝试解决我的问题,尝试了几种方法,但都失败了。最后你帮助我解决了这个路由器问题。啊,非常感谢你,兄弟。 - Sharifur Robin
"react": "^16.8.6", "react-router": "^5.0.1" 需要额外的配置吗?我已经尝试了这些解决方案(无论是 Nginx 还是 Apache),但都没有奏效。 - Mark A. Tagliaferro
你好,能否告诉我如果React文件夹是别名而不是根目录,这将如何运作? - krishna lodha
1
非常适用于nginx!谢谢! - Maxim
显示剩余6条评论

54

在你的index.html文件的head中添加以下内容:

<base href="/">
<!-- This must come before the CSS and JavaScript code -->

然后,当使用Webpack开发服务器时,请使用此命令。

webpack-dev-server --mode development --hot --inline --content-base=dist --history-api-fallback

--history-api-fallback 是重要部分。


值得一提的是,如果您使用的基础 href 不是 /,那么 HashRouter 将无法工作(如果您正在使用哈希路由)。 - scrowler
"<base href="/">" 标签解决了我的问题。非常感谢。Webpack 开发服务器一直试图从路径/<bundle> 获取打包文件,但失败了。 - Malisbad
我正在使用React.js + Webpack模式。我在package.json文件中添加了--history-api-fallback参数。然后页面刷新正常工作。每当我改变代码时,网页会自动刷新。"scripts": { "start": "rimraf build && cross-env NODE_ENV='development' webpack --mode development && cross-env NODE_ENV=development webpack-dev-server --history-api-fallback", ... } - NinjaDev
<base href="/" /> 挽救了局面。 - sujithramanathan

42

如果您使用Create React App:

在Create React App页面上,有一个针对许多主要托管平台的问题解决方案的详细说明,您可以在此处找到网站。例如,我使用React Router v4和Netlify作为我的前端代码托管平台。这只需要在我的public文件夹中添加一个文件(“_redirects”)和一行代码即可解决:

/*  /index.html  200

现在,当用户在浏览器中输入或刷新时,我的网站会正确呈现像mysite.com/pricing这样的路径。


2
如果您正在使用Apache HTTP服务器,那么.htaccess将无法工作,请检查Apache版本,因为在2.3.9及更高版本中,默认的AllowOverrideNone,请尝试将AllowOverride更改为All查看此答案 - Kevin
相同的问题,使用 _redirects 文件在 Netlify 和 React Router 6 中解决了我的问题。 - Reno
谢谢,我也在使用Netlify来托管我的前端代码。这对我很有用。为了澄清我的困惑,您需要在public文件夹中创建“_redirects”文件夹,然后在您创建的新文件内添加代码行“/* /index.html 200”。请注意,不要在文件名或文件内容中包含引号。这真是救命稻草。 - Joey Phillips

37
路由器可以通过两种不同的方式调用,具体取决于导航是在客户端还是服务器上进行。您已经配置为在客户端运行。关键参数是run method的第二个参数,即位置。
当您使用React Router Link组件时,它会阻止浏览器导航并调用transitionTo进行客户端导航。您正在使用HistoryLocation,因此它使用HTML5历史API完成导航的幻象,通过模拟地址栏中的新URL。如果您正在使用较旧的浏览器,则无法正常工作。您需要使用HashLocation组件。
当您刷新页面时,将绕过所有React和React Router代码。服务器会收到对/joblist的请求,并且必须返回一些内容。在服务器上,您需要将请求的路径传递给run方法,以便它呈现正确的视图。您可以使用相同的路由映射,但可能需要不同的Router.run调用。正如Charles所指出的那样,您可以使用URL重写来处理此问题。另一种选择是使用Node.js服务器处理所有请求,并将路径值作为位置参数传递。
Express.js 中,可能会像这样:
var app = express();

app.get('*', function (req, res) { // This wildcard method handles all requests

    Router.run(routes, req.path, function (Handler, state) {
        var element = React.createElement(Handler);
        var html = React.renderToString(element);
        res.render('main', { content: html });
    });
});

请注意,请求路径被传递到 run 中。为此,您需要拥有一个服务器端视图引擎,可以将渲染后的 HTML 传递给它。使用 renderToString 并在服务器上运行 React 时,还有许多其他考虑因素。一旦页面在服务器上呈现,当您的应用程序在客户端加载时,它将再次呈现,根据需要更新服务器端呈现的 HTML。

2
请问一下,能否解释一下这个语句的含义:“当你点击刷新时,会绕过所有的React和React Router代码”?为什么会发生这种情况? - VB_
React路由通常用于处理浏览器中的不同“路径”。有两种典型的方法:旧式哈希路径和新的历史API。浏览器刷新将发出服务器请求,这将绕过您的客户端React路由器代码。您需要在服务器上处理路径(但仅当您使用历史API时)。您可以使用React路由器来处理此问题,但需要执行类似于我上面描述的操作。 - Todd
这个答案中的第一个参数routes是什么?因为当你包含像<Route name="root" path="/" handler={App} />这样的东西时,会出现“SyntaxError: Unexpected token <”的错误。 - Code Uniquely
2
抱歉...你的意思是需要一个express服务器来呈现“非根”路由吗?我最初很惊讶,同构的定义没有包括数据获取在其概念中(如果您想在服务器端呈现带有数据的视图,事情会变得非常复杂,尽管这是显而易见的SEO需求-同构声称它)。现在我就像“WTF react”,说真的...纠正我如果我错了,Ember / Angular / Backbone是否需要一个服务器来呈现路由?我真的不理解使用路由的这种膨胀要求。 - Ben
1
这是否意味着,如果我的React应用程序不是同构的,那么在React Router中刷新功能就无法正常工作? - Matt
显示剩余5条评论

36
如果您通过AWS静态S3托管和CloudFront托管来托管React应用程序,则可能会出现问题。这个问题是由于CloudFront响应了403 Access Denied消息,因为它期望在我的S3文件夹中存在/some/other/path,但是该路径仅在React路由中的React Router内部存在。
解决方法是设置分布式Error Pages规则。转到CloudFront设置并选择您的分布式。接下来,转到“Error Pages”选项卡。单击“创建自定义错误响应”,然后添加403的条目,因为这是我们收到的错误状态代码。
Response Page Path设置为/index.html,状态代码设置为200。
最终结果让我惊讶的简单。索引页面被提供,但URL在浏览器中保留,因此一旦React应用程序加载,它便检测URL路径并导航到所需的路径。 错误页面403规则

所以..你的所有URL都可以工作,但返回状态码403?这是不正确的。期望的状态码应该在2xx范围内。 - Stijn de Witt
@StijndeWitt 我不确定你是否理解正确。CloudFront将处理一切 - 客户端将看不到任何403状态码。重新路由发生在服务器端,呈现索引页面,维护URL,并提供正确的路由作为结果。 - th3morg
@StijndeWitt 这是来自CloudFront的特定403错误,所以如果您发出的API调用导致您的站点异步返回403响应,那么它会正常工作。我无法想象有什么情况下您希望从CloudFront本身公开403行为,但也许有一种情况。不过,这个解决方案应该适用于99%以上的使用情况。 - th3morg
2
感谢@th3morg。这个设置也适用于多级路径吗?如果任何路径在根级别,如domain/signup、domain/login、domain/<documentId>,它对我非常有效,但是对于多级路径,如domain/document/<documentId>,它不起作用。 - Pargles
运行得非常好。自定义错误响应(发送自定义错误响应而不是从源接收到的错误)。选择“是”,并提供上述路径和响应代码。 - Nidhin
显示剩余2条评论

34

如果您将React应用程序托管在IIS上,只需添加一个包含以下内容的web.config文件:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <httpErrors errorMode="Custom" existingResponse="Replace">
            <remove statusCode="404" subStatusCode="-1" />
            <error statusCode="404" path="/" responseMode="ExecuteURL" />
        </httpErrors>
    </system.webServer>
</configuration>

这将告诉IIS服务器向客户端返回主页,而不是404错误,并且无需使用散列历史记录。


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