b

把应用部署到网上

接下来,让我们将第2章节中制作的前端连接到我们自己的后端。

在前面的部分中,前端可以从作为后端的 json 服务器向地址 http://localhost:3001/notes 索取便笺列表。

我们的后端有一个稍微不同的 url 结构,便笺可以从 http://localhost:3001/api/notes 中获取到。

让我们像下面这样修改 src/services/notes.js 中的baseUrl属性 :

import axios from 'axios'
const baseUrl = 'http://localhost:3001/api/notes'
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

// ...

export default { getAll, create, update }

现在前端的 GET 请求由于某些原因不能工作: http://localhost:3001/api/notes:

fullstack content

这是怎么回事? 我们可以从浏览器和Postman访问后端,没有任何问题。

Same origin policy and CORS

【同源政策和 CORS】

问题出在一个叫 CORS 的东西上,或者叫跨来源资源共享。

根据维基百科 :

Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the first resource was served. A web page may freely embed cross-origin images, stylesheets, scripts, iframes, and videos. Certain "cross-domain" requests, notably Ajax requests, are forbidden by default by the same-origin security policy. Cross-origin resource sharing (CORS)是一种机制,它允许一个网页上受限制的资源(例如字体),从提供一手资源的域名以外的另一个域名请求跨来源资源共享。 一个网页可以自由地嵌入跨来源的图片、样式表、脚本、 iframe 和视频。 默认情况下,同源安全策略禁止某些“跨域”请求,特别是 Ajax 请求。

在我们的上下文中,问题出在了,默认情况下,运行在浏览器应用的 JavaScript 代码只能与相同 的服务器通信。

因为我们的服务器位于本地主机端口3001,而我们的前端位于本地主机端口3000,所以它们不具有相同的源。

请记住,同源策略和 CORS 并不是特定于 React 或 Node 的。 它们实际上是 web 应用操作的通用原则。

我们可以通过使用 Node 的cors 中间件来允许来自其他源的请求。

使用命令安装cors

npm install cors --save

使用中间件并允许来自所有来源的请求:

const cors = require('cors')

app.use(cors())

前端工作正常了!但是,在后端还没有实现更改便笺重要性的功能。

你可以 从Mozillas 页面阅读更多关于 CORS的内容。

Application to the Internet

【将应用部署到网上】

现在整个栈已经准备就绪,让我们将应用迁移到互联网上。 我们将使用古老的 Heroku https://www.Heroku.com

如果您以前从未使用过 Heroku,您可以从Heroku 文档或通过谷歌搜索找到指令。

向项目的根目录添加一个名为 Procfile的文件,告诉 Heroku 如何启动应用。

web: node index.js

更改应用在index.js 文件底部使用的端口定义,如下所示:

const PORT = process.env.PORT || 3001app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

现在我们使用定义在环境变量的端口,如果环境变量 PORT 是未定义的,则使用端口3001。

Heroku 会在环境变量的基础上配置应用端口。

在项目目录中创建一个 Git 仓库,并使用如下内容添加 .gitignore

node_modules

使用命令heroku create创建一个 Heroku 应用,将你的代码提交到仓库并将其推送到Heroku,git push Heroku master

如果一切顺利,应用就能正常工作:

fullstack content

如果没有运行成功,可以通过使用命令heroku logs 读取 heroku logs 来发现问题。

NB At least in the beginning it's good to keep an eye on the heroku logs at all times. The best way to do this is with command heroku logs -t which prints the logs to console whenever something happens on the server. 注意:至少在开始的时候,随时关注 heroku 日志是有好处的。 实现这一点的最佳方法是使用命令 heroku logs -t ,该命令会让服务器上发生任何事情时将日志打印到控制台。

NB If you are deploying from a git repository where your code is not on the master branch (i.e. if you are altering the notes repo from the last lesson) you will need to run git push heroku HEAD:master. If you have already done a push to heroku, you may need to run git push heroku HEAD:master --force. 如果你从Git 仓库中拉取,所部署的代码不是master分支(比如,如果你正在修改上节课的 notes repo,你需要运行 git push heroku HEAD:master . 如果你已经推送到了heroku, 你可能需要运行 git push heroku HEAD:master --force

前端也与 Heroku 的后端一起工作。 你可以通过更改前端的后端地址,更改为后端在 Heroku 的地址http://localhost:3001

下一个问题是,我们如何将前端部署到互联网? 我们有多种选择。 接下来我们来看看其中的一个。

Frontend production build

【前端生产构建】

到目前为止,我们一直在开发模式 中运行 React code。 在开发模式下,应用被配置为提供清晰的错误消息,立即向浏览器渲染代码更改,等等。

当应用被部署时,我们必须创建一个生产构建或一个为生产而优化的应用版本。

使用create-react-app 创建的应用的生产构建可以使用命令npm run build创建。

让我们从前端项目的根目录运行这个命令。

这将创建一个名为build 的目录(其中包含应用中唯一的 HTML 文件index. HTML) ,其中包含目录static。 我们应用的 JavaScript 代码的Minified版本将生成到static 目录。 即使应用代码位于多个文件中,所有的 JavaScript 都将被缩小到一个文件中。 实际上,来自所有应用依赖项的所有代码也将缩小到这个单一文件中。

缩小后的代码可读性不是很好,代码的开头是这样的:

!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c<i.length;c++)f=i[c],o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={2:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})

Serving static files from the backend

【从后端服务部署静态文件】

部署前端的一个选择是将生产构建( build 目录)复制到后端仓库的根目录,并配置后端以显示前端的 main page (文件 build/index.html)作为其主页。

我们从将前端的生产构建复制到后端的根目录。 使用一台Mac 或 Linux 计算机,可以通过命令从前端目录进行复制

cp -r build ../../../osa3/notes-backend

如果你使用的Windows操作系统,你可以使用copy 或者 xcopy 命令。要么就简单地使用复制粘贴即可。

后端目录现在应该如下所示:

fullstack content

为了让 express 显示 static content、 页面 index.html 和它用来fetch的 JavaScript 等等,我们需要一个来自 express 的内置中间件,称为static

当我们在中间件声明中添加如下内容时

app.use(express.static('build'))

每当 express 收到一个 HTTP GET 请求时,它都会首先检查build 目录是否包含与请求地址对应的文件。 如果找到正确的文件,express 将返回该文件。

现在 HTTP GET 向地址www.serversaddress.com/index.html www.serversaddress.com 的GET请求,将显示 React 前端。 Get 请求到地址 www.serversaddress.com/notes 将由后端代码处理。

因为在我们的情况下,前端和后端都在同一个地址,所以我们可以声明 baseUrl 为relative URL。 这意味着我们可以省略声明服务器的部分。

import axios from 'axios'
const baseUrl = '/api/notes'
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

// ...

更改之后,我们必须创建一个新的生产构建,并将其复制到后端存储库的根。

该应用现在可以从后端 地址 http://localhost:3001 中使用:

fullstack content

我们的应用现在的工作方式与我们在第0章节中研究的单页应用 示例应用完全一样。

当我们使用浏览器访问地址 http://localhost:3001 时,服务器从build 仓库返回index. html 文件。 档案的摘要内容如下:

<head>
  <meta charset="utf-8"/>
  <title>React App</title>
  <link href="/static/css/main.f9a47af2.chunk.css" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script src="/static/js/1.578f4ea1.chunk.js"></script>
  <script src="/static/js/main.104ca08d.chunk.js"></script>
</body>
</html>

该文件包含一些指令,用于获取定义应用样式的 CSS 样式表,以及两个script 标签,这些标记说明浏览器获取应用的 JavaScript 代码——即实际的 React 应用。

React代码从服务器地址 http://localhost:3001/api/notes 获取便笺,并将它们渲染到屏幕上。 服务器和浏览器之间的通信可以在开发控制台的Network 选项卡中看到:

fullstack content

确保应用的生产版本在本地正常工作之后,将前端的生产构建提交到后端存储库,并将代码再次推送到 Heroku。

除了我们还没有添加改变后端便笺重要性的功能之外,应用运行得非常好。

fullstack content

我们的应用将便笺保存到一个变量中。 如果应用崩溃或重新启动,所有数据都将消失。

应用需要一个数据库。在我们引入数据库之前,让我们先了解几个知识点。

Streamlining deploying of the frontend

【流程化前端部署】

为了在没有额外手工工作的情况下创建前端的新的生产构建,我们在后端存储库的package.json 中添加一些 npm-scripts:

{
  "scripts": {
     //...
    "build:ui": "rm -rf build && cd ../../osa2/materiaali/notes-new && npm run build --prod && cp -r build ../../../osa3/notes-backend/",
    "deploy": "git push heroku master",
    "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && npm run deploy",    
    "logs:prod": "heroku logs --tail"
  }
}

脚本 npm run build:ui用于构建前端,并在后端存储库下复制生产版本。npm run deploy 会将当前的后端版本发布到heroku.

npm run deploy:full 会将这两者结合起来,并包含更新后端存储库所需的git 命令。

还有一个脚本 npm run logs:prod 用于显示 heroku 日志。

注意,我构建的脚本中的目录路径 build:ui 依赖于文件系统中存储库的位置。

注意 在Windows中,npm 脚本默认是运行在cmd.exe 这个默认的shell中的,而它并不支持bash命令。因此如果希望以上的bash命令运转良好,你可以将默认的shell换成bash(默认Windows安装Git时已经安装了Bash):

Proxy

【代理】

前端上的更改导致它不能再在开发模式下工作(当使用命令 npm start 启动时) ,因为到后端的连接无法工作。

fullstack content

这是由于将后端地址更改为了一个相对 URL:

const baseUrl = '/api/notes'

因为在开发模式下,前端位于地址localhost: 3000,所以对后端的请求会发送到错误的地址localhost:3000/api/notes。 而后端位于localhost: 3001

如果这个项目是用 create-react-app 创建的,那么这个问题很容易解决。 将如下声明添加到前端仓库的package.json 文件中就足够了。

{
  "dependencies": {
    // ...
  },
  "scripts": {
    // ...
  },
  "proxy": "http://localhost:3001"}

在重新启动之后,React 开发环境将作为一个代理工作。 如果 React 代码对服务器地址http://localhost:3000发出了一个 HTTP 请求,而不是 React 应用本身管理的地址(即当请求不是为了获取应用的 CSS 或 JavaScript) ,那么该请求将被重定向到 HTTP://localhost:3001 的服务器。

现在前端也工作良好,可以在开发和生产模式下与服务器一起工作。

我们方法的一个劣势,是前端部署的复杂程度。 部署新版本需要生成新的前端生产构建并将其复制到后端存储库。 这使得创建一个自动化的部署管道变得更加困难。 部署管道是指通过不同的测试和质量检查将代码从开发人员的计算机转移到生产环境的自动化控制的方法。

有多种方法可以实现这一点(例如将后端和前端代码放到同一仓库中) ,但我们现在不讨论这些。

在某些情况下,将前端代码部署为它自己的应用可能是合理的。 通过create-react-app 创建的应用是简单的

后端的当前代码可以在分支part3-3 中的Github上找到。 前端代码的更改位于 前端仓库frontend repositorypart3-1 分支。