Springboot+Vue前后端联调和数据交互

693 阅读9分钟

码字不易,喜欢就点个关注❤,持续更新技术内容。

1 前后端联调

现在将后台管理页面(前端)和后台服务服务端进行集成,之前都是通过MockJS拦截前端请求随机生成数据来模拟服务器的数据响应的。当服务端接口开发完成后,前端就可以通过连接服务端程序指定的端口,然后向指定功能接口发送请求。

如当我们要进入后台管理页面中,需要通过导航守卫进行登录权限验证,如token认证,用户信息请求,路由跳转等,在讲解Vue技术那篇文章中已经做了更详细的描述。

如下登录过程,向服务端发送POST请求,此时请求的URL中没有IP地址和端口号,我们需要在.env.development配置文件中添加URL前缀。

export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data: data
  })
}
ENV = 'development'# base api
VUE_APP_BASE_API = 'http://localhost:xxxx'

请求会服务器交给到以下控制器进行处理,然后返回包含token的结果集:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserMapper userMapper;
​
    @PostMapping("/login")
    //前端一般传递的数据是json格式,必须使用对象接收,同时需要添加@RequestBody注解
    public Result login(@RequestBody User user){
        //生成token
        String token = JwtUtils.generateToken(user.getUname());
​
        //返回包含token的结果集
        return Result.ok().data("token", token);
    }
}

接下来我们进行登录操作的讲解:

首先我们先启动前端项目:

进入登录组件页面,然后点击登录,导航守卫进行拦截走一遍权限认证,如没有token进入登录页登录,输入后点击登录会发送一次请求,传递账号和密码到服务端生成token后放在响应数据response在action中存储token,最后才进入首页。(实际上进入首页前还发送一次获取用户信息的请求。)

如果没有token想直接进入非白名单页面(一般是除登录页面以外的),则需要进入登录页进行上面过程后再重定向到想去的页面。

因为此时后端未启动,不能与服务端端口建立连接,也就发送不了网络请求:

image-20230808150830087.png

image-20230808150547879.png

启动服务端,与服务端端口建立连接,发送网络请求(注意跨域问题,在文首有相关内容的链接):

如图成功获取token和用户信息:

image-20230808171709521.png

image-20230808172349781.png

2 组件更改和数据交互

初步进行前后端联调后,如果有的页面组件不符合我们的需求,我们要改成我们想要的样子,然后通过Axios完成数据异步加载。

修改组件页面。

异步加载列表数据,并渲染到页面组件上。

首先在路由模块中找出组件与路由的关联,对路由对应的组件进行更改。

  {
    path: '/example',
    component: Layout,
    redirect: '/example/table',
    name: 'Example',
    meta: { title: '用户管理', icon: 'el-icon-s-help' },
    children: [
      {
        path: 'table',
        name: 'Table',
        component: () => import('@/views/table/index'),
        meta: { title: '用户列表', icon: 'table' }
      },
      {
        path: 'tree',
        name: 'Tree',
        component: () => import('@/views/tree/index'),
        meta: { title: '添加用户', icon: 'tree' }
      }
    ]
  },

所以要在在Table组件的进行更改,包括表单,表格,以及分页条,选择适当的组件后进行修改为符合自己需求的表单表格,注意需要在该表格用于展示用户的信息。

首先我们添加一个来包含整个组件页面(template中只能有一个根标签),然后在其中添加elementui的表格标签,在其中再添加表格列表,可以通过普通列定义其属性,方便进行渲染,还可以定义普通列的标签名以及调整适当的宽度。

注意如果响应数据打他中的性别值是0或1,则需要进行判断然后再渲染,在需要进行判断后渲染的普通列中加入一个插槽:

<template slot-scope="scope">
    {{ scope.row.gender == 1 ? '男':'女' }}
</template>

表示判断每一条对象中的gender为"1"则显示为"男",否则为"女"。表格全部代码如下(注意还需要在数据模型返回绑定数据标签才会生效):

      <el-table :data="tableData" border>
          <el-table-column prop="id" label="序号" width="50"></el-table-column>
          <el-table-column prop="classes" label="班级" width="120"></el-table-column>
          <el-table-column prop="uname" label="姓名" width="120"></el-table-column>
          
          <el-table-column prop="gender" label="性别" width="50">
              <template slot-scope="scope">
                  {{ scope.row.gender == 1 ? '男':'女' }}
              </template>
          </el-table-column>
​
          <el-table-column prop="birthday" label="生日"></el-table-column>
​
          <el-table-column label="操作">
            <el-button type="primary" size="mini">编辑</el-button>
            <el-button type="primary" size="mini">删除</el-button>
          </el-table-column>
      </el-table>

剩下的表单和分页条可查看文档加入到Table组件中。

image-20230809011401646.png

最后向服务端发送axios异步请求然后完成数据的渲染。

然后我们定义一个组件生命周期函数created(钩子),当组件被创建展示为页面前调用,在该方法中我们可以向后端发送axios请求数据。如在fetchData方法中封装了getList方法,该方法负责向后端指定接口发送请求:

前端发送的异步请求的方法,一般在api目录下定义。如在getList方法封装的request方法中定义请求路径url,以及请求方法GET,以及要向后端传递的参数。(该参数是后面分页请求要传递的页号)。这样后端就能接收到该请求并响应对应的结果集。

所以当我们想在单页面应用模板vue-admin-template中向后端发送请求时,可以在api目录下定义对应的方法,在request方法中定义对应的属性。

image-20230809231011959.png

<script>
import { getList } from '@/api/table'
  export default {
    data() {
      return {
        tableData: [],
        searchForm: {
          "name":"",
          gengder:""
        },
        value1:""
      }
    },
    created: function() {
      this.fetchData()
    },
    methods: {
      fetchData() {
        getList().then(response => {
          this.tableData = response.data.data
        })
      },
    }
  }
</script>

如下图在前后端联调后,前端向后端发送请求后,后端返回响应数据data,通过赋值渲染到组件页面中:

image-20230809004225554.png

上面只展现了各个组件的修改,简单地响应的数据,并未实现分页、编辑以及删除等功能。

2.1 分页功能

先分析,当我们点击分页的页号时,需要请求后端响应该页号的几条数据,如果是升序排序,就是向下读取几条数据作为一页;降序则是向上读取几条数据作为一页。然后返回响应数据然后进行赋值渲染。

      <!-- 分页条 -->
      <el-pagination background layout="total, prev, pager, next"
      @size-change="handleSizeChange"
      @current-change="changePage"
      :page-size="pageSize"
      v-if="isShow"
      :total="total"></el-pagination>

可以看到第一次进入页面,当组件创建完毕时自动向后端发送请求,后端读取几条数据作为第一页返回,然后赋值渲染给组件页面;而当我们点击分页号时,会在分页组件中触发changePage方法(依然是调用getList方法向后端发送异步请求)。可以看到,该方法重新向后端发送请求,并传递分页的页号,后端接收到后根据该页号进行分页查询,最后封装到结果集中返回,然后前端接收后进行赋值和渲染。这就完成了数据的分页显示。

<script>
import { getList } from '@/api/table'
  export default {
    data() {
      return {
        tableData: [],
        searchForm: {
          "name":"",
          gengder:"",
        },
      value1:"",
      total: 1,
      pageSize: 1,
      isShow: false  //默认当为响应数据时不显示分页条
      }
    },
    created: function() {
      this.fetchData()
    },
    methods: {
      fetchData() {
        getList().then(response => {
          this.tableData = response.data.items.records
          //总记录
          this.total = response.data.items.total
          //每页显示条数
          this.pageSize = response.data.items.size
          //当响应回数据时才显示分页
          this.isShow = true
        })
      },
      
      changePage(pageNum){
        getList(pageNum).then((response => {
          this.tableData = response.data.items.records
        }))
      },
      
    }
  }
</script>

如下后端后端的网络接口,观察理解以下,可以看到如果前端没有传递参数(也就是我们第一次打开页面时),后端默认响应第一页的数据,另外在查询条件中,通过id升序查询(如果用的是Sqlserver必须加该条件,否则报"@P0"附近有语法错误),最后封装到结果集中返回:

    @GetMapping("/getAll")
    public Result getAl(@RequestParam(defaultValue = "1") int pageNum){
        //分页对象
        Page<User> page = new Page<>(pageNum, 3);
        //Sqlserver中需要加一个唯一字段排序,否则系统排序结果不唯一
        QueryWrapper<User> wrapper = new QueryWrapper();
        wrapper.orderByAsc("id");
        Page<User> userPage = userMapper.selectPage(page, wrapper);
​
        return Result.ok().data("items", userPage);
    }

2.2 添加用户

在介绍编辑和删除之前,先对用户数据表进行添加用户。添加用户较简,只需在前端获取到用户数据就可以发送给后台进行插入。

在在用户表格中点击添加,触发doAdd方法。在doAdd方法中通过编程式路由跳转到添加页面。

<el-button type="primary" @click="doAdd">添加</el-button>
      doAdd(){
        //通过编程式路由跳转到添加页面
        this.$router.push("/users/add");
      },

在添加页面的表单中输入要添加的用户数据后确认添加,触发onSubmit方法,在方法中调用导入的网络请求方法add并传递表单数据。

        onSubmit() {
            add(this.form).then(res=>{
                this.$message({
                    message: '添加成功',
                    type: 'success'
                })
            });
        }

在add网络请求方法中向url发送post请求并发送填入的用户表单数据:

export function add(data) {
  return request ({
    url: '/user/add',
    method: 'post',
    data
  })
}

后端服务器接收请求后调用相应控制器的接口进行处理:

    @ApiOperation("添加用户")
    @PostMapping("/add")
    public Result addUser(@RequestBody User user) {
        userMapper.insertUser(user);
        return Result.ok();
    }

在用户控制器中定义了一个接收post请求的接口,通过@RequestBody注解接收实体类对象,调用UserMapper用户映射接口中自定义的insertUser方法添加用户。因为如果直接调用MybatisPlus的insert方法,该方法会根据用户User类定义的属性自动生成插入的sql语句,这样包括id在内的所有属性都作为字段插入数据。但是在sqlsever中自增长的id字段不能显示插入并报错。所以在进行插入操作时,需要在UserMapper用户映射接口中定义Mybatis发送手写的sql插入语句。

另外,在自定义插入sql语句时,需要字段和只一一对应,否则会出现混插的情况。

@Insert("insert into t_user values(#{uname}, #{pwd}, #{birthday}, #{gender}), #{classes}")
//或者
@Insert("insert into t_user(uname, pwd, birthday, gender, classes) 
        values(#{uname}, #{pwd}, #{birthday}, #{gender}), #{classes}")

另另外,其实MybatisPlus对sqlsever数据库进行插入操作的问题可以通过@TableField(exist = false)注解来说明id属性不作为字段处理,但是后面的查询、更新和删除操作又需要用到id,所以不能去掉,只能委屈一下插入操作了。

image-20230812233235338.png

2.3 编辑和删除功能

编辑用户与添加用户类似,不过还要在前台展示要编辑的用户数据,方便编辑;在后台,接收到传递来的用户对象时,需要提取用户id作为条件进行用户的删除。

首先进行较删除功能难一点的编辑功能。

2.3.1 编辑

在用户表格中点击编辑,调用handleEdit方法并传递当前行的对象:

<el-button type="primary" size="mini" @click="handleEdit(scope)">编辑</el-button>

在handleEdit方法中,将flag设置为true显示编辑浮窗,然后将传递的对象赋值给form表单对象进行展示,方便用户对比编辑。

      handleEdit(scope) {
        //首先打开浮窗
        this.flag=true;
        //1.将传递的对象数据赋值给要编辑的表单,这种方式直接影响用户表格数据的显示
        this.form = scope.row
        
        //2.或者是将对象里面属性展开,不用原来的对象赋值,避免影响用户表格数据的显示
        // this.form = {...scope.row}
      },
<!--定义浮窗的标签-->
<el-dialog title="编辑" width="50%" :visible.sync="flag"></el-dialog>

修改完表单数据后,点击浮窗的确定按钮调用doEdit方法,并关掉浮窗:

<el-button type="primary" @click="doEdit();flag=false">确 定</el-button>

在doEdit方法中,调用从api目录导入的edit方法发送异步请求,将编辑好的表单作为参数传递。

      doEdit(){
        edit(this.form).then(res=>{
                this.$message({
                    message: '编辑成功',
                    type: 'success'
                })
            });
      },

在api目录下js文件中的edit网络请求方法中定义请求的url、请求操作以及传递的参数:

export function edit(data) {
  return request ({
    url: '/user/edit',
    method: 'put',
    data // data: {data}
  })
}

然后后端服务器接收请求后调用相应控制器的接口进行处理:

@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {
    @Autowired
    private UserMapper userMapper;
​
    @PutMapping("/edit")
    public Result editById(@RequestBody User user) {
        UpdateWrapper<User> wrapper = new UpdateWrapper<>();
        int id = user.getId();
        System.out.println(user);
        wrapper.eq("id",id);
        userMapper.update(user, wrapper);
        return Result.ok();
    }
}
​

在用户控制器中定义了一个接收put请求的接口,通过@RequestBody注解接收实体类对象,然后提取对象中的id作为条件对象传给MybatisPlus中的update更新方法进行修改。因是更新数据,数据库只返回影响行数,我们只需要返回请求成功的结果集即可。

image-20230812124655326.png

2.3.2 删除

删除比较简单,在表格中点击删除,直接调用doDelete方法传递该行数据的id:

<el-button type="primary" size="mini" @click="doDelete(scope.row.id)">删除</el-button>

在doDelete方法中直接调用网络请求方法:

doDelete(id) {
    del(id).then(res => {
        this.$message({
            message: '删除成功',
            type: 'success'
        })
    })
},

在del方法中发送异步请求:

export function del(id) {
  return request ({
    url: '/user/del',
    method: 'delete',
    params: { id } //跟data一样,如果参数为params也可以简写为params
  })
}

后端控制器中定义的delete请求接口直接根据传递过来的id参数,调用BaseMapper接口中的deleteById删除用户:

    @ApiOperation("根据id删除用户")
    @DeleteMapping("/del")
    public Result deleteUser(int id){
        userMapper.deleteById(id);
        return Result.ok();
    }

image-20230812124821036.png