S3静态网站托管将所有路径路由到Index.html


250

我正在使用S3托管将使用HTML5 pushStates的JavaScript应用。问题是,如果用户对任何URL加书签,它将无法解析为任何内容。我需要的是能够处理所有url请求并在我的S3存储桶中提供根index.html的功能,而不仅仅是进行完全重定向。然后我的javascript应用程序可以解析URL并提供适当的页面。

有没有办法告诉S3为所有URL请求提供index.html而不是进行重定向?这类似于通过提供单个index.html来设置apache来处理所有传入请求,例如本示例:https : //stackoverflow.com/a/10647521/1762614。我真的想避免只运行Web服务器来处理这些路由。从S3执行所有操作都非常有吸引力。


您找到解决方案了吗?
w2lame

Answers:


301

在CloudFront的帮助下,无需URL hack即可轻松解决该问题。

  • 创建S3存储桶,例如:react
  • 使用以下设置创建CloudFront分配:
    • 默认根对象:index.html
    • 原始域名:S3存储桶域,例如:react.s3.amazonaws.com
  • 转到错误页面选项卡,单击创建自定义错误响应
    • HTTP错误代码:403:禁止(404:找不到,如果是S3静态网站)
    • 自定义错误响应:是
    • 响应页面路径:/index.html
    • HTTP响应码:200:确定
    • 点击创建

8
谢谢。迄今为止最好的答案。
jgb

而不太明显的是您保留位置,以便可以进行“自然”路由。
Lukasz Marek Sielski '16

5
这对我来说就像是一种魅力,只有我需要的自定义错误代码是404,而不是403
Jeremy S.

4
有点hack,但是效果很好:)如果CloudFront只允许我们将一系列路径映射到S3文件(不带重定向),那就太好了。
鲍勃,

3
这对我不起作用。此解决方案总是重定向到主页,而不是正确的页面……
Jim

201

我能够使它工作的方式如下:

在您的域的S3控制台的“ 编辑重定向规则”部分中,添加以下规则:

<RoutingRules>
  <RoutingRule>
    <Condition>
      <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
    </Condition>
    <Redirect>
      <HostName>yourdomainname.com</HostName>
      <ReplaceKeyPrefixWith>#!/</ReplaceKeyPrefixWith>
    </Redirect>
  </RoutingRule>
</RoutingRules>

这会将所有未找到404的路径重定向到您的根域,并带有哈希值版本的路径。因此,如果/ posts中没有文件,则http://yourdomainname.com/posts将重定向到http://yourdomainname.com/#!/posts

但是,要使用HTML5 pushState,我们需要接受此请求并根据hash-bang路径手动建立正确的pushState。因此,将其添加到index.html文件的顶部:

<script>
  history.pushState({}, "entry page", location.hash.substring(1));
</script>

这将获取哈希并将其转换为HTML5 pushState。从现在开始,您可以使用pushStates在您的应用程序中使用非哈希爆炸路径。


4
这个解决方案很棒!实际上,如果配置了html模式,则angularjs将自动执行历史记录pushState。
wcandillon

1
如果您的url中包含GET参数,这将在旧版本的IE中失败,对吗?您如何解决这个问题?
clexmond 2014年

3
谢谢!稍微调整一下,这对我来说对骨干网效果很好。我为旧版浏览器添加了一张支票: <script language="javascript"> if (typeof(window.history.pushState) == 'function') { window.history.pushState(null, "Site Name", window.location.hash.substring(2)); } else { window.location.hash = window.location.hash.substring(2); } </script>
AE Gray

10
与成功react-router使用HTML5 pushStates和溶液<ReplaceKeyPrefixWith>#/</ReplaceKeyPrefixWith>
费利克斯D.

5
它不适用于野生动物园,并且是部署策略的大问题。编写小型js重定向是一种简陋的方法。同样,重定向的数量也是一个问题。我试图弄清楚是否有一种方法可以使所有S3 url始终指向index.html。
moha297 '16

129

其他人提到的基于S3 /重定向的方法几乎没有问题。

  1. 多重重定向会在您的应用路径解析后发生。例如:www.myapp.com/path/for/test被重定向为www.myapp.com/#/path/for/test
  2. 由于SPA框架的作用,“#”的出现会导致网址栏中闪烁。
  3. SEO受到影响是因为-“嘿!它的Google强迫他进行重定向”
  4. Safari支持您的应用程序。

解决方案是:

  1. 确保您已为网站配置了索引路由。通常是index.html
  2. 从S3配置中删除路由规则
  3. 将Cloudfront放在S3存储桶的前面。
  4. 为您的Cloudfront实例配置错误页面规则。在错误规则中指定:

    • Http错误代码:404(和403或其他根据需要的错误)
    • 错误缓存最小TTL(秒):0
    • 自定义响应:是
    • 响应页面路径:/index.html
    • HTTP响应码:200

      1. 对于SEO需求+确保您的index.html不缓存,请执行以下操作:
    • 配置一个EC2实例并设置一个Nginx服务器。

    • 为您的EC2实例分配一个公共IP。
    • 创建一个将您创建的EC2实例作为实例的ELB
    • 您应该能够将ELB分配给您的DNS。
    • 现在,配置您的nginx服务器执行以下操作:Proxy_pass将所有请求传递到您的CDN(仅适用于index.html,直接从您的云端服务其他资产),对于搜索漫游器,按照Prerender.io之类的服务重定向流量

我可以提供有关nginx设置的更多详细信息,请注意。很难学到它。

一旦云前端分布更新。使您的Cloudfront缓存一次失效,使其处于原始模式。在浏览器中点击网址,一切都应该很好。


6
由于S3在文件不存在时会以403 Forbidden进行响应,因此我认为还必须对Http错误代码403重复上述步骤4
Andreas

4
对我来说,这是导致预期的(接受的)行为的唯一答案
mabe.berlin

1
@ moha297在第5点中,您是否基本上将站点配置为从nginx提取,然后从CDN代理(index.html和搜寻器请求除外)?如果是这样,您会不会失去CDN边缘服务器的优势?
拉胡尔·帕特尔

2
@ moha297您能否解释一下此评论:“您永远不要从CDN提供index.html”?我看不到通过CloudFront从S3提供index.html的问题。
卡尔·G

1
有关如何处理TTL为0的更多信息,请参见stackoverflow.com/a/10622078/4185989(简短版本:它由Cloudfront缓存,但If-Modified-SinceGET请求发送到原始位置)-对于不想要的人可能是一个有用的考虑因素像第5步中那样设置服务器
。– jmq

18

这是切线的,但这是为那些希望在S3上使用Rackt的 React Router库和(HTML5)浏览器历史记录的用户提供的提示。

假设某个用户访问/foo/bear了您的由S3托管的静态网站。根据David 先前的建议,重定向规则会将其发送到/#/foo/bear。如果您的应用程序是使用浏览器历史记录构建的,那将不会有多大好处。但是,此时已加载了您的应用程序,它现在可以操纵历史记录。

在我们的项目中包括Rackt 历史记录(另请参见从React Router项目中使用自定义历史记录),您可以添加一个了解哈希历史记录路径的侦听器并适当地替换该路径,如本示例所示:

import ReactDOM from 'react-dom';

/* Application-specific details. */
const route = {};

import { Router, useRouterHistory } from 'react-router';
import { createHistory } from 'history';

const history = useRouterHistory(createHistory)();

history.listen(function (location) {
  const path = (/#(\/.*)$/.exec(location.hash) || [])[1];
  if (path) history.replace(path);
});

ReactDOM.render(
  <Router history={history} routes={route}/>,
  document.body.appendChild(document.createElement('div'))
);

回顾一下:

  1. David的S3重定向规则将直接/foo/bear指向/#/foo/bear
  2. 您的应用程序将加载。
  3. 历史记录侦听器将检测#/foo/bear历史记录符号。
  4. 并用正确的路径替换历史记录。

Link标记将正常运行,所有其他浏览器历史记录功能也将正常运行。我注意到的唯一缺点是在初始请求时发生的插页式重定向。

这是受AngularJS解决方案的启发而产生的,我怀疑它可以轻松地适应任何应用程序。


2
这对我有帮助,迈克尔,谢谢!您可能想从历史记录中更改引用,而仅使用BrowserHistory。即browserHistory.listen
Marshall Moutenot

别客气!乐意效劳。同样,我也同意,对于这个特殊的用例,我已经更新了代码片段以解决来自React Router的弃用警告。
迈克尔·阿勒斯

随着react-router v3.0.0的发布,此功能不起作用,因为react-router v3.0.0使用历史记录v3.0.0
Varand Pezeshkian

任何想法如何防止history.listen无限调用循环?由替换路径引起的我猜。
Mirko Flyktman

14

我看到此问题的4个解决方案。前三个已经包含答案,最后一个是我的贡献。

  1. 将错误文档设置为index.html。
    问题:响应正文正确,但状态代码为404,这会损害SEO。

  2. 设置重定向规则。
    问题#!加载时URL被污染,页面闪烁。

  3. 配置CloudFront。
    问题:所有页面将从原点返回404,因此您需要选择是否不缓存任何内容(建议为TTL 0),或者是否要缓存并在更新站点时遇到问题。

  4. 渲染所有页面。
    问题:渲染页面的额外工作,特别是在页面频繁更改时。例如,新闻网站。

我的建议是使用选项4。如果预渲染所有页面,则预期的页面不会出现404错误。该页面将加载正常,框架将控制并正常用作SPA。您还可以设置错误文档以显示通用的error.html页面,以及将404错误重定向到404.html页面的重定向规则(不使用hashbang)。

关于403禁止的错误,我完全不让它们发生。在我的应用程序中,我认为主机存储桶中的所有文件都是公共的,我与所有人具有读取权限选项进行设置。如果您的站点具有私有页面,则让用户查看HTML 布局不成问题。您需要保护的是数据,这是在后端完成的。

另外,如果您拥有私人资产(例如用户照片),则可以将其保存在另一个存储桶中。因为私人资产与数据需要同样的照顾,因此不能与用于托管应用程序的资产文件进行比较。


1
并且您的网站有一个很好的示例,可用于为所有页面进行预渲染。 zanon.io/posts/…..- 谢谢
frekele

第四种方法是否解决了用户重新加载pushState URL的问题?它可以很好地处理导航,但是在重新加载后,它仍然可以到达服务器。
Alpha

@Alpha,我不确定我是否正确理解了您的问题,但是在重新加载时,它将作为新请求。S3将收到请求并再次返回预渲染的页面。在这种情况下,没有服务器。
Zanon

@Zanon啊,我想我明白了。我认为这是为了缓存预渲染的页面,并避免使用S3不存在的资源。这样可以省去那些具有动态元素的路线,对吗?
Alpha

1
@Zanon我终于明白了-谢谢!是的,这就是我的意思。:)
Alpha

13

今天我遇到了同样的问题,但是@ Mark-Nutter的解决方案无法完全删除我的angularjs应用程序中的hashbang。

实际上,您必须转到“ 编辑权限”,单击“ 添加更多权限”,然后在存储桶中向所有人添加正确的列表。通过此配置,AWS S3现在将能够返回404错误,然后重定向规则将正确地捕获这种情况。

像这样 : 在此处输入图片说明

然后,您可以转到“ 编辑重定向规则”并添加以下规则:

<RoutingRules>
    <RoutingRule>
        <Condition>
            <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
        </Condition>
        <Redirect>
            <HostName>subdomain.domain.fr</HostName>
            <ReplaceKeyPrefixWith>#!/</ReplaceKeyPrefixWith>
        </Redirect>
    </RoutingRule>
</RoutingRules>

如果您不将hashbang方法用于SEO,则可以在此处用域和KeyPrefix#!/ 替换HostName subdomain.domain.fr 。

当然,仅当您在angular应用程序中已经设置了html5mode时,所有这些方法才有效。

$locationProvider.html5Mode(true).hashPrefix('!');

我唯一的问题是您拥有一堆hashbang,而nginx规则则没有。用户在页面上并刷新:/ products / 1->#!/ products / 1-> / products / 1
Intellix

1
我认为您应该为403错误添加规则,而不是向所有人授予列表权限。
Hamish Moffatt

11

使Amazon S3提供Angular 2+应用程序并直接使用URL的最简单解决方案是在S3存储桶配置中同时将index.html指定为Index和Error文档。

在此处输入图片说明


11
这与被严重否决的答案是相同的答案。它工作正常,但仅在body响应的时有效。状态码将是404,这会损害SEO。
Zanon

因为这仅适用于body如果您导入的脚本,head则当您直接点击网站上的任何子路由时它们将无法工作
Mohamed Hajr 19'1

4

由于问题仍然存在,尽管我提出了另一种解决方案。我的情况是我想在合并之前将所有拉取请求自动部署到s3进行测试,以使其可以在[mydomain] / pull-requests / [pr number] /
(例如www.example.com/pull-requests/822/ )

就我所知,不使用s3规则的场景将允许使用html5路由在一个存储桶中包含多个项目,因此尽管最受好评的建议适用于根文件夹中的项目,但不适用于自己子文件夹中的多个项目。

所以我将域指向我的服务器,在下面的nginx配置下完成了这项工作

location /pull-requests/ {
    try_files $uri @get_files;
}
location @get_files {
    rewrite ^\/pull-requests\/(.*) /$1 break;
    proxy_pass http://<your-amazon-bucket-url>;
    proxy_intercept_errors on;
    recursive_error_pages on;
    error_page 404 = @get_routes;
}

location @get_routes {
    rewrite ^\/(\w+)\/(.+) /$1/ break;
    proxy_pass http://<your-amazon-bucket-url>;
    proxy_intercept_errors on;
    recursive_error_pages on;
    error_page 404 = @not_found;
}

location @not_found {
    return 404;
}

它尝试获取文件,如果找不到,则假定它是html5路由并尝试。如果您有未找到路线的404角度页面,那么您将永远不会到达@not_found并返回返回的404角度页面而不是未找到的文件,这可以通过@get_routes中的if规则或其他方法解决。

我不得不说我在nginx config和使用正则表达式方面不太舒服,我通过一些试验和错误使它正常工作,因此在此过程中,我确信仍有改进的余地,请分享您的想法。

注意:如果在S3配置中包含s3重定向规则,则将其删除。

和btw在Safari中工作


嗨..感谢您的解决方案...您将该nginx conf文件放在哪里以进行s3部署。与我需要创建.exextensions文件夹并将其放入proxy.config文件的弹性beantalk相同吗?
user3124360

@ user3124360不确定弹性beanstack,但是在我的情况下,我将域名指向ec2实例,并在那里配置了nginx。因此请求转到CLIENT-> DNS-> EC2-> S3-> CLIENT。
安德鲁·阿纳乌托夫

是的,我正在做一些非常相似的事情……在这里使用nginx配置github.com/davezuko/react-redux-starter-kit/issues/1099 ...感谢您发布conf文件..我知道如何开发此EC2- >现在开始进行S3连接
user3124360

3

在寻找同样的问题。我最终混合使用了上述建议的解决方案。

首先,我有一个带有多个文件夹的s3存储桶,每个文件夹代表一个react / redux网站。我还将Cloudfront用于缓存失效。

因此,我必须使用路由规则来支持404并将其重定向到哈希配置:

<RoutingRules>
    <RoutingRule>
        <Condition>
            <KeyPrefixEquals>website1/</KeyPrefixEquals>
            <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
        </Condition>
        <Redirect>
            <Protocol>https</Protocol>
            <HostName>my.host.com</HostName>
            <ReplaceKeyPrefixWith>website1#</ReplaceKeyPrefixWith>
        </Redirect>
    </RoutingRule>
    <RoutingRule>
        <Condition>
            <KeyPrefixEquals>website2/</KeyPrefixEquals>
            <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
        </Condition>
        <Redirect>
            <Protocol>https</Protocol>
            <HostName>my.host.com</HostName>
            <ReplaceKeyPrefixWith>website2#</ReplaceKeyPrefixWith>
        </Redirect>
    </RoutingRule>
    <RoutingRule>
        <Condition>
            <KeyPrefixEquals>website3/</KeyPrefixEquals>
            <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
        </Condition>
        <Redirect>
            <Protocol>https</Protocol>
            <HostName>my.host.com</HostName>
            <ReplaceKeyPrefixWith>website3#</ReplaceKeyPrefixWith>
        </Redirect>
    </RoutingRule>
</RoutingRules>

在我的js代码中,我需要使用baseNamereact-router 的配置来处理它。首先,请确保您的依赖项是可互操作的,我安装的history==4.0.0wich与不兼容react-router==3.0.1

我的依赖项是:

  • “ history”:“ 3.2.0”,
  • “ react”:“ 15.4.1”,
  • “ react-redux”:“ 4.4.6”,
  • “ react-router”:“ 3.0.1”,
  • “ react-router-redux”:“ 4.0.7”,

我创建了一个history.js用于加载历史记录的文件:

import {useRouterHistory} from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';

export const browserHistory = useRouterHistory(createBrowserHistory)({
    basename: '/website1/',
});

browserHistory.listen((location) => {
    const path = (/#(.*)$/.exec(location.hash) || [])[1];
    if (path) {
        browserHistory.replace(path);
    }
});

export default browserHistory;

这段代码允许用散列处理服务器发送的404,并在历史记录中替换它们以加载我们的路由。

现在,您可以使用此文件来配置商店和根文件。

import {routerMiddleware} from 'react-router-redux';
import {applyMiddleware, compose} from 'redux';

import rootSaga from '../sagas';
import rootReducer from '../reducers';

import {createInjectSagasStore, sagaMiddleware} from './redux-sagas-injector';

import {browserHistory} from '../history';

export default function configureStore(initialState) {
    const enhancers = [
        applyMiddleware(
            sagaMiddleware,
            routerMiddleware(browserHistory),
        )];

    return createInjectSagasStore(rootReducer, rootSaga, initialState, compose(...enhancers));
}
import React, {PropTypes} from 'react';
import {Provider} from 'react-redux';
import {Router} from 'react-router';
import {syncHistoryWithStore} from 'react-router-redux';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import variables from '!!sass-variable-loader!../../../css/variables/variables.prod.scss';
import routesFactory from '../routes';
import {browserHistory} from '../history';

const muiTheme = getMuiTheme({
    palette: {
        primary1Color: variables.baseColor,
    },
});

const Root = ({store}) => {
    const history = syncHistoryWithStore(browserHistory, store);
    const routes = routesFactory(store);

    return (
        <Provider {...{store}}>
            <MuiThemeProvider muiTheme={muiTheme}>
                <Router {...{history, routes}} />
            </MuiThemeProvider>
        </Provider>
    );
};

Root.propTypes = {
    store: PropTypes.shape({}).isRequired,
};

export default Root;

希望能帮助到你。您会注意到,使用此配置,我使用redux注入器和一个自制的sagas注入器,​​通过路由异步加载javascript。不要介意这些观点。


3

现在,您可以使用Lambda @ Edge重写路径

这是一个有效的lambda @ Edge函数:

  1. 创建一个新的Lambda函数,但使用其中一个预先存在的蓝图代替空白函数。
  2. 搜索“ cloudfront”,然后从搜索结果中选择“ cloudfront响应生成”。
  3. 创建函数后,将内容替换为以下内容。我还必须将节点运行时更改为10.x,因为在编写本文时cloudfront不支持节点12。
'use strict';
exports.handler = (event, context, callback) => {

    // Extract the request from the CloudFront event that is sent to Lambda@Edge 
    var request = event.Records[0].cf.request;

    // Extract the URI from the request
    var olduri = request.uri;

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var newuri = olduri.replace(/\/$/, '\/index.html');

    // Log the URI as received by CloudFront and the new URI to be used to fetch from origin
    console.log("Old URI: " + olduri);
    console.log("New URI: " + newuri);

    // Replace the received URI with the URI that includes the index page
    request.uri = newuri;

    return callback(null, request);
};

在您的Cloudfront行为中,您将对其进行编辑以在“查看器请求”上添加对该Lambda函数的调用 在此处输入图片说明

完整教程:https : //aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/


1
您的代码示例仅return callback(null, request);
漏掉

2

如果您登陆此处寻找适用于React Router和AWS Amplify Console的解决方案-您已经知道您不能直接使用CloudFront重定向规则,因为Amplify Console不会为应用程序提供CloudFront分发。

但是,解决方案非常简单-您只需要在Amplify Console中添加重定向/重写规则,如下所示:

放大React Router的控制台重写规则

请参阅以下链接以获取更多信息(以及屏幕截图中的易于复制的规则):


0

我本人正在寻找答案。S3似乎仅支持重定向,您不能仅重写URL并以静默方式返回其他资源。我正在考虑使用我的构建脚本来简单地在所有必需的路径位置中复制index.html。也许这也会为您服务。


2
为每条路径生成索引文件也让我很想,但要像example.com/groups/5/show这样的动态路径很难。如果您看到我对这个问题的回答,我相信可以在很大程度上解决问题。这有点骇人听闻,但至少可以奏效。
Mark Nutter

最好将其部署在Nginx服务器之后,并为所有传入的URL返回index.html。我已经成功完成了余烬应用程序的heroku部署。
moha297 '16

-3

只是提出极其简单的答案。如果您托管在S3上,则只需对路由器使用哈希定位策略。

导出常量AppRoutingModule:ModuleWithProviders = RouterModule.forRoot(routes,{useHash:true,scrollPositionRestoration:'enabled'});

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.