前后端分离
1.将应用的前后端代码分开写
前后端分离的背景
- 传统的javaweb开发前端使用jsp开发,jsp页面是由前端工作人员学出—>html静态页面---->后端—>jsp,当前端出现问题时,页面就要再交付给前端,而前端看到的东西已经不是之前看到的了,从而导致冲突,影响开发效率。
- 前后端耦合度太高,使用前后端分离方式进行开发,使得前端只需要编写客户端代码,后端只需要编写服务端代码,提供数据接口即可,前端通过ajax请求访问后端数据接口,将model展示到客户端即可
- 解决方式,前后端分离,前后端开发者只需要约定好接口文档(URL,参数,参数类型),独立开发即可,前端可以造假数据进行测试,完全不需要后端,两个独立开发,最终对接即可,真正实现了前后端应用的解耦合,极大的提升了开发效率。
- 单体应用—>纯前端应用和后端应用,前端应用:数据展示和用户交互,后端应用:负责处理数据接口,前端的html通过ajax,基于restful的后端接口
传统单体应用
基于前后端分离的方式
总结:请后端分离就是将一个单体应用拆分成两个独立的应用。前端应用于后端应用通过json格式的数据进行交互
技术栈:springboot+vue
springboot:后端应用开发
vue:前端应用开发
Element ui 后台管理系统主要标签
-
el-container:构建整个框架 -
el-aside:构建左侧菜单 -
el-menu:构建左侧菜单内容,常用属性 ? :default-openeds 默认展开菜单,通过菜单的index值关联 ? :default-active 默认选中的菜单,通过菜单的index值关联 4.el-submenu:可展开的菜单。常用属性 ? index:菜单的下标,文本类型,不能是数值类型 5.template:对应el-submenu的菜单名 6.i标签:设置菜单图标,通过class属性设置 ? el-icon-message ? el-icon-menu ? el-icon-setting 7.el-menu-item:菜单的子节点,常用属性 ? index:菜单的下标,文本类型,不能是数值类型
vue router动态加载右侧菜单
menu与router的绑定
- 标签添加router属性
- 在页面中添加标签,来动态渲染router
- 标签里的index值就是要跳转的router
Elementui表单校验
定义rules对象,在rules对象中设置表单各个选项的校验规则,具体语法如下
<el-form-item label="书名" prop="name">
rules: {
name: [
{ required: true, message: '请输入书籍名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
]
}
//给rules里面的name选项设置校验规则,name对应<el-form-item>标签的prop属性
//required: true 是否为必填项
// message: '请输入书籍名称' 提示消息
//trigger: 'blur' 触发3事件,失去焦点
项目实战
数据库表
项目技术栈
- 前端界面ui:ElementUI
- 前端技术:VUE
- 数据库:mysql
- 后端技术:springboot
- 操作数据库:springboot jpa
前端UI设计
- 页面布局:侧边栏+头部+主体
- 侧边栏的数据从vue的index.js路由页面读取
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router :default-openeds="['0','1']">
<el-submenu v-for="(item,index) in $router.options.routes" :key="index" :index="index+''">
<template slot="title"><i class="el-icon-message"></i>{{item.name}}</template>
<el-menu-item v-for="(item2,index2) in item.children" :key="index" :index="item2.path" :class="$route.path==item2.path?'is-active':''">{{item2.name}}</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
index.js路由配置
routes:[
{
path: '/',
name:'书籍列表',
component: Container,
redirect:"/page1",
children:[
{
path: '/page1',
name:'添加书籍',
component: page1
},
{
path: '/page2',
name:'修改用户',
component: page2
}
]
},
{
path: '/',
name: '课程管理',
component: Container,
children: [
{
path: '/page3',
name: '课程查看',
component: page3
},
{
path: '/page4',
name: '课程添加',
component: page4
}
]
}
]
扩展:若index.js中配置了一些必须要的路由但是侧边栏并不需要这些路由时,可以在路由里添加一个属性,通过v-if来决定是否需要展示
侧边栏具体效果:
3.展示数据UI
<template>
<div>
<el-table
:data="tableData"
border
style="width: 100%">
<el-table-column
fixed
prop="id"
label="编号"
width="150">
</el-table-column>
<el-table-column
prop="name"
label="书名"
width="120">
</el-table-column>
<el-table-column
prop="author"
label="作者"
width="120">
</el-table-column>
<el-table-column
label="操作"
width="100">
<template slot-scope="scope">
<el-button @click="edit(scope.row)" type="text" size="small">修改</el-button>
<el-button @click="deleteBook(scope.row)" type="text" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
layout="prev, pager, next"
page-size="5"
:total="total"
@current-change="page">
</el-pagination>
</div>
</template>
<script>
export default {
methods: {
edit(row) {
this.$router.push({
path:'/page3',
query:{
id:row.id
}
})
},
deleteBook(row) {
const _this = this
this.$confirm('是否确定删除《' + row.name + '》', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
_this.$axios.delete('http://localhost:8181/book/delete/' + row.id).then(function (response) {
// _this.$router.push('/page1')
window.location.reload();//动态刷新页面
}),this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
page(currenPage){
const _this = this
this.$axios.get("http://localhost:8181/book/findAll/"+currenPage+"/5").then(function (response) {
_this.tableData = response.data.content
_this.total = response.data.totalElements
})
}
},
created() {
const _this = this
this.$axios.get("http://localhost:8181/book/findAll/1/5").then(function (response) {
_this.tableData = response.data.content
_this.total = response.data.totalElements
})
},
data() {
return {
total:null,
tableData: null
}
}
}
</script>
展示数据UI的form表单通过:data=“tableData”,与data()里面的tableData动态绑定了
分页条的total属性与data里面的total动态绑定,同时点击下一页时触发了一个点击事件,从而达到页面切换效果
<el-pagination
background
layout="prev, pager, next"
page-size="5"
:total="total"
@current-change="page">
</el-pagination>
界面展示:
4.添加页面UI
<template>
<el-form style="width: 40%" :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="书名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input v-model="ruleForm.author"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
ruleForm: {
name: '',
author:''
},
rules: {
name: [
{ required: true, message: '请输入书籍名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入书籍作者', trigger: 'change' }
]
}
};
},
methods: {
submitForm(formName) {
const _this = this
this.$refs[formName].validate((valid) => {
if (valid) {
_this.$axios.post('http://localhost:8181/book/save',_this.ruleForm).then(function (response) {
// console.log(response)
if(response.data=='success'){
_this.$alert('《'+_this.ruleForm.name+'》添加成功', '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push('/page1')
}
});
// _this.$router.push('/page1')
// _this.$message('添加成功')
}
})
// alert('submit!');
console.log(_this.ruleForm)
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
添加页面实现了数据的校验,以及良好的数据提示页面,给用户良好的体验
界面展示:
5.修改页面UI
<template>
<el-form style="width: 40%" :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="书名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input v-model="ruleForm.author"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
ruleForm: {
name: '',
author:''
},
rules: {
name: [
{ required: true, message: '请输入书籍名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入书籍作者', trigger: 'change' }
]
}
};
},
methods: {
submitForm(formName) {
const _this = this
this.$refs[formName].validate((valid) => {
if (valid) {
_this.$axios.put('http://localhost:8181/book/update',_this.ruleForm).then(function (response) {
// console.log(response)
if(response.data=='success'){
_this.$alert('《'+_this.ruleForm.name+'》修改成功', '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push('/page1')
}
});
// _this.$router.push('/page1')
// _this.$message('添加成功')
}
})
// alert('submit!');
console.log(_this.ruleForm)
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
created() {
const _this = this
this.$axios.get("http://localhost:8181/book/findById/"+this.$route.query.id).then(function (response) {
console.log(response.data)
_this.ruleForm = response.data
})
// alert(this.$route.query.id)
}
}
</script>
<style scoped>
</style>
因为直接从前端获取的数据不安全,容易被篡改,所以数据统一从数据库中查询出来,展示到页面之中,同时修改界面也有良好的用户体验
界面展示:
6.删除功能:
<el-button @click="deleteBook(scope.row)" type="text" size="small">删除</el-button>
deleteBook(row) {
const _this = this
this.$confirm('是否确定删除《' + row.name + '》', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
_this.$axios.delete('http://localhost:8181/book/delete/' + row.id).then(function (response) {
// _this.$router.push('/page1')
window.location.reload();//动态刷新页面
}),this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
删除操作给用户足够的反应时间以及误操作的取消操作
界面展示:
后端接口提供
- 解决跨域数据传输问题:
package com.sunset.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CrosConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("Content-Type","X-Requested-With","accept,Origin","Access-Control-Request-Method","Access-Control-Request-Headers","token")
.allowedMethods("*")
.allowedOriginPatterns("*")
.allowCredentials(true);
}
}
2.实体类定义
package com.sunset.entity;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;@Entitypublic class Book { @Id
3.dao层定义(适用springboot jpa操作数据库)
package com.sunset.repository;import com.sunset.entity.Book;import org.springframework.data.jpa.repository.JpaRepository;public interface BookRepository extends JpaRepository<Book,Integer> {
4.service层:因为业务较为简单,所以没有写service层
5.controller层
package com.sunset.controller;import com.sunset.entity.Book;import com.sunset.repository.BookRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.web.bind.annotation.*;@RestController@RequestMapping("/book")public class BookController { @Autowired private BookRepository bookRepository; @GetMapping("/findAll/{page}/{size}") public Page<Book> findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size){ Pageable pageable = PageRequest.of(page-1,size); return bookRepository.findAll(pageable); } @PostMapping("/save") public String save(@RequestBody Book book){ Book result = bookRepository.save(book); if(result!=null){ return "success"; }else return "error"; } @GetMapping("/findById/{id}") public Book findById(@PathVariable("id") Integer id){ return bookRepository.findById(id).get(); } @PutMapping("/update") public String update(@RequestBody Book book){ Book result = bookRepository.save(book); if(result!=null){ return "success"; }else return "error"; } @DeleteMapping("/delete/{id}") public String delete(@PathVariable("id") Integer id){ bookRepository.deleteById(id); return "success"; }}
controller层统一采用restful风格编程
前后端通信
采用vue集成的axios进行数据传输,与通信
egg:修改书籍信息模块:
<el-button @click="edit(scope.row)" type="text" size="small">修改</el-button> edit(row) { this.$router.push({ path:'/page3', query:{ id:row.id } }) }
- edit事件将路由push到page3页面,同时将id传过去
- page3页面在路由刚请求的时候加载created函数,该函数与后端的接口进行数据传输,将数据保存在data里面。进行数据的双向绑定
created() { const _this = this this.$axios.get("http://localhost:8181/book/findById/"+this.$route.query.id).then(function (response) { console.log(response.data) _this.ruleForm = response.data }) // alert(this.$route.query.id) } }
- 用户根据要求重新修改数据后,点击提交按钮触发submitForm事件
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
submitForm(formName) {
const _this = this
this.$refs[formName].validate((valid) => {
if (valid) {
_this.$axios.put('http://localhost:8181/book/update',_this.ruleForm).then(function (response) {
// console.log(response)
if(response.data=='success'){
_this.$alert('《'+_this.ruleForm.name+'》修改成功', '消息', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push('/page1')
}
});
// _this.$router.push('/page1')
// _this.$message('添加成功')
}
})
// alert('submit!');
console.log(_this.ruleForm)
} else {
console.log('error submit!!');
return false;
}
});
}
- submitForm事件与后端提供的数据接口进行对接,通过axios进行数据通信、
总结
该前后端分离项目相较之下还是比较简单的,但实现了基本的CRUD操作,前端通过VUE,ElementUI进行架构,后端通过springboot,基于restful的数据接口进行对接,实现了前后端的解耦合
|