前端SPA+后端Node分离项目的线上部署——Nginx

1,140 阅读5分钟

1.打包前端应用

通过构建工具webpack或者vite,使用最原始的npm run build来把自己的前端应用打包成为静态资源文件夹(会在根目录下生成一个dist文件夹),不能直接打开html文件,不然会不能提供正常的服务,需要服务器开启一个端口作为容器,这里不是说只能用nginx,因为后面还需要反向代理请求接口所以都用nginx比较方便,还可以用其他的比如anywhere开启8080端口作为这个静态文件等,(有时候会出现本地的时候css样式可用,但到了静态资源上css就会混乱,有可能是因为两个css权重相同,但是打包成静态文件后引入的顺序与开发环境的时候不同就会出错,这时增加权重即可)

2.安装nginx

为了做演示,在自己windows电脑上先安装nginx作为演示,后面真正上线是在腾讯云服务器linux系统上安装运行nginx(利用宝塔面板还是很简单的)

下载完成这个解压文件后,在nginx根地址通过命令行输入start nginx开启服务,访问localhost(因为默认端口是80所以localhost==localhost:80)会显示如下页面就代表安装成功(默认会有配置文件以及写好了80端口的配置,所以我们能访问到welcome的页面),接下来就需要自己进行配置了

image.png

3.了解配置文件

解压好下载的nginx文件夹后,打开conf文件夹里面的nginx.conf,里面有个http模块(http{}包裹着),里面有个server模块,一个server模块可以绑定一个服务器的某个端口,但要注意如果有多个server或者location等并且有不同的匹配规则,要注意不同规则的优先级,匹配不会按照写入的顺序来,而是各自优先级高的先被匹配(只是做个提醒,具体可以看其他文章)

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ .php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ .php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

这是下载好后默认的配置,里面listen代表监听的端口,server_name代表域名,支持正则通配符等进行匹配,root代表根地址(支持绝对路径,默认相对路径),index代表主页(当前匹配到的地址都会访问nginx的根地址/html中的index.html或者index.htm文件,有前后顺序,直到匹配到文件就停止), location表示当前服务器下匹配的路径,然后在里面配置匹配到的地址应该做出什么动作 此处location/表示匹配localhost:80/下的所有地址然后都访问到 nginx的根地址/html/index.html所以我们访问localhost就能看到他的欢迎页面

4.自己配置访问静态资源

根据配置规则,我们将打包好的前端dist目录放到nginx根目录下,通过root填写相对路径即可访问,也可以通过root设置为绝对路径访问就无需将打包好的dist目录放到nginx根目录下即可访问
接下来是单页面应用路由的问题

5.SPA单页面应用404的问题

在解决问题之前先来了解路由的两种模式

前端路由分为两种模式

  • Hash模式

hash原理:hash通过监听浏览器的onhashchange()事件变化,查找对应的路由规则
表现形式:通过拼接#到服务器跟地址下分发不同的路由,如localhost:80/#/a/b/c
优点:

  • 路由并不会重新产生一次新的请求,比如从localhost:80/#/a/b/clocalhost:80/#/a/b/c/d自始至终都是请求了一次localhost:80,#后的路由变化与实际请求无关
  • 不用在服务端进行额外配置

缺点:

  • 不利于SEO(例如百度的搜索引擎),虽然本来单页面就不利于SEO
  • 难看,不简洁
  • html本身的锚点跳转会无效
  • History模式

history原理: history中的 pushState() 和 replaceState() 和一个事件onpopstate监听URL变化
表现形式:与hash不同,不需要添加#,与传统的地址一样,如localhost:80/a/b/c
优点:hash的缺点都是history的优点
缺点:需要服务端额外配置


单页面应用实际上是一个页面通过js跳转路由,前后端交互更新页面数据,浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制,所以会导致一个问题:url输入地址实际上向后端发起了一次get请求然后此地址就是访问文件在服务器下的目录,找到则返回该文件,找不到返回404,所以此时因为后端当前路由实际并没有相应的静态资源,所以会返回404,此时页面就会为空

解决方案:

nginxlocation配置中追加try_files $uri $uri/ /index.html `

image.png

这里的$url代表当前的请求地址 /index.html当前root地址下的index.html文件,表示请求时如果返回404(未在后端找到文件)则返回root地址下的index.html文件,此时就不会一片空白了,因为返回了当前的单页面html,history模式路由每次变化都会请求一次地址,所以找不到都返回当前的html文件,hash模式虽然不会每次切换都发起新的请求,但是第一次访问还是会请求的

5.配置接口的反向代理

配置好了静态文件的访问,使得localhost:80/下的所有路由都会访问到html文件,但是我们请求的接口如果不加代理就会访问的是当前根地址,例如当前是localhost:80,我们的路由history形式为localhost:80/xxx表示页面的路由,但是我们向后端发请求交互数据的时候例如是/getlist/1此时默认为向localhost:80/getlist/1发送了请求,此时会造成什么情况?当然是被第一个location /规则匹配,返回的结果是一个html文件,这当然不对,我们都知道,在开发环境解决这个问题是通过例如在vue.config.js配置serverproxy设置代理服务器,通过一个固定的前缀比如/api把他重定向到后端端口,例如配置了规则后/api/getlist/1就变成了localhost:后端端口/getlist/1请求json数据,解决了跨域的问题, nginx的原理也是一样,把/api下后面的路径把他反向代理到后端端口

解决方式

image.png 通过proxy_pass反向代理到后端

注意事项

1.斜杠问题,很重要

  • 如果你开发环境下跨域写多了,根据习惯你会自然而然写成
 location /api {
          proxy_pass   http://localhost:3000;
       }

你输入localhost:80/api/xxx却变成localhosr:3000/api/xxx而不是localhost:3000/xxx

  • 于是你在proxy_pass后加上了/,你以为这样就好了
location /api {
         proxy_pass   http://localhost:3000/;
      }

你输入localhost:80/api/xxx却变成localhosr:3000//xxx而不是localhost:3000/xxx

  • 最终,只有这样你才实现了最终的成功
location /api/ {
         proxy_pass   http://localhost:3000/;
      }

2.前后端分离项目端口不能在同一个端口

有的人会觉得如果前后端服务都在一个端口下应该就没有跨域问题了,这是对的,比如通过node提供后端静态资源的访问,把静态资源放在node后端提供静态资源服务确实会在同一个端口下没问题,但是我们这里使用nginx提供静态资源服务,如果前后端都在一个端口,例如

server{ 
    listen 3000; #前端提供静态资源访问的端口
    server_name localhost;
location / {
          root   D:\gitee\koa2-weibo\front\dist;#自己静态资源dist的根目录
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
      }
}

会造成我们访问localhost:80时是向后端发起了一次请求然后找不到文件返回404,此时后端的优先级大于静态资源

3.proxy_pass和root不能同时存在

现在我们分离了前后端各自的端口为80和3000但 也许你们没有分离请求用的接口路由和页面切换用的路由,于是你们会写成

server{ 
    listen 80;#前端提供静态资源访问的端口
    server_name localhost;
location / {
          root   D:\gitee\koa2-weibo\front\dist;#自己静态资源dist的根目录
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          proxy_pass   http://localhost:3000/; #后端提供接口服务的端口
      }
}

我们理想状态是访问localhost:80时可以访问到静态资源html文件,然后后端端口也可以正常为前端端口提供接口服务,但是实际上这样的配置会造成只有proxy_pass生效,然后我们访问localhost:80或者是切换了页面路由后都是向后端发起了一次请求,root配置的静态资源并不会返回,所以我们要做到通过前缀分离请求用的接口路由和前端页面切换用的路由

解决方式

server{ 
    listen 80; #前端提供静态资源访问的端口
    #!!!这里要注意,上线之后一定要换成自己的域名或者ip地址
    server_name localhost;
location / {
# 分离前端页面切换的路由
          root   D:\gitee\koa2-weibo\front\dist;#自己静态资源dist的根目录
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
      }
      
location /api/ {
# 分离请求后端的路由 通过/api反向代理到后端
#!!!这里反向代理上线之后也不用换成自己的域名,因为实际上请求打在你的服务器的接口你这里只是做一个转发,以你的的服务器的视角来说是localhost
         proxy_pass   http://localhost:3000/;
      }
}

4. nginx更新配置后重启nginx服务页面还是没有变化

我们更新完配置代码后会通过nginx -s reload重启nginx服务以保证配置生效,但是有时候会发现浏览器重新刷新还是原来配置文件的样子,以下是可能的情况

  • 重启nginx之前,你已经了多个nginx进程,但你只关闭了一个进程,所以之前存在的进程会优先于你当前重启的进程导致配置失效 解决方式:打开任务管理器,关闭所有nginx进程再次开启服务
  • 由于浏览器缓存的原因,有时候即便你nginx进程全部关闭还是能访问指定端口,而且配置没有更新 解决方式:打开浏览器设置,根据默认项清理缓存即可,然后重新强制刷新页面

image.png

配置SSL证书提供https服务

  1. 首先到腾讯云获取免费ssl证书
  2. 进入ssl控制台后点击获取免费证书按照流程一步步确认并绑定自己域名
  3. 下载证书,选择腾讯云宝塔面板即可 image.png
  4. 解压压缩包拿到里面的.key和.pem后缀的文件用宝塔上传到你自己服务器的目录下
  5. 添加nginx的ssl配置
server {
#http默认80端口,设置rewrite自动携带https前缀转发到443端口(https默认端口)
       listen 80;
       server_name  www.xxx.xyz;
       rewrite ^(.*) https://$server_name$1 permanent; 
       
}
server {
    #SSL使用443端口
    listen 443 ssl;
    
    #SSL证书绑定的单域名
    server_name www.xxx.xyz;
    
    #证书pem文件在服务器的具体位置
    ssl_certificate /www/wwwroot/xxx.xyz.pem;
    
    #证书key文件在服务器的具体位置
    ssl_certificate_key /www/wwwroot/xxx.xyz.key;
    
    #缓存SSL握手产生的参数和加密密钥的时长
    ssl_session_timeout 5m;
    
    #使用的加密套件的类型
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; 
    
    #表示使用的TLS协议的类型
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    
    #加密套件优先选择服务器的加密套件
    ssl_prefer_server_ciphers on; 
    
     
    location / {
    #原来的配置,上文怎么写的这里就怎么写
    }
}