java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > springboot vue项目上线部署

springboot+vue项目从第一行代码到上线部署全流程

作者:天天扭码

本文详细介绍了如何从零开始搭建一个基于Spring Boot和Vue.js的前后端分离项目,并涵盖项目需求分析、技术选型、项目结构设计、前后端交互、部署上线等全过程,感兴趣的朋友跟随小编一起看看吧

一、引言

相信很多前端工程师或后端工程师都有一个全栈梦,或者想了解一下自己写的代码是如何融入到整个项目的,或者想知道自己的“另一半”为什么会写出那么臭的代码……

后端工程师会想:“前端小子怎么回事,我“费尽心思”传的那么几十个数据,他怎么就展示了一两个?”

于此同时前端工程师看着“费尽心思”的数据暗想,“byd,怎么每个接口的数据传输的格式都不一样,还有你登录接口为什么要求用户输入自己的ID主键?”

于是两者气愤的去找了全栈的流程,想找出对方代码“又臭又长”的原因,机缘巧合下他们共同点开了这一篇博客……

本文将带领大家走完一个springboot+vue前后端分离项目的全流程,以尽可能简短的语言,帮助大家理解项目的前后端交互和上线部署

注:本人为大二在校生,代码风格参考黑马程序员和尚硅谷,有所不足,敬请斧正

二、项目效果展示

项目连接☞:120.27.247.221:80 (电脑打开哈)

部分项目效果展示——

登录页面

主页

个人信息页面

发表文章页面

注:这是一个简单的小网站,也是用其部分代码来带大家走完全流程的上线网站,里面小bug不少(其实是懒得改,不是)

三、项目准备

注:这里不包括插件和依赖噢,用到的话后面会说明的

后端部分——

IDEA专业版、jdk17、Mysql-8.0.31、Maven-3.6.1

前端部分——

VSCode、node.js

部署部分——

云服务器ECS(或者随便什么服务器了)

四、部分项目结构的分析 后端部分

1)项目的总体结构

项目分为三个模块,分别是common、pojo、server三个模块

三个模块有各自的作用和存放的项目文件,使项目更加清晰有条理

2)common模块

common模块负责管理在整个项目都可以使用到的方法等,如utils就可能放置一些jwt工具,oss工具之类的,exception中放置处理异常的方法,json中放置json转换器等

3)pojo模块

pojo模块负责管理项目中全局可以用到的实体类等,比如Admin、User等实体类

4)server模块

server模块负责管理一些具体的业务逻辑,比如登录功能,发表文章功能等,这里需要特殊说明的是“controller、service、dao”三层架构,是后端项目常用的项目结构,controller层负责规定方法请求的url,service层负责处理方法的具体逻辑,比如信息校验之类的,dao层负责依据方法的需求处理数据库,这三层架构层层递进,是springboot项目的核心

前端部分

注:这个项目的前端结构挺草率,毕竟本人主要从事后端,但是基本的结构还是能分清的,小项目够用

这个虽然比较乱,但是基本的结构还是可以介绍一下的

1.public

放置的是项目用到的一部分静态资源(特别是那种可以复用的资源,比如其他大佬写的炫酷页面)

2.api

放置的是调用后端接口的方法,比如下面这一坨代码

import request from '@/utils/request'
import type { userLogin } from '@/types/userLogin'
// 枚举管理注册模块接口地址
enum API {
  // 注册接口地址
  REGISTER = '/user/register',
  GET_CODE = '/auth',
  VERIFY_CODE = '/auth',
  LOGIN = '/user/login',
  FIND_PASSWORD = '/user/changePasswordByEmail',
}
//注册函数
export const goRegister = (user: userLogin) => {
  // 直接传递 user 对象
  return request.post(API.REGISTER, user)
}
export const getCode = (email: string) => {
  return request
    .post(`${API.GET_CODE}/send-code?email=${encodeURIComponent(email)}`) // 将 email 作为查询参数
    .then((response) => response.data)
    .catch((error) => {
      console.error('获取验证码失败:', error)
      throw error // 抛出错误以便调用者处理
    })
}
export const verifyCode = (email: string, code: string) => {
  return request
    .post(
      `${API.VERIFY_CODE}/verify-code?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}`
    ) // 将 email 作为查询参数
    .then((response) => response.data)
    .catch((error) => {
      console.error('校验验证码失败:', error)
      throw error // 抛出错误以便调用者处理
    })
}

其中比如/user/register都是后端提供的接口的路径,前端在这里调用后端的接口,goRegister()便是前端通过封装后端接口得出的一个方法

3.components

其中一般封装全局组件,比如你随便打开一个页面,你滑动页面但是一直保持不动的那一部分,或者是你一个需要重复使用的组件,比如后面要介绍的富文本编辑器

4.pages

其中是一个个的页面,比如你现在退出了阅读我这篇文章回到了主页,主页和读我这篇文章的页面就是两个不同的“page”

5.router

里面管理的是每个页面的路由,你现在可以看一下你浏览器的导航栏,是不是有类似http://xxx/xxx/xxx这里的/xxx就是一个路由的路径它会带你去到某一个“page”

6.style

里面是放置的一些全局可以用到的样式,比如这个小项目就是在里面放了清楚全局默认样式的样式

7.types

里面放置一些实体类,用来接收后端传来的数据,或者往后端去传数据

8.utils

顾名思义,里面是一些工具,比如封装一个我们自己的axios,或者一些解析token的工具

累死我了,先写这么多吧

牛马是累不死的,继续写

五、前后端的跨域配置

这一部分来解决连后端跨域配置的问题,相信很多小伙伴有一个问题,前端是如何找到后端接口的,为什么前端用的“/user”是自己电脑上的后端项目规定的“/user”,如果小编电脑上正好也定义了一个“/user”呢,为什么不会调用小编电脑上的“/user”接口。其实如果在前后端项目上如果不做配置,就是你后端写了一个“/user”,前端想用也是找不到“/user”的,那么怎么可以让彼此找到呢?我们只需要做一些小小的配置即可

后端配置

   @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 允许访问的路径
                .allowedOrigins("http://localhost:5173") // 允许的源
                .allowedHeaders("*") // 允许的请求头
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
                .allowCredentials(true); // 是否允许发送凭证
    }

这是有关跨域的一部分配置,允许了"http://localhost:5173"的连接,而"http://localhost:5173"正是本地前端的运行端口

前端

  //配置代理跨域
  server: {
    proxy: {
      '/api': {
        target: "http://localhost:8080",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }

这里是前端的配置,它规定了前端去哪里去找后端接口,去"http://localhost:8080"找,这正是后端的端口

简而言之,后端允许前端去连接,前端也愿意去连接后端,这就成了

六、部分功能的前后端交互

1.登录功能 

后端部分

jwt工具

package com.qac.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
    private final static String signKey = "Admin";//管理员登录signKey
    private final static Long expire = 7200000L;//过期时间为12个小时
    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }
    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

这个工具其实没必要去理解它的每一部分的底层是怎么生成的,大家只需要明白,如果登录成功它会生成一个令牌,之后我们每次调用后端的方法都会先验证是否携带的这个令牌,如果没有就不让调用,这个令牌会在浏览器本地存储(前端去做),另外提一点,这个令牌的校验是通过拦截器完成的,拦截器如下

package com.qac.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.qac.result.Result;
import com.qac.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
        //1.获取请求url
        String url=req.getRequestURL().toString();
        LoginCheckInterceptor.log.info("请求的url--{}",url);
        //获取请求头中的token
        String jwt=req.getHeader("token");
        if(!StringUtils.hasLength(jwt)){
            LoginCheckInterceptor.log.info("token为空,未登录...");
            Result error=Result.error("NOT_LOGIN");
            //这里不是json格式,要转化
            String notLogin= JSONObject.toJSONString(error);
            res.getWriter().write(notLogin);
            return false;
        }
        //解析token,解析失败就登录失败
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            LoginCheckInterceptor.log.info("登录失败");
            Result error=Result.error("NOT_LOGIN");
            //这里不是json格式,要转化
            String notLogin= JSONObject.toJSONString(error);
            res.getWriter().write(notLogin);
            return false;
        }
        LoginCheckInterceptor.log.info("token合法,放行");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle....");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterHandle....");
    }
}

还是那句话,不理解底层没关系,知道它的作用就可以了

到这里还有一个关于拦截器的小问题,那就是它会不会把登录注册的接口给拦截了,如果没有做任何配置的话,答案是会的,会出现这种情况“你想登录的话就得先登录,不能登录的话就不能登录”,我们只需要做一些配置就可以了——

    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/avatar")
                .excludePathPatterns("/user/register")
                .excludePathPatterns("/user/changePasswordByEmail")
                .excludePathPatterns("/auth/**");
    }

这里的意思是.excludePathPatterns("/user/login")这类的接口不会被拦截器拦截,就算你没有登录也可以进去

controller层

    @ApiOperation("用户登录")
    @PostMapping("/login")
    private Result login(@RequestBody UserLogin userLogin){
        log.info("员工登录:{}",userLogin);
        User user=service.login(userLogin);
        //登录成功
        if (user!=null){
            HashMap<String, Object> claims = new HashMap<>();
            claims.put("id",user.getId());
            claims.put("email",user.getEmail());
            String jwt= JwtUtils.generateJwt(claims);//jwt中包含了登录信息
            return Result.success(jwt);
        }
        return Result.error("用户名或密码错误");
    }

正如前文所诉,这里规定了用户登录接口的url,其中userLogin便是前端传来的数据,里面封装了前端传来的邮箱和密码,后端封装的userLogin类如下

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLogin implements Serializable {
    public String email;
    public String password;
}

service层

    @Override
    public User login(UserLogin userLogin) {
        return dao.getByUserEmailAndPassword(userLogin);
    }

这里也是直接选择调用了dao层的方法去数据库查询数据了

dao层

    <select id="getByUserEmailAndPassword" resultType="com.qac.entity.User">
        select *from user where email =#{email} AND password = #{password};
    </select>

查一下数据库中有没有这个用户

在以上的后端处理完之后,我们来梳理一下传给前端的是什么数据,我们会发现,后端最终传给前端的都是类似于Result.success(jwt)的数据,这里解释一下Result是封装的一个类,其中的.success()也是其中的一个静态方法,如下

package com.qac.result;
import lombok.Data;
import java.io.Serializable;
/**
 * 后端统一返回结果
 * @param <T>
 */
@Data
public class Result<T> implements Serializable {
    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据
    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }
    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }
    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }
}

有了这个类以后后端传给前端的数据就是“规规矩矩”的了,就不会出现本文引言中前端工程师的抱怨了,OK,前端工程师看到这里就可以撤了(开玩笑的,千万别撤)。

当前端调用了后端的这个接口之后,它会得到什么具体的数据呢?可以在controller层看到,传的有效数据就是那个生成的jwt,其中包涵了用户的email,id等内容,还有就是告诉前端是登录成功了还是失败了,OK,后端传来了这么完美的数据,让我们看看前端工程师如何处理吧

前端部分

这次的前端工程师倒是很开心,因为后端不需要用户给他传用户的主键ID了

嗯......不开玩笑,前端写一个登录页面就写了五百多行代码,这里就只挑重要的代码展示了

1.后端登录接口的封装

import request from '@/utils/request'
import type { userLogin } from '@/types/userLogin'
// 枚举管理注册模块接口地址
enum API {
  LOGIN = '/user/login',
}
export const goLogin = (email: string, password: string) => {
  return request.post(API.LOGIN, { email, password })
}

这里有人可能会问,request是个啥,其实它是封装过的一个axios对象,这个一两句说不清楚,自己先看一下代码——

import router from '@/router'
import axios from 'axios'
import {globals} from "@/main"
const serverUrl=globals.$config?.serverUrl || 'http://120.27.247.221:8090'
const request = axios.create({
  baseURL: serverUrl,
  timeout: 200000
})
//请求拦截器
request.interceptors.request.use((config: any) => {
 // 可以通过请求头携带公共参数
  config.headers['content-type'] = 'application/json'
  const token = JSON.parse(localStorage.getItem('user') || '{}')
  config.headers['token'] = token
  return config
})
//相应拦截器
request.interceptors.response.use(
  (response: any) => {
    if (response.data.msg === 'NOT_LOGIN') {
      router.push('/home')
    }
    return response.data
  },
  (error: { message: string | undefined }) => {
    //处理http网络错误
    return Promise.reject(new Error(error.message))
  }
)
//对外暴露axios
export default request

这一部分的信息量也不小,我原理不说,给大家说一下这一段代码是什么作用

const serverUrl=globals.$config?.serverUrl || 'http://120.27.247.221:8090'

这个是获取后端接口的地址,现在说太早,等项目部署部分再说哈

const request = axios.create({
  baseURL: serverUrl,
  timeout: 200000
})

后端的同学可以认为这是创建了一个axios类的对象,然后又给他重写了一下方法,之后我们用request就是用axios,而且是我们自己的axios

//请求拦截器
request.interceptors.request.use((config: any) => {
 // 可以通过请求头携带公共参数
  config.headers['content-type'] = 'application/json'
  const token = JSON.parse(localStorage.getItem('user') || '{}')
  config.headers['token'] = token
  return config
})
//相应拦截器
request.interceptors.response.use(
  (response: any) => {
    if (response.data.msg === 'NOT_LOGIN') {
      router.push('/home')
    }
    return response.data
  },
  (error: { message: string | undefined }) => {
    //处理http网络错误
    return Promise.reject(new Error(error.message))
  }
)

这里可以再调用方法时判断浏览器本地有没有有效的token,如果没有就router.push('/home'),就是让他返回登录页面

2.登录功能的部分html代码和方法的调用

 <form class="form1" @submit.prevent="handleLogin" v-show="login&&!register">
              <p class="heading">登录</p>
              <el-input
                v-model="user.email"
                style="width: 250px; height: 45px"
                placeholder="邮箱"
              />
              <div v-show="!validateEmail(user.email)" class="warning">请输入有效的邮箱格式!</div>
              <el-input
                v-model="user.password"
                style="width: 250px; height: 45px"
                type="password"
                placeholder="密码"
                show-password
              />
              <div v-show="!validatePassword(user.password)" class="warning">
                密码至少8个字符,包含数字和特殊字符!
              </div>
              <div class="align">
                <span class="function" @click="toFind">找回密码</span>
                <span class="function" @click="toRegister">注册账号</span>
              </div>
              <button class="btn" type="submit" >登录</button>
        </form>

这里重点看一下登录的按钮(最后一行代码哈),这意味着点击之后会执行提交表单的方法,也就是方法handleLogin()(第一行代码哈)

下面是handleLogin()方法

const handleLogin = async () => {
  if (validateEmail(user.email) && validatePassword(user.password)) {
    try {
      const response = await goLogin(user.email, user.password)
      console.log('Login successful:', response)
      if (response.data == null) {
        alert('邮箱或密码错误')
      } else {
        localStorage.setItem('user', JSON.stringify(response.data))
        alert('登录成功')
        router.push('mainArticle')
      }
    } catch (error) {
      console.error('Login failed:', error)
    }
  } else {
    alert('邮箱或密码格式错误')
  }
}

可以看到,调用的封装后端登录接口的方法----goLogin,并且将用户输入的email、password传到了后端,此外,把response.data保存到了本地,也就是把后端传来的jwt保存到了本地,登录成功之后就router.push('mainArticle'),即跳转到主页

至此前后端在登录功能上的本地联调就此结束

现在只是写了登录接口前后端逻辑,以后可能会把,个人信息管理或者是富文本编辑器的功能再写出来,也可能不会写,或许永远不会写,或许明天就写

七、上线部署

上线部署应该是最简单的一部分,但同时也是最神秘的一部分或者说是最难找资料的一部分,这里我把完整的流程给大家

准备工作

1.准备一个云服务器,我选择的是阿里云ECS云服务器

这里给大家留一个小门槛,去搞一台ECS云服务器(实际上是本人不想写这个了)

2.先进入你的这个页面哈,点击“安全组”

3.然后点击管理规则

4.把下面的端口全都放行了,点那个快速添加的按钮可以放行端口噢

5.接下来就可以远程连接你的云服务器了

首次连接应该会让你设置密码啥的,反正很简单,这里就跳过了哈

6.进入云服务器,在云服务器上下载一个宝塔面板,去浏览器上就可以下载

网址是 宝塔面板下载,免费全能的服务器运维软件 (bt.cn)

7.因为我的服务器是windows所以下载Windows版的

注:当时也是windows版的教程很少,我也废了很大功夫

8.打开之后应该是这样的,可以自己设置账号密码

9.设置好之后在自己的电脑浏览器打开面板地址(是在自己的浏览器不是云服务器中噢)

注:我这里密码忘了,不让我登了,大家一定要记好自己的密码呀

OK,解决了,随手告诉大家怎么找回密码吧,下面便是教程,亲测有用如何修改Windows面板密码教程 - Windows面板 - 宝塔面板论坛 (bt.cn)

登入之后是这个界面

10.点击软件商店去下载一些需要的东西,这里大家抄作业就行了

注:如果大家碰到了下载慢的问题,重新启动面板就可以解决了

11.之后我们来配置一下数据库,添加一个数据库

12.点击管理,会跳转到新的网址(如果报404等错误有两个原因,1.安全组没有放行,2.没有按照我的软件去安装)

13.导入你的sql文件

14.秉着教人教到会的原则,我教一下大家如何去导出本地数据库到sql文件

右键表格-->导入/导出-->将数据导出到文件

15.按照我这个选择去导出哈,不然容易导出奇奇怪怪的数据

16.jdk的配置

宝塔上只能下载jdk1.8或以下,但是我们的版本是jdk17,所以我们要在本地上传jdk17

点击打开即可上传

后端部署

1.我们部署时部署的是项目的jar包,我们可以通过Maven项目的package功能得到jar包,点击package进行打包

2.打包完成后我们拿出jar包,可能会生成多个jar包,我们只拿有配置文件的模块的那个jar包,这里是Server模块

3.把项目的配置文件也拿出来,这个项目是application-dev.yml

4.将java项目的jdk环境配置一下(之前上传的jdk17)

5.自己把jar包和配置文件上传到云服务器哈,和上传jdk的方法一样

6.改一下项目的配置文件,将数据库的配置该成云服务器上的数据库,划红圈的那一部分哈

7.部署JAVA项目

点击添加Java项目,按照如下配置即可,注意项目的jar路径要是你的jar包的路径,而且配置文件要和jar包的位置同级,端口要是没有被占用的端口

至此后端就部署完成了!

前端部署

1.首先要改一下你的前端项目跨域配置

这里的目标url要是http://+你的云服务器公网ip+你的后端项目端口

2.之后将前端项目打包

在终端打开输入指令

npm run build

如果你的项目又语法错误就先改正,不然是打包不了的

3.将打包好的文件上传到云服务器

就是画圈的文件“dist”

4.部署PHP项目

点击添加站点,域名即是你的云服务器公网ip,根目录是你上传“dist”的目录

这里注意一下dist目录下不能再有一个dist目录,如果会报错的话,最好检查一下自己的目录结构是否正确

至此线上部署完成,完结撒花喽!

八、结语

本文介绍了springboot+vue项目的从代码编写到上线的全流程,制作不易,如果帮助到大家的话可以高抬贵手给一个赞,感谢

以后有机会的话,我会给大家将一些其他内容,比如大数据项目全流程,或者AI编程之类的,后会有期了!

到此这篇关于springboot+vue项目从第一行代码到上线部署全流程的文章就介绍到这了,更多相关springboot vue项目上线部署内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文