下载地址:
前端:https://download.csdn.net/download/2401_83418369/90811402
后端:https://download.csdn.net/download/2401_83418369/90811405
一、前端vue部分的搭建
这里直接看另一期刊的搭建Vue前端工程部分
前端vue+后端ssm项目_vue框架和ssm框架配置-CSDN博客
二、修改项目基础页面
修改App.vue页面

修改HomeView.vue 页面

删除HelloWorld.vue组件,新增一个组件Header

<script >
export default {
name:"Header"
}
</script>
<template>
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #ccc; display:
flex">
<div style="width: 200px; padding-left: 30px; font-weight: bold; color: dodgerblue">后
台管理</div>
<div style="flex: 1"></div>
<div style="width: 100px">下拉框</div>
</div>
</template>
<style scoped>
</style>

创建全局样式global.css

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
不要忘记在main.js文件中导入(导入才能生效)

导入 ElementPlus并运用

快速开始 | Element Plus (element-plus.org)
测试一下:添加一个按钮


增加下拉列表的组件

<script >
export default {
name:"Header"
}
</script>
<template>
<div style="height: 50px;line-height: 50px;border-bottom: 1px solid #ccc;display: flex">
<div style="width: 200px;padding-left: 30px;font-weight: bold;color:dodgerblue">后台管理</div>
<div style="flex: 1"></div>
<div style="width: 100px">
<el-dropdown>
<span class="el-dropdown-link">
tom
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<style scoped>
</style>
导入到App.vue页面

<template>
<div>
<!-- 引用组件-->
<Header/>
<div style="display: flex">
<!-- 侧边栏:将侧边栏放入盒子里面然后设置盒子的展示是弹性的-->
<Aside/>
<!-- 通过路由展示-->
<router-view style="flex: 1"/>
</div>
</div>
</template>
<style>
</style>
<script>
import Header from "@/components/Header.vue";
import Aside from "@/components/Aside.vue";
export default {
name:"Layout",
components:{
Header,Aside
}
}
</script>
在HomeView.vue页面添加一个按钮


添加 增加、查询的组件和一个表格

<template>
<!--这个主要是用来路由到的页面,默认访问的页面-->
<div>
<!-- 添加按钮和查询框-->
<div style="margin: 10px 5px;display:inline-block">
<el-button type="primary">新增</el-button>
<el-button>其他</el-button>
</div>
<div style="display: inline-block">
<el-input v-model="input" style="width: 150px" placeholder="请输入关键字" />
<el-button type="success">提交</el-button>
</div>
<!-- 表格-->
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column sortable prop="date" label="日期" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="address" label="地址" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" @click="handleClick">
删除
</el-button>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
// @ is an alias to /src
// import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'HomeView',
components: {
},
//数据池的方法
data(){
return{
tableData:[
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
}
}
}
</script>

三、后端springboot环境搭建
主要是配置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>com.study</groupId>
<artifactId>springboot_furn</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.5.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
<!-- 引入Mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
</dependencies>
</project>
在application.yaml文件 配置数据源和端口

创建启动类

四、添加家居
完成后台代码从 mapper -> service -> controller , 并对每层代码进行测试 , 到 controller 这一层,使用 Postman 发送 http 请求完成测试
创建数据库和表
CREATE DATABASE springboot_vue;
USE springboot_vue;
CREATE TABLE furn(
`id` INT(11) PRIMARY KEY AUTO_INCREMENT, ## id
`name` VARCHAR(64) NOT NULL, ## 家居名
`maker` VARCHAR(64) NOT NULL, ## 厂商
`price` DECIMAL(11,2) NOT NULL, ## 价格
`sales` INT(11) NOT NULL, ## 销量
`stock` INT(11) NOT NULL ## 库存
);
INSERT INTO furn(`id` , `name` , `maker` , `price` , `sales` , `stock`)
VALUES(NULL , '北欧风格小桌子' , '熊猫家居' , 180 , 666 , 7);
INSERT INTO furn(`id` , `name` , `maker` , `price` , `sales` , `stock`)
VALUES(NULL , '简约风格小椅子' , '熊猫家居' , 180 , 666 , 7 );
INSERT INTO furn(`id` , `name` , `maker` , `price` , `sales` , `stock` )
VALUES(NULL , '典雅风格小台灯' , '蚂蚁家居' , 180 , 666 , 7 );
INSERT INTO furn(`id` , `name` , `maker` , `price` , `sales` , `stock` )
VALUES(NULL , '温馨风格盆景架' , '蚂蚁家居' , 180 , 666 , 7 );
SELECT * FROM furn;
创建 Result.java 该工具类用于返回结果(json 格式),这个工具类,在网上也可找到,直接拿来使用 , SSM 项目也用过类似的工具类

package com.study.furn.utils;
public class Result<T> {
private String code;
private String msg;
private T data;
public Result() {
}
public Result(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Result(T data){
this.data=data;
}
//返回成功的result不携带data数据
public static Result success(){
Result<Object> objectResult = new Result<>();
objectResult.setCode("200");
objectResult.setMsg("success");
return objectResult;
}
//返回成功的result携带data数据,将静态方法定义为泛型方法,这个泛型方法和泛型类的泛型参数是独立的
//静态方法的泛型类型参数与泛型类的类型参数名称相同但是编译器会进行区分
public static<K> Result success(K data){
Result<K> tResult = new Result<>(data);
tResult.setCode("200");
tResult.setMsg("success");
return tResult;
}
//返回错误的result不携带data
public static Result error(String code,String msg){
Result<Object> objectResult = new Result<>();
objectResult.setCode(code);
objectResult.setMsg(msg);
return objectResult;
}
//返回失败的result携带data数据
public static<K> Result error(K data,String code,String msg){
Result<K> tResult = new Result<>(data);
tResult.setCode(code);
tResult.setMsg(msg);
return tResult;
}
}
编写dao层

这里直接继承父接口BaseMapeer
package com.study.furn.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.furn.bean.Furn;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FurnMapper extends BaseMapper<Furn> {
}
在测试类中进行测试 :


编写Service层

package com.study.furn.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.study.furn.bean.Furn;
public interface FurnService extends IService<Furn> {
}
package com.study.furn.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.study.furn.bean.Furn;
import com.study.furn.dao.FurnMapper;
import com.study.furn.service.FurnService;
import org.springframework.stereotype.Service;
@Service
public class FurnServiceImpl extends ServiceImpl<FurnMapper, Furn> implements FurnService {
}
测试类中进行测试:


编写controller层

package com.study.furn.controller;
import com.study.furn.bean.Furn;
import com.study.furn.service.FurnService;
import com.study.furn.utils.Result;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 这里使用复合注解确保所有的方法返回给前端的数据都是json格式的数据
*/
@RestController
public class FurnController {
@Resource
private FurnService furnService;
/**
* 这是一个保存Furn对象的方法
* @param furn
* @return
*/
@PostMapping("/save")
public Result save(@RequestBody Furn furn){
furnService.save(furn);
return Result.success();
}
}
使用Postman进行测试:

注意细节:
1、@RequestBody注解是将json格式的数据封装到方法的参数对象中,如果前端发送的表单数据/请求参数,不使用@RequestBody注解也会进行自动封装(还要注意的是请求头的Content-Type要和请求的数据类型匹配:表单的数据类型是application/x-www-form-urlencoded,json数据类型是application/json)
2、使用@RequestBody注解接收 JSON 时,若 ID 字段显式设为 null
,可能导致 ORM(如 Hibernate)抛出异常,解决的方法是在id字段上添加@TableId(type = IdType.AUTO)注解

在前端项目中添加表单组件
<template>
<!--这个主要是用来路由到的页面,默认访问的页面-->
<div>
<!-- 添加按钮和查询框-->
<div style="margin: 10px 5px;display:inline-block">
<el-button type="primary" @click="add">新增</el-button>
<el-button>其他</el-button>
</div>
<div style="display: inline-block">
<el-input v-model="input" style="width: 150px" placeholder="请输入关键字" />
<el-button type="success">提交</el-button>
</div>
<!-- 表格-->
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column sortable prop="date" label="日期" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="address" label="地址" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" @click="handleClick">
删除
</el-button>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 下面是对话框和表单
<el-input v-model="form.name" style="width: 80%"></el-input>
这里的form.name 表示form对象的属性name
必须和后端的对象字段一样因为要将这些字段信息生成json格式打包给后端,
这里的属性名可以动态生成,不需要在数据池里面编写
-->
<el-dialog
title="提示"
v-model="dialogVisible"
width="30%">
<el-form :model="form" label-width="120px">
<!-- 家居名 -->
<el-form-item label="家居名">
<el-input v-model="form.name" style="width: 80%"></el-input>
</el-form-item>
<!-- 厂商 -->
<el-form-item label="厂商">
<el-input v-model="form.maker" style="width: 80%"></el-input>
</el-form-item>
<!-- 价格 -->
<el-form-item label="价格">
<el-input v-model="form.price" style="width: 80%"></el-input>
</el-form-item>
<!-- 销量 -->
<el-form-item label="销量">
<el-input v-model="form.sales" style="width: 80%"></el-input>
</el-form-item>
<!-- 库存 -->
<el-form-item label="库存">
<el-input v-model="form.stock" style="width: 80%"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- 直接写dialogVisible = false,因为只用一句,所以可以直接写在属性上-->
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
// @ is an alias to /src
// import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'HomeView',
components: {
},
//数据池的方法
data(){
return{
//这里默认表单是不显示的
dialogVisible:false,
form:{},
tableData:[
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
}
},
methods:{
add(){
this.dialogVisible=true;
//但调用该方法后,将form对象的信息进行清除,
// 因为表单里的数据和数据池的数据是双向绑定的如果填写了表单的数据那么数据池的数据就有了
// 防止下次点击后出现上一次填写的数据,必须清空数据池的数据
this.form={}
}
}
}
</script>

在vue项目的终端中安装Axios
npm i axios -S

编写request请求对象
//导入Axios包
import axios from "axios";
//通过Axios创建对象Request 发送请求到后端
const request = axios.create({
timeout:5000
})
//Request拦截器加上统一的处理
//比如Content-Type,请求头中添加Content-Type表示请求体中的数据类型是json数据,
// 后端@RequestBody注解在内容协商中能匹配成功
request.interceptors.request.use(config=>{
config.headers['Content-Type'] = 'application/json;charset=utf-8'
return config
},error=>{
return Promise.reject(error)
})
//response拦截器拦截响应对象,统一处理返回的结果
request.interceptors.response.use(response=>{
let res = response.data
//如果返回的是文件,就返回
if (response.config.responseType==='blob'){
return res
}
//如果是String,就转成json对象
if (typeof res==='string'){
res = res ? JSON.parse(res):res
}
return res
},error=>{
return Promise.reject(error)
})
//导出Request对象,在其他组件就可以使用
export default request
在HomeView.vue页面中编写save方法发送json数据给后端

<template>
<!--这个主要是用来路由到的页面,默认访问的页面-->
<div>
<!-- 添加按钮和查询框-->
<div style="margin: 10px 5px;display:inline-block">
<el-button type="primary" @click="add">新增</el-button>
<el-button>其他</el-button>
</div>
<div style="display: inline-block">
<el-input v-model="input" style="width: 150px" placeholder="请输入关键字" />
<el-button type="success">提交</el-button>
</div>
<!-- 表格-->
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column sortable prop="date" label="日期" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="address" label="地址" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" @click="handleClick">
删除
</el-button>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 下面是对话框和表单
<el-input v-model="form.name" style="width: 80%"></el-input>
这里的form.name 表示form对象的属性name
必须和后端的对象字段一样因为要将这些字段信息生成json格式打包给后端,
这里的属性名可以动态生成,不需要在数据池里面编写
-->
<el-dialog
title="提示"
v-model="dialogVisible"
width="30%">
<el-form :model="form" label-width="120px">
<!-- 家居名 -->
<el-form-item label="家居名">
<el-input v-model="form.name" style="width: 80%"></el-input>
</el-form-item>
<!-- 厂商 -->
<el-form-item label="厂商">
<el-input v-model="form.maker" style="width: 80%"></el-input>
</el-form-item>
<!-- 价格 -->
<el-form-item label="价格">
<el-input v-model="form.price" style="width: 80%"></el-input>
</el-form-item>
<!-- 销量 -->
<el-form-item label="销量">
<el-input v-model="form.sales" style="width: 80%"></el-input>
</el-form-item>
<!-- 库存 -->
<el-form-item label="库存">
<el-input v-model="form.stock" style="width: 80%"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- 直接写dialogVisible = false,因为只用一句,所以可以直接写在属性上-->
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
// @ is an alias to /src
import request from "@/utils/request"
export default {
name: 'HomeView',
components: {
},
//数据池的方法
data(){
return{
//这里默认表单是不显示的
dialogVisible:false,
form:{},
tableData:[
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
}
},
methods:{
add(){
this.dialogVisible=true;
//但调用该方法后,将form对象的信息进行清除,
// 因为表单里的数据和数据池的数据是双向绑定的如果填写了表单的数据那么数据池的数据就有了
// 防止下次点击后出现上一次填写的数据,必须清空数据池的数据
this.form={}
},
save(){
request.post("http://localhost:9090/save",this.form).then(
response=>{
console.log("response",response)
}
)
}
}
}
</script>
这里遇到一个跨域请求的问题:

跨域请求问题是指由于浏览器的**同源策略(Same-Origin Policy)**限制,导致网页无法直接访问不同源(协议、域名、端口任一不同)的资源,从而引发的访问限制问题。
1. 跨域问题的本质
跨域问题的根源是浏览器的同源策略,该策略规定:
- 同源条件 :协议、域名、端口必须完全相同。例如,
https://example.com
与http://example.com
(协议不同)、example.com
与api.example.com
(子域名不同)均属于跨域
- 目的:防止恶意网站通过脚本窃取敏感数据(如Cookie、用户信息等),确保用户数据安全。
2. 跨域请求的触发场景
以下情况会触发跨域限制:
- Ajax/Fetch请求:前端通过JavaScript发起的HTTP请求目标不同源的服务端接口。
- 资源加载:跨域加载图片、CSS、JavaScript文件等静态资源。
- Web API调用:如调用第三方API(如支付接口、地图服务)时,若未配置CORS则会被拦截。
3. 跨域问题的表现
浏览器拦截:即使请求成功发送到服务端并返回数据,浏览器仍会拦截响应,导致前端无法获取结果。
错误提示 :常见控制台报错如 No 'Access-Control-Allow-Origin' header is present
解决方法:代理服务器
- 原理:前端请求同源代理服务器,由代理转发请求至目标服务器,规避浏览器限制。常用工具如Nginx或后端框架(如Spring Boot)的代理配置
在vue.config.js文件中添加代理

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
module.exports={
devServer:{
port:10000 ,//启动端口
// 设置代理
proxy:{
'/api':{ //设置拦截器,拦截器的格式 斜杆+名字 拦截规则:匹配所有以/api开头的请求路径
target:'http://localhost:9090', //目标服务器地址:将匹配的请求转发到http://localhost:9090
changeOrigin:true, //设置同源
pathRewrite:{ //路径重写
'/api':'' //api被替换为空,前端统一使用代理前缀(如 /api),但后端接口路径不含该前缀
//前端请求:/api/user/list
//实际转发:http://backend.com/user/list
// /api 这部分被 target的路径代替
}
}
}
}
}
在HomeView.vue页面中进行修改

<template>
<!--这个主要是用来路由到的页面,默认访问的页面-->
<div>
<!-- 添加按钮和查询框-->
<div style="margin: 10px 5px;display:inline-block">
<el-button type="primary" @click="add">新增</el-button>
<el-button>其他</el-button>
</div>
<div style="display: inline-block">
<el-input v-model="input" style="width: 150px" placeholder="请输入关键字" />
<el-button type="success">提交</el-button>
</div>
<!-- 表格-->
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column sortable prop="date" label="日期" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="address" label="地址" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" @click="handleClick">
删除
</el-button>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 下面是对话框和表单
<el-input v-model="form.name" style="width: 80%"></el-input>
这里的form.name 表示form对象的属性name
必须和后端的对象字段一样因为要将这些字段信息生成json格式打包给后端,
这里的属性名可以动态生成,不需要在数据池里面编写
-->
<el-dialog
title="提示"
v-model="dialogVisible"
width="30%">
<el-form :model="form" label-width="120px">
<!-- 家居名 -->
<el-form-item label="家居名">
<el-input v-model="form.name" style="width: 80%"></el-input>
</el-form-item>
<!-- 厂商 -->
<el-form-item label="厂商">
<el-input v-model="form.maker" style="width: 80%"></el-input>
</el-form-item>
<!-- 价格 -->
<el-form-item label="价格">
<el-input v-model="form.price" style="width: 80%"></el-input>
</el-form-item>
<!-- 销量 -->
<el-form-item label="销量">
<el-input v-model="form.sales" style="width: 80%"></el-input>
</el-form-item>
<!-- 库存 -->
<el-form-item label="库存">
<el-input v-model="form.stock" style="width: 80%"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- 直接写dialogVisible = false,因为只用一句,所以可以直接写在属性上-->
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
// @ is an alias to /src
import request from "@/utils/request"
export default {
name: 'HomeView',
components: {
},
//数据池的方法
data(){
return{
//这里默认表单是不显示的
dialogVisible:false,
form:{},
tableData:[
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
}
},
methods:{
add(){
this.dialogVisible=true;
//但调用该方法后,将form对象的信息进行清除,
// 因为表单里的数据和数据池的数据是双向绑定的如果填写了表单的数据那么数据池的数据就有了
// 防止下次点击后出现上一次填写的数据,必须清空数据池的数据
this.form={}
},
save(){
request.post("/api/save",this.form).then(
response=>{
console.log("response",response)
this.dialogVisible=false
}
)
}
}
}
</script>
1. 代理机制的核心逻辑
通过将浏览器发起的跨域请求 转发到同源的后端代理服务,再由代理服务转发到实际目标服务器,从而绕过浏览器的同源策略限制。具体流程如下:
- 前端请求路径 :前端代码向同源地址(如
http://localhost:10000/api/data
)发送请求。 - 代理拦截 :webpack-dev-server 根据
/api
规则拦截请求,替换目标地址为http://localhost:9090/data
(通过pathRewrite
移除/api
前缀)。 - 服务端转发:代理服务将请求转发到目标服务器,并返回响应给前端。
- 浏览器无感知 :浏览器始终认为请求来自同源(
localhost:10000
),因此不会触发跨域拦截
过程:浏览器发送请求到vue,vue中进行拦截请求替换目标地址再请求到后端,后端响应数据到前端
2. 关键配置项的作用
(1)target: 'http://localhost:9090'
- 功能:指定目标服务器的真实地址,告知代理将请求转发至此地址。
- 本质 :将前端路径中的
/api
替换为http://localhost:9090
,实现请求重定向
2)changeOrigin: true
- 作用 :修改请求头中的
Host
和Origin
为目标服务器地址(localhost:9090
),使目标服务器认为请求来自合法源。 - 必要性 :部分服务器会验证请求来源,若未设置此参数,目标服务器可能因
Host
不匹配而拒绝请求
3)pathRewrite: { '/api': '' }
- 功能 :重写请求路径,移除前端添加的
/api
前缀。例如,/api/user
将被改写为/user
,确保目标接口路径正确。 - 灵活性:允许前端统一使用代理前缀,后端接口无需修改即可适配
3. 代理为何能绕过跨域限制?
(1)浏览器同源策略的规避
- 浏览器视角 :所有请求均指向本地开发服务器(如
localhost:10000
),未触发跨域规则。 - 实际路径 :代理服务器作为中间层,将请求转发至外部目标服务器(
localhost:9090
),而 服务器间通信不受浏览器同源策略限制
4. 与其他方案的对比
方案 | 优势 | 局限性 |
---|---|---|
代理服务器 | 无需后端配合修改,适合本地开发调试;配置简单灵活 | 仅适用于开发环境,生产环境需通过Nginx等实现代理 |
CORS | 标准化解决方案,支持所有HTTP方法 | 需后端配置响应头,对第三方API无法控制 |
JSONP | 兼容老旧浏览器 | 仅支持GET请求,存在安全风险 |
5. 注意事项
- 仅限开发环境 :代理配置通常在
webpack.config.js
或vue.config.js
中设置,不适用于生产环境。生产环境需通过Nginx反向代理或后端服务处理跨域 - 复杂路径匹配 :可通过正则表达式定义更灵活的代理规则(如
/api/**
匹配多级路径),满足复杂接口需求
进行测试:


五、显示家居信息
完成后台代码从 mapper -> service -> controller , 并对每层代码进行测试 , 到 controller 这一层,使用 Postman 发送 http 请求完成测试,由于mybatis-plus已经提供了父接口的方法,所以不需要再编写查询方法
在controller层添加查询方法

使用Postman进行测试
在前端vue项目中修改,先清空数据池中tableData中的数据

表格部分的字段进行修改

下面是Template标签 的组件
<template>
<!--这个主要是用来路由到的页面,默认访问的页面-->
<div>
<!-- 添加按钮和查询框-->
<div style="margin: 10px 5px;display:inline-block">
<el-button type="primary" @click="add">新增</el-button>
<el-button>其他</el-button>
</div>
<div style="display: inline-block">
<el-input v-model="input" style="width: 150px" placeholder="请输入关键字" />
<el-button type="success">提交</el-button>
</div>
<!-- 表格
:data="tableData" 单向渲染数据,从数据池的data数据池的tableData字段里获取数据,
<=> v-bind:data -->
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column sortable prop="id" label="ID" />
<el-table-column prop="name" label="家居名" />
<el-table-column prop="maker" label="厂家" />
<el-table-column prop="price" label="价格" />
<el-table-column prop="sales" label="销量" />
<el-table-column prop="stock" label="库存" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" @click="handleClick">
删除
</el-button>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 下面是对话框和表单
<el-input v-model="form.name" style="width: 80%"></el-input>
这里的form.name 表示form对象的属性name
必须和后端的对象字段一样因为要将这些字段信息生成json格式打包给后端,
这里的属性名可以动态生成,不需要在数据池里面编写
-->
<el-dialog
title="提示"
v-model="dialogVisible"
width="30%">
<el-form :model="form" label-width="120px">
<!-- 家居名 -->
<el-form-item label="家居名">
<el-input v-model="form.name" style="width: 80%"></el-input>
</el-form-item>
<!-- 厂商 -->
<el-form-item label="厂商">
<el-input v-model="form.maker" style="width: 80%"></el-input>
</el-form-item>
<!-- 价格 -->
<el-form-item label="价格">
<el-input v-model="form.price" style="width: 80%"></el-input>
</el-form-item>
<!-- 销量 -->
<el-form-item label="销量">
<el-input v-model="form.sales" style="width: 80%"></el-input>
</el-form-item>
<!-- 库存 -->
<el-form-item label="库存">
<el-input v-model="form.stock" style="width: 80%"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- 直接写dialogVisible = false,因为只用一句,所以可以直接写在属性上-->
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
添加list方法


将data字段的数据填充到数据池中
注意response拦截器进行了拦截处理:将字符串转成json格式的对象,并且将变量进行赋值
<template>
<!--这个主要是用来路由到的页面,默认访问的页面-->
<div>
<!-- 添加按钮和查询框-->
<div style="margin: 10px 5px;display:inline-block">
<el-button type="primary" @click="add">新增</el-button>
<el-button>其他</el-button>
</div>
<div style="display: inline-block">
<el-input v-model="input" style="width: 150px" placeholder="请输入关键字" />
<el-button type="success">提交</el-button>
</div>
<!-- 表格
:data="tableData" 单向渲染数据,从数据池的data数据池的tableData字段里获取数据,
<=> v-bind:data -->
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column sortable prop="id" label="ID" />
<el-table-column prop="name" label="家居名" />
<el-table-column prop="maker" label="厂家" />
<el-table-column prop="price" label="价格" />
<el-table-column prop="sales" label="销量" />
<el-table-column prop="stock" label="库存" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" @click="handleClick">
删除
</el-button>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 下面是对话框和表单
<el-input v-model="form.name" style="width: 80%"></el-input>
这里的form.name 表示form对象的属性name
必须和后端的对象字段一样因为要将这些字段信息生成json格式打包给后端,
这里的属性名可以动态生成,不需要在数据池里面编写
-->
<el-dialog
title="提示"
v-model="dialogVisible"
width="30%">
<el-form :model="form" label-width="120px">
<!-- 家居名 -->
<el-form-item label="家居名">
<el-input v-model="form.name" style="width: 80%"></el-input>
</el-form-item>
<!-- 厂商 -->
<el-form-item label="厂商">
<el-input v-model="form.maker" style="width: 80%"></el-input>
</el-form-item>
<!-- 价格 -->
<el-form-item label="价格">
<el-input v-model="form.price" style="width: 80%"></el-input>
</el-form-item>
<!-- 销量 -->
<el-form-item label="销量">
<el-input v-model="form.sales" style="width: 80%"></el-input>
</el-form-item>
<!-- 库存 -->
<el-form-item label="库存">
<el-input v-model="form.stock" style="width: 80%"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- 直接写dialogVisible = false,因为只用一句,所以可以直接写在属性上-->
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
//导入组件
import request from "@/utils/request";
export default {
name: 'HomeView',
components: {
},
//数据池的方法
data(){
return{
//这里默认表单是不显示的
dialogVisible:false,
form:{},
tableData:[]
}
},
//钩子函数, created()函数调用后
//数据池和方法池的数据都进行了初始化
created() {
//调用list方法展示数据
this.list()
},
methods:{
list(){
request.get("/api/list").then(response=>{
this.tableData=response.data
})
},
add(){
this.dialogVisible=true;
//但调用该方法后,将form对象的信息进行清除,
// 防止下次点击后出现上一次填写的数据
this.form={}
},
save(){
request.post("/api/save",this.form).then(response=>{
console.log("response",response)
this.dialogVisible=false
//增加家居后调用查询
this.list()
})
},
}
}
</script>

六、修改家居信息
完成后台代码从 mapper -> service -> controller , 并对每层代码进行测试 , 到 controller 这一层,使用 Postman 发送 http 请求完成测试,由于mybatis-plus已经提供了父接口的方法,所以不需要再编写修改方法
在controller层添加修改方法

使用Postman进行测试

在前端vue项目中添加方法得到回显的数据
直接通过前端scope.row获取当前行的代理对象
发现这是个代理对象,需要转换 Proxy 为原始对象

将代理对象转成原始对象,再将原始json对象绑定到 form表单中并进行展示



save方法如下:根据form表单中是否有数据来区分要进行增加数据还是修改数据,同时根据响应的状态码来回显状态信息
save(){//添加、修改
if (this.form.id){//如果表单中有数据id就是修改操作
request.put("/api/update",this.form).then(
response=>{
if (response.code==="200"){//响应的状态码
ElMessage({
type:"success",
message:"修改成功"
})
}else {
ElMessage({
type:"error",
message:"修改失败"
})
}
//刷新数据
this.list()
//关闭对话框
this.dialogVisible=false
}
)
}else {//添加
request.post("/api/save",this.form).then(response=>{
console.log("response",response)
//增加家居后调用查询,刷新数据
this.list()
this.dialogVisible=false
})
}
}

补充回显数据的方式2:
将代理对象的id取出,然后将id发送给后端,后端根据id查询对应的Furn对象,然后再将对象进行回显

在HomeView.vue页面中添加一个方法

同样能回显数据,这种方式回显才能确保数据库的真实数据
七、删除家居信息
完成后台代码从 mapper -> service -> controller , 并对每层代码进行测试 , 到 controller 这一层,使用 Postman 发送 http 请求完成测试,由于mybatis-plus已经提供了父接口的方法,所以不需要再编写删除方法
在controller层添加删除方法

使用Postman进行测试

在前端vue实现删除的方法
将删除按钮进行修改


实现删除的方法

删除按钮:
<!-- 删除按钮 -->
<el-popconfirm title="确认删除吗?" @confirm="handleDel(scope.row.id)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
删除方法:
handleDel(id){
request.delete("/api/delete?id="+id).then(
response=> {
if (response.code === "200") {//响应的状态码
ElMessage({
type: "success",
message: "删除成功"
})
} else {
ElMessage({
type: "error",
message:"删除失败"
})
}
//刷新数据
this.list()
}
)
}

八、分页显示列表
在配置类中配置分页拦截器

package com.study.furn.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 这是一个mybatis-plus配置类,在这里配置分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//配置分页拦截器
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return mybatisPlusInterceptor;
}
}
在controller层添加分页方法

使用Postman进行测试

在表格下面添加导航栏组件

<!-- 添加分页导航-->
<div style="margin:10px 0">
<el-pagination
@size-change="handlePageSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</div>
数据池中绑定初始化数据


完善导航栏的两个方法,双向数据绑定

修改list方法为分页方法
修改请求的路径并传两个参数(当前页,每页的数量)给后端,后端返回的total绑定到数据池


list方法如下 :
list(){
// request.get("/api/list").then(response=>{
// this.tableData=response.data
// })
request.get("/api/page",{
params:{
pageNum:this.currentPage, //传给后端,当前页是第几页
pageSize:this.pageSize//传给后端,当前页的数量
}
}).then(response=>{
this.tableData=response.data.records
this.total=response.data.total
})
},

九、切换数据源为druid数据源

新创建一个配置类
package com.study.furn.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* 数据源配置类
*/
@Configuration
public class DruidDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
}
在application.yaml文件中配置也是可以的

十、带条件查询分页显示列表
完成后台代码从 mapper -> service -> controller , 并对每层代码进行测试 , 到 controller 这一层,使用 Postman 发送 http 请求完成测试,由于mybatis-plus已经提供了父接口的方法,所以不需要再编写方法
在controller层添加方法

使用Postman进行测试

在查询输入框中绑定数据池,查询按钮中绑定点击方法
这里还是调用list方法


修改list方法


十一、添加家居表单完善前端校验
在数据池中添加校验规则

//校验规则
rules:{
name:[{
required:true,message:"请输入家居名",trigger:"blur"
}],
maker:[{
required:true,message:"请输入厂家名",trigger:"blur"
}],
price:[{
required:true,message:"请输入价格",trigger:"blur"
},{
pattern:/^([1-9]\d*|0)(\.\d+)?$/ ,message: "请输入数字",trigger:"blur"
}
],
sales:[{
required:true,message:"请输入销量",trigger:"blur"
},{
pattern:/^([1-9]\d*|0)$/ ,message: "请输入数字",trigger:"blur"
}],
stock:[{
required:true,message:"请输入库存",trigger:"blur"
},{
pattern:/^([1-9]\d*|0)$/ ,message: "请输入数字",trigger:"blur"
}]
}
表单中绑定校验规则


在点击确定按钮时进行校验,如果校验通过才发送请求到后端

清空上一次的校验

//清空上一次的校验
this.$nextTick(() => {
this.$refs['form'].resetFields();
});

十二、添加家居表单完善后端校验
后端校验主要是防止别人绕过前端校验直接发送save请求到后端
引入jsr303数据校验支持
记得刷新Maven

使用注解对furn的字段进行校验

使用Postman进行测试

在数据池中添加一个后端校验对象
在save方法中添加代码根据后端的状态码来处理

在表单中展示后端的校验信息

清空上一次的后端校验信息


总结:
该项目使用了前后端分离,前端的主体框架Vue3+后端的基础框架Spring-Boot
1.前端技术栈: vue3 + Axios + ElementsPlus
2.后端技术栈:SpringBoot + MyBatis Plus
3.数据库-Mysql
4.项目依赖管理-Maven
5.分页-MyBatis Plus 分页插件
6.切换数据源DruidDataSources
7.项目前端我们使用到 request 和 response拦截器,并且我们解决了跨域问题
目前该项目还有很多bug,后续再完善...