24. 前后端分离
前端:客户端,例如网页及相关组件都是属于前端开发领域;
后端:服务器端;
在传统的开发模式下,当服务器端处理了某种请求后,就会执行转发或重定向操作,使得客户端的浏览器访问另一个页面,整个开发过程,或需要开发的组件都是由服务器端开发人员完成的(即使使用到了前端的网页技术,甚至有专门的人员开发网页,最终也需要整合到服务器端的项目中,从项目的角度来看,并没有分离)。
如需希望实现前后端分离,首先,就要使得服务器端不会过度甚至根本就不依赖网页,当处理了客户端的请求后,直接将相关数据响应到客户端去,完全不关心数据如何显示的问题,各客户端发出请求后将收到这些数据,然后自行根据客户端技术进行处理即可。
使用前后端分离的做法,可以使得开发人员是分离的,即前端开发人员开发前端的产品,后端开发人员开发服务器端需要实现的功能,分工明确,同时,由于后端不再处理页面显示,不需要使用到网页,在处理请求后,响应时,响应的数据内容将更加少,则传输数据耗时更短,流量开销更小,用户体验更好,同时,这种模式更加适用于多种不同的客户端。
简单来说:前后端分离的典型特征就是“服务器端处理完请求后,不再关心数据的呈现的问题,只是单纯的将数据响应到客户端,由客户端自行处理数据的显示”。
在前后端分离的做法中,后端负责提供“接口”,此“接口”表示一种对接的方式,通常表现为服务器端项目中的控制器组件,它负责与前端进行“对接”,前端只需要根据后端的约定(请求路径、请求参数、请求类型等)来提交请求,就可以得到某种数据结果,前端根本不需要关心后端是如何实现这些功能的,当然,后端也不会向前端暴露实现的细节,基于这样的特点,后端提供的数据处理功能,对于前端来说,也是API。
通常,如果服务器端向客户端提供API接口,在URL中通常会体现出相关的字样,例如:
@RestController
@RequestMapping ( "/api/v1/users" )
public class UserController {
}
关于请求路径的设计,并没有绝对的要求,也没有相关的规定,如果自己没有设计URL的思路,可以采取:
请求某种类型的数据列表:/api/v1/users
请求某种类型的某个数据:/api/v1/users/9527
请求操作某种类型的某个数据:/api/v1/users/9527/update
大致原则是:
访问数据列表时,如果访问列表的方法非常单一(例如用户列表,通常就只有1种显示条件,而商品列表却可以有很多种条件),在设计URL时,数据种类名称使用复数,右侧不再添加任何字符串; 访问某条数据时,在以上基础上,在右侧添加数据的唯一标识,通常是数据的id,例如:/api/版本/数据种类/id
; 对某种数据进行操作时,在以上基础上,在右侧添加需要执行的命令,例如:/api/版本/数据种类/id/数据操作
; 以上设计方式仅供参考。
25. 显示导航栏标签列表-持久层
从tag数据表中查询数据,就可以获取标签的数据列表,需要执行的SQL语句大致是:
SELECT id, name FROM tag ORDER BY id
为了更直接的封装查询结果,应该先在cn.tedu.straw.portal.vo
包下创建TagVO
类,用于封装以上查询的结果:
package cn. tedu. straw. portal. vo ;
@Data
public class TagVO {
private Integer id;
private String name;
}
由于使用了新的数据类型封装查询结果,就需要自行编写持久层的功能!
先在TagMapper
接口中添加抽象方法:
/**
* 查询所有标签数据
*
* @return 所有标签数据的列表
*/
List < TagVO > findAll ( ) ;
再在TagMapper.xml
中配置抽象方法映射的SQL语句:
< select id = " findAll" resultType = " cn.tedu.straw.portal.vo.TagVO" >
SELECT
id, name
FROM
tag
ORDER BY
id
</ select>
创建TagMapperTests
测试类,进行单元测试:
package cn. tedu. straw. portal. mapper ;
@SpringBootTest
@Slf4j
public class TagMapperTests {
@Autowired
TagMapper mapper;
@Test
void findAll ( ) {
List < TagVO > tags = mapper. findAll ( ) ;
log. debug ( "tags count = {}" , tags. size ( ) ) ;
for ( TagVO tag : tags) {
log. debug ( ">>> tag : {}" , tag) ;
}
}
}
26. 显示导航栏标签列表-业务层
在ITagService
中添加抽象方法:
public interface ITagService extends IService < Tag > {
/**
* 获取标签列表
* @return 标签列表
*/
List < TagVO > getTags ( ) ;
/**
* 获取缓存的标签列表
* @return 缓存的标签列表
*/
List < TagVO > getCachedTags ( ) ;
}
在TagServiceImpl
中实现抽象方法:
@Service
@Slf4j
public class TagServiceImpl extends ServiceImpl < TagMapper , Tag > implements ITagService {
@Autowired
private TagMapper tagMapper;
/**
* 缓存的标签列表
*/
private List < TagVO > tags = new CopyOnWriteArrayList < > ( ) ;
@Override
public List < TagVO > getTags ( ) {
// 判断有没有必要锁住代码
if ( tags. isEmpty ( ) ) {
// 锁住代码
synchronized ( tags) {
// 判断有没有必要重新加载数据
if ( tags. isEmpty ( ) ) {
tags. addAll ( tagMapper. findAll ( ) ) ;
log. debug ( "create tags cache ..." ) ;
log. debug ( ">>> tags : {}" , tags) ;
}
}
}
return tags;
}
@Override
public List < TagVO > getCachedTags ( ) {
return tags;
}
}
以上做法是一种缓存的做法,由于Spring管理对象是单例的,所以,当项目运行时,以上TagServiceImpl
类的对象只会存在1个,其中的tags
属性肯定也只会有1个,并且,Spring通过单例状态管理的对象是常驻内存的,所以,tags
存储的数据会一直在内存中,并不会消失,就起到了“缓存”的作用,当频繁获取标签数据时,都直接将tags
数据返回即可,并不需要反复查询数据库!
当然,使用了以上缓存后,每次获取标签数据时,都是获取的以上缓存数据,即使数据库的数据被修改了,以上缓存也不会更新,就会导致获取到的数据不准确!可以在数据发生变化后将缓存清空,则缓存数据会重新加载,缓存中的数据就是新的数据了!也可以使用定时更新的机制,也就是每间隔一定的时间,自动将缓存中的数据清空,则下次尝试访问数据时,由于缓存中没有数据,就会从数据库中进行查询,从而得到新的、准确的数据!
关于定期清空缓存,可以使用计划任务来实现:
@Component
@EnableScheduling
@Slf4j
public class CacheSchedule {
@Autowired
private ITagService tagService;
@Scheduled ( initialDelay = 10 * 60 * 1000 , fixedRate = 10 * 60 * 1000 )
public void clearCache ( ) {
tagService. getCachedTags ( ) . clear ( ) ;
log. debug ( "clear tags cache ..." ) ;
}
}
创建TagServiceTests
测试类,进行测试:
@SpringBootTest
@Slf4j
public class TagServiceTests {
@Autowired
ITagService service;
@Test
void getTags ( ) {
List < TagVO > tags = service. getTags ( ) ;
log. debug ( "tags count = {}" , tags. size ( ) ) ;
for ( TagVO tag : tags) {
log. debug ( ">>> tag : {}" , tag) ;
}
}
}
27. 显示导航栏标签列表-控制器层
由于现在发出请求后,需要响应数据到客户端,所以,在表示响应结果的R
类中,需要添加新的属性用于表示“响应到客户端的数据”,用户提交不同的请求时,期望得到的数据可能是不同的,例如,可能希望得到当前用户的信息,或当前用户发布的提问的列表,或当前用户的收藏列表等,所以,在声明“数据”的类型时,要么使用Object
,可以表示任何类型,要么使用泛型,使用时再决定具体的类型!
以使用泛型为例,在R
类中添加属性:
private T data;
由于类中使用了泛型的占位符,必须在类的声明中也补充声明占位符:
public class R < T > {
}
同时,为了更加快捷的响应结果,还添加方法:
public static < T > R ok ( T data) {
return new R < T > ( ) . setState ( State . OK) . setData ( data) ;
}
然后,在TagController
中,处理请求:
@RestController
@RequestMapping ( "/api/v1/tags" )
public class TagController {
@Autowired
private ITagService tagService;
// http://localhost:8080/api/v1/tags
// http://localhost:8080/api/v1/tags/
@GetMapping ( "" )
public R < List < TagVO > > getTags ( ) {
return R . ok ( tagService. getTags ( ) ) ;
}
}
28. 显示导航栏标签列表-前端页面
先将static 下的question 文件夹拖拽到templates 文件夹下,拖拽时弹出的对话框中不要勾选任何选项,直接确定即可。
在SystemController
中添加处理请求的方法,以转发页面:
@GetMapping ( "/question/create.html" )
public String createQuestion ( ) {
return "question/create" ;
}
完成后,通过http://localhost:8080/question/create.html即可打开“发表提问”的页面。
在页面的顶部导航区域,需要显示问题的标签列表。
在question/create.html 页面中:
约184行:为<div>
添加id="navTagsApp"
约187行:为<a>
添加v-for="tag in tags"
,为<small>
添加v-text="tag.name"。
以上v-for
是用于遍历的,添加在<a>
标签上,就会遍历生成当前<a>
标签的全部代码,其表达式中tag in tags
表示在Vue中存在名为tags
的数据,该数据应该是数组类型的,在遍历过程中,每个数组元素都使用tag
作为名称,该语法可参考Java语法中的增强for循环;以上v-text
是用于绑定<small>
标签中将要显示的文本,由于它在<a>
标签的内部,所以可以访问到遍历过程中得到的tag
数据,服务器端向客户端响应的“标签Tag”数据中既包含id
也包括name
,此处需要显示name
,所以表达式的值是tag.name
。
当前页面中,显示导航栏的标签列表的操作是多个页面都需要使用的,为了便于统一使用,应该将相关的JS代码写在独立的.js
文件中,则多个页面都可以引用该文件!
在static 文件夹下默认就存在js 文件夹,该文件夹中已经存在一些测试使用的JS文件,先将这些文件全部删除!然后,在js 文件夹,创建commons 文件夹,并在这个文件夹中创建nav_tags.js 文件。
先在nav_tags.js 中编写测试代码:
let navTagsApp = new Vue ( {
el: '#navTagsApp' ,
data: {
tags: [
{ id: 1 , name: '第1阶段' } ,
{ id: 2 , name: '第2阶段' } ,
{ id: 3 , name: '第3阶段' } ,
{ id: 4 , name: '第4阶段' }
]
}
} ) ;
并在create.html 中引用以上js文件:
< script src = " /js/commons/nav_tags.js" > </ script>
测试无误后,完整的JS代码:
let navTagsApp = new Vue ( {
el: '#navTagsApp' ,
data: {
tags: [
{ id: 1 , name: '第1阶段' } ,
{ id: 2 , name: '第2阶段' } ,
{ id: 3 , name: '第3阶段' } ,
{ id: 4 , name: '第4阶段' }
]
} ,
methods: {
loadTags : function ( ) {
$. ajax ( {
url: '/api/v1/tags' ,
type: 'get' ,
dataType: 'json' ,
success : function ( json ) {
navTagsApp. tags = json. data;
}
} ) ;
}
} ,
created : function ( ) {
this . loadTags ( ) ;
}
} ) ;
29. 发布问题表单中显示标签下拉列表
在question/create.html 中,第209行,将原有的<select>
标签整个改为:
< v-select :options = " tags" v-model = " selectedTags"
multiple required placeholder = " 请选择问题的分类标签(可多选)" >
</ v-select>
第190行,将<div>
标签的原id="app"
改为id="createQuestionApp"。
在js 文件夹下创建question 文件夹,并在这个文件夹中创建create.js 文件,用于编写当前页面中需要执行的代码。
在create.html 中引用以上新创建的js文件:
< script src = " /js/question/create.js" > </ script>
接下来,在create.js 中添加测试代码:
Vue. component ( 'v-select' , VueSelect. VueSelect) ;
let createQuestionApp = new Vue ( {
el: '#createQuestionApp' ,
data: {
tags: [ 'Spring' , 'SpringMVC' , 'MyBatis' , 'SpringBoot' ] ,
selectedTags: [ ]
}
} ) ;
完成后,重新打开页面,在问题标签的下拉列表中就可以看到以上定义的4个选项。
一般情况下,客户端向服务器提交数据时,可以选择的话,应该尽量提交id
相关的值,而不是提交字符串的值!假设某标签的id
是8
,名称是SpringBoot
,最终客户端提交数据时,应该将8
提交到服务器端,而不是把SpringBoot
提交到服务器端!
以上tags
的值是字符串数组,最终提交时,selectedTags
中也会是字符串数据!应该生成列表项时,为每个标签数据指定id
,以保证用户选中某些选项后,可以获取这些标签数据的id,最终才可以将这些id提交到服务器端!
为v-select
绑定的:options
就是列表项数据,该数据可以是JSON对象的数组,默认情况下,每个JSON对象中的label
属性表示列表项显示的文本,value
属性表示将要提交的值,所以,可以将以上测试代码改为:
Vue. component ( 'v-select' , VueSelect. VueSelect) ;
let createQuestionApp = new Vue ( {
el: '#createQuestionApp' ,
data: {
tags: [
{ label: 'MyBatis Plus' , value: 1 } ,
{ label: 'Spring Security' , value: 2 } ,
{ label: 'Spring Validation' , value: 3 } ,
{ label: 'Lombok' , value: 4 } ,
{ label: 'Vue' , value: 5 }
] ,
selectedTags: [ ]
}
} ) ;
作业
1. 显示真实的问题标签到下拉列表
提示:当从服务器端获取到数据后,对数据进行遍历,可以:
for ( let i = 0 ; i < json. data. length; i++ ) {
let op = {
label: json. data[ i] . name,
value: json. data[ i] . id
} ;
tags[ i] = op;
}
2. 显示老师列表到下拉列表
需要从持久层到业务层,到控制器层,到前端页面,层层开发,每开发一层,及时测试。
查询老师列表的SQL语句:
select id, nickname, gender, phone from user where type=1 order by id;
先创建TeacherVO
类。
在UserMapper
接口中添加:
List < TeacherVO > findTeachers ( ) ;
在UserMapper.xml
中配置映射。
在UserMapperTests
中测试。
在IUserService
中添加:
List < TeacherVO > findTeachers ( ) ;
List < TeacherVO > findCachedTeachers ( ) ;
在UserServiceImpl
中实现以上2个方法,实现过程可参考TagServiceImpl
。
在CacheSchedule
的计划任务中,清除Tag
数据缓存时,一并清除Teacher
数据缓存。
在UserServiceTests
中测试。
将请求路径设计为http://localhost:8080/api/v1/users/teacher/list,处理请求的方法的返回值是R<List<TeacherVO>>
。
在前端页面中,参考“标签”的做法,显示“老师”的下拉列表。