分享

项目之前后端分离及导航栏标签列表(7)

 海拥 2021-11-30

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相关的值,而不是提交字符串的值!假设某标签的id8,名称是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>>

在前端页面中,参考“标签”的做法,显示“老师”的下拉列表。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多