SpringBoot从零开始的问答社区 (3)首页-Thymeleaf模板引擎
代码仓库
Github->https://github.com/noahlin27/Astera
Gitee->Astera: Astera project for Spring Boot
首页主要分为两个部分,一个是网站的导航栏,一个是问题帖的动态列表。我们先把较为简单的动态列表实现,常见问答社区的首页主要是一些热门的帖子,这里先用数据库里最新的帖子来代替。
开发新功能模块
数据库
通常每次开发一个新的模块,都是按照一定的顺序的,而从数据库的设计开始几乎是毋庸置疑的。一般来说,一个问题帖子会包含:标题,内容,发帖作者,发帖时间,点赞,评论,收藏等,所以我设计的字段有:id, title, content, user_id, create_time, comment_count。作者的信息都存在 User 表里了,只需要用 user_id 去查;点赞功能准备以后用redis缓存实现,所以选择不存到数据库。
model
在 model 包下添加 Question 类,成员变量和数据库字段一一对应:id, title, content, createTime, userId, commentCount,然后给每个变量加上 get、set 方法,一个 POJO类就这样写好了。
dao
在 dao 包下添加 QuestionDAO 接口,目前我们需要能够添加问题和读取最新的若干条问题,也就是一个 insert 方法和一个 select 方法,insert 方法按照之前写 UserDAO 时的 @Insert 注解去写就好,在写 select 方法时,考虑到后面需要用到某个用户发表问题的列表,我们可以在 SQL 语句中加个判断,这就要用到 mybatis 的 xml 配置文件。依然是在 QuestionDAO 中先定义好接口方法。
List<Question> selectLatest(@Param("userId") int userId, @Param("offset") int offset, @Param("limit") int limit);
mapper
这部分在第(1)篇中写到过,其实 mapper 包的命名和 QuestionMapper.xml 的命名取成 *DAO.xml 之类的也都行,注意跟 mybatis 配置的 mapper-locations 参数匹配就ok,xml 中的 <mapper namespace="..."><select id="..." resultType="..."> 的 "..." 部分也是同理。
当需要获取所有人发的问题时,userId 的值为0;当需要获取某一位用户发表的问题时,userId 的值为用户的 id。
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-
- <mapper namespace="top.noahlin.astera.dao.QuestionDAO">
- <sql id="TABLE_NAME">question</sql>
- <sql id="SELECT_FIELDS">id, title, content, create_time, user_id, comment_count</sql>
-
- <select id="selectLatest" resultType="top.noahlin.astera.model.Question">
- SELECT
- <include refid="SELECT_FIELDS"></include>
- FROM
- <include refid="TABLE_NAME"></include>
- <if test="userId!=0">
- WHERE user_id = #{userId}
- </if>
- ORDER BY id DESC
- LIMIT #{offset}, #{limit}
- </select>
- </mapper>
service
在 service 包下添加 QuestionService 接口,创建一个名为 impl 的子目录,将 QuestionServiceImpl 实现类放在 impl 包下。先在 QuestionService 中定义好需要的方法,包括它的参数和返回值,然后在实现类中把具体功能实现。
为了方便给视图模板传各种参数,在 model 中定义一个 ViewObject 类,它的成员变量类型为 Map<String, Object>,get 方法为返回一个 key 的值,set 方法为 put 一个键值对。
- public class ViewObject {
- private final Map<String, Object> objs = new HashMap<>();
-
- public void set(String key, Object value) {
- objs.put(key, value);
- }
-
- public Object get(String key) {
- return objs.get(key);
- }
- }
接下来,要完成首页的问题动态列表,需要 service 提供的方法也很简单:增加问题、获取问题列表。要注意一下的是获取问题列表的方法,这里用到了 List<View Object>。
理论上来说,直接把一个实体类对象返回给前端是不正确的,这样做会把不必要的数据、私密的数据传到前端,但是这整个项目都由我一个人写,并且是前后端不分离的,再加上此项目仅用于“练手”这一条,就简单的这样子写一下了。比较正规的写法当然是根据前端需要的数据,设计包含对应字段的 dto 或者 vo。
- public List<ViewObject> getQuestionList(int userId) {
- List<Question> questions = questionDAO.selectLatest(userId, 0, 10);
- List<ViewObject> vos = new ArrayList<>();
- for (Question question : questions) {
- ViewObject vo = new ViewObject();
- vo.set("user", userDAO.selectById(question.getUserId()));
- vo.set("question", question);
- vos.add(vo);
- }
- return vos;
- }
controller
在 controller 包下创建 IndexController ,处理 / 和 /index 请求,返回 index.html 模板,其中 “questionListVO” 是模板需要的参数。
- @GetMapping({"/", "/index"})
- public String index(HttpServletRequest request) {
- request.setAttribute("questionListVO", questionService.getQuestionList(0));
- return "index";
- }
Thymeleaf 使用
Thymeleaf 是 spring 推荐的模板引擎,因为没有用过其他的模板,也只能说它用着感觉还行。第一次写前端的代码,在原来的 HTML、CSS、JS 文件基础上去改,是可以实现需求的,入门比较简单,这里简单介绍一下用法。
添加 thymeleaf 的依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
代码测试阶段,application 配置文件关闭 thymeleaf 缓存
- spring:
- thymeleaf:
- cache: false
在<html>标签中加属性 xmlns:th="http://www.thymeleaf.org" 就可以使用 thymeleaf 的 th 指令了。常用的指令:
th:fragment="片段名(参数)" 供反复使用的代码片段
th:replace="文件路径::片段名(参数)" 调用代码片段
带参数值的属性都可以用到 th 指令,包括但不仅限于:
th:text="${参数}" 转义文本(防 html 注入)
th:utext="${参数}" 不作处理的文本
th:href="@{${参数}}" 链接
控制流程:
th:if="${参数}" th:if="${参数} == 0" 逻辑判断,可以只传 boolean 参数,也可以在 HTML 中增加逻辑运算
th:each="i:${参数}" 循环
自定义变量:
th:object="${参数1}" th:text="*{参数2}" 保存对象,方便多次调用属性
相当于th:text="${参数1.参数2}"
内置对象:
#strings、#dates、#arrays 等,可直接调用内置的函数