前后端分离简介
前后端分离
前后端分离就是将⼀个应⽤的前端代码和后端代码分开写,为什么要这样做?
如果不使⽤前后端分离的⽅式,会有哪些问题?
传统的 Java Web 开发中,前端使⽤ JSP 开发,JSP 不是由后端开发者来独⽴完成的。
前端—》HTML 静态⻚⾯ —〉后端 —》JSP
这种开发⽅式效率极低,可以使⽤前后端分离的⽅式进⾏开发,就可以完美地解决这⼀问题。
前端只需要独⽴编写客户端代码,后端也只需要独⽴编写服务端代码提供数据接⼝即可。
前端通过 Ajax 请求来访问后端的数据接⼝,将 Model 展示到 View 中即可。
前后端开发者只需要提前约定好接⼝⽂档(URL、参数、数据类型…),然后分别独⽴开发即可,前端
可以造假数据进⾏测试,完全不需要依赖于后端,最后完成前后端集成即可,真正实现了前后端应⽤的
解耦合,极⼤地提升了开发效率。
单体—》前端应⽤ + 后端应⽤
前端应⽤:负责数据展示和⽤户交互。
后端应⽤:负责提供数据处理接⼝。
前端 HTML —》Ajax —〉RESTful 后端数据接⼝。
传统的单体应⽤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkFL4q5b-1610345829474)(C:\Users\王东梁\AppData\Roaming\Typora\typora-user-images\image-20210111112048520.png)]
前后端分离的结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q5usU00y-1610345829478)(C:\Users\王东梁\AppData\Roaming\Typora\typora-user-images\image-20210111112106144.png)]
前后端分离就是将⼀个单体应⽤拆分成两个独⽴的应⽤,前端应⽤和后端应⽤以 JSON 格式进⾏数据交互。
实现技术
Spring Boot + Vue
使⽤ Spring Boot 进⾏后端应⽤开发,使⽤ Vue 进⾏前端应⽤开发。
Vue + Element UI
Vue 集成 Element UI
Element UI 后台管理系统主要的标签:
el-container:构建整个⻚⾯框架。
el-aside:构建左侧菜单。
el-menu:左侧菜单内容,常⽤属性:
:default-openeds:默认展开的菜单,通过菜单的 index 值来关联。
:default-active:默认选中的菜单,通过菜单的 index 值来关联。
el-submenu:可展开的菜单,常⽤属性:
index:菜单的下标,⽂本类型,不能是数值类型。
template:对应 el-submenu 的菜单名。
i:设置菜单图标,通过 class 属性实则。
el-icon-messae
el-icon-menu
el-icon-setting
el-menu-item:菜单的⼦节点,不可再展开,常⽤属性:
index:菜单的下标,⽂本类型,不能是数值类型。
Vue router 来动态构建左侧菜单
导航1
⻚⾯1
⻚⾯2
导航2
⻚⾯3
⻚⾯4
menu 与 router 的绑定
1、 标签添加 router 属性。
2、在⻚⾯中添加 标签,它是⼀个容器,动态渲染你选择的 router。
3、 标签的 index 值就是要跳转的 router。
Element UI 表单数据校验
定义 rules 对象,在 rules 对象中设置表单各个选项的校验规则
required: true, 是否为必填项
message: ‘error’, 提示信息
trigger: ‘blur’,触发事件
Demo最终效果:
项目结构:
环境准备
- jdk 1.8
- maven 3.6.3
- IntelliJ IDEA 2019.3
- 安装vue.js插件
- 安装vue.js插件
- SpringBoot *2.2.2* .RELEASE
- @vue/cli 4.2.2 (+ node.js)
👉 Vue CLI3开发环境搭建
新建Vue工程
-
命令行输入
vue ui
( vue3+版本才有)
-
项目创建完成后添加插件element UI
-
用IDEA打开刚才创建的Vue项目,在IDEA的命令行终端输入
npm run serve
启动Vue ( ctrl + c 停止vue )即可访问项目首页 http://localhost:8080
三、新建Spring Boot工程
- 创建Spring Boot工程需要的组件:
Lombok用于自动生成各种属性的 Setter 和 Getter 方法,省去手动生成。 - 在数据库library中创建book表
DROP TABLE IF EXISTS `book`;
/*!40101 SET @saved_cs_client = @@character_set_client */;SET character_set_client = utf8mb4 ;
CREATE TABLE `book` (`id` int(10) NOT NULL AUTO_INCREMENT,`name` varchar(20) DEFAULT NULL,`author` varchar(20) DEFAULT NULL,`publish` varchar(20) DEFAULT NULL,`pages` int(10) DEFAULT NULL,`price` float(10,2) DEFAULT NULL,`bookcaseid` int(10) DEFAULT NULL,`abled` int(10) DEFAULT NULL,PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=119 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;--
-- Dumping data for table `book`
--LOCK TABLES `book` WRITE;
/*!40000 ALTER TABLE `book` DISABLE KEYS */;
INSERT INTO `book` VALUES (1,'解忧杂货店','东野圭吾','电子工业出版社',102,27.30,9,1),
(2,'追风筝的人','卡勒德·胡赛尼','中信出版社',330,26.00,1,1),
(3,'人间失格','太宰治','作家出版社',150,17.30,1,1),
(4,'这就是二十四节气','高春香','电子工业出版社',220,59.00,3,1),
(5,'白夜行','东野圭吾','南海出版公司',300,27.30,4,1),
(6,'摆渡人','克莱儿·麦克福尔','百花洲文艺出版社',225,22.80,1,1),
(7,'暖暖心绘本','米拦弗特毕','湖南少儿出版社',168,131.60,5,1),
(8,'天才在左疯子在右','高铭','北京联合出版公司',330,27.50,6,1),
(9,'我们仨','杨绛','生活.读书.新知三联书店',89,17.20,7,1),
(10,'活着','余华','作家出版社',100,100.00,6,1),
(11,'水浒传','施耐庵','三联出版社',300,50.00,1,1),
(12,'三国演义','罗贯中','三联出版社',300,50.00,2,1),
(13,'红楼梦','曹雪芹','三联出版社',300,50.00,5,1),
(14,'西游记','吴承恩','三联出版社',300,60.00,3,1);
/*!40000 ALTER TABLE `book` ENABLE KEYS */;
UNLOCK TABLES;
删除mould。。。本地从新安装
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-46CTfKkF-1610345829481)(C:\Users\王东梁\AppData\Roaming\Typora\typora-user-images\image-20210110170030932.png)]
根据Element官网给出的样式实例代码进行相关修改
修改后的App.Vue
如下
<template><div id="app"><el-container style="height: 500px; border: 1px solid #eee"><el-aside width="200px" style="background-color: rgb(238, 241, 246)"><el-menu :default-openeds="['1', '3']"><el-submenu index="1"><template slot="title"><i class="el-icon-message"></i>导航一</template><el-menu-item-group><template slot="title">分组一</template><el-menu-item index="1-1">选项1</el-menu-item><el-menu-item index="1-2">选项2</el-menu-item></el-menu-item-group></el-submenu> </el-menu></el-aside><el-main><router-view></router-view></el-main></el-container></div>
</template><style>.el-header {background-color: #B3C0D1;color: #333;line-height: 60px;}.el-aside {color: #333;}
</style><script>export default {data() {const item = {date: '2016-05-02',name: '王小虎',address: '上海市普陀区金沙江路 1518 弄'};return {tableData: Array(20).fill(item)}}};
</script>
新建index.vue
<template><router-view></router-view>
</template><script>export default {name: "Index"}
</script><style scoped></style>
App.Vue
实现整体框架(侧边栏),每次页面跳转的时候这个框架的样式是不变的.App.vue
中的router-view
套index
,index
的router-view
套其他界面,这样才能保证侧边栏一直存在。此后的每次页面跳转都只更改index的router-view
中的内容
menu 与 router的绑定,实现页面跳转
- 新建前端页面
在views中新建
BookManager.vue
(查询图书页面)、BookUpdate.vue
(图书信息修改页面)、AddBook.vue
(添加图书页面)`
- 配置路由(router/
index.js
)
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from "../views/Index";
import BookManager from "../views/BookManager";
import AddBook from "../views/AddBook";
import BookUpdate from "../views/BookUpdate";Vue.use(VueRouter)const routes = [{path: '/',name: '图书管理',show: true,component: Index,/* 首页地址直接跳转到/BookManager */redirect: "/BookManager",children: [{path: '/BookManager',name: '查询图书',component: BookManager},{path: '/AddBook',name: '添加图书',component: AddBook},]},{path: '/BookUpdate',// name: '修改图书',component: BookUpdate,show: false//开始时候先不显现出来}]const router = new VueRouter({mode: 'history',base: process.env.BASE_URL,routes
})export default router
- 修改
App.vue
, 实现menu 与 router的绑定(页面跳转)
- 标签添加router属性
- 在index页面中添加route-view标签,他是一个容器,动态渲染你选择的router
- el-menu-item标签的index值就是要跳转的router
<template><div id="app"><el-container style="height: 700px; border: 1px solid #eee"><el-aside width="200px" style="background-color: rgb(238, 241, 246)"><el-menu router :default-openeds="['0', '1']"><!--循环遍历routes中的对象,有几个对象就有几个item--><!--添加index属性标识不同的submenu--><!--默认的index是字符串类型,而不是整数类型,多以需要转化为字符类型--><!--v-if=item.show 表示根据路由配置中的show:ture/false决定是否显示该菜单 --><el-submenu v-for="(item,index) in $router.options.routes" :index="index+''" v-if="item.show"><template slot="title"><i class="el-icon-setting"></i>{{item.name}}</template><el-menu-item v-for="(item2,index2) in item.children" :index="item2.path":class="$route.path==item2.path?'is-active':''">{{item2.name}}</el-menu-item></el-submenu></el-menu></el-aside><el-main><router-view></router-view></el-main></el-container></div>
</template>
前后端信息传递,显示图书信息
- JPA 连接数据库配置
application.yml
spring:datasource:# 连接数据库libraryurl: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjpa:# 打印sqlshow-sql: trueproperties:hibernate:# 格式化sqlformat_sql: trueserver:port: 8181
vue已经默认占用了8080端口,修改springboot的端口为8181防止冲突
- 整合SpringData JPA, 编写一个实体类(bean)和数据表进行映射
在spring boot项目中新建实体类 entity/Book.java
/*** 使用Entity将该类与数据库表绑定,根据类名和表名经行绑定(默认类名小写就是表名);** Data是lombok插件的方法,自动帮我们生成各种setter和getter方法*/
@Entity
@Data
public class Book {// 和数据库表中的id绑定,表示这是主键@Id// 设置主键Id自增@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;private String name;private String author;private String publish;private Integer pages;private Integer price;
}
- 编写一个Dao接口来操作实体类对应的数据表(Repository)
新建 repository/ BookRepository.java
Spring Data 提供了统一的 repository 接口,实现了数据库的相关基本操作
继承 JpaRepository
来完成对数据库的操作
public interface BookRepository extends JpaRepository<Book, Integer> {}
- 新建Controller层处理浏览器请求
controller/ BookHandler.java
/*** RestController = @Controller + @ResponseBody* Controller 将当前修饰的类注入SpringBoot IOC容器,使得从该类所在的项目跑起来的过程中,这个类就被实例化。* 当然也有语义化的作用,即代表该类是充当Controller的作用* ResponseBody 该类中所有的API接口返回的数据都会以Json字符串的形式返回给客户端** RequestMapping 提供路由信息,负责URL到Controller中的具体函数的映射。服务器发送 /book 请求时执行该类** 通过Handler数据才能调给前端使用*/
@RestController
@RequestMapping("/book")
public class BookHandler {@Autowiredprivate BookRepository bookRepository;/*** 分页显示** GetMapping是一个组合注解 是@RequestMapping(method = RequestMethod.GET)的缩写* page/size 表示从第几页开始,每页几个*/@GetMapping("/findAll/{page}/{size}")public Page<Book> findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size){PageRequest request = PageRequest.of(page,size);return bookRepository.findAll(request);}
}
运行该项目
http://localhost:8181/book/findAll/0/6
表示从第0页开始,显示6个数据
- 前端发送Ajax请求8181端口获取后端数据
通过 axios 组件请求 Ajax
axios 是目前应用最为广泛的 Ajax 封装库
- 终止vue服务后 在命令行输入
vue add axios
安装axios
- 修改
BookManager.vue
- created() 方法页面一打开就会调用
- page() 方法实现点击页数跳转
<el-table-columnfixed="right"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><!--delete是关键字,不能自定义--></template></el-table-column> <el-paginationbackgroundlayout="prev, pager, next":page-size="pageSize":total="total"@current-change="page"><!--点击页数跳转--></el-pagination><script>export default {methods: {page(currentPage){const _this = this<!--通过axios发送ajax 请求后端数据-->axios.get('http://localhost:8181/book/findAll/'+(currentPage-1)+'/10').then(function(resp){console.log(resp)_this.tableData = resp.data.content_this.pageSize = resp.data.size_this.total = resp.data.totalElements})}},data() {return {pageSize: '',total: '',tableData: []}},created(){const _this = thisaxios.get('http://localhost:8181/book/findAll/0/10').then(function(resp){console.log(resp)_this.tableData = resp.data.content_this.pageSize = resp.data.size_this.total = resp.data.totalElements})}}
</script>
访问 http://localhost:8080/BookManager
(刷新浏览器)
查看控制台,出现跨域问题,前端无法获取后端数据
6. 解决跨域问题
在后端解决跨域问题:
新建配置类 config/ CrosConfig.java
/*** 解决跨域问题*/
@Configuration
public class CrosConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").allowCredentials(true).maxAge(3600).allowedHeaders("*");}
}
修改
流程:
- 前端主页面点击修改按钮
- 后端根据id查询此图书信息
findById
- 前端发送ajax请求获取数据并跳转到修改页面
BookUpdate.vue
- 前端传递修改后的信息给后端
update
函数处理 - 返回主页面
BookHandler.hava/*** 根据Id查询图书信息*/@GetMapping("/findById/{id}")public Book findById(@PathVariable("id") Integer id){return bookRepository.findById(id).get();}/*** 修改图书信息** PUT请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来改资源)* Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增资源)*/@PutMapping("/update")public String update(@RequestBody Book book){Book result = bookRepository.save(book);if(result != null){return "success";}else{return "error";}}
BookUpdate.vue
核心代码
<el-form-item><el-button type="primary" @click="submitForm('ruleForm')">修改</el-button><el-button @click="resetForm('ruleForm')">重置</el-button></el-form-item><script>export default {data() {return {// 数据ruleForm: {id: '',name: '',author: '',publish: '',pages: '',price: '',},//校验规则rules: {name: [{ required: true, message: '图书名称不能为空', trigger: 'blur' }],author: [{ required: true, message: '作者姓名不能为空', trigger: 'blur' }],publish: [{ required: true, message: '出版社不能为空', trigger: 'blur' }],price: [{ required: true, message: '价格不能为空', trigger: 'blur' }],}};},methods: {submitForm(formName) {const _this = thisthis.$refs[formName].validate((valid) => {if (valid) {/*put和后端的PutMapping对应!!!*/axios.put('http://localhost:8181/book/update', this.ruleForm).then(function(resp){if(resp.data == 'success'){// 消息弹出框---- element官网找个模板直接复制_this.$alert('《' + _this.ruleForm.name + '》修改成功!', '提示', {confirmButtonText: '确定',callback: action => {// 跳转界面_this.$router.push('/BookManager')}});}})} else {return false;}});},resetForm(formName) {this.$refs[formName].resetFields();}},created() {const _this = thisaxios.get('http://localhost:8181/book/findById/' + this.$route.query.id ).then(function(resp){_this.ruleForm = resp.data})}}
</script>
添加
BookHandler.java/*** 添加图书** RequestBody 通过映射把前端Json数据转化成java对象*/@PostMapping("/add")public String save(@RequestBody Book book){Book result = bookRepository.save(book);if(result != null){return "success";}else{return "error";}}AddBook.vue
<el-form-item><el-button type="primary" @click="submitForm('ruleForm')">提交</el-button><el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>methods: {submitForm(formName) {const _this = thisthis.$refs[formName].validate((valid) => {if (valid) {axios.post('http://localhost:8181/book/add', this.ruleForm).then(function(resp){if(resp.data == 'success'){// 消息弹出框---- element官网找个模板直接复制_this.$alert('《' + _this.ruleForm.name + '》添加成功!', '提示', {confirmButtonText: '确定',callback: action => {// 跳转界面_this.$router.push('/BookManager')}});}})} else {return false;}});},resetForm(formName) {this.$refs[formName].resetFields();}}
删除
删除之后直接返回主界面
BookHandler.java
@DeleteMapping("/delete/{id}")public void delete(@PathVariable("id") Integer id){bookRepository.deleteById(id);}BookManger.vue
<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><!--delete是关键字,不能自定义-->
</template>deleteBook(row){const _this = thisaxios.delete('http://localhost:8181/book/delete/' + row.id).then(function(resp){// 消息弹出框---- element官网找个模板直接复制_this.$alert('《' + row.name + '》删除成功!', '提示', {confirmButtonText: '确定',callback: action => {// 动态刷新此页面window.location.reload()}});})},