跳到内容

b

把应用部署到网上

接下来让我们把我们在第二章节中制作的前端连接到我们自己的后端。

在上一部分中,前端可以从我们作为后端的json-server中询问笔记列表,地址是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 }

我们也需要改变App.js中效果中指定的url。

  useEffect(() => {
    axios
      .get('http://localhost:3001/api/notes')
      .then(res => {
        setNotes(res.data)
      })
  }, [])

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

fullstack content

这里发生了什么?我们可以从浏览器和postman访问后端,没有任何问题。

Same origin policy and CORS

问题在于一个叫做CORS的东西,或者说跨源资源共享。

根据维基百科

跨源资源共享(CORS)是一种机制,它允许网页上的限制性资源(如字体)从第一个资源所来自的域之外的另一个域被请求。一个网页可以自由嵌入跨源图像、样式表、脚本、iframe和视频。某些 "跨域 "请求,特别是Ajax请求,在默认情况下是被同源安全策略所禁止的。

在我们的环境中,问题在于,默认情况下,在浏览器中运行的应用的JavaScript代码只能与同一来源的服务器通信。

因为我们的服务器在localhost 3001端口,而我们的前端在localhost 3000端口,它们没有相同的起源。

请记住,同源策略和 CORS 并不是专门针对 React 或 Node。它们实际上是网络应用操作的普遍原则。

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

在你的后端仓库中,用命令安装cors

npm install cors

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

const cors = require('cors')

app.use(cors())

前端就可以工作了!然而,改变笔记重要性的功能还没有在后端实现。

你可以从Mozillas页面上阅读更多关于CORS的信息。

我们的应用的设置现在看起来如下。

fullstack content

在浏览器中运行的react应用现在从运行在localhost:3001的node/express-server获取数据。

Application to the Internet

现在整个堆栈已经准备好了,让我们把我们的应用移到互联网上。我们将使用古老的Heroku来完成。

如果你以前从未使用过Heroku,你可以从Heroku文档中找到说明,或者通过谷歌搜索。

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

web: npm start

index.js文件的底部改变我们应用使用的端口的定义,像这样。

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

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

Heroku根据环境变量来配置应用的端口。

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

node_modules

https://devcenter.heroku.com/ ,创建Heroku账户

使用命令安装Heroku包:npm install -g heroku

用命令heroku create创建一个Heroku应用,将你的代码提交到版本库,并用命令git push heroku main将其移到Heroku。

如果一切顺利,应用就能工作。

fullstack content

如果没有,可以通过命令heroku logs阅读heroku日志来发现问题。

NB 至少在开始的时候,随时注意heroku的日志是很好的。最好的方法是使用命令heroku logs -t,它可以在服务器上发生任何事情时将日志打印到控制台。

NB 如果你从一个git仓库部署,而你的代码不在主分支上(例如,如果你正在改变上一课的notes repo),你将需要运行git push heroku HEAD:master。如果你已经做了推送到heroku,你可能需要运行git push heroku HEAD:main --force

前端也可以和Heroku的后端一起工作。你可以通过把前端的后端地址改为Heroku中的后端地址,而不是http://localhost:3001来检查。

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

Frontend production build

到目前为止,我们一直在开发模式中运行React代码。在开发模式下,应用被配置为给出清晰的错误信息,立即向浏览器渲染代码变化,等等。

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

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

注意:在撰写本文时(2022年1月20日)create-react-app有一个错误,导致以下错误 TypeError: MiniCssExtractPlugin不是一个构造函数

这里可以找到一个可能的修正。在文件package.json中添加以下内容

{
  // ...
  "resolutions": {
    "mini-css-extract-plugin": "2.4.5"
  }
}

然后运行命令

rm -rf package-lock.json
rm -rf node_modules
npm cache clean --force
npm install

在这些npm run build之后,应该可以工作。

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

这将创建一个名为build的目录(其中包含我们应用的唯一HTML文件,index.html),该目录包含static。我们应用的Minified版本的JavaScript代码将被生成到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目录)复制到后端仓库的根目录,并配置后端以显示前端的主页(文件build/index.html)作为其主页面。

我们首先把前端的生产构建复制到后端的根目录下。在Mac或Linux电脑上,复制可以在前端目录下用命令完成

cp -r build ../notes-backend

如果你使用的是Windows电脑,你可以用copyxcopy命令代替。否则,只需进行复制和粘贴。

后端目录现在应该是这样的。

fullstack content

为了使express显示静态内容,页面index.html和它获取的JavaScript等,我们需要express的一个内置的中间件,叫做static

当我们在中间件的声明中加入以下内容时

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

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

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

由于我们的情况,前端和后端都在同一个地址,我们可以将baseUrl声明为一个相对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

准备用于产品部署的设置看起来如下。

fullstack content

与在开发环境中运行应用时不同,现在所有东西都在同一个节点/express-backend中,该节点在localhost:3001中运行。当浏览器进入页面时,文件index.html被渲染。这导致浏览器获取React应用的产品版本。一旦开始运行,它就从localhost:3001/api/notes这个地址获取json-data。

The whole app to internet

在确保应用的生产版本在本地运行后,将前端的生产构建提交到后端仓库,并再次将代码推送到Heroku。

应用工作得很好,只是我们还没有在后端添加改变笔记重要性的功能。

fullstack content

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

该应用需要一个数据库。在我们引入一个数据库之前,让我们先看一下几件事。

现在的设置看起来如下。

fullstack content

节点/express-backend现在驻扎在Heroku服务器上。当访问形式为https://glacial-ravine-74819.herokuapp.com/ 的根地址时,浏览器会加载并执行React应用,从Heroku服务器上获取json数据。

Streamlining deploying of the frontend

为了创建一个新的前端生产构建,不需要额外的手工工作,让我们在后端仓库的package.json中添加一些npm脚本。

{
  "scripts": {
    //...
    "build:ui": "rm -rf build && cd ../part2-notes/ && npm run build && cp -r build ../notes-backend",
    "deploy": "git push heroku main",
    "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中的目录路径取决于文件系统中存储库的位置。

NB 在Windows上,npm脚本在cmd.exe中执行,作为默认的shell,不支持bash命令。为了让上述bash命令发挥作用,你可以将默认的shell改为Bash(在默认的Git for Windows安装中),方法如下。

npm config set script-shell "C:\\Program Files\\git\\bin\\bash.exe"

另一个选择是使用 shx

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的服务器。

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

我们的方法的一个消极方面是部署前端是多么的复杂。部署一个新的版本需要生成新的前端生产版本并将其复制到后端仓库。这使得创建一个自动化的部署管道更加困难。部署管道是指通过不同的测试和质量检查,将代码从开发者的电脑中转移到生产环境中的一种自动化和可控的方式。构建一个部署管道是本课程第11部分的主题。

有多种方法来实现这个目标(例如,将后端和前端的代码放在同一个仓库),但我们现在不会去讨论这些。

在某些情况下,将前端代码部署为自己的应用可能是明智的。对于用create-react-app创建的应用,这是直接的

后端的当前代码可以在Github的分支part3-3中找到。前端代码的变化在frontend repositorypart3-1分支中。