将客户端渲染的 create-react-app 部署到 Microsoft Azure

Avatar of Adebiyi Adedotun
Adebiyi Adedotun

DigitalOcean 为您的旅程每个阶段提供云产品。立即开始使用 200 美元的免费积分!

将 React 应用程序部署到 Microsoft Azure 很简单。除了...它不是。细节是魔鬼。如果您要部署 create-react-app(或类似的基于前端 JavaScript 框架,需要pushState-based 路由)到 Microsoft Azure,我相信本文会对您有所帮助。我们将尝试避免客户端和服务器端路由协调的麻烦。

首先,一个简短的故事。

早在 2016 年,当 Donovan Brown(微软高级 DevOps 项目经理)在当年的微软 Connect 大会上发表了“但在我的机器上能运行”演讲时,我还是一名初级网页开发者。他的演讲主题是微服务和容器。

[...] 那些日子已经过去了,你的经理不会再冲进你的办公室,惊慌失措地告诉你发现了一个 bug。无论我如何努力,我都无法重现它,它在我的机器上运行得很好。她说:好吧,Donovan,那我们就把你的机器运过去,因为那是它唯一能正常运行的地方。但我喜欢我的机器,所以我不会让她把它运走...

我也有类似的挑战,但它与路由有关。我正在开发一个网站,它使用 React 前端和 ASP.NET Core 后端,作为两个独立的项目部署到 Microsoft Azure。这意味着我们可以分别部署这两个应用程序,并享受分离关注点带来的好处。我们也知道在出现问题时应该git blame谁。但它也有缺点,前端与后端路由协调就是这些缺点之一。

有一天,我将一些新代码推送到我们的暂存服务器。不久后,我收到一条消息,告诉我网站在页面刷新时出现故障。它抛出了 404 错误。起初,我认为这不是我的责任。一定是服务器配置问题。事实证明,我既是对的又是错的。

我之所以知道这是一个服务器配置问题(尽管当时我不知道它与路由有关)是对的。我之所以否认这是我的责任,是错误的。直到我开始疯狂地搜索网页,才发现了在官方文档页面部署选项卡下将 create-react-app 部署到 Azure 的用例

构建用于生产的 React

构建用于生产的 React 应用程序时(假设我们使用的是create-react-app),值得注意的是生成的文件夹。运行npm run build将生成一个 build 文件夹,其中包含一个优化的应用程序静态版本。要将应用程序放到实时服务器上,我们只需要将 build 文件夹的内容提供给服务器即可。如果我们在localhost上工作,则不会涉及实时服务器,因此它并不总是等同于将应用程序放到实时服务器上。

通常,build 文件夹将具有以下结构

→ build
  → static
    → css
      → css files
    → js
      → js files
    → media
      → media files
  → index.html
  → other files...

使用 React Router 进行客户端路由

React Router 在内部使用HTML5 pushState 历史记录 APIpushState的作用非常有趣。例如,从页面https://css-tricks.cn导航(或在 react router 中使用Link)到页面https://css-tricks.cn/archives/将导致 URL 栏显示https://css-tricks.cn/archives/,但不会导致浏览器加载页面/archives,甚至不会检查它是否存在。将此与 React 的组件化模型结合起来,它成为了一件在更改路由的同时显示基于这些路由的不同页面——而无需服务器的无处不在的眼睛试图在其自己的目录中提供页面的事情。那么,当我们通过将代码推送到实时服务器引入服务器时会发生什么?文档解释得更好

如果您使用在后台使用 HTML5 pushState 历史记录 API 的路由器(例如,使用 browserHistory 的 React Router),许多静态文件服务器将无法正常工作。例如,如果您使用 React Router 为 /todos/42 设置路由,开发服务器将正确响应 localhost:3000/todos/42,但上面提到的 Express 不会响应生产构建的请求。这是因为当对 /todos/42 进行新的页面加载时,服务器会查找文件 build/todos/42 并且找不到它。需要配置服务器以响应对 /todos/42 的请求,方法是提供 index.html。

不同的服务器需要不同的配置。例如,Express 需要以下内容

app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
});

如 create-react-app 文档中所述但请记住,这假设我们在服务器根目录托管 create-react-app,它利用通配符路由(*)来捕获所有路由,并通过提供 build 文件夹中位于服务器应用程序根目录的index.html文件来响应所有路由请求。此外,这与后端紧密耦合。如果是这种情况,我们很可能会拥有这种文件夹结构(假设后端位于 NodeJS 中)

→ Server
  → Client (this is where your react code goes)
    → build (this is the build folder, after you npm run build)
    → src
    → node_modules
    → package.json
    → other front-end files and folders
  → Other back-end files and folders

由于我的前端(create-react-app)和后端(ASP.NET)是两个不同的项目,因此通过导航目录来提供静态文件在某种程度上是不可能的

事实上,由于我们正在部署静态应用程序,我们不需要后端。正如 Burke Holland 所说:“静态”意味着我们没有部署任何服务器代码;只有前端文件。

我一直在这里提到 ASP.NET,因为在我的研究过程中,我发现配置 Azure 需要在wwwroot文件夹中配置一个配置文件,而 ASP.NET 的文件夹结构通常有一个wwwroot文件夹。还记得应用程序的后端位于 ASP.NET 中吗?但这只是其中的一部分。wwwroot文件夹似乎隐藏在 Azure 的某个地方。我不能在不部署create-react-app的情况下向您展示。所以让我们开始做吧。

开始使用 Microsoft Azure 上的应用服务

要开始,如果您还没有 Azure 帐户,请获取免费试用版,然后前往Azure 门户

  1. 导航到 所有服务Web应用服务
    从所有服务导航到 Azure 门户的 Web,再到应用服务

  2. 我们要添加一个新应用程序,为它命名,订阅(如果您正在使用免费试用版,或者您已经拥有订阅,则应该是免费的),资源组(创建一个或使用现有的),然后点击面板底部的创建按钮。
    在 Azure 门户上创建一个新的应用服务。
  3. 我们应该收到一个通知,告知资源已创建。但它不会立即显示,因此点击“刷新”——我还有其他资源,但这里使用的是 AzureReactDemo2。您将点击您新创建的应用程序的名称,在我的情况下是 AzureReactDemo2。
    在 Azure 门户上显示所有应用服务。
  4. 刀片显示有关应用程序的信息,左侧的导航栏包含管理应用程序所需的所有内容(概述、活动日志、部署中心...)。

例如,部署中心是管理应用程序部署的地方,插槽是管理暂存、生产、测试等内容的地方。配置是管理环境变量、节点版本以及——一个重要的内容——Kudu 的地方。

概述屏幕显示应用程序状态、URL 等的概览。点击 URL 查看实时站点。

显示 Azure CLI 上应用服务的各个部分。

应用程序已启动并运行!

显示应用服务的默认实时页面。

我们所做的是创建了一个新的应用服务,但我们的代码还没有放到 Azure 上。如前所述,我们只需要将构建用于生产的 React 生成的 build 文件夹的内容提供给 Azure,但我们还没有生成它。所以让我们回到本地,获取一些 React 应用程序。

转到本地

我们需要创建一个新的 React 应用程序,并将react-router 安装为依赖项。

npx create-react-app azure-react-demo
cd azure-react-demo

我们还想要安装 react-router(实际上是react-router-dom

npm i react-router-dom

一切正常,用npm start启动应用程序,我们应该看到默认页面。

显示 React 生成的默认页面。

因为这将是关于测试路由,所以我需要制作一些页面。我已经修改了我的本地版本并将其上传到 GitHub。我敢肯定你能够使用 React 和 React Router。 下载演示。

我的文件夹看起来像这样

显示修改后的 create-react-app 应用程序中的文件夹和文件。

更改后的文件具有以下代码

// App.js
import React, { Component } from "react";
import "./App.css";
import Home from "./pages/Home";
import Page1 from "./pages/Page1";
import Page2 from "./pages/Page2";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";

class App extends Component {
  render() {
    return (
      <Router>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/page1" component={Page1} />
          <Route path="/page2" component={Page2} />
        </Switch>
      </Router>
    );
  }
}

export default App;
// Page1.js
import React from "react";
import { Link } from "react-router-dom";

const Page1 = () => {
  return (
    <div className="page page1">
      <div className="flagTop" />
      <div className="flagCenter">
        <h1 className="country">Argentina (PAGE 1)</h1>
        <div className="otherLinks">
          <Link to="/page2">Nigeria</Link>
          <Link to="/">Home</Link>
        </div>
      </div>
      <div className="flagBottom" />
    </div>
  );
};

export default Page1;
// Page2.js
import React from "react";
import { Link } from "react-router-dom";

const Page2 = () => {
  return (
    <div className="page page2">
      <div className="flagTop" />
      <div className="flagCenter">
        <h1 className="country">Nigeria (PAGE 2)</h1>
        <div className="otherLinks">
          <Link to="/page1">Argentina</Link>
          <Link to="/">Home</Link>
        </div>
      </div>
      <div className="flagBottom" />
    </div>
  );
};

export default Page2;
/* App.css */
html {
  box-sizing: border-box;
}

body {
  margin: 0;
}

.page {
  display: grid;
  grid-template-rows: repeat(3, 1fr);
  height: 100vh;
}

.page1 .flagTop,
.page1 .flagBottom {
  background-color: blue;
}

.page2 .flagTop,
.page2 .flagBottom {
  background-color: green;
}

.flagCenter {
  display: flex;
  align-items: center;
  flex-direction: column;
  justify-content: center;
  text-align: center;
}

.page a {
  border: 2px solid currentColor;
  font-weight: bold;
  margin: 0 30px;
  padding: 5px;
  text-decoration: none;
  text-transform: uppercase;
}

.flags {
  display: flex;
  width: 100%;
}

.flags > .page {
  flex: 1;
}

本地运行应用程序,因此路由在单击链接时以及页面刷新时都能正常传递。

将应用程序部署到 Azure

现在,让我们将它部署到 Azure!这需要几个步骤。

步骤 1:前往部署中心

在 Azure 上,我们需要转到部署中心。有很多选项,每个选项都有其优缺点。我们将使用本地 Git(意味着您的本地 Git 应用程序直接连接到 Azure)进行源代码管理,使用 Kudu 进行构建提供程序。

请记住,在选择选项后单击继续或完成,否则门户网站将一直盯着你看。

显示 Azure 门户上的部署中心,并选择源代码管理作为部署新 App Service 的第一步。
显示 Azure 门户上的部署中心中的构建提供程序部分。

在第三步之后,Azure 为您生成一个本地 Git 存储库。它还提供了一个远程链接,用于指向您的 React 应用程序。

此时需要注意的一点是,当您推送时,Azure 会要求您提供 GitHub 凭据。它位于部署选项卡下。有两种:应用和用户。应用程序凭据将特定于某个应用程序。用户将是您作为用户具有读/写访问权限的所有应用程序的通用凭据。您可以不用用户凭据而使用应用程序凭据,但我发现过一段时间后,Azure 停止要求凭据,而是自动告诉我身份验证失败。我设置了自定义用户凭据。无论哪种方式,您都应该能够通过它。

显示 Azure 门户上 App Service 的部署凭据。

在 React 应用程序中,修改后,我们需要进行生产构建。这一点很重要,因为我们要上传的是构建文件夹的内容。

我们需要告诉 Kudu 我们将使用哪个 Node 引擎,否则构建很可能会失败,
由于已报告react-scripts要求 Node 版本高于 Azure 上的默认设置。还有其他方法可以做到这一点,但最简单的方法是在package.json中添加一个节点引擎。我在这里使用版本 10.0。不幸的是,我们不能随意添加,因为 Azure 有它支持的 Node 版本,其他版本不受支持。使用以下命令在 CLI 中检查:az webapp list-runtimes

将首选的 Node 版本添加到package.json文件,如下所示:

"engines": {
  "node": "10.0"
}
显示 Azure CLI 中的 Azure 运行时列表。

步骤 2:构建应用程序

要构建 React 应用程序,让我们在终端中运行npm build

步骤 3:初始化 Git 存储库

导航到构建文件夹并在其中初始化一个 Git 存储库。克隆存储库的 URL 在概述页面上。根据您使用的是什么凭据(应用程序或用户),它会有所不同。

显示 Azure 上 App Service 的概述以及 Git 克隆 URL。
git init
git add .
git commit -m "Initial Commit"
git remote add azure <git clone url>
git push azure master

现在,使用概述页面上的 URL 访问实时应用程序。如您所见,应用程序在/page2刷新时失败。查看网络选项卡,抛出了 404 错误,因为页面尝试从服务器获取 — 在我们已经设置的客户端路由中,页面甚至不应该从服务器获取。

显示失败的页面请求和网络选项卡以进行验证。

配置 Azure 以协调客户端和服务器端路由

在 public 文件夹中,让我们添加一个web.config XML 文件,内容如下:

<?xml version="1.0"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="React Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

我故意决定不格式化代码片段,因为 XML 对格式非常严格。如果您遗漏了格式,则该文件将无效。您可以为文本编辑器下载一个 XML 格式化程序。对于 VSCode,那就是 XML Tools 插件

显示 VSCode 中的 XML 格式化程序和 XML 格式化文件。

此时可以重新构建应用程序,尽管我们会丢失构建文件夹中的 Git 信息,因为新构建会覆盖旧构建。这意味着必须再次添加它,然后进行推送。

现在应用程序按如下所示工作!呼。

我们不希望每次都必须npm run build — 这就是持续部署的用武之地。查看下面的链接以获取相应的参考。

结论

Azure 功能很多,它可以为您做很多事情。这很好,因为有时候您需要它做一些看似非常具体的事情 — 就像我们在协调客户端和服务器端路由时看到的那样 — 而它已经为您提供了支持。

也就是说,我将为您提供一些相关资源,您可以将其作为参考,以将 React 应用程序部署到 Azure。