Vue和Springboot初步前后端分离建立项目连接(解决前后端跨域问题)
前后端分离
前端端口号配置5000
后端端口号配置8080
1.vue前端初始化
在主页本专栏上篇《Vue复习》的初始化Vue基础上,加入向后端发送get请求的代码,此处以Customer.Vue为例。
<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创建项目引入依赖
<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写项目初始化配置文件
spring:application:name: crm2026#配置数据源datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/gcbs2025?characterEncoding=UTF-8&useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# 配置mybatis
mybatis-plus:configuration:log-prefix: mybatis.log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImplmap-underscore-to-camel-case: truelazy-loading-enabled: truecache-enabled: falsetype-aliases-package: com.ysy.crm2026.modelmapper-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的注解
/*
* 会员
* */
@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;
}
import java.time.LocalDate;
@Getter
@Setter
public class MemberSearchBean extends Member {//以查询出生日期范围举例,假设有这个需求private LocalDate birthdayFrom;private LocalDate birthdayTo;
}
/*
* 审计字段
* *///因为可能每个表都有审计字段,所以把这个字段的类单独定义出来,又因为他不属于业务,不能单独创建对象,所以定义为抽象类
public abstract class AuditEntity {private String createdBy;private LocalDate createdTime;private String updatedBy;private LocalDate updatedTime;
}
2.3.2DAO(Mapper)层
@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层
/*** 查询全部会员* @param page 分页对象* @param msb 查询条件* @return 结果* */public interface MemberService {Page<Member> findAll(Page<Member> page, MemberSearchBean msb);
}
@Service
public class MemberServiceImpl implements MemberService {private MemberMapper memberMapper;@Autowiredpublic void setMemberMapper(MemberMapper memberMapper) {this.memberMapper = memberMapper;}@Overridepublic Page<Member> findAll(Page<Member> page, MemberSearchBean msb) {return memberMapper.findAll(page,msb);}
}
2.3.4Api层
此处为了封装响应对象,定义了一个名为JsonResult的封装类:
@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类:
@RestController
@RequestMapping(value = "/api/v1/members", produces = MediaType.APPLICATION_JSON_VALUE)
public class MemberApi {private MemberService memberService;@Autowiredpublic void setMemberService(MemberService memberService) {this.memberService = memberService;}@GetMappingpublic 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的自动分页拦截器
@Configuration
public class CommonConfig {//mybatis-plus自动分页拦截器@Beanpublic 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中有这段请求代码:
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里加入以下配置
//代理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进行修改:
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中的代理:
//代理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,代码如下
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"
完整代码如下:
<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>