在我上一篇文章中,我们介绍了如何设置一个 Web 应用程序,它从 CloudFront 提供 CSS 和 JavaScript 的块和包。我们将其集成到 Vite 中,以便当应用程序在浏览器中运行时,从应用程序根 HTML 文件请求的资产将从 CloudFront 作为 CDN 拉取。
虽然 CloudFront 的边缘缓存确实提供了优势,但从这些多个位置提供应用程序的资源并非没有成本。让我们看一下我自己 Web 应用程序的 WebPageTest 跟踪,它运行的是上一篇博文中配置的配置。

这篇文章将向您展示如何解决这个问题。我们将逐步介绍如何将整个 Web 应用程序托管在 CloudFront 上,并让 CloudFront 将数据、身份验证等不可缓存的请求转发(或“代理”)到我们的基础 Web 服务器。
请注意,这比我们在上一篇文章中看到的内容要多得多,并且基于您的 Web 应用程序的确切需求,这些说明可能对您有所不同,因此您的里程可能会有所不同。我们将更改 DNS 记录,并且根据您的 Web 应用程序,您可能需要添加一些缓存标头以防止某些资产被缓存。我们将深入探讨所有这些内容!
您可能想知道我们在上一篇文章中介绍的设置是否提供任何好处,因为我们在本文中所做的事情。考虑到较长的连接时间,我们是否应该放弃 CDN,而是从 Web 服务器提供所有资产,以避免更长的等待时间?我用自己的 Web 应用程序对这种情况进行了测量,上面的 CDN 版本确实更快,但差距并不大。最初的 LCP 页面加载速度大约快了 200-300 毫秒。请记住,这仅仅是针对初始加载。一旦建立了这个连接,边缘缓存应该为所有后续的异步加载的块提供更大的价值。
设置我们的 DNS
我们的最终目标是从 CloudFront 提供我们的整个 Web 应用程序。这意味着当我们访问我们的域名时,我们希望结果来自 CloudFront,而不是它目前链接到的任何 Web 服务器。这意味着我们必须修改我们的 DNS 设置。我们将为此使用 AWS Route 53。
我使用mydemo.technology
作为示例,这是我拥有的域名。我将在这里向您展示所有步骤。但到您阅读本文时,我已经从我的 Web 应用程序中删除了这个域名。因此,当我开始向您展示实际的 CNAME 记录等时,那些将不再存在。

转到 Route 53 主页,然后单击托管区域

单击创建托管区域并输入应用程序的域名

我们还没有真正完成任何事情。我们告诉 AWS 我们希望它为我们管理这个域名,AWS 向我们提供了它将通过其路由我们的流量的名称服务器。为了实现这一点,我们需要转到我们的域名注册的地方。应该有一个地方让您输入您自己的自定义名称服务器。
请注意,我的域名是在 GoDaddy 上注册的,这体现在本文中的屏幕截图中。UI、设置和选项可能与您在注册商中看到的有所不同。

警告:我建议在进行更改之前写下原始名称服务器以及所有 DNS 记录。这样,如果出现故障,您将拥有将系统回滚到开始之前状态所需的一切。即使一切正常,您仍然需要将所有其他记录重新添加到 Route 53 中,例如 MX 记录等。
设置 CloudFront 分发
让我们创建一个 CloudFront 分发来托管我们的 Web 应用程序。我们在上一篇文章中介绍了基础知识,因此我们将直接进入主题。与上次相比,一个很大的变化是我们在源域中输入的内容。不要输入顶级域名,例如 your-app.net。您需要的是托管应用程序的基础域。如果那是 Heroku,那么输入 Heroku 提供给您的 URL。

接下来,如果您计划通过安全的 HTTPS 连接使用此站点,请确保更改默认协议

这部分至关重要。如果您的 Web 应用程序正在运行身份验证、托管数据或任何其他操作,请确保启用除 GET 以外的其他动词。如果您跳过这部分,那么用于身份验证、修改数据等的任何 POST 请求将被拒绝并失败。如果您的 Web 应用程序只做的是提供资产,而所有这些事情都由外部服务处理,那么太棒了!您拥有很棒的设置,您可以跳过此步骤。

与上次相比,我们必须对缓存密钥和源请求设置进行一些更改

我们需要创建一个缓存策略,其最小 TTL 为 0,因此我们发送回的非缓存标头将被正确尊重。您可能还想启用所有查询字符串。当多个 GraphQL 请求与不同的查询字符串一起发出时,我看到了奇怪的行为,这些查询字符串被忽略了,导致所有这些请求在 CloudFront 的角度看来都相同。
我的策略最终看起来像这样

对于源请求策略,如果需要,我们应该确保发送查询字符串和 Cookie 以使身份验证和数据查询正常工作。需要明确的是,这决定了是否从 CloudFront 发送 Cookie 和查询字符串到您的 Web 服务器(例如 Heroku 或类似服务器)。
我的看起来像这样

最后,对于响应标头策略,我们可以从列表中选择“CORS 带预检”。最后,您的前两个将具有不同的名称,具体取决于您设置它们的方式。但我的看起来像这样

让我们将我们的域名(无论它是什么)连接到这个 CloudFront 分发。不幸的是,这比您预期的要多。我们需要向 AWS 证明我们实际上拥有这个域名,因为对于亚马逊来说,我们可能没有。我们在 Route 53 中创建了一个托管区域。我们获取了它提供的名称服务器并将它们注册到 GoDaddy(或您注册域名的任何地方)。但亚马逊还不知道这一点。我们需要向亚马逊证明我们确实控制着该域名的 DNS。
首先,我们将请求 SSL 证书。

接下来,让我们请求证书链接

现在,我们将选择请求公共证书选项

我们需要提供域名

而且,在我的情况下,证书正在等待

因此,我将单击它

这证明我们拥有并控制着这个域名。在一个单独的标签中,返回 Route 53,然后打开我们的托管区域

现在我们需要创建 CNAME 记录。复制记录名称的第一部分。例如,如果 CNAME 是_xhyqtrajdkrr.mydemo.technology
,那么请放入_xhyqtrajdkrr
部分。对于记录值,请复制整个值。

假设您将 AWS 名称服务器注册到您的域名主机 GoDaddy 或任何其他地方,AWS 很快就能 ping 它刚刚要求您创建的 DNS 条目,查看它期望的响应,并验证您的证书。
您在开始时设置的名称服务器可能需要一些时间才能传播。理论上,这可能需要长达 72 小时,但对我来说通常会在 1 小时内更新。
您将在域名上看到成功

…以及证书

呼!快要完成了。现在让我们将所有这些连接到我们的 CloudFront 分发。我们可以返回到 CloudFront 设置屏幕。现在,在自定义SSL 证书下,我们应该看到我们创建的证书(以及您之前创建的任何其他证书)

然后,让我们添加应用程序的顶级域名

剩下的就是告诉 Route 53 将我们的域名路由到这个 CloudFront 分发。因此,让我们回到 Route 53 并创建另一个 DNS 记录。
我们需要为 IPv4 输入 A 记录,为 IPv6 输入 AAAA 记录。对于两者,请将记录名称保留为空,因为我们只注册我们的顶级域名,而不是其他任何内容。
选择 A 记录类型。接下来,将记录指定为别名,然后将别名映射到 CloudFront 分发。这应该会打开一个选项,让您选择您的 CloudFront 分发,并且由于我们之前在 CloudFront 中注册了域名,因此在进行选择时,您应该只看到该分发,而不是其他任何分发。

我们为 IPv6 支持所需的 AAAA 记录类型重复完全相同的步骤。
运行您的 Web 应用程序,并确保它确实有效。它应该可以正常工作!
需要测试和验证的事项
好的,虽然我们技术上已经完成了,但很有可能仍然有一些事情要做才能满足您的 Web 应用程序的确切需求。不同的应用程序有不同的需求,我到目前为止所展示的内容指导我们完成了通过 CloudFront 路由内容以获得更好性能的常见步骤。很可能您的应用程序中有一些独特的事情需要更多关注。因此,为此,我将介绍一些您在设置过程中可能遇到的几个可能的额外项目。
首先,确保您拥有的任何 POST 都已正确发送到您的源。假设 CloudFront 正确配置为将 Cookie 转发到您的源,这应该已经可以正常工作,但检查一下并无坏处。
更大的问题是所有其他发送到您的 Web 应用程序的 GET 请求。默认情况下,CloudFront 收到的任何 GET 请求(如果已缓存),都会使用缓存的响应提供给您的 Web 应用程序。这可能是灾难性的。任何使用 GET 发送到任何 REST 或 GraphQL 端点的数据请求都会被 CDN 缓存。如果您正在发布服务工作者,那也会被缓存,而不是正常的行为,正常的行为是在后台发送当前版本并在有更改时更新。
为了告诉 CloudFront *不要* 缓存某些内容,请确保将 "Cache-Control"
标头设置为 "no-cache"
。如果您使用的是像 Express 这样的框架,您可以为您的数据访问设置中间件,就像这样
app.use("/graphql", (req, res, next) => {
res.set("Cache-Control", "no-cache");
next();
});
app.use(
"/graphql",
expressGraphql({
schema: executableSchema,
graphiql: true,
rootValue: root
})
);
对于像服务工作者这样的东西,您可以在静态中间件之前为这些文件设置特定规则
app.get("/service-worker.js", express.static(__dirname + "/react/dist", { setHeaders: resp => resp.set("Cache-Control", "no-cache") }));
app.get("/sw-index-bundle.js", express.static(__dirname + "/react/dist", { setHeaders: resp => resp.set("Cache-Control", "no-cache") }));
app.use(express.static(__dirname + "/react/dist", { maxAge: 432000 * 1000 * 10 }));
等等。彻底测试一切,因为有很多可能出错的地方。在您进行每次更改后,请确保在 CloudFront 中运行完整的无效化并清除缓存,然后重新运行您的 Web 应用程序以测试是否有东西被正确地从缓存中排除。您可以在 CloudFront 的 **无效化** 选项卡中执行此操作。打开它并将 /*
放在值中,以清除所有内容。
一个可行的 CloudFront 实现
现在我们已经运行了所有东西,让我们在 WebPageTest 中重新运行我们的跟踪

就这样,我们不再像之前那样为我们的资产建立连接。对于我自己的 Web 应用程序,我在 LCP 中看到了 500 毫秒的实质性改进。这是一个不错的胜利!
在 CDN 上托管整个 Web 应用程序可以提供所有优点。我们获得了静态资源的边缘缓存,但没有连接成本。不幸的是,这种改进不是免费的。正确设置所有必要的代理并不完全直观,然后仍然需要设置缓存头以避免不可缓存的请求进入 CDN 的缓存。
这似乎是很多工作——有没有办法使用预连接资源提示?
它可能没有那么快——您仍然需要创建一个新的连接——但这意味着您可以更早地开始打开该连接。而且我怀疑这会更简单。