Back to blog
Jun 12, 2024
9 min read

开发和线上前后端如何进行通信

开发和线上前后端如何进行通信

开发环境中,前后端如何进行通信?

在现代开发过程中,前端工程设置代理(例如,vite中设置proxy)成为前后端联调过程中的标准解决方案,这是为什么?

为什么要设置代理?解决什么问题?

问题:当前端应用运行在 http://localhost:3000 ,而后端API服务运行在 http://localhost:4000 时,前端向后端发起的请求会被浏览器阻止,并抛出 CORS 错误。这是因为两个服务器运行在不同的端口,浏览器认为它们不属于同一个“源”。 CORS是浏览器的一种安全机制,目的是防止恶意网站从另一个域请求数据。默认情况下,浏览器会阻止从一个域发起的请求访问另一个域上的资源。这种策略被称为同源策略

在绝大多数场景下,无论是本地环境还是线上环境,前端应用和后端应用都不会存在与同一个源中。呐如何解决浏览器的 CORS 导致的错误呐? 现行的标准解决方案(绕过同源策略):通过在开发服务器上配置代理,前端请求会先发送到代理服务器,然后由代理服务器转发请求到后端API服务器。这使得浏览器认为请求是发往同一源,从而绕过了同源策略。

那么问题来了,为什么设置了代理服务器就能够绕过同源策略?描述一下代理服务器的工作原理

  1. 代理服务器运行在前端工程中:代理服务器通常配置为在前端开发服务器上运行,因此其域名和端口与前端页面地址一致。例如,如果前端开发服务器运行在 http://localhost:3000,代理服务器也运行在同一个地址。由于代理服务器的域名和端口与前端页面一致,浏览器不会触发CORS机制,前端请求可以顺畅地发送到代理服务器上。
  2. 代理服务器与后端服务器的连接:CORS是浏览器的安全机制,而代理服务器本身并不受CORS限制。因此,代理服务器可以自由地与任何后端服务器通信,而不会遇到跨域问题。代理服务器接收到前端的请求后,会将请求转发到真实的后端服务器。这个过程对浏览器是透明的,浏览器认为请求是发往同一源的,而实际请求在代理服务器层被转发到了后端服务器。
    1. 假设前端运行在 http://localhost:3000,后端API运行在http://localhost:4000
    2. 前端代码请求http://localhost:3000/api/data。由于请求的域名和端口与前端页面一致,浏览器不会触发CORS限制。
    3. 代理服务器接收到请求后,将其转发到后端服务器 http://localhost:4000/data
    4. 代理服务器可以修改请求头,例如Origin,使后端服务器认为请求来自同一源。
    5. 后端服务器处理请求并将响应返回给代理服务器。代理服务器将后端服务器的响应转发回前端。前端接收到响应,完成请求-响应周期。
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
};
  1. server 属性用于配置开发服务器的各种选项
  2. proxy 属性用于配置代理规则。它是一个对象,键是需要代理的路径,值是相应的代理配置。
  3. '/api' 路径,是一个字符串,用于匹配需要代理的请求路径。在这个例子中,所有以 /api 开头的请求都会被代理。
  4. target 属性:target 是目标服务器的地址,即需要代理到的后端服务器。在这个例子中, targethttp://localhost:4000,表示所有匹配 /api 路径的请求会被转发到 http://localhost:4000
  5. changeOrigin 是一个布尔值,用于控制代理服务器是否修改请求头中的 Origin 字段。如果设置为 true,代理服务器会将 Origin 修改为目标服务器的地址。在这个例子中,changeOrigin: true 表示代理服务器会将请求头中的 Origin 字段改为 http://localhost:4000,从而避免后端服务器因为跨域问题拒绝请求。
    1. Origin 主要作用是让后端服务器能正确判断请求的来源,决定是否允许该请求访问资源
    2. 通常来说, Origin 是浏览器自动添加的(在跨域和安全敏感场景下)
    3. 设置 changeOrigin 的核心目的,有些服务器会对请求来源有严格的检测,为了防止服务被拒绝所以将 Origin 设置为真实的服务器地址
  6. rewrite:将路径中的 /api 前缀去掉,确保请求路径正确映射到后端服务器的路径。
    1. 通过重写请求路径,使前端请求路径能够正确映射到后端服务器的实际路径上。
    2. 为什么要重写路径?真实案例说明一下
      1. 在多环境场景下,后端API请求路径中通常会包含版本前缀(例如 /v1、/v2)以便于版本管理。在开发环境中,前端代码不需要硬编码这些版本信息,通过代理服务器的 rewrite 功能,可以在转发请求时动态添加这些前缀。rewrite: (path) => path.replace(/^\/api/, '/v1'), // 添加 /v1 版本前缀
      2. 在开发阶段,前后端开发可能是并行进行的,路径可能会有所调整和变化。通过代理服务器,可以在不修改前端代码的情况下适配不同的后端路径结构。

为什么是代理的形式?而不是其他的方案成为事实的标准?

后端服务完全可以通过添加CORS配置来允许跨域请求,这样前端就不需要代理服务器。 但在真实的实践中,会带来下面几个问题

  1. 路径变更和维护成本:
    1. 当后端接口地址根据需求变动时,前端也需要同步更新。这个过程不仅需要在前端存储这些变量,还需要在每次后端变更时进行前端代码和配置的同步修改。
    2. 将路径和CORS的复杂度分散到前后端都需要维护,增加了沟通成本和开发复杂度。如果可以将复杂度聚合到某一端,这是降低复杂度和沟通的绝佳方案
  2. **后端安全风险:**即使后端通过环境区分 CORS 配置,也存在配置错误或漏配的风险,使得后端API的暴露。对于整个应用的安全来说,后端安全显著高于前端,减少一切非必要的配置和变动,是保证安全的前提
  3. 代理服务器的优势:
    1. 集中管理:通过代理服务器,所有的跨域配置和路径重写都可以集中管理,前后端只需要关注各自的逻辑实现即可,前后端数据交换只需要在代理配置中设置即可
    2. 灵活性:代理服务器可以灵活地处理路径重写、跨域请求等问题,而无需频繁修改前后端代码。例如,前端路径可以保持简洁,而代理服务器可以在转发请求时动态添加必要的前缀或修改路径。
    3. 开发和生产环境一致:代理服务器可以模拟生产环境的请求路径和跨域配置,使得开发和测试环境与生产环境一致,减少由于环境差异导致的问题。
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:4000/v1', // v1是版本前缀,前端无需再代码中硬编码版本前缀信息
        changeOrigin: true, // 将源信息改为目标服务器
        rewrite: (path) => path.replace(/^\/api/, ''), // 灵活的进行路径重写(去掉/api前缀)
      },
    },
  },
};

生产环境中,前后端如何进行通信?

在生产环境下

  1. 前端会在build之后,会生成一堆静态文件
    1. 如果此时直接用浏览器打开构建之后的目录(dist)中的index.html,看到的结果大概率是一个白屏页面,为什么?
  2. 后端会将代码直接放到服务器运行(省略…)

build之后的静态文件直接打开,页面白屏,这是为什么?

打开控制台会发现报错信息 CORS

Access to script at 'file:///Users/ntscshen/Documents/react-education/slash-admin/dist/assets/index-5f4d125b.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.

浏览器打开的index.html的真实路径是:file:///Users/ntscshen/Documents/react-education/slash-admin/dist/index.html 浏览器加载当前html,一次渲染执行,然后依次加载相对路径 .js, .css 文件,首先被命中的就是上面报错的js文件 file:///Users/ntscshen/Documents/react-education/slash-admin/dist/assets/index-5f4d125b.js

浏览器的同源策略file:// 协议下的资源视为不同的源。这就导致命中了 CORS

file:///Users/ntscshen/Documents/react-education/slash-admin/dist/index.html file:///Users/ntscshen/Documents/react-education/slash-admin/dist/assets/index-5f4d125b.js

使用http服务器打开时,就可以完美访问对应的资源,为什么?

当使用Nginx等HTTP服务器时,配置会明确指定静态文件的根目录root 指令),以及如何处理请求路径(try_files 指令)。 当通过 HTTP 服务器(Nginx) 来访问资源时,所有的资源请求都来自于同一个源(例如:http://localhost),因此不会触发 CORS 检查,因为协议(http)、主机(localhost)、端口(默认80)都是相同的

 server {
   listen 80;
   server_name localhost;

   # 配置静态文件路径
   location / {
     root /Users/ntscshen/Documents/react-education/slash-admin/dist; # 静态文件的根目录
     try_files $uri /index.html; # 兜底,确保返回index.html
   }
 }

server { ... }:定义一个虚拟服务器。一个Nginx实例可以配置多个虚拟服务器,每个虚拟服务器都用一个 server 块来定义。

listen 80;:指定Nginx监听的端口号。在这里,Nginx会监听80端口上的HTTP请求。客户端访问 http://localhost 时,Nginx会处理这些请求。

server_name localhost;

  1. 指定服务器名称。在这里,设置为 localhost,表示这个虚拟服务器将处理发往 localhost 的请求。
  2. 当客户端请求 http://localhost 时,Nginx会匹配到这个虚拟服务器来处理请求。

location / { ... } :定义一个位置块,用于匹配客户端请求的URI。/:表示匹配所有请求路径。

root

  1. 指定文档根目录。设置为 /Users/ntscshen/Documents/react-education/slash-admin/dist,表示Nginx将从该目录中查找请求的资源。
  2. 如果客户端请求 http://localhost/assets/index-5f4d125b.js, Nginx将尝试从 /Users/ntscshen/Documents/react-education/slash-admin/dist/assets/index-5f4d125b.js 返回文件内容。

try_files $uri /index.html;

  1. 尝试按照顺序查找文件。如果请求的文件存在,则返回该文件;如果不存在,则返回 index.html 文件。
  2. $uri:表示请求的URI。例如,客户端请求 http://localhost/about 时,$uri 等于 /about
  3. /index.html:如果 $uri 对应的文件不存在,则返回 index.html 文件。在单页应用中,使用客户端路由,所有路由都应该返回 index.html,由前端去处理路由
 server {
   listen 80;
   server_name localhost;

   # 配置静态文件路径
   location / {
     root /Users/ntscshen/Documents/react-education/slash-admin/dist; # 静态文件的根目录
     try_files $uri /index.html; # 兜底,确保返回index.html
   }

   # 配置代理请求到后端服务
   location /api/ {
     proxy_pass http://localhost:3000;
     proxy_set_header Host $host;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto $scheme;
   }
 }

proxy_pass 是代理请求的核心配置,而其他 proxy_set_header 指令在大多数场景下都是通用的,目的是为了传递客户端的真实信息给后端服务器

location /api/ {...}:和Vite中的 proxy: { '/api': { ... } } 代理配置在逻辑上非常相似。当请求的URI以 /api/ 开头时,Nginx将如何处理这些请求。它定义了一个反向代理,将这些请求转发到指定的后端服务器。

proxy_pass http://localhost:3000;

  1. 指定将匹配 /api/ 路径的请求转发到 http://localhost:3000。这意味着所有以 /api/ 开头的请求都会被Nginx代理到运行在本地3000端口的后端服务中。
  2. 如果请求 http://localhost/api/userNginx将其转发到 http://localhost:3000/api/user

proxy_set_header Host $host;

  • 将请求头中的 Host 字段设置为客户端原始请求的 Host 值。
  • 确保后端服务器收到的 Host 头与客户端请求的 Host 头一致,这在需要根据 Host 头进行处理的应用中非常重要。

proxy_set_header X-Real-IP $remote_addr;

  • 将请求头中的 X-Real-IP 字段设置为客户端的IP地址。
  • 让后端服务器知道真实的客户端IP地址,而不是Nginx代理服务器的IP地址。

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  • 将请求头中的 X-Forwarded-For 字段设置为包含客户端IP地址的链条。
  • X-Forwarded-For 头通常包含所有经过的代理服务器的IP地址,帮助后端服务器识别最终客户端的真实IP地址。
  • $proxy_add_x_forwarded_for 是一个Nginx变量,它会将客户端的IP地址附加到现有的 X-Forwarded-For 头中。

proxy_set_header X-Forwarded-Proto $scheme;

  • 将请求头中的 X-Forwarded-Proto 字段设置为客户端请求的协议(HTTP或HTTPS)。
  • 后端服务器可以通过 X-Forwarded-Proto 头了解客户端使用的协议,这在处理重定向、生成URL等情况下非常有用。

CentOS中安装和使用Nginx

安装、找到配置文件的位置、修改配置文件、测试配置文件的语法、修改语法在测试、重启Nginx服务、在浏览器中测试当前服务器是否正确命中对应服务

安装

sudo yum -y install nginx   # 安装 nginx
sudo yum remove nginx  # 卸载 nginx

测试Nginx配置文件的语法 & 显示配置文件的位置

sudo nginx -t

输出的结果

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

使用ps命令找到Nginx进程。

ps aux | grep nginx

配置文件位置一般在,要以超级用户权限编辑文件

sudo vi /etc/nginx/nginx.conf

查看错误日志

cd /var/log/nginx
cat error.log

重启Nginx

sudo systemctl restart nginx

启动Nginx

sudo systemctl start nginx

停止Nginx

sudo systemctl stop nginx

重载Nginx配置

sudo systemctl reload nginx

重载配置也可以使用 sudo nginx -s reload,这需要依赖于在当前系统中存在一个运行中的 Nginx 主进程

我的几次错误,

第一次,就是root后面的分号忘记写了 第二次是信息复制不全导致注释错误,都是通过查看错误日志然后一一修改的,修改完成之后通过 sudo nginx -t 测试语法是否通过,通过之后再重载配置,或者重启Nginx。 第三次是没有权限(Permission denied)

[root@VM-4-15-centos]/var/log/nginx# cat error.log
2024/06/13 18:03:46 [emerg] 20414#20414: invalid number of arguments in "root" directive in /etc/nginx/nginx.conf:63
2024/06/13 18:07:01 [emerg] 21558#21558: invalid number of arguments in "root" directive in /etc/nginx/nginx.conf:63
2024/06/13 18:13:43 [emerg] 23623#23623: invalid number of arguments in "root" directive in /etc/nginx/nginx.conf:63
2024/06/13 18:13:44 [emerg] 23627#23627: invalid number of arguments in "root" directive in /etc/nginx/nginx.conf:63
2024/06/13 18:32:54 [emerg] 30527#30527: unknown directive "nformation" in /etc/nginx/nginx.conf:5
2024/06/13 18:32:56 [emerg] 30555#30555: unknown directive "nformation" in /etc/nginx/nginx.conf:5
2024/06/13 18:32:57 [emerg] 30558#30558: unknown directive "nformation" in /etc/nginx/nginx.conf:5
2024/06/13 18:33:13 [emerg] 30641#30641: unknown directive "nformation" in /etc/nginx/nginx.conf:5


2024/06/14 10:54:14 [crit] 10990#10990: *1 stat() "/home/lighthouse/slash-admin/dist/" failed (13: Permission denied), client: 218.109.201.245, server: 124.223.78.185, request: "GET / HTTP/1.1", host: "124.223.78.185:8088"
2024/06/14 10:54:14 [crit] 10990#10990: *1 stat() "/home/lighthouse/slash-admin/dist/index.html" failed (13: Permission denied), client: 218.109.201.245, server: 124.223.78.185, request: "GET / HTTP/1.1", host: "124.223.78.185:8088"
2024/06/14 10:54:14 [crit] 10990#10990: *1 stat() "/home/lighthouse/slash-admin/dist/index.html" failed (13: Permission denied), client: 218.109.201.245, server: 124.223.78.185, request: "GET / HTTP/1.1", host: "124.223.78.185:8088"
2024/06/14 10:54:14 [crit] 10990#10990: *1 stat() "/home/lighthouse/slash-admin/dist/index.html" failed (13: Permission denied), client: 218.109.201.245, server: 124.223.78.185, request: "GET / HTTP/1.1", host: "124.223.78.185:8088"
2024/06/14 10:54:14 [crit] 10990#10990: *1 stat() "/home/lighthouse/slash-admin/dist/index.html" failed (13: Permission denied), client: 218.109.201.245, server: 124.223.78.185, request: "GET / HTTP/1.1", host: "124.223.78.185:8088"

权限问题修改方案

  1. 首先要查找 Nginx 用户,通过 sudo ps aux | grep nginx,并检查 Nginx 的配置文件 - 查看 user 指令和当前用户是否匹配
  2. 检查文件权限: sudo ls -ld /home/lighthouse/slash-admin/dist - 确认 Nginx 用户对相关文件和目录有正确的权限

sudo ps aux | grep nginx

出现权限问题时,第一步查找 Nginx 用户是因为 Nginx 进程的用户决定了 Nginx 对文件和目录的访问权限。

  1. 确认实际运行 Nginx 进程的用户,确保与你期望的一致。
  2. 确认配置文件中指定的用户与你实际查找到的一致,避免配置错误。

通常来说都是 nginx,但是要首先进行排除

root     10989  0.0  0.0  39312   976 ?        Ss   10:50   0:00 nginx: master process /usr/sbin/nginx
nginx    10990  0.0  0.1  41816  2472 ?        S    10:50   0:00 nginx: worker process
nginx    10991  0.0  0.0  41816  1976 ?        S    10:50   0:00 nginx: worker process

第一个root进程,这是Nginx的主进程(master process),由root用户启动。它负责管理和监控所有的Nginx工作进程(worker processes)。 第二个和第三个nginx进程,这些是Nginx的工作进程(worker processes),由主进程启动。工作进程负责处理客户端的请求和连接。每个工作进程运行在nginx用户下,确保系统安全性。

sudo ps aux | grep nginx 的意思是查找所有包含nginx字样的进程信息。这通常用于检查Nginx服务的状态,确保它正确运行。

  • sudo ps aux 是一个命令,用于显示系统上所有正在运行的进程。这里:

    • sudo:以超级用户权限执行命令,确保可以查看所有进程。
    • ps:显示当前系统的进程状态。
    • aux:是ps命令的选项,显示所有用户的进程信息,包含进程的详细信息。
  • | grep nginx 是一个管道命令一个过滤工具,用于筛选包含nginx字样的进程。具体来说:

    • |:管道符号,将前一个命令的输出作为下一个命令的输入。
    • grep nginx:在输入的数据中查找包含nginx的行。

所以,sudo ps aux | grep nginx 的意思是查找所有包含nginx字样的进程信息。这通常用于检查Nginx服务的状态,确保它正确运行。

这里的 Nginx 工作进程,通常是由 Nginx 配置文件中的 worker_processes 参数指定的 worker_processes 4;,这些工作进程负责处理所有的请求。 Nginx 配置中的 server 块,用于定义虚拟主机。 工作进程和 server 虚拟主机之间的关系

  1. 工作进程处理所有的 server 块中的请求,这些请求被均匀地分配到工作进程中进行处理。
  2. 工作进程数量和 server 块数量之间没有直接的关联。无论配置多少个 server 块,Nginx 只会启动指定数量的工作进程来处理所有请求。

检查文件权限

sudo ls -ld /home/lighthouse/slash-admin/dist
  • sudo:以超级用户权限运行命令。这通常用于确保你有足够的权限查看特定目录或文件的详细信息。
  • ls:列出目录内容的命令。
  • -l:使用长格式显示文件和目录的信息,包括权限、所有者、大小和修改时间等。
  • -d:表示显示目录本身的信息,而不是目录下的内容。
  • /home/lighthouse/slash-admin/dist:目标目录的路径。

输出结果如下

drwxr-x--- 3 lighthouse lighthouse 4096 6  13 09:29 /home/lighthouse/slash-admin/dist
  • d:表示这是一个目录(directory)。

  • rwxr-x---:这是权限字段,分为三部分:

    • rwx:表示所有者(owner)的权限,r表示读(read),w表示写(write),x表示执行(execute)。在这个例子中,所有者有读、写和执行权限。
    • r-x:表示组(group)的权限,r表示读(read),-表示没有写权限,x表示执行权限。组用户有读和执行权限,但没有写权限。
    • ---:表示其他用户(others)的权限,---表示没有任何权限。
  • 3:表示硬链接的数量(通常指向这个目录的链接数量)。

  • lighthouse:这是目录的所有者。

  • lighthouse:这是目录的所属组。

  • 4096:表示目录的大小(以字节为单位)。目录的大小通常是4096字节,这是文件系统分配的一个块的大小。

  • 6月 13 09:29:这是目录最后一次修改的时间。

  • /home/lighthouse/slash-admin/dist:这是目录的路径。

  • 权限解释:rwxr-x---

    • 所有者(lighthouse):有读、写、执行权限。
    • 所属组(lighthouse):有读、执行权限,没有写权限。
    • 其他用户:没有任何权限。

由于 Nginx 进程通常运行在 nginx 用户下,而 nginx 用户不属于 lighthouse 组,也不是文件的所有者,因此根据当前权限设置,nginx 用户没有权限访问 /home/lighthouse/slash-admin/dist 目录。

解决办法

  1. 修改目录所有者为 Nginx 用户:sudo chown -R nginx:nginx /home/lighthouse/slash-admin/dist,将 dist 目录及其所有子目录和文件的所有者和组更改为 nginx。这样可以确保 Nginx 进程对该目录及其内容拥有完全控制权。
  2. 设置目录权限:为其他用户添加读取和执行权限:sudo chmod -R 755 /home/lighthouse/slash-admin/dist/,将 dist 目录及其所有子目录和文件的权限设置为 755。这样可以确保 nginx 用户(作为所有者)有读、写、执行权限,所属组和其他用户有读和执行权限。
    • chmod:改变文件或目录的权限(change mode)。
    • -R:递归地应用更改,即包括目录中的所有子目录和文件。
    • 755:设置权限为 755,具体解释如下:
      • 7(所有者权限):读、写、执行(rwx)。 - 所有者(nginx 用户)可以读、写、执行。
      • 5(所属组权限):读、执行(r-x)。 - 所属组用户可以读和执行,但不能写。
      • 5(其他用户权限):读、执行(r-x)。 - 其他用户可以读和执行,但不能写。
    • /home/lighthouse/slash-admin/dist/:目标目录的路径。
  3. 重载Nginx配置:sudo systemctl reload nginx
  4. 重新验证当前文件是否被正确的设置了权限: ls -ld /home/lighthouse/slash-admin/dist/
  5. 这中间权限可能还有存在问题,这可能是当前目录的上一层目录的权限问题导致的,例如: /home/lighthouse 没有权限导致的,你可以将 /home/lighthouse 目录的权限修改为 755,这样其他用户可以读取和执行(进入)该目录
  6. 通常来说这些步骤完成之后,使用服务器的公网IP地址访问 Nginx server 中特定的端口,即可访问当前的 build 好的前端项目

在生产环境中,使用反向代理服务(如: Nginx) 进行请求转发,使前后端看起来在同一个域名下运行,这已经成为一种事实上的标准

Nginx解决的核心问题

  1. 反向代理和缓存:通过反向代理功能,Nginx 能够缓存后端服务器的响应,减少后端服务器的负载,提高响应速度,同时隐藏后端服务器的实际地址,提升安全性。
  2. 静态资源服务:Nginx 高效地处理静态资源请求,减少了后端服务器的负载,并通过缓存和压缩优化了资源传输。

反向代理是什么?有正向代理吗?

不管反向还是正向,后面都跟着代理,它们都可以实现请求转发。它们的主要区别是使用场景作用对象 正向代理:服务于客户端,解决客户端访问外部资源的问题 方向代理:服务于服务端,解决负载均衡、安全和缓存问题

知道这些概念也没什么卵用,一眨眼就忘记了。 在 Vite 中设置 proxy 的场景属于 反向代理

Nginx的使用

Nginx反向代理服务器,其主要作用是接收客户端的请求,并将其转发到后端服务器

部署后端 Nest.js 应用

前端build之后是一个静态文件,因此需要在服务器上使用 Nginx 启动一个服务去服务于这些静态资源 前端在开发环境中,有Vite之类的脚手架,会自动刷新渲染之类的。 后端在开发环境中,有cli去监听变化自动重启,那么在线上也需要类似的东西防止一些特殊情况导致服务器停止之后,无法重启。 - 使用 pm2

第一步:Nest.js 服务本地 build 之后,通过 FileZilla 传递到服务器对应目录下之后,使用 node 执行里面的 main.js 文件

  1. 如果你在线上直接使用 node 命令去执行 main.js 文件,得到的结果大概率是 Error: Cannot find module '@nestjs/core' 这是因为,无论本地开发环境还是线上环境,都需要正确的安装依赖项
  2. 因此,需要将本地开发环境的 package.json 文件存储到和当前 dist 目录的同级目录中,并运行。 pnpm install
  3. 如果是一个新的服务器,安装好文件后,大概率直接运行时OK的 node dist/main.js,但在我的文件中有端口被专用的错误提示 Error: listen EADDRINUSE: address already in use :::3000 +5ms ,将 main.js 的监听端口改掉之后即可

第二步:设置 pm2 监听

  1. pm2 start npm --name nest-slash-admin-serve -- run start:prod
pm2 start pnpm --name nest-slash-admin-serve -- run start:prod
  • pm2 start pnpm:使用 PM2 启动 pnpm。
  • —name nest-slash-admin-serve:将这个进程命名为 nest-slash-admin-serve,便于管理。
  • — run start:prod:这是传递给 pnpm 的参数,run start:prod 是 pnpm 脚本的一部分,用于启动应用程序。

使用 PM2 配置文件是一种更推荐的方式的方式,可以更容易管理和扩展

启动性能优化

虽然整个应用跑起来了,但是加载速度太慢 http://124.223.78.185:8088/assets/index-5f4d125b.js (4.3MB)当前静态文件下载时间超过8秒

  1. 启动 gzip 压缩,以显著减少传输数据量
    1. 开启压缩之后,大小为(1.4MB),下载时间为 2.43 秒,提升了 3 - 4 倍下载速度
  2. 启动浏览器缓存

配置 gzip

gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
gzip_proxied any;
gzip_vary on;
gzip_disable "msie6";
gzip_comp_level 6;
  1. gzip on;:启用 gzip 压缩。当客户端支持 gzip 时,Nginx 将对响应内容进行 gzip 压缩。

  2. gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    • 指定要进行 gzip 压缩的 MIME 类型。只有指定的类型会被压缩,其他类型将按原样传输。
    • 示例
      • text/plain:普通文本文件
      • text/css:CSS 样式表
      • application/json:JSON 数据
      • application/javascript:JavaScript 文件
      • text/xml:XML 文件
      • application/xml:XML 数据
      • application/xml+rss:RSS 源
      • text/javascript:JavaScript 文件(旧的 MIME 类型)
  3. gzip_min_length 1000;:设置启用 gzip 压缩的最小响应体大小(以字节为单位)。仅当响应体大小大于 1000 字节时才进行压缩。过小的文件可能因为压缩而导致体积反而增加,所以设置一个合理的最小值可以避免这种情况。

  4. gzip_proxied any;:启用对经过代理的请求的 gzip 压缩。指定在什么情况下对经过代理的请求进行 gzip 压缩。any 表示对所有的代理请求都进行 gzip 压缩。

    • 其他选项
      • off:不对代理请求进行压缩
      • expired:仅对 Expires 头过期的请求进行压缩
      • no-cache:仅对 Cache-Control 头包含 no-cache 的请求进行压缩
      • no-store:仅对 Cache-Control 头包含 no-store 的请求进行压缩
  5. gzip_vary on;:在响应头中添加 Vary: Accept-Encoding。通知客户端和中间代理缓存不同的压缩版本。这有助于正确地处理支持和不支持 gzip 压缩的客户端。

  6. gzip_disable "msie6";:禁用对某些用户代理的 gzip 压缩。不对特定的客户端(如 Internet Explorer 6)进行 gzip 压缩,因为它们可能处理不当。"msie6" 表示禁用对 MSIE 6(Internet Explorer 6)的 gzip 压缩。

  7. gzip_comp_level 6;:设置 gzip 压缩级别。压缩级别范围为 1 到 9,其中 1 为最快但压缩比最低,9 为最慢但压缩比最高。6 是一个常用的折中值,提供较好的压缩比和合理的压缩速度。

配置缓存

location / {
    root /home/lighthouse/frontend/dist;
    try_files $uri $uri/ /index.html;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000";
}
  1. location /

    • 这个块处理所有根路径(即 /)的请求。
  2. root /home/lighthouse/frontend/dist

    • 设置根目录为 /home/lighthouse/frontend/dist,即所有请求的文件将从这个目录查找。
  3. try_files $uri $uri/ /index.html

    • 这个指令尝试按以下顺序查找文件:
      1. $uri:请求的 URI(如 /path/to/file)。
      2. $uri/:请求的 URI 加上斜杠(如 /path/to/file/)。
      3. /index.html:如果前两者都找不到,则返回 index.html
    • 这对于单页应用程序(SPA)很常见,确保所有路由请求都返回 index.html,并由前端路由处理。
  4. expires 30d

    • 设置浏览器缓存的过期时间为 30 天。
    • 当客户端接收到包含 Expires 头的响应后,会在 30 天内缓存该资源。
  5. add_header Cache-Control "public, max-age=2592000"

    • 添加 Cache-Control 头部,设置 max-age 为 2592000 秒(即 30 天)。
    • public:表示响应可以被任何缓存存储,包括浏览器和 CDN。
    • max-age=2592000:表示资源在 2592000 秒内(约 30 天)有效,客户端可以直接从缓存中获取该资源。

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000";
}
  1. location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$

    • 这个块处理所有匹配正则表达式 \.(js|css|png|jpg|jpeg|gif|ico|svg)$ 的请求,即所有以 .js.css.png.jpg.jpeg.gif.ico.svg 结尾的文件。
    • ~* 表示不区分大小写匹配。
  2. expires 30d

    • 同上,设置浏览器缓存的过期时间为 30 天。
  3. add_header Cache-Control "public, max-age=2592000"

    • 同上,添加 Cache-Control 头部,设置 max-age 为 2592000 秒(即 30 天)。

下载速度偏慢,这是为什么?

当前前端项目中最大的文件是 1.4MB ,在腾讯云服务器(服务器1)上 2核/2GB/4Mbps带宽 上下载时间为2.51秒,而在更大带宽的服务器上只需要200多毫秒 我测试了两台不同服务器,一台在美国,一台在中国(服务器1),美国的进入队列时间明显高出中国的服务器,这是因为跨国网络访问通常会有更高的延迟导致的问题 文件大小、字节、每秒能够传输的数据量:1MB大概每秒传输125KB,4MB大概是500KB,因此1.4MB在理论带宽为4Mbps的场景下,下载完成大约需要2.8秒,是符合实际预期的

通常来说,想加快下载速度就1个标准解决方案,接入CDN。

上述方案是加钱方案,在不增加成本的情况看下,加快用户访问的办法只有一个,添加缓存(除了第一次访问需要稍等片刻,其他时间基本秒开)

现代开发中最常见的缓存优化方案:前端工程中自动生成带有MD5的文件名,并设置长期的缓冲期限(例如:1年),这样浏览器就可以高效的利用缓存,减少不必要的网络请求,提高性能,当代码中有任何变化,构建出来的静态文件名也会再次变更,这是一个全新的文件,继续重复前面的流程

添加如下Nginx配置即可,我的本地是遇到在Chrome中缓存始终不生效问题,这大概率是插件影响的,你可是使用隐私模式或者其他浏览器测试

server {
    listen 8088;
    server_name 124.111.22.333;

    # 配置静态文件路径
    root /home/lighthouse/slash-admin/dist;

    # 处理静态资源请求,设置缓存头部
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|otf)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # 处理API请求的代理
    location /api/ {
        proxy_pass http://localhost:3100;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 处理其他所有请求,返回index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}

pm2 相关设置

安装

pnpm install -g pm2

启动

pm2 start app.js --name my-app

列出所有应用

pm2 list

查看应用日志

pm2 logs my-app # 查看实时日志
pm2 logs my-app --err # 查看特定应用的错误日志

停止应用

pm2 stop my-app
pm2 stop all # 停止所有应用

重启所有应用

pm2 restart my-app

从 PM2 进程列表中删除特定应用:

pm2 delete my-app
pm2 delete all # 删除所有应用

查看特定应用状态

pm2 status my-app

保存进程列表:保存当前进程列表,以便在系统重启后自动恢复

pm2 save

设置 PM2 开机启动:确保 PM2 在系统重启后自动启动,并恢复保存的进程列表

pm2 startup