MERN 技术栈高级教程(十)
十五、部署
有很多方法可以在云上部署问题跟踪器。你可以选择自己动手,比如在 Amazon AWS、Google Cloud 和 Microsoft Azure 上运行你自己的基于 Linux 的实例,并在这些实例上运行 Node.js 和 MongoDB,就像你在本书中在本地计算机上所做的一样。
但是我发现平台即服务(PaaS)选项更容易部署和维护。因此,在这一章中,我选择了最受欢迎的 PaaS 之一,Heroku。在为您的生产应用做出选择之前,您可以尝试一个免费层。在这一章中,我将指导你在 Heroku 上部署问题跟踪器应用。
Git 仓库
在 Heroku 上部署应用最简单的方法是使用 Git 存储库。到目前为止,我们还没有讨论过如何在团队中控制和共享问题跟踪器源代码以进行协作。您已经在本地计算机上创建了所有文件,并且很可能还没有使用存储库。现在是这么做的好时机,因为我们无论如何都需要在 Heroku 上部署。您可以使用 CVS、SVN、Git 或任何其他现代源代码控制系统,但是由于 Heroku 需要 Git,我们也将使用相同的系统来满足问题跟踪器应用的协作需求。
我们将要创建的存储库不应该与本书附带的存储库混淆,在 https://github.com/vasansr/pro-mern-stack-2 。这本书的存储库已经以一种方便的方式进行了组织,以便跟踪书中的代码更改,但它对于现实生活中的应用来说并不理想。因此,不要克隆或派生这个存储库;相反,从我们到目前为止编写的代码开始。
你可以使用 GitHub 或 BitBucket 或任何云 Git 服务,甚至你自己的托管 Git 服务器。我假设 GitHub 用于下面的说明。其他服务的说明即使不完全相同,也是相似的。
让我们从在 GitHub 中创建这些存储库开始。在 https://github.com 登录 GitHub(如果您没有帐户,请创建一个),使用用户界面,探索如何创建存储库。我们有两个可部署的应用:API 服务器和 UI 服务器。我们将需要两个存储库,每个存储库一个。让我们称他们为tracker-api和tracker-ui。您还需要 Git 命令行实用程序:git。我不会详细介绍如何安装git或者设置它来访问 GitHub。做这些有不同的选择,你会在互联网上找到很多资源,GitHub 网站本身也会帮助你。
从 Git 命令行访问 GitHub 存储库有两种选择:使用 SSH 或使用 HTTPS。在下面的说明中,我将假设您已经使用 SSH 设置了 Git 进行访问。如果您喜欢 HTTPS,那么您必须在下面的命令中相应地更改存储库的远程 URL。
创建存储库之后,让我们用当前的代码库初始化它们。首先,让我们来处理 API 代码库。首先,我们需要在 API 目录中初始化 Git。以下命令将完成这项工作:
$ cd api
$ git init
接下来,我们需要添加所有由 Git 管理的文件。您可以手动添加每个文件。但是使用名为.gitignore的文件更容易指定要排除哪些文件,所以我们就这么做吧。API 目录下该文件的内容如清单 15-1 所示。列表
node_modules
.env
15-1api/.gitignore: List of Files to Exclude from Git Management
现在,要添加 API 目录中的所有文件,不包括清单 15-1 中显示的文件,让我们执行以下操作:
$ git add .
$ git commit -m "First commit"
在这一章中,会有很多情况需要用到你的 GitHub 用户名。定义一个环境变量会很方便,这样命令就可以从书或书的 GitHub 库中复制粘贴,而无需修改。为此,让我们使用一个名为GITHUB_USER的环境变量。此时,您需要将这个变量设置为您的 GitHub 用户名。
下面的命令假设一台 Linux 计算机或一台 Mac 计算机,使用$GITHUB_USER访问环境变量。在 Windows PC 上,您需要使用%GITHUB_USER%来代替,或者用您的 GitHub 用户名替换变量名。这同样适用于我们将在本章中使用的其他变量。
现在,让我们将代码推送到 GitHub:
$ git remote add origin git@github.com:$GITHUB_USER/tracker-api.git
$ git push -u origin master
在 Git 中添加一个 remote 会在本地存储库和远程存储库之间建立一个链接,在本例中,远程存储库位于 GitHub 上。成功之后,您应该能够在 GitHub 网站中看到源代码。检查并确保所有代码,包括脚本子目录,都已创建。
UI 目录也需要类似的步骤。但是要排除的文件是不同的。除了目录node_modules和文件.env之外,我们还有通过编译生成的文件,这些文件不需要签入 Git 存储库。这些是整个dist目录和public目录中的.js和.js.map文件。让我们包含这些并创建一个.gitignore文件,其内容在清单 15-2 中列出。列表
dist
node_modules
.env
public/*.js
public/*.js.map
15-2ui/.gitignore: Files to Exclude from the UI Directory
现在,为了初始化、添加文件并将 UI 目录推送到 GitHub,让我们按照创建存储库后出现的 GitHub 提示符中的描述执行以下命令:
$ cd ui
$ git init
$ git add .
$ git commit -m "First commit"
$ git remote add origin git@github.com:$GITHUB_USER/tracker-ui.git
$ git push -u origin master
此时,您可以在 GitHub 上在线浏览 UI 存储库,以确保文件确实已经被推送到 GitHub。
MongoDB
在我们在 Heroku 上部署服务器之前,让我们首先确保在云上有一个 MongoDB 数据库。请务必重新阅读第 6“MongoDB”中题为“安装”的章节。如果您已经在云上使用了 MongoDB 数据库,那么就没什么可做的了。如果没有,选择该章中描述的云选项之一,并按照说明在云上创建一个数据库。
将连接 URL 以及用户 ID 和密码放在手边会很方便,所以让我们为此设置一个名为DB_URL的环境变量,并在下面的命令中使用它。如果您刚刚从本地数据库转移到云上的新数据库,您还需要初始化数据库。为此,请使用以下命令:
$ cd api
$ mongo $DB_URL scripts/init.mongo.js
$ mongo $DB_URL scripts/generate_data.mongo.js
赫罗库
首先要做的是创建一个 Heroku 账户,如果你还没有的话。这可以从 https://heroku.com 开始。一旦你有了登录账号,你就可以从 https://devcenter.heroku.com/articles/heroku-cli#download-and-install 安装 Heroku CLI。我们将为部署执行的大多数命令也可以从 Heroku web 用户界面获得。但是使用 CLI,更容易按照本书中的说明执行命令。
安装后使用 CLI 的第一件事是登录。
$ heroku login
这应该会响应以下提示,这将打开浏览器以获取登录信息。
heroku: Press any key to open up the browser to login or q to exit:
如果您已经通过 web 用户界面登录 Heroku,登录应该是自动的。否则,它可能会提示您输入用户 ID 和密码。登录后,您应该会在控制台中看到以下内容。
Logging in... done
Logged in as YOUR_MAIL_ID
在接下来的小节中,我们将在 Heroku 上创建和部署 API 应用,然后是 UI 应用。
API 应用
部署 API 应用需要对应用进行一些更改,这是 Heroku 所期望的。
首先,应用可以监听的端口由 Heroku 动态分配。原因是每个应用都部署在一个容器中,而不是一个专用的主机中。因此,Heroku 为应用分配一个端口,并为该应用向该端口发送流量。但是,在互联网上,同一个端口反映为 HTTP (80)或 HTTPS (443)端口。Heroku 有一个防火墙可以做到这一点。Heroku 然后设置一个环境变量,让应用知道容器将从哪个端口接收流量。这个环境变量简称为PORT。这是应用必须监听的端口。
所以,让我们把我们一直使用的环境变量API_SERVER_PORT改成PORT。server.js的变化如清单 15-3 所示。列表
...
const port = process.env.API_SERVER_PORT || 3000;
...
15-3api/server.js: Change in Environment Variable Name for Server Port
如果你一直在使用一个.env,在那个文件中也必须做一个类似的改变,但是这只是为了在你的本地计算机上测试。它不会影响 Heroku 部署,因为在 Heroku 中没有使用.env设置环境变量。清单 15-4 显示了示例.env文件中的相同变化。列表
...
## Server Port
API_SERVER_PORT=3000
...
15-4api/sample.env: Change in Variable Name for Server Port
在生产环境中运行时,Apollo server 默认禁用 Playground。但是有操场是好的,因为数据是公开的,让我们启用它。这需要在创建 Apollo 服务器时修改代码来设置选项,如清单 15-5 所示。列表
...
const server = new ApolloServer({
...
playground: true,
introspection: true,
});
...
15-5api/api_handler.js: Enable GraphQL Playground in Production
Heroku 部署应用并确定环境、语言等。使用自动检测算法。存储库中存在的package.json足以让 Heroku 检测到这是一个 Node.js 环境。但是由于有不同版本的 Node.js 引擎可供 Heroku 使用,我们需要告诉 Heroku 我们的应用需要在哪个版本上运行。此外,由于我们将在运行之前安装软件包,我们还需要指定我们想要使用的 npm 版本。方法是在package.json中设置engines属性。这一变化如清单 15-6 所示。列表
...
"engines": {
"node": "10.x",
"npm": "6.x"
},
"scripts": {
...
15-6api/package.json: Engine Specification for Heroku
为了使这些改变永久化,让我们提交它们。但是提交只会影响本地存储库。将这些更改也推送到 GitHub remote 是一个好主意,这样您团队中的其他人也可以获得这些更改。
$ cd api
$ git commit -am "Changes for Heroku"
$ git push origin master
现在,我们准备在 Heroku 上部署应用。正如 Heroku 文档中所建议的,我们首先需要创建并初始化应用。为此需要使用 Heroku CLI 命令create。应用的名称需要是通用的,所以让我们使用您的 GitHub 用户名作为应用名称的一部分,尽量减少所选名称不可用的可能性。
$ heroku create tracker-api-$GITHUB_USER
Creating tracker-api-GITHUB_USER... done
https://tracker-api-GITHUB_USER.herokuapp.com/ | https://git.heroku.com/tracker-api- GITHUB_USER.git
相反,如果它显示一个应用名称已被使用的错误,您将不得不为该应用尝试不同的名称。当它成功时,创建将在 Heroku 上添加一个 Git 远程存储库,这个存储库在本地将被称为heroku,推送到这个存储库将具有部署应用的效果。
但是在此之前,让我们设置 API 服务器需要的环境变量。我们需要数据库的网址和 JWT 的秘密。至于 cookie 域,我们先设置为 API 服务器和 UI 服务器的域的共有部分,也就是herokuapp.com。
$ heroku config:set \
DB_URL=$DB_URL \
JWT_SECRET=YOUR_SPECIAL_SECRET \
COOKIE_DOMAIN=herokuapp.com
这个命令假设一台 Linux 或 Mac 计算机。当使用 Windows PC 时,你将不得不对变量使用%DB_URL%语法,并在一行中键入整个命令,而不是使用多行并在结尾使用\字符。
注意,我们不需要设置PORT变量,因为它是在启动应用时由 Heroku 设置的,这是我们可以使用的唯一端口。现在,我们可以通过对 Heroku remote 进行简单的 Git 推送操作,将应用部署到云上。
$ git push heroku master
这应该会在控制台上显示大致如下的输出:
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
...
remote: -----> Installing binaries
remote: engines.node (package.json): 10.x
remote: engines.npm (package.json): 6.x
remote:
...
remote:
remote: -----> Building dependencies
remote: Installing node modules (package.json + package-lock)
...
remote: -----> Pruning devDependencies
remote: removed 126 packages and audited 2780 packages in 6.746s
remote: found 0 vulnerabilities
...
remote: -----> Build succeeded!
remote: Released v4
remote: https://tracker-api-$GITHUB_USER.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
你应该仔细阅读这些信息,确保没有任何差错或意外。在这一点上特别感兴趣的是消息:
remote: Installing node modules (package.json + package-lock)
这意味着 Heroku 在从 Git 存储库复制文件之后,也在目标目录上运行了npm install。由于 Git 存储库中也有package.json和package-lock.json文件,它安装的版本也会自动匹配您在开发过程中使用的版本。
注意
Heroku 也安装了在devDependencies中列出的包,但是后来删除了这些包,这可以从消息Pruning devDependencies中看到。
如果一切正常,您应该会看到最后一行,表示部署已经验证并完成。现在,您可以通过使用操场来测试 API。这也将确保 API 服务器可以连接到云上的 MongoDB 服务器。要访问游乐场,请浏览到https://tracker-api-$GITHUB_USER.herokuapp.com/graphql(用您的 GitHub 用户 ID 替换$GITHUB_USER)。
您也可以只在控制台中键入heroku open,浏览器选项卡或窗口应该会打开,显示以前的 URL,但没有/graphql路径,导致出现“无法获取/”的消息。一旦窗口打开进入游乐场,你将需要追加/graphql。
如果事情似乎不工作,要排除故障,您可以通过执行以下命令行来查看服务器的控制台输出:
$ heroku logs
API 服务器的正常成功启动应该如下所示:
2018-12-30T12:20:34.841550+00:00 app[web.1]: > pro-mern-stack-2-api@1.0.0 start /app
2018-12-30T12:20:34.841552+00:00 app[web.1]: > nodemon -e js,graphql -w . -w .env server.js
2018-12-30T12:20:35.498072+00:00 app[web.1]: [nodemon] 1.18.9
2018-12-30T12:20:35.500474+00:00 app[web.1]: [nodemon] to restart at any time, enter `rs`
2018-12-30T12:20:35.501650+00:00 app[web.1]: [nodemon] watching: *.* .env
2018-12-30T12:20:35.502464+00:00 app[web.1]: [nodemon] starting `node server.js`
2018-12-30T12:20:37.028765+00:00 app[web.1]: CORS setting: true
2018-12-30T12:20:38.639869+00:00 heroku[web.1]: State changed from starting to up
2018-12-30T12:20:38.512917+00:00 app[web.1]: Connected to MongoDB at mongodb+srv://UUU:PPP@XXX.mongodb.net/issuetracker?retryWrites=true
2018-12-30T12:20:38.523184+00:00 app[web.1]: API server started on port 46837
用户界面应用
UI 服务器需要 UI 服务器中类似的一组步骤。首先,让我们更改设置监听端口的变量的名称。清单 15-7 和 15-8 中显示了所需的更改。列表
...
const port = process.env.UI_SERVER_PORT || 8000;
...
15-7ui/server/uiserver.js: Change in Name of PORT Environment Variable
...
UI_SERVER_PORT=8000
...
Listing 15-8ui/sample.env: Change in Name of PORT Environment Variable
在 API 应用中,源代码足以让服务器启动并运行服务器。UI 应用是不同的,因为它需要在启动服务器所需的文件准备好之前进行编译。还需要将 Bootstrap 的静态 CSS 和 JavaScript 文件链接或复制到public目录。
有两个 npm 脚本可以实现这一点。脚本postinstall是在npm install结束后立即运行的脚本。这是一个特定于 Node.js 的脚本,它将由 npm 自动运行。因此,当开发人员在本地运行npm install以及 Heroku 在部署后运行npm install时,它都会生效。另一个剧本是heroku-postbuild,是针对 Heroku 的。也就是说,这个脚本只在 Heroku 部署上运行,而不是当开发人员在本地计算机上运行npm install时。
对于开发人员来说,安装后运行编译是浪费时间,因为他们通常会通过 Webpack 使用 HMR。此外,将 Bootstrap 文件链接到 public 将意味着我们必须假设它是一台 Mac 或 Linux 计算机。所以,让我们只做脚本heroku-postbuild中的这两个步骤。此外,我们需要对package.json进行更改,以指定 Node.js 和 npm 版本,就像我们对 API 服务器所做的那样。清单 15-9 显示了对package.json的所有更改。列表
...
"main": "index.js",
"engines": {
"node": "10.x",
"npm": "6.x"
},
...
"dev-all": "rm dist/* && npm run watch-server-hmr & sleep 5 && npm start",
"heroku-postbuild": "npm run compile && ln -fs ../node_modules/bootstrap/dist
public/bootstrap"
...
15-9ui/package.json: Changes for Specifying engine, postinstall, and post-build
为了使更改永久化,让我们将它们提交到本地 Git 存储库,并将它们推送到 GitHub remote。
$ cd ui
$ git commit -am "Changes for Heroku"
$ git push origin master
现在,我们准备部署 UI 服务器,但是首先我们需要在 Heroku 上创建应用。让我们称这个应用为tracker-ui-GITHUB_USER并创建它。
$ heroku create tracker-ui-$GITHUB_USER
在我们通过将服务器推到 Heroku remote 来启动它之前,我们需要配置 UI 服务器需要的 API 和认证端点。我们已经在 shell 中设置了GITHUB_USER环境变量,因此使用它,让我们设置这些配置变量。对于 Google 认证,我们还设置了另一个变量,叫做GOOGLE_CLIENT_ID。将这个变量设置为从 Google 开发人员控制台获得的 Google 客户端 ID。
$ heroku config:set \
UI_API_ENDPOINT=https://tracker-api-$GITHUB_USER.herokuapp.com/graphql \
UI_AUTH_ENDPOINT=https://tracker-api-$GITHUB_USER.herokuapp.com/auth \
GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
部署服务器与我们部署 API 服务器是一样的。我们需要把储存库推到 Heroku 遥控器上。
$ git push heroku master
在部署日志中,您应该会看到与 API 服务器部署非常相似的消息。但重要的是,您还应该看到这样的编译步骤:
...
remote: -----> Building dependencies
remote: Installing node modules (package.json + package-lock)
...
remote: Running heroku-postbuild
remote:
remote: > pro-mern-stack-2-ui@1.0.0 heroku-postbuild ↲ /tmp/build_605a3a265a979f27ab6e5296a8297eb9
remote: > npm run compile && ln -fs ../node_modules/bootstrap/dist public/bootstrap
remote:
remote:
remote: > pro-mern-stack-2-ui@1.0.0 compile ↲ /tmp/build_605a3a265a979f27ab6e5296a8297eb9
remote: > webpack --mode production
remote:
remote: Hash: 0288037a5cd24d5397fc7b520cbaa24cafcace5c
...
remote: -----> Caching build
remote: - node_modules
remote:
remote: -----> Pruning devDependencies
remote: removed 454 packages and audited 3507 packages in 8.764s
remote: found 0 vulnerabilities
remote:
...
remote: -----> Launching...
remote: Released v1
remote: https://tracker-ui-$GITHUB_USER.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
一旦应用启动并运行,您应该能够查看主要问题页面。但是在导航到任何其他页面时,您应该会看到一个 CORS 错误。这是因为 API 服务器没有被设置为UI_SERVER_ORIGIN,它将被默认为http://localhost:8000。现在我们知道了 UI 服务器的 URL,让我们在 API 服务器中设置它。
$ cd api
$ heroku config:set \
UI_SERVER_ORIGIN=https://tracker-ui-$GITHUB_USER.herokuapp.com
现在,如果您浏览到应用 URL(或者,只需在ui目录中键入heroku open,您应该会看到问题跟踪器 UI,其中加载了初始问题集。您还应该能够成功导航到其他页面。这些其他页面也应该在该页面上刷新浏览器时工作。
但是你会发现这只对未经认证的用户有效。由于正在使用新域以及 CORS 和 cookie 的考虑,此时登录将不起作用。但是在我们解决这个问题之前,让我们看看代理模式是否有效。
代理模式
代理模式应该看起来正常工作,因为没有 CORS 或 cookie 的考虑。让我们通过设置相应的环境变量来设置代理模式。
$ cd ui
$ heroku config:set \
UI_API_ENDPOINT=https://tracker-ui-$GITHUB_USER.herokuapp.com/graphql \
UI_AUTH_ENDPOINT=https://tracker-ui-$GITHUB_USER.herokuapp.com/auth \
UI_SERVER_API_ENDPOINT=https://tracker-api-$GITHUB_USER.herokuapp.com/graphql \
API_PROXY_TARGET=https://tracker-api-$GITHUB_USER.herokuapp.com
现在,如果您尝试应用,第一个页面加载将成功,但是在导航到其他视图时,您将看到浏览器呈现的请求将超时,并且在开发人员工具的 Network 选项卡中,您将发现 API 调用失败。如果您尝试使用代理 URL https://tracker-ui-$GITHUB_USER.herokuapp.com/graphql的 Playground,您将会看到一个错误页面,同样是因为对 API 服务器的代理请求失败。发生这种情况是因为 Heroku 路由 HTTP 请求方式。
事实上,许多 web 应用在 Heroku 上共享相同的资源。不仅仅是计算资源,它们还共享 IP 地址。因此,有必要为每个请求指定该请求应该到达哪个应用,以便它可以被路由到适当的应用。这是使用 HTTP 请求中的Host头来完成的。这项技术被称为基于名字的虚拟主机,它也受到流行的 Web 服务器如 Apache 和 nginx 的支持。
浏览器会自动将这个头设置为 URL 的主机名部分,因此从浏览器到 API 服务器或 UI 服务器的任何请求都会被 Heroku 路由到正确的应用。但是当代理请求时,http-proxy-middleware不会自动这样做。默认情况下,它使用从浏览器接收到的原始Host头,并将其复制到对 API 服务器的请求中。
因此,当浏览器发起请求说https://tracker-ui-$GITHUB_USER.herokuapp.com/graphql时,会发生以下一系列事件:
-
浏览器解析出
tracker-ui-$GITHUB_USER.herokuapp.com的 IP 地址,这是所有 Heroku 应用共享的众多常用 IP 地址之一。 -
浏览器将
Host头设置为tracker-ui-$GITHUB_USER.herokuapp.com。 -
Heroku 查看
Host头并将请求路由到 UI 服务器。 -
代理中间件拦截该请求(因为目标是
/graphql),并尝试将其转发给tracker-api-$GITHUB_USER.herokuapp.com。 -
代理中间件为 API 服务器解析主机,这也会产生一个公共的 IP 地址。
-
代理中间件向 IP 地址转发相同的请求,即
Host头为tracker-ui-$GITHUB_USER.herokuapp.com。 -
Heroku 接收请求,查看
Host头,并将其路由到 UI 服务器!这将导致无限循环,直到请求超时。
我们在本地计算机上尝试代理模式的原因是没有基于虚拟主机的路由。所有对http://localhost:3000的请求都直接登陆到 API 服务器,这不会导致任何问题。
让它在 Heroku 中正确工作的补救方法是改变http-proxy-middleware的行为。我们需要它,以便将Host头设置为与目标 URL 相同,而不是原始头。代理中间件选项中的标志changeOrigin控制这种行为,我们需要做的就是将它设置为true。清单 15-10 显示了此次修复对uiserver.js的更改。列表
...
if (apiProxyTarget) {
app.use('/graphql', proxy({ target: apiProxyTarget, changeOrigin: true }));
app.use('/auth', proxy({ target: apiProxyTarget, changeOrigin: true }));
}
...
15-10ui/server/uiserver.js: Change Origin When Proxying Requests
现在,如果您提交这些更改,并使用git push heroku master将它们推送到 Heroku,您会发现应用工作得非常好。您应该确认身份验证有效,并且在浏览器刷新时保持有效。
非代理模式
即使两个应用有相同的域(heroku.com),非代理操作模式也不起作用。浏览器不会在这两个应用之间共享 cookie,因为heroku.com列在公共后缀列表中。这个列表正是针对这样的情况,你可以在 https://publicsuffix.org/learn/ 了解更多。
虽然一开始看起来很不方便,但你现在可能已经意识到,如果你的两个应用可以共享同一个 cookie,那么它也会在 Heroku 上的所有其他应用之间共享,甚至是那些属于其他 Heroku 用户的应用。当然,这一点也不安全。因此,Heroku 通过将域名herokuapp.com添加到公共后缀列表中,确保了该域名与顶级域名处于同等地位。这导致浏览器拒绝域为herokuapp.com的set-cookie报头。
在 UI 和 API 服务器之间共享 cookies 的唯一方法是使用自定义域。然后,该域上的子域可以用作 UI 和 API 服务器,从而支持共享 cookies,使非代理操作模式能够工作。因此,您必须为自己创建一个自定义域,以便在 Heroku 上尝试非代理操作模式。
有很多方法可以注册自己的域名,包括免费的。选择一个并创建一个域。完成之后,创建一个名为CUSTOM_DOMAIN的环境变量,并将其设置为该域。例如,如果您已经将myfreedomain.tk注册为您的自定义域名,则将CUSTOM_DOMAIN设置为myfreedomain.tk,包括顶级域名.tk。在下面的命令中,我们将使用ui.$CUSTOM_DOMAIN和api.$CUSTOM_DOMAIN作为 UI 和 API 应用的子域。
使用自定义域的一个缺点是 Heroku 默认情况下不支持 SSL,或者免费。您需要升级到 Heroku 上的付费帐户,才能为您的自定义域启用 SSL。因此,在本章的剩余部分,我们将使用基于http://而不是https://的 URL。
首先,您必须授权新的基于域的 URL 作为 Google Developer 项目中授权的 JavaScript 源。原点会是http://ui.$CUSTOM_DOMAIN。这还需要您在 Google developer console 的 OAuth 同意屏幕选项卡中添加一个授权域作为$CUSTOM_DOMAIN。注意,添加 JavaScript 源后,需要一段时间才能生效。如果你计划在将来使用 SSL,在 origin now 本身中添加https://版本可能是个好主意。
接下来,我们需要将这些域添加到 Heroku,以便它能够识别这些域,并将发往这些域的 HTTP 流量定向到我们创建的 Heroku 应用。让我们首先在 UI 应用中这样做。
$ cd ui
$ heroku domains:add ui.$CUSTOM_DOMAIN
这将显示如下所示的输出:
Adding ui.$CUSTOM_DOMAIN to tracker-ui-$GITHUB_USER ... done
▸ Configure your app's DNS provider to point to the DNS Target
▸ sheltered-tor-u2t67pge87ki9sbr6iqw1h.herokudns.com.
▸ For help, see https://devcenter.heroku.com/articles/custom-domains
按照控制台输出的指示,您需要配置 DNS,以便对ui.$CUSTOM_DOMAIN的请求到达 Heroku 托管的 UI 应用。这需要在您的域提供商中使用他们的 UI 来完成。您将需要创建一个 CNAME 记录,这将为一个域创建一个别名。本质上,您需要将自定义域映射为真实域的别名,例如 Heroku 自动为应用分配的sheltered-tor-u2t67pge87ki9sbr6iqw1h.herokudns.com。
然后,我们需要将 API 应用的域添加到 Heroku:
$ cd api
$ heroku domains:add api.$CUSTOM_DOMAIN
然后,您需要在域提供商的记录中设置 DNS 别名映射,就像您对 UI 服务器所做的那样。如果您使用 GoDaddy 托管您的域,您应该会在域管理器中看到类似图 15-1 的屏幕。
图 15-1
为 ui 和 api 创建记录后域管理器的屏幕截图
接下来,让我们将 UI 应用的 API 和auth端点设置为新的基于定制域的 API 服务器的 URL,同时我们切换到非代理操作模式。
$ cd ui
$ heroku config:set \
UI_API_ENDPOINT=http://api.$CUSTOM_DOMAIN/graphql \
UI_SERVER_API_ENDPOINT=http://api.$CUSTOM_DOMAIN/graphql \
UI_AUTH_ENDPOINT=http://api.$CUSTOM_DOMAIN/auth
$ heroku config:unset \
API_PROXY_TARGET
最后,为了让 CORS 正常工作,并在 UI 和 API 应用之间共享 cookies,我们需要如下配置 API 服务器:
$ cd api
$ heroku config:set \
UI_SERVER_ORIGIN=http://ui.$CUSTOM_DOMAIN \
COOKIE_DOMAIN=$CUSTOM_DOMAIN
服务器现在会自动重启,如果您测试应用,它应该像在代理模式下一样无缝地工作。
摘要
尽管我们让问题跟踪器应用在您的本地计算机上工作,但将其部署到云,尤其是部署到平台即服务,会带来一些挑战。
在代理模式下,当代理请求时,我们必须正确设置http-proxy-middleware来改变Host头。在非代理模式中,您了解到使用默认应用域不起作用,我们必须使用自定义域。我们还必须对代码进行更改,但是这些更改也与本地开发的应用兼容。
我们提到了 Git 和版本控制,并且我们为存储库使用了两个遥控器:一个用于 GitHub 上的团队协作,另一个用于 Heroku。这只是管理版本和发布的一种方式。如果你在一个团队中工作,想要协作,你可以在 GitHub remote 中这样做,当发布的时候,你可以把它推到 Heroku remote 中。我没有深入研究管理发布的其他选项,因为这些都是您的项目特有的,也不是真正的 MERN 主题。
当我们接近这本书的结尾时,让我们在下一章讨论 MERN 堆栈还能做什么。我们将停止改变应用,只看其他相关的技术和库,可能对你的 MERN 项目有用。
十六、展望未来
我希望到现在为止,我已经成功地在你的脑海中植入了 MERN 堆栈的基本原理。更重要的是,我希望我已经让你能够更上一层楼,因为这绝不是问题跟踪器应用或你心目中的任何其他项目的终结。如果你真的试着回答了每一章的练习,你现在应该知道在哪里可以得到更多的信息。
可以添加更多的功能,可以使用更多的技术来帮助您向前发展。在这一章中,如果你决定在实际项目中使用 MERN,我会提到一些你可能会考虑的技术。但是这仅仅是对可能性的简单介绍;到目前为止,我们不会向我们创建的应用添加任何代码。
请注意,这些新事物不一定适合您的应用。当您遇到困难或者希望随着应用的增长自动处理一些重复的代码时,您需要仔细评估它们。本书的前几章应该已经给了你足够的信心,让你可以使用 MERN 堆栈,并手动或自己解决所有问题。但是在很多情况下,其他人面临类似的问题,并创建了库来解决它们。我建议你寻找一个现有的解决方案,但是要等到你清楚你想要解决的是什么。
猫鼬
大多数使用关系数据库的技术栈可以用对象关系映射(ORM)库来补充。这些增加了一个抽象层,让开发人员看到对象本身,而不是带有行和列的表。
对于 MongoDB,乍一看,似乎没有必要将关系映射到数据库在内存中存储到对象的方式,因为对象会自然地映射到 MongoDB 文档,而无需中间层的转换。但是对象文档映射(ODM)层还可以提供其他功能。mongose(https://mongoosejs.com)是 MongoDB 的一个流行的 ODM 库,它提供了以下内容:
-
模式定义:这很有用,因为 MongoDB 不像 SQL 数据库那样强制执行模式。对于 SQL 数据库,数据库会自动捕获模式错误,而我们忽略了问题跟踪器应用的模式验证。使用 Mongoose,您可以定义模式,并根据模式自动验证新文档。从 3.6 版本开始,MongoDB 本身支持模式,但是与 Mongoose 相比,这种支持似乎很原始。
-
验证:在必需的检查和数据类型检查方面,Mongoose 拥有比 GraphQL 更多的内置验证器。这些包括字符串长度和数字的最小和最大检查。此外,您可以添加自定义验证器,如电子邮件 ID 验证器,并跨对象类型重用它们。
-
同构:有一个浏览器组件允许在浏览器中使用模式验证。这可以在用户提交表单之前在 UI 中显示错误,从而提高可用性。
-
模型:虽然我们封装了
issue.js中与问题相关的所有代码,但是我们并没有将函数附加到问题对象上。使用 Mongoose,您可以编写真正面向对象的模型,可以在对象中封装数据和方法。模型也让开发人员更直观地编写代码,例如,使用Object.save()而不是db.collection.insertOne()。
对于较小的项目,如问题跟踪器,可能不需要 Mongoose。如果需要,您可以轻松地提取和共享issue.js中的验证,以重用代码。但是对于多人在一个团队中工作的大型项目,使用 Mongoose 肯定会避免开发过程中的错误,并作为对象模式的文档,这对团队的新人尤其有帮助。
流量
如果你读过 React,很可能你也听说过 Redux 和/或 Flux 模式。由于它的受欢迎程度,很容易就开始使用它。但在找到解决方案之前,让我们先看看有哪些改进的机会。
当我们添加用户登录时,我们发现我们必须将用户信息和登录操作沿着组件层次结构向上传输,然后再向下传输到使用它的组件。对于介于两者之间、不需要知道信息的组件来说,这似乎有点浪费(以及不必要的耦合增加)。例如,组件NavBar本身对用户名或登录状态没有什么用处。它所做的只是将知识向下传递给SignInNavItem。
我们通过对真正的全局状态变量使用 React 上下文解决了这个问题。此外,为了初始化,我们还创建了一个全局store对象。如果您确实遇到了需要在许多组件之间共享状态的情况,然而,状态并不是真正的全局的,您将感觉到需要向store对象中添加越来越多的内容。但是任何全球性的东西都需要一些限制或契约来避免不受控制的变化所造成的混乱。这就是 Flux 架构模式试图定义的。
Flux 支持单向数据流,因此状态的所有更改都通过调度程序来传递,调度程序控制更改的顺序,从而避免了因相互依赖而导致的无限循环。尽管这种模式是由开发 React 的同一批人(也就是脸书)发明的,但这种模式并不仅限于在 React 中使用。以下是我从脸书的 React 博客中引用的对通量的一个非常简洁而完整的描述:
当用户与 React 视图交互时,视图通过调度器发送一个动作(通常表示为带有一些字段的 JavaScript 对象),通知保存应用数据和业务逻辑的各个存储*。当存储改变状态时,它们会通知视图某些内容已经更新。这与 React 的声明性模型配合得特别好,该模型允许存储发送更新,而无需指定如何在状态之间转换视图。*
本质上,模式由正式定义的动作组成,例如,由用户发起的创建问题。该动作被分派到一个商店。这会影响商店的状态。典型地,一个被称为缩减器的函数被用来描述一个动作对状态的影响:新的状态是当前状态和动作的函数。
Redux 和 Mobx 是 React 的作者推荐的两种流行的选择,可用于全局状态管理,它们在很大程度上遵循了 Flux 模式的概念。这些框架或通量模式的效果是,您将不得不编写大量样板代码,也就是说,看起来非常像其他代码的代码,看起来没有必要。对于每个用户操作,您必须正式定义一个操作、一个调度程序和一个缩减器,在给定当前状态的情况下,返回新状态的内容。
让我们以删除问题的动作为例。您必须定义一组正式的动作,包括像DELETE_ISSUE这样的常量。然后,您必须定义一个 reducer,它是一个接受各种动作及其参数并返回一个新状态的函数(每个不同的动作有一个switch-case)。然后,您必须创建一个 dispatcher,它将动作和参数转换成实际的动作,比如向服务器发送请求。
如果应用中的状态机非常复杂,比如说,如果删除请求可以从 UI 中的几个不同位置发起(甚至可以从 UI 外部发起,就像其他用户的操作一样),并且除了删除表中的一行之外,还有许多其他含义,那么所有这些都是值得的。没有多少应用面临这种复杂性。
我可以满怀信心地向你保证,总有一天你的应用会在一个页面(想象一下你的脸书页面)中变得足够大,你会知道你需要 Redux 或 Mobx,以及为什么。在此之前,更明智的做法可能是在继续学习这些新模式的同时,只使用基础知识和基本原理来完成工作。
创建 React 应用
我们做了很多工作来使用 Webpack 和 Babel 建立 React 编译和开发环境。如果您对设置这一切不感兴趣,或者不想更好地控制优化和定制,您可能想考虑应用初始化器。
一个这样的应用初始化器是 Create React App。这可以帮助您快速入门,并为一个纯 react 应用设置所有必要的工具,如 Webpack 和 Babel。这可以满足问题跟踪器 UI 服务器的几乎所有要求。以下命令可能已经创建了初始版本:
$ npx create-react-app tracker-ui
注意,我们不需要安装任何 Node.js 模块。命令行npx临时安装了运行命令行create-react-app所需的任何东西并运行它。现在,在目录tracker-ui中,您会发现一个src目录,其中有两个文件:一个 JavaScript 文件和一个 CSS 文件。这只是一个开端;您可以在此添加更多文件来为您的项目编写代码。现在,要启动应用,您可以使用熟悉的start脚本:
$ npm start
这不仅会编译和提供捆绑包,还会自动打开一个浏览器标签并将其指向应用的 URL,http://localhost:3000/.您可以在 https://facebook.github.io/create-react-app/docs/getting-started 查看 Create React App 的用户指南。在您的项目中使用该工具之前,您必须记住以下几点:
-
这就创建了一个纯 React 应用,也就是说,它没有服务器端组件。这意味着没有创建 Express 服务器,因此您不能做问题跟踪器 UI 服务器所做的事情:代理请求和在服务器上呈现。
-
这只能用于 UI 服务器。API 服务器将需要保持其在问题跟踪器应用中的状态。
-
如果您确实需要代理服务器和服务器渲染,您可以从创建 React 应用开始,并通过执行
npm run eject和安装 Express 等自定义配置来“弹出”生成的应用。弹出已创建的应用具有使所有配置可见并允许自定义的效果,但它会阻止您轻松升级到 Create React 应用的新版本。 -
Create React App 使用 Webpack 处理所有资产,包括 CSS。由于问题跟踪器依赖于 React-Bootstrap,而我们没有模块化的 CSS,这并不十分理想,尽管可以通过包含 Bootstrap 的 CSS 文件来实现。例如,参见源文件
index.js,如何包含index.css。
因此,如果您的需求是一个纯粹的 React 应用,或者如果您愿意为 UI 服务器进行配置更改以包含 Express,那么您可以使用 Create React App 作为起点。或者,你可以在 https://github.com/facebook/create-react-app#popular-alternatives 查看一些基于你的项目类型的流行备选方案。
mern.io
如果您希望在 MERN 堆栈应用中遵循的大多数流行实践上有一个良好的开端,那么从一个已经用所有样板代码以及一组示例对象精心制作的项目开始是很方便的,您可以调整或复制这些示例对象来快速完成您的工作。
在 http://mern.io 可以找到一个专为 MERN 技术栈打造的脚手架工具。这个项目包括一个漂亮的工具,可以用来创建一个 MERN 应用的框架。这个包本身叫做mern-cli,这是一个命令行实用程序,它创建了一个基于 MERN 堆栈的应用。创建新的 MERN 应用(仅在 Linux 或 MacOS 上)的命令如下:
$ npm install -g mern-cli
$ mern init mernApp
如果您的 Node.js 版本是 10,您可能会得到一些编译器警告,现在可以安全地忽略它们。您会发现在目录mernApp下有一个完整的功能应用。为了快速查看它是否真的工作,您需要做的就是导航到目录,安装所有需要的包(使用npm install,然后运行npm start:
$ cd mernApp
$ npm install
$ npm start
将会有更多的警告,因为该项目使用了旧版本的 npm 包,现在已经发现有一些漏洞。如果您忽略这些警告,将浏览器指向http://localhost:8000,您将看到一个可以创建和删除博客帖子的功能应用。我们在问题跟踪器应用中所做的工作与mernApp的区别如下:
-
应用
mernApp不使用 Bootstrap 程序或 ReactBootstrap 程序。它有自己的样式表来设计它的内容。但是包含 Bootstrap 并不困难,它可以按照我们在 React-Bootstrap 章节中所做的步骤来完成(第十一章)。 -
mernApp使用 Mongoose 和 Redux,这两种技术我们在前面的章节中讨论过,但没有在问题跟踪器应用中使用。 -
在
mernApp中没有认证或会话处理。 -
代码被组织成模块,这些模块是代码的内聚部分,它们一起工作以公开重要的功能。默认创建的唯一模块是
Post模块,您可以根据需要创建更多模块。 -
服务器是一个单独的服务器,不像问题跟踪器那样,我们有单独的 API 和 UI 服务器。
-
使用基于 REST 的 API,而不是 GraphQL。
尽管有这些差异,这个项目还是显示出了希望。但是它有点过时,不能在 Windows 上工作,在创建应用的过程中抛出了许多警告,并且它没有得到积极的维护。但是 3.0 版本正在开发中。也许当新版本发布时,它会成为基于 MERN 的应用的应用初始化器。
护照
当你需要集成更多的认证提供者,比如脸书或 Twitter,如果你遵循我们在第十四章“认证”中采用的方法,你必须为每个认证选项编写不同的代码分支。
Node.js 包 Passport ( http://www.passportjs.org/ )通过创建一个可以插入多个认证策略的单一框架来解决这个问题。Passport 本身只规定了应用开发人员的框架和接口。每个策略(例如,Google 策略)都是作为一个单独的包来实现的。
请注意,Passport 是一个后端专用包。来自 UI 的所有身份验证请求都需要通过后端传递。这与我们在问题跟踪器中作为 Google 登录的一部分实现的不同,在问题跟踪器中,我们使用 Google 的客户端库直接向 Google 的身份验证引擎发起身份验证请求。一旦认证成功,我们将 Google 认证令牌从 UI 传递到后端进行验证。
相比之下,Passport 使用 Google 支持的 Open ID Connect 协议作为替代( https://developers.google.com/identity/protocols/OpenIDConnect )。在这种方法中,UI 调用应用的后端进行身份验证,而不是 Google 的身份验证引擎。然后用户被重定向到谷歌账户页面,而不是弹出窗口。然后,使用一组到应用后端的回调 URL,需要处理身份验证的成功和失败。
Open ID 方法适合服务器呈现的应用和用户需要从一开始就登录的应用。对于像问题跟踪器这样的 SPA,这将导致应用页面的几次刷新。相比之下,问题跟踪器应用中使用的直接方法不会导致浏览器刷新,而是在页面中更新身份验证信息。但是与 Passport 在处理多个身份验证提供者时提供的所有实现的便利性相比,这只是一个小麻烦。
那都是乡亲们!
我希望对你来说,这是一次穿过 MERN 堆栈水域的有趣航行,就像我一样。通过思考编程模型、范例和 MERN 堆栈让我大开眼界的新思维,我学到了很多。
我确保我们研究了 MERN 堆栈中每个部分的具体细节和附带的工具,而不是使用像 Passport 或 Create React App 这样的框架来简化工作。我希望你喜欢弄脏自己的手,以及随之而来的学习,尽管完成这项工作更难。
但这还远远没有结束。几个月后,我有理由相信情况会有所不同。谁知道呢?浏览器本身可能会适应或结合虚拟 DOM 技术,使得 React 变得多余!或者,你会看到一个新的框架(不是一个库),锚点作为 MVC 产品中的视图部分。或者,我们使用的新版本库可能会提出一种全新的做事方式。
关键是继续寻找这些新的发展,同时非常、非常深入地分析为什么它们对你的应用和团队有用或没用。
为展望更美好的未来干杯。