Bootstrap

前后端分离项目开发部署实战(SpringBoot+Vue)

前言

本篇博文主要是想串联一下前后端开发以及服务器部署的基本流程和要点,在开发内容和用到的技术上不涉及很深的东西,但麻雀虽小五脏俱全,完全便于对某方面不是很熟悉的同学快速入门。

  • 适用人群:前端开发、后端开发、服务器运维
  • 开发语言:Java、JavaScript
  • 开发框架:StringBoot、Vue2.0
  • 服务器部署:Linux、Docker、Nginx

一、后端项目开发部署

我们快速开发部署一个最简单的say-hello项目,提供一个后端API接口。
该接口的功能非常简单,就是根据传过来的参数msg,返回hello, {msg}内容。

1. 后端开发

  1. Java代码

SayHelloMain.java

@SpringBootApplication
public class SayHelloMain
{
    public static void main( String[] args )
    {
        SpringApplication.run(SayHelloMain.class, args);
    }
}

SayHelloController.java

@RestController
public class SayHelloController {
    @GetMapping("/say-hello")
    public Object sayHello(String msg) {
        Map<String, Object> result = new LinkedHashMap<>(3, 0.95F);
        result.put("code", "0000");
        result.put("data", "hello, " + msg);
        return result;
    }
}
  1. 配置文件

application.yml

server:
  port: 80
  servlet:
    context-path: /test

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>ace.gjh</groupId>
  <artifactId>api-say-hello</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>api-say-hello</name>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>

    <!-- SpringBoot web -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

  1. 接口调用示例
GET http://localhost/test/say-hello?msg=world

响应:

{
    "code": "0000",
    "data": "hello, world"
}

2. 后端部署

后端项目编译打包后生成文件api-say-hello-1.0-SNAPSHOT.jar

2.1 制作docker镜像

Dockerfile脚本:

FROM lapierre/java-alpine:8
MAINTAINER ACE_GJH
ENV WORK_DIR /opt/deploy/say-hello
RUN mkdir -p $WORK_DIR
WORKDIR $WORK_DIR
ENV JAR_NAME api-say-hello-1.0-SNAPSHOT.jar
COPY ./$JAR_NAME $WORK_DIR
EXPOSE 80
ENV JAVA_OPT="-Xmx32m -Xms32m -Xmn16g"
CMD java $JAVA_OPT -jar $JAR_NAME

运行docker镜像构建命令:

docker build -t say-hello:v1.0 .
2.2 启动docker容器
docker run --name say-hello -d say-hello:v1.0

注意,容器启动时并没有映射服务器任何端口,所以现在还不能通过服务器外部端口访问,只能在容器之间访问。

二、nginx部署

nginx是本次部署的重点。后端服务已经部署完成了,nginx承担着后端API的反向代理和前端项目的发布。

1. nginx配置

  1. 获取后端服务的ip地址

这里的"say-hello"指的是刚才部署的后端服务容器名。这一步可以查询后端容器在docker网络上的ip地址,使得nginx容器可以通过docker网络访问后端服务。

docker inspect say-hello | grep '"IPAddress"'
  1. 编写nginx配置文件

配置文件中只建立一个虚拟服务就可以,location的/api匹配规则可以判断是访问后端服务,nginx会截取/api后面的uri重定向到真实的后端服务。

nginx.conf

# 用户组
user nginx;
# 工作进程 默认即可,一般可设为CPU核数
worker_processes auto;
# 错误日志存放目录
error_log /var/log/nginx/error.log;
# pid文件存放位置
pid /run/nginx.pid;
# 加载模块配置文件
include /usr/share/nginx/modules/*.conf;
# 单个工作进程最大的并发连接数(IO多路复用)
events {
    worker_connections 1024;
}

# http协议
http {
    # 文件扩展名与类型映射表
    include   mime.types;
    # 默认文件类型:字节流
    default_type  application/octet-stream;
    # 启用gzip压缩功能
    gzip on;
    gzip_min_length 1k;
    gzip_buffers    4 16k;
    gzip_http_version 1.0;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
    gzip_vary on;

    # 建立虚拟服务
    server {
        listen 80;
        server_name  localhost;
        
        # 匹配后端API的url,进行反向代理
        location /api {
            rewrite ^/api/(.*)$ /$1 break; #获取/api后面的uri进行重定向
            proxy_pass http://172.17.0.2:80/; # 实际后端服务地址
        }
    }

}

2. 启动nginx容器

docker run --name nginx -d -p 18080:80 -v $(pwd)/deploy/log:/var/log/nginx -v $(pwd)/deploy/nginx.conf:/etc/nginx/nginx.conf nginx:1.21.4

访问效果:

GET http://192.168.56.101:18080/api/test/say-hello?msg=gjh
{
    "code": "0000",
    "data": "hello, gjh"
}

三、前端项目开发部署

博主算是第一次开发前端项目,所以前端开发部分就记录的详细一些,希望前端同学可以多多指正。

1. 前端开发

1.1 拉取vue基本框架

按此教程的npm方法安装:菜鸟教程:Vue.js 安装

npm -v
npm install -g cnpm --registry=https://registry.npmmirror.com
# 全局安装 vue-cli
$ cnpm install --global vue-cli
# 初始化新项目
$ vue init webpack show-hello

 Project name show-hello
? Project description a test project
? Author ACE_GJH
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner karma
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recommended) no
cd show-hello
cnpm install
cnpm run dev

浏览器访问http://localhost:8080/可以看到vue经典logo
vue-logo

1.2 配置vue代理

因为是前后端分离项目,需要配置后端服务代理以防止出现跨域问题。
我使用的是vue2.0版本,配置代理的方法如下:
config/index.js文件中找到proxyTable属性,修改内容如下

    proxyTable: {
      '/api': {
        target: 'http://192.168.56.101:18080',
        changeOrigin: true,
        pathRewrite: {
          // 注意,这样是将/api/test/say-hello?msg=China
          // 替换为http://192.168.56.101:18080/api/test/say-hello?msg=China
          '^/api': '/api'
        }
      }
    }

这一步的作用是为了解析axios请求中/api开头的uri,要访问哪个主机地址,并且将原uri中的/api替换为/api作为新uri访问真实主机。

1.3 配置axios
  1. 安装axios
cnpm install axios
  1. 引入axios

main.js中添加如下两行代码,将axios声明为全局变量

import axios from 'axios'
Vue.prototype.$axios = axios
1.4 开发组件
  1. 项目目录
    src
  2. 主组件

App.vue

<template>
  <div id="app">
    <div id="btn">
      <router-link to="/"><button>首页</button></router-link>
      <router-link to="/luffy"><button>路飞</button></router-link>
      <router-link to="/zoro"><button>索隆</button></router-link>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
#btn {
  text-align: center;
  margin-bottom: 50px;
}
#btn button {
  width: 80px;
  margin-left: 10px;
  margin-right: 10px;
}
</style>
  1. 首页组件

Home.vue

<template>
  <div class="hello">
    <img src="../assets/onepiece.jpeg">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'Home',
  data: function () {
    return {
      msg: '欢迎来到大航海时代!'
    }
  }
}
</script>
  1. 其他组件

HelloLuffy

<template>
  <div class="hello">
    <img src="../assets/Luffy.jpeg">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloLuffy',
  data: function () {
    return {
      msg: null
    }
  },
  created () {
    this.getMsg()
  },
  methods: {
    getMsg () {
      return this.$axios.get('/api/test/say-hello?msg=Luffy')
        .then(response => {
          console.log('response', response)
          this.msg = response.data.data
        })
    }
  }
}

</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
div img {
  width: 200px;
  height:200px;
}
</style>

HelloZoro.vue

<template>
  <div class="hello">
    <img src="../assets/Zoro.jpeg">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloZoro',
  data: function () {
    return {
      msg: null
    }
  },
  created () {
    this.getMsg()
  },
  methods: {
    getMsg () {
      return this.$axios.get('/api/test/say-hello?msg=Zoro')
        .then(response => {
          console.log('response', response)
          this.msg = response.data.data
        })
    }
  }
}

</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
div img {
  width: 200px;
  height:200px;
}
</style>

1.5 配置路由

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import HelloLuffy from '@/components/HelloLuffy'
import HelloZoro from '@/components/HelloZoro'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/luffy',
      name: 'luffy',
      component: HelloLuffy
    },
    {
      path: '/zoro',
      name: 'zoro',
      component: HelloZoro
    }
  ]
})

2. 前端部署

2.1 打包项目
cnpm run build
2.2 修改服务器上nginx配置

这一步添加容器内/opt/nginx/frontend目录作为前端项目的根目录

    # 建立虚拟服务
    server {
        listen 80;
        server_name  localhost;
        
        # 前端项目访问
        location / {
            # 定义前端项目根目录位置;
            root /opt/nginx/frontend; 
            index index.html;
            error_page 404 /index.html;
        }

        # 匹配后端API的url,进行反向代理
        location /api {
            rewrite ^/api/(.*)$ /$1 break; #获取/api后面的uri进行重定向
            proxy_pass http://172.17.0.2:80/; # 实际后端服务地址
        }
    }
2.3 修改nginx容器启动命令

这一步添加了一个参数,将容器中前端根目录/opt/nginx/frontend挂载到服务器上的./deploy/frontend目录。启动时注意先将之前启动的容器停止、删除掉。

docker run --name nginx -d -p 18080:80 -v $(pwd)/deploy/frontend:/opt/nginx/frontend -v $(pwd)/deploy/log:/var/log/nginx -v $(pwd)/deploy/nginx.conf:/etc/nginx/nginx.conf nginx:1.21.4
2.4 部署前端项目

将打包后dist文件夹中的所有文件,拷贝到映射容器前端项目根目录的目录中。

frontend/
├── index.html
└── static
    ├── css
    ├── img
    └── js

3. 页面效果

  1. 首页
    首页
  2. 切换其他页面
    点击“路飞”按钮,跳转到路飞页面。
    luffy
    点击“索隆”按钮,跳转到索隆页面。
    zoro

四、升级:如何部署多个前端项目?

1. 问题分析

前面我们看到,在前端项目的根目录下,只有一个index.htmlstatic,如果按照这样的部署方式,当我们通过IP+端口号的方式访问时,就只能访问一个前端项目。如果我们需要部署多个前端项目,还需要增加端口和nginx配置。我们能不能通过在uri前面加前缀的方式来访问不同的项目?
答案是肯定的。我们可以在请求url和服务器项目文件路径这两个地方同步“做手脚”,保证前端请求的url路径和实际文件路径一致即可。例如项目one,原来的静态资源url是/static/css/xxx这种,现在服务器上多了一层文件目录one,则现在的静态资源url只要变成/one/static/css/xxx就可以了。

frontend/
└── one
    ├── index.html
    └── static
        ├── css
        ├── img
        └── js

2. 问题解决

解决方法只需要在之前的基础上,修改3个地方就可以了。
假设项目一的uri访问前缀为/one

2.1 前端项目修改资源路径

config/index.js中的build属性里,将assetsPublicPath属性由原来的/改为/one/,注意one后面有“/”。
解释一下:静态资源的请求路径前缀由assetsPublicPath+assetsSubDirectory拼接而成,默认是/static,现在则变成了/one/static

2.2 根目录新建子目录

需要在前端项目根目录下建立one文件夹。之前frontend相当于打包后的dist文件夹,现在frontend相当于dist的父级目录。
之前的目录:

frontend/
├── index.html
└── static
    ├── css
    ├── img
    └── js

现在的目录:

frontend/
└── one
    ├── index.html
    └── static
        ├── css
        ├── img
        └── js
2.3 访问url

之前的访问url:http://192.168.56.101:18080
现在的访问url:http://192.168.56.101:18080/one/,没错最后有个/,这是由nginx的location匹配和转换规则决定的,不然会出问题。
new-index
跳转页面也没问题:
new-luffy

3. 补充:路由模式是否有影响?

前面我们的前端项目中路由都是用的默认hash模式,如果换成history模式,会有影响吗?我们一起试一下。

3.1 修改路由模式

router/index.js中指定路由模式。
在这里插入图片描述

3.2 部署测试

将history模式的项目作为项目二打包部署到服务器上,进行访问。
果然,除了三个按钮显示出来了,其他都没有显示。
two-index

3.3 破解之道

我将鼠标指针放在“路飞”按钮上,我看到浏览器左下角出现了它的跳转地址:
地址
history模式和hash模式相比,去掉了/#/,但这个跳转地址前面少/two,所以解决这个问题应该也很简单。
在路由上添加base请求前缀:
base
部署后再次访问:
history-index
点击“路飞”按钮跳转页面:
history-luffy
点击“索隆”按钮跳转页面:
history-zoro
非常完美!

小结

通过本次开发部署,串联了前端Vue和后端SpringBoot框架的使用、nginx配置,以及docker容器化部署。并且研究了如何通过前端项目和nginx的配合,实现一个端口部署多个前端项目。同时也看到了hash和history路由模式的区别所在。
本次的开发部署过程也是收获满满的,正所谓“日拱一卒无有尽 ,功不唐捐终入海”。

;