Vue和Springboot初步前后端分离建立项目连接(解决前后端跨域问题)

前后端分离

前端端口号配置5000

后端端口号配置8080

1.vue前端初始化

在主页本专栏上篇《Vue复习》的初始化Vue基础上,加入向后端发送get请求的代码,此处以Customer.Vue为例。

java 复制代码
<template>
  <h1>客户管理界面</h1>
</template>


<style>

</style>


<script setup>
import {onMounted} from  "vue"  //引入钩子
import axios  from "axios";
import api from "@/util/api.js";
//当组件挂载完毕之后触发
onMounted(async ()=>{
  // await阻塞等待 必须用在async函数中
  let resp = await axios( {  //await 确保了axios()后的 console.log(resp) 只有在 API 请求完成并获得响应后才会执行,这是处理异步数据获取的标准模式。
     url:"http://localhost:8080/api/v1/members",
  
    method:"get",
    params:{
      page:1,
      limit:10,
      name: "晓"
    }
  });
  console.log(resp);
});

</script>

2.初始化springboot整合ssm的后端程序

2.1创建项目引入依赖

pom 复制代码
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        mybatis-plus-->
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.12</version>
        </dependency>
        <!--        mybatis-plus的自动分页插件-->
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>3.5.12</version>
        </dependency>
    </dependencies>

2.2写项目初始化配置文件

yml 复制代码
spring:
  application:
    name: crm2026

#配置数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/gcbs2025?characterEncoding=UTF-8&useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456
# 配置mybatis
mybatis-plus:
  configuration:
    log-prefix: mybatis.
    log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl
    map-underscore-to-camel-case: true
    lazy-loading-enabled: true
    cache-enabled: false
  type-aliases-package: com.ysy.crm2026.model
  mapper-locations: classpath*:/mapper/**/*.xml
server:
  port: 8080
logging:
  level:
    mybatis: debug

2.3完成查询会员信息的基础Mvc框架搭建

项目结构:

2.3.1model层

@TableName("t_member")和 @TableField(condition = SqlCondition.LIKE)等都是mabatisplus的注解

java 复制代码
/*
* 会员
* */
@TableName("t_member") //指明该类对应哪个数据库的表,方便查询
@Getter
@Setter
public class Member extends AuditEntity {
    @TableId(type = IdType.AUTO)//指定主键类型
    private  Integer id;
    @TableField(condition = SqlCondition.LIKE)//表示用like拼接,不写默认用=拼接
//    @TableField("mobile")  phone对应数据库表里的mobile字段
    private String  phone;
    private String password;
    @TableField(condition = SqlCondition.LIKE)
    private String name;
    private String pinyin;
    private String sex;

    private LocalDate birthday;
    @TableField(condition = SqlCondition.LIKE)
    private String  email;
    @TableField(condition = SqlCondition.LIKE)
    private String  wechat;
    private String  description;
}
java 复制代码
import java.time.LocalDate;
@Getter
@Setter
public class MemberSearchBean extends Member {
    //以查询出生日期范围举例,假设有这个需求
    private LocalDate birthdayFrom;
    private LocalDate birthdayTo;
}
java 复制代码
/*
* 审计字段
* */

//因为可能每个表都有审计字段,所以把这个字段的类单独定义出来,又因为他不属于业务,不能单独创建对象,所以定义为抽象类
public abstract class AuditEntity {
    private String createdBy;
    private LocalDate createdTime;
    private  String updatedBy;
    private LocalDate updatedTime;
}
2.3.2DAO(Mapper)层
java 复制代码
@Mapper
//BaseMapper也是Mybaitsplus的,有一些便捷的增删改查操作接口方便
public interface MemberMapper extends BaseMapper<Member> {
    //  Page类是Mybatisplus自带的,可以用于分页


    default Page<Member> findAll(Page<Member> page, MemberSearchBean msb) {
        LambdaQueryWrapper<Member> qw = Wrappers.lambdaQuery(msb);//LambdaQueryWrapper  一个查询器
        if(msb.getBirthdayFrom()!=null){
            qw.ge(Member::getBirthday,msb.getBirthdayFrom());//qw封装一个大于等于msb.getBirthdayFrom()的Member里的birthday字段
        }
        if(msb.getBirthdayTo()!=null){
            qw.lt(Member::getBirthday,msb.getBirthdayTo());//qw封装一个小于等于msb.getBirthdayTo()的Member里的birthday字段
        }
        return selectPage(page,qw);//selectPage也是BaseMapper里的,返回分页查询的内容
    }

}
2.3.3Service层
java 复制代码
/**
 *  查询全部会员
 * @param page 分页对象
 * @param msb  查询条件
 * @return  结果

* */

public interface MemberService {
    Page<Member> findAll(Page<Member> page, MemberSearchBean msb);
}
java 复制代码
@Service
public class MemberServiceImpl implements MemberService {
    private MemberMapper memberMapper;

    @Autowired
    public void setMemberMapper(MemberMapper memberMapper) {
        this.memberMapper = memberMapper;
    }

    @Override
    public Page<Member> findAll(Page<Member> page, MemberSearchBean msb) {
        return memberMapper.findAll(page,msb);
    }
}
2.3.4Api层

此处为了封装响应对象,定义了一个名为JsonResult的封装类:

java 复制代码
@Getter
@Setter
@AllArgsConstructor
public class JsonResult<T> {
    private int code;
    private boolean success;
    private String msg;
    private T data;

    public static JsonResult<?> success() {
        return success(null);
    }

    public static <T> JsonResult<T> success(T data) {
        return new JsonResult<>(200, true, "操作成功", data);
    }

    public static JsonResult<?> fail(int code, String msg) {
        return new JsonResult<>(code, false, msg, null);
    }

    public static JsonResult<?> fail(String msg) {
        return fail(500, msg);
    }
}

MemberApi类:

java 复制代码
@RestController
@RequestMapping(value = "/api/v1/members", produces = MediaType.APPLICATION_JSON_VALUE)
public class MemberApi {
    private MemberService memberService;

    @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping
    public ResponseEntity<JsonResult<?>> findAll(@RequestParam(defaultValue = "1") Integer pageNo,//默认值为1
                                                 @RequestParam(defaultValue = "20") Integer pageSize,
                                                 MemberSearchBean msb) {
        //分页对象
        Page<Member> page = new Page<>(pageNo,pageSize);
        //查询全部会员
        page = this.memberService.findAll(page,msb);
    return  ResponseEntity.ok(JsonResult.success(page));
    }

}

2.4引入mybatisplus的自动分页拦截器

java 复制代码
@Configuration
public class CommonConfig {
    //mybatis-plus自动分页拦截器
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

目前进行到这,一个接收"http://localhost:8080/api/v1/members"的get请求的后端框架以及一个向后端发送get请求的前端框架已经搭建完毕

但此时,启动启动器,前端得不到后端返回的数据,这是因为,浏览器默认的遵守同源协议,也就是一个域只能访问他自己域里的内容,不能跨域访问,从前端5000端口号向后端8080端口号发送请求接受请求,造成了跨域,因此数据不能正常传输,会报错。

2.5解决跨域问题

一开始,前端的Customer.vue中有这段请求代码:

java 复制代码
 let resp = await axios( {  //await 确保了axios()后的 console.log(resp) 只有在 API 请求完成并获得响应后才会执行,这是处理异步数据获取的标准模式。
     url:"http://localhost:8080/api/v1/members",
  
    method:"get",
    params:{
      page:1,
      limit:10,
      name: "晓"
    }
  });

注意这的 url:"http://localhost:8080/api/v1/members",指的是前端向这个地址发送请求,但我们在vue的配置文件里定义的是5000端口,遵循上面提到的同源策略,在这发送这个url请求是发不过去的,因此:

2.5.1首先在vite.config.js里加入以下配置
java 复制代码
  //代理
    proxy:{
      "/api":{ //只要前端地址有/api,就给代理到target的请求
        target:"http://localhost:8080",
        changeOrigin:true,
        rewrite:url=>url.replace(/^\/api/,"/api/v1")//匹配以/api开头的字符串.替换到/api/v1

      }
    }

这段代码指的是,给上述vue前端添加一个代理,但前端请求的url路径中,发现有"/api",就将这个"/api"匹配到下面定义target:"http://localhost:8080",后面。并且,将"/api",替换成"/api/v1"

此时Customer.vue中有这段请求代码的url进行修改:

java 复制代码
 let resp = await axios( {  //await 确保了axios()后的 console.log(resp) 只有在 API 请求完成并获得响应后才会执行,这是处理异步数据获取的标准模式。
     url:"api/members",
  
    method:"get",
    params:{
      page:1,
      limit:10,
      name: "晓"
    }
  });

url:"api/members",其实这个全称是:http://localhost:5000/api/members,此时相当于是Custome.vue向`http://localhost:5000/api/members路径发送请求,端口号是5000,可以绕过同源策略不触发,然后在这个请求发送的过程中,vite.config.js中的代理:

java 复制代码
//代理
    proxy:{
      "/api":{ //只要前端地址有/api,就给代理到target的请求
        target:"http://localhost:8080",
        changeOrigin:true,
        rewrite:url=>url.replace(/^\/api/,"/api/v1")//匹配以/api开头的字符串.替换到/api/v1

      }
    }

这段代码发挥作用,将http://localhost:5000/api/members拦截,识别到了"/api",此时,将http://localhost:5000替换成了http://localhost:8080,然后执行
rewrite:url=>url.replace(/^\/api/,"/api/v1"),把"/api"替换成了"/api/v1"。

此时,经过这个代理拦截后的一系列操作,请求的url已经变成了http://localhost:8080/api/v1//members,也就是后端要接收的正确路径,这样,其实就可以进行前后端交互了。

因为还在项目中定义了一个util包,里面有个api.js,代码如下

java 复制代码
import axios from "axios";
//创建一个axios实例
let api = axios.create({
    baseURL:"/api",
    timeout:3000
});
//配置响应拦截器
api.interceptors.response.use(resp=>{
    return resp.data;
},resp=>{
    return Promise.reject(resp.data.msg||"后台服务异常");
});
export default api;

先不用看配置响应拦截器,只看创建一个axios实例,里面定义了 baseURL:"/api",意思是所有通过该实例的请求会自动拼接 /api 前缀,也就是说,咱们的Customer.vue里,url还可以简写:

首先引入import api from "@/util/api.js";

然后url改成 url:"/members"

完整代码如下:

java 复制代码
<template>
  <h1>客户管理界面</h1>
</template>


<style>

</style>


<script setup>
import {onMounted} from  "vue"  //引入钩子
import axios  from "axios";
import api from "@/util/api.js";
//当组件挂载完毕之后触发
onMounted(async ()=>{
  // await阻塞等待 必须用在async函数中
  let resp = await axios( {  //await 确保了axios()后的 console.log(resp) 只有在 API 请求完成并获得响应后才会执行,这是处理异步数据获取的标准模式。
    // url:"http://localhost:8080/api/v1/members",
    url:"/members",//vite.config.js跨域配置和util的api.js配置后这样写
    method:"get",
    params:{
      page:1,
      limit:10,
      name: "晓"
    }
  });
  console.log(resp);
});

</script>