前后端分离
前端端口号配置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>