之前学习了一下前后端联调的一般步骤和Nginx的简单配置,现在以讲师管理功能为例来实战一下。
项目环境
后端:SpringBoot + MyBatisPlus +MySQL+Nginx
前端:vue-cli + axios
后端
1、编写讲师管理Controller,包含根据id删除讲师、分页查询讲师列表、新增讲师等请求接口。
@Api(description = "讲师管理")
@RestController
@RequestMapping("/admin/edu/teacher")
@CrossOrigin //跨域支持
public class TeacherAdminController {
@Autowired
private TeacherService teacherService;
@ApiOperation(value = "根据id删除讲师")
@DeleteMapping("{id}")
public R removeById(@ApiParam(name = "id",value="讲师id",required = true)
@PathVariable String id){
teacherService.removeById(id);
return R.ok();
}
@ApiOperation(value = "分页查询讲师列表")
@GetMapping("{page}/{limit}")
public R pageList(@ApiParam(name = "page",value = "当前页码",required = true)
@PathVariable Long page,
@ApiParam(name = "limit",value = "每页记录数",required = true)
@PathVariable Long limit,
@ApiParam(name = "teacherQuery",value = "查询对象",required = true)
TeacherQuery teacherQuery){
if(page <=0 || limit <=0){
//抛出统一异常,好处:所有开发人员协同时可保证错误结构一致
throw new CodingException(ResultCodeEnum.PARAM_ERROR);
}
Page<Teacher> pageParam = new Page<>(page,limit);
//调用自己扩展的查询方法
teacherService.pageQuery(pageParam,teacherQuery);
//结果封装
List<Teacher> records = pageParam.getRecords();
long total = pageParam.getTotal();
return R.ok().data("total",total).data("rows",records);
}
@ApiOperation(value = "新增讲师")
@PostMapping
public R save(@ApiParam(name = "teacher",value = "讲师对象",required = true)
@RequestBody Teacher teacher){
teacherService.save(teacher);
return R.ok();
}
}
2、编写讲师服务接口,并继承IService接口。
public interface TeacherService extends IService<Teacher> {
void pageQuery(Page<Teacher> pageParam,TeacherQuery teacherQuery);
}
3、编写讲师服务接口实现类,并继承ServiceImpl类。
@Service
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper,Teacher> implements TeacherService {
@Override
public void pageQuery(Page<Teacher> pageParam,TeacherQuery teacherQuery){
//MP 让我们可以通过面向对象的方式编写SQL
QueryWrapper<Teacher> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("sort");
if(teacherQuery == null){
baseMapper.selectPage(pageParam,queryWrapper);
return;
}
String name = teacherQuery.getName();
String level = teacherQuery.getLevel();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
if(!StringUtils.isEmpty(name)){
queryWrapper.like("name",name);//%name%
}
if(!StringUtils.isEmpty(level)){
queryWrapper.eq("level",level);// =level
}
if(!StringUtils.isEmpty(begin)){
queryWrapper.ge("gmt_create",begin); // >begin
}
if(!StringUtils.isEmpty(end)){
queryWrapper.le("gmt_create",end); // <end
}
baseMapper.selectPage(pageParam,queryWrapper);
}
}
4、编写讲师Mapper接口,并继承BaseMapper接口,无需定义方法。
public interface TeacherMapper extends BaseMapper<Teacher> {}
5、本例中后台有多个端口,使用Nginx来分发前端请求,前端请求直接访问8210端口即可,Nginx配置如下。
server{
listen 8210; #监听端口(默认监听接口为80)
server_name localhost; #服务名
location ~ /edu/{ #请求路径含 /edu/的请求转发到8110端口
proxy_pass http://localhost:8110;
}
location ~ /admin/sysuser{ #请求路径含 /admin/sysuser的请求转发到8210端口
proxy_pass http://localhost:8210;
}
}
前端
1、前端使用axios框架来访问后端接口获取数据,axios请求调用函数封装在
src/utils/request.js
中,请求后端接口的根路径
BASE_API
定义在
config/dev.env.js
中,根据不同运行环境,env.js的前缀有所不同,如
prod.env.js
(生产环境),这里则使用的是开发环境。
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv,{
NODE_ENV:'"development"',
BASE_API:'"http://localhost:8210"'
})
2、在
src/api
目录下添加
teacher.js
,封装后端请求的api操作,一个Controller对应一个api操作。
import request from '@utils/request'
const api_name = "/admin/edu/teacher"
export default {
//分页查询讲师列表
getPageList(page,limit,searchObj){
return request({
url:`${api_name}/${page}/${limit}`,
method:'get',
params:searchObj
})
},
//删除指定id讲师
removeById(id) {
return request({
url:`${api_name}/${id}`,
method:'delete',
})
},
//新增讲师
save(teacher) {
return request({
url:`${api_name}`,
method:'post',
data:teacher
})
}
}
3、在
src/views/edu/teacher
目录下添加新增讲师页
form.vue
,编写页面脚本。
<template>
<div>
<! -- 省略html代码 -->
</div>
</template>
<script>
import teacher from '@/api/edu/teacher'
//绑定表单数据
const defaultForm = {
name:'',
sort:1,
level:2,
career:'',
intro:'',
avatar:''
}
export default {
data(){
return {
teacher:defaultForm,
saveBtnDisabled:false //保存按钮是否禁用
}
},
methods:{
//watch监视 当路由发生变化时,执行此方法
watch:{
$router(to,from){
console.log("watch $router")
this.init();
}
},
//钩子函数 此方法在页面渲染前调用,如果页面是同一个,数据不会刷新
created(){
console.log("created")
this.init()
}
init(){
if(this.$router.params && this.$router.params.id){//如果请求中含有id参数,则需要从后台查询数据填充表单
const id = this.$router.params.id
this.fetchDataById(id)
}else{
this.teacher = {...defaultForm} //对象拷贝
}
},
save(){
this.saveBtnDisabled = true;
this.saveData();
},
saveData(){
teacher.save(this.teacher).then(response =>{
return this.$message({
type:'success',
message:'保存成功!'
}).then(response =>{
this.$router.push({path:'/edu/teacher'}) //路由跳转
})
})
},
//后台数据查询
fetchDataById(id){
teacher.getById(id).then(response =>{
console.log(response)
this.teacher = response.data.item
})
}
}
}
</script>
4、在
src/views/edu/teacher
目录下添加讲师列表页
list.vue
,编写页面脚本。
<template>
<div class="app-container">
<!--查询表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item>
<el-input v-model="searchObj.name" placeholder="讲师名" />
</el-form-item>
<el-form-item>
<el-select v-model="searchObj.level" clearable placeholder="讲师头衔">
<el-option :value="1" label="高级讲师" />
<el-option :value="2" label="首席讲师" />
</el-select>
</el-form-item>
<el-form-item label="添加时间">
<el-date-picker
v-model="searchObj.begin"
type="datetime"
placeholder="选择开始时间"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="00:00:00"
/>
</el-form-item>
<el-form-item>
<el-date-picker
v-model="searchObj.end"
type="datetime"
placeholder="选择截止时间"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="00:00:00"
/>
</el-form-item>
<el-button type="primary" icon="el-icon-search" @click="fetchData()">查询</el-button>
<el-button type="default" @click="resetData()">清空</el-button>
</el-form>
<!-- 表格 -->
<el-table
v-loading="listLoading"
:data="list"
element-loading-text="数据加载中"
border
fit
highlight-current-row
>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">{{ (page - 1) * limit + scope.$index + 1 }}</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="80" />
<el-table-column label="头衔" width="80">
<template slot-scope="scope">{{ scope.row.level===1?'高级讲师':'首席讲师' }}</template>
</el-table-column>
<el-table-column prop="intro" label="简介" />
<el-table-column prop="gmtCreate" label="添加时间" width="160" />
<el-table-column prop="sort" label="排序" width="60" />
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<router-link :to="'/edu/teacher/edit/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit">修改</el-button>
</router-link>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeDataById(scope.row.id)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
layout="total, prev, pager, next, jumper"
@current-change="fetchData"
/>
</div>
</template>
<script>
import teacher from '@/api/edu/teacher'
export default {
data(){
listLoading:true,// 显示loading信息
list:null,// 数据列表
total:0,// 总记录数
page:1,// 页码
limit:10,// 每页记录数
searchObj:{} // 查询条件
},
created(){//当页面加载完成还未渲染时调用
this.fetchData();
},
methods:{
//查询讲师列表
fetchData(){
console.log('加载列表')
teacher.getPageList(this.page,this.limit,this.searchObj).then(response=>{
console.log(response)
if(response.success === true){
this.list = response.data.rows
this.total = response.data.total
}
this.listLoading = false //不显示loading信息
})
},
//清空数据
resetData(){
this.searchObj={}
this.fetchData();
},
//删除讲师
removeDataById(id){
this.$confirm('此操作将永久删除该文件,是否继续','提示',{
confirmButtonText:'确定',
cancelButtonText:'取消',
type:'warning'
}).then(()=>{
return teacher.removeById(id);
}).then(()=>{
this.fetchData();
this.$message({
type:'success',
message:'删除成功'
})
}).catch(()=>{
this.$message({
type:'info',
message:'已取消删除'
})
})
}
}
}
</script>
5、在
src/router/index.js
中配置讲师路由。
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '../views/layout/Layout'
export const constantRouterMap = [{
path: '/login',
component: () =>
import ('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () =>
import ('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: 'Dashboard',
children: [{
path: 'dashboard',
component: () =>
import ('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
// 讲师路由管理!
{
path: '/edu/teacher',
component: Layout,
redirect: '/edu/teacher/list',
name: 'Teacher',
meta: {
title: '讲师管理'
},
children: [{
path: 'list',
component: () =>
import ('@/views/edu/teacher/list'),
meta: {
title: '讲师列表'
}
},
{
path: 'create',
component: () =>
import ('@/views/edu/teacher/form'),
meta: {
title: '添加讲师'
},
{
path:'edit/:id',
name:'EduTeacherEdit',
component:()=>
import('@/views/edu/teacher/form'),
meta:{
title:'编辑讲师',
noCache:true
},
hidden:true
}}]
},
{ path: '*', redirect: '/404', hidden: true }
]
export default new Router({
// mode: 'history', //后端支持可开
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
6、在终端中输入命令
npm run dev
启动项目进行测试即可。
总结
1、后台接口如果部署多台服务器或多个端口时,可以使用Nginx来对前端的请求进行分发。
2、前端业务代码编写顺序:
env.js->api->views->router
3、当页面路由变化,而数据需要刷新时,可以使用watch来进行监视路由变化,一旦路由发生变化则执行指定的更新操作
4、后台接口最好进行统一异常处理,这样做的好处是可以让所有开发人员的错误结构保持一致。