文章目录
MVC概念
- M:Model模型
- V:View视图
- C:Controller控制器
MVC是在表述层开发中运用的一种设计理念。主张把封装数据的『模型』、显示用户界面的『视图』、协调调度的『控制器』分开。
好处:
- 进一步实现各个组件之间的解耦
- 让各个组件可以单独维护
- 将视图分离出来以后,我们后端工程师和前端工程师的对接更方便
💡提示
MVC和三层架构之间关系
服务器端渲染 vs 前后端分离
-
服务器端渲染:前端工程师把前端页面一整套做好交给后端工程师
除了我们熟悉的JSP,还有Velocity、Freemarker、Thymeleaf等视图模板技术。虽然具体语法各不相同,但是它们都有一个共通的特点,就是在固定内容中可以穿插表达式等形式的动态内容。将视图模板中的动态内容转换为对应的Java代码并执行,然后使用计算得到的具体数据替换原来的动态部分。这样整个文件的动态内容就可以作为确定的响应结果返回给浏览器。在这种模式下,前端工程师将前端页面全部开发完成,交给后端程序员加入到项目中。此时不可避免的需要后端程序员根据需要对前端代码进行补充和调整。
-
前后端分离:开会商量JSON格式,然后分头开发。在后端程序尚不可用时,前端工程师会使用Mock.js生成假数据使用,在后端程序可用后再连接实际后端程序获取真实数据。
前后端分离模式下,前端程序和后端程序使用JSON格式进行交互,所以项目启动时前端工程和后端工程师需要坐在一起开会,商量确定JSON格式的具体细节。然后分头开发。后端工程师在把后端的代码发布到测试服务器前,前端工程师无法调用后端程序拿到真实数据,所以使用Mock.js生成假数据。直到后端工程师开发完成,后端程序发布到了测试服务器上,前端工程师再从Mock.js切换到实际后端代码。
无疑,前后端分离是目前大型项目的趋势 ==> 方便项目管理
但其实各有优劣:(需要根据实际情况选择使用的技术)
对比 服务端渲染 前后端分离 性能 差(渲染需要占用服务器资源) 好(服务器负责处理数据、客户端负责渲染) 开发成本 低 高(需要写两套代码,并且涉及到前后端交互) 管理难度 难(代码耦合、复用性堪忧) 易(前后端各管各的,出了问题,一般能独自解决;而且纯前端的环境,前端能使用前端包管理工具,只需关心功能的开发,大大提高开发效率) 用户体验 差(尽管能使用ajax技术,但主体业务必须通过页面刷新实现) 好(有能力实现零刷新,就像在使用本地程序一样)
Thymeleaf
Thymeleaf、JSP、Freemarker、Velocity等等,它们有一个共同的名字:服务器端模板技术。
Thymeleaf优势
-
SpringBoot官方推荐使用的视图模板技术,和SpringBoot完美整合
-
不经过服务器运算仍然可以直接查看原始值,对前端工程师更友好
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p th:text="${hello}">Original Value</p> </body> </html>
物理视图、逻辑视图
-
物理视图
在Servlet中,将请求转发到一个HTML页面文件时,使用的完整的转发路径就是物理视图。💡 提示
如果我们把所有的HTML页面都放在某个统一的目录下,那么转发地址就会呈现出明显的规律:
/pages/user/login.html /pages/user/login_success.html /pages/user/regist.html /pages/user/regist_success.html ...
路径的开头都是:/pages/user/ (视图前缀)
路径的结尾都是:.html (视图后缀)
-
逻辑视图
物理视图=视图前缀+逻辑视图+视图后缀
💡 提示
上面的例子中:
视图前缀 逻辑视图 视图后缀 物理视图 /pages/user/ login .html /pages/user/login.html /pages/user/ login_success .html /pages/user/login_success.html
在服务器端引入Thymeleaf环境
# 引入依赖
-
加入jar包
-
配置上下文参数
物理视图=视图前缀+逻辑视图+视图后缀
<!-- 在上下文参数中配置视图前缀和视图后缀 --> <context-param> <param-name>view-prefix</param-name> <param-value>/WEB-INF/view/</param-value> </context-param> <context-param> <param-name>view-suffix</param-name> <param-value>.html</param-value> </context-param>
💡 提示
说明:param-value中设置的前缀、后缀的值不是必须叫这个名字,可以根据实际情况和需求进行修改。
💡 提示
为什么要放在WEB-INF目录下?
-
原因:
WEB-INF目录不允许浏览器直接访问,所以我们的视图模板文件放在这个目录下,是一种保护。以免外界可以随意访问视图模板文件。
访问WEB-INF目录下的页面,都必须通过Servlet转发过来,简单说就是:不经过Servlet访问不了。
这样就方便我们在Servlet中检查当前用户是否有权限访问。 -
访问:
那放在WEB-INF目录下之后,重定向进不去怎么办?
重定向到Servlet,再通过Servlet转发到WEB-INF下。
-
# 创建Servlet基类
这个类大家直接复制粘贴即可,将来使用框架后,这些代码都将被取代。
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ViewBaseServlet extends HttpServlet {
private TemplateEngine templateEngine;
@Override
public void init() throws ServletException {
// 1.获取ServletContext对象
ServletContext servletContext = this.getServletContext();
// 2.创建Thymeleaf解析器对象
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
// 3.给解析器对象设置参数
// ①HTML是默认模式,明确设置是为了代码更容易理解
templateResolver.setTemplateMode(TemplateMode.HTML);
// ②设置前缀
String viewPrefix = servletContext.getInitParameter("view-prefix");
templateResolver.setPrefix(viewPrefix);
// ③设置后缀
String viewSuffix = servletContext.getInitParameter("view-suffix");
templateResolver.setSuffix(viewSuffix);
// ④设置缓存过期时间(毫秒)
templateResolver.setCacheTTLMs(60000L);
// ⑤设置是否缓存
templateResolver.setCacheable(true);
// ⑥设置服务器端编码方式
templateResolver.setCharacterEncoding("utf-8");
// 4.创建模板引擎对象
templateEngine = new TemplateEngine();
// 5.给模板引擎对象设置模板解析器
templateEngine.setTemplateResolver(templateResolver);
}
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1.设置响应体内容类型和字符集
resp.setContentType("text/html;charset=UTF-8");
// 2.创建WebContext对象
WebContext webContext = new WebContext(req, resp, getServletContext());
// 3.处理模板数据
templateEngine.process(templateName, webContext, resp.getWriter());
}
}
# HelloWorld
-
创建index.html
-
在index.html编写超链接
<a href="/view/TestThymeleafServlet">初步测试Thymeleaf</a>
-
创建Servlet
<servlet> <servlet-name>TestThymeleafServlet</servlet-name> <servlet-class>com.atguigu.thymeleaf.servlet.TestThymeleafServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>TestThymeleafServlet</servlet-name> <url-pattern>/TestThymeleafServlet</url-pattern> </servlet-mapping>
-
修改Servlet继承的父类
-
在doGet()方法中跳转到Thymeleaf页面
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1.声明当前请求要前往的视图名称 String viewName = "target"; // 2.调用ViewBaseServlet父类中的解析视图模板的方法 super.processTemplate(viewName, request, response); }
-
Thymeleaf页面
<!DOCTYPE html> <!-- 在html标签内加入Thymeleaf名称空间的声明 --> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!-- 在p标签的基础上,使用Thymeleaf的表达式,解析了一个URL地址 --> <p th:text="@{'/aaa/bbb/ccc'}">Thymeleaf将在这里显示一个解析出来的URL地址</p> </body> </html>
基本语法 ⭐️
# th名称空间
# 表达式语法: 标签文本值 th
文本值 th:text
<p th:text="标签体新值">标签体原始值</p>
- 不经过服务器解析,直接用浏览器打开HTML文件,看到的是『标签体原始值』
- 经过服务器解析,Thymeleaf引擎根据th:text属性指定的『标签体新值』去替换『标签体原始值』
属性值 th:value
<input type="text" name="username" th:value="文本框新值" value="文本框旧值" />
语法:任何HTML标签原有的属性,前面加上『th:』就都可以通过Thymeleaf来设定新值。
地址 @{…}
<p th:text="@{/aaa/bbb/ccc}">标签体原始值</p>
@{}
的作用是在字符串前附加『上下文路径』
e.g. 项目名为 /view
经过解析后得到:
/view/aaa/bbb/ccc
页面获取值 ${...}
(OGNL)
Servlet代码:
request.setAttribute("reqAttrName", "<span>hello-value</span>");
页面代码:
<p>有转义效果:[[${reqAttrName}]]</p>
<p>无转义效果:[(${reqAttrName})]</p>
执行效果:
<p>有转义效果:<span>hello-value</span></p>
<p>无转义效果:<span>hello-value</span></p>
${...}
中的表达式本质是 OGNL
OGNL:Object-Graph Navigation Language对象-图 导航语言
对象图
从根对象触发,通过特定的语法,逐层访问对象的各种属性。
OGNL语法
- 起点
在Thymeleaf环境下,${}中的表达式可以从下列元素开始:- 访问属性域的起点
- 请求域属性名
- session
- application
- param
- 内置对象
- #request
- #session
- #lists
- #strings
- 访问属性域的起点
- 属性访问语法
- 访问对象属性:使用getXxx()、setXxx()方法定义的属性
- 对象.属性名
- 访问List集合或数组
- 集合或数组[下标]
- 访问Map集合
- Map集合.key
- Map集合[‘key’]
- 访问对象属性:使用getXxx()、setXxx()方法定义的属性
# 访问域对象
💡 提示
PS:在我们使用的视图是JSP的时候,域对象有4个
- pageContext
- request:请求域
- session:会话域
- application:应用域
所以在JSP的使用背景下,我们可以说域对象有4个,现在使用Thymeleaf了,没有pageContext。
请求域
在请求转发的场景下,我们可以借助HttpServletRequest对象内部给我们提供的存储空间,帮助我们携带数据,把数据发送给转发的目标资源。
请求域:HttpServletRequest对象内部给我们提供的存储空间
操作请求域
Servlet中代码:
String requestAttrName = "helloRequestAttr";
String requestAttrValue = "helloRequestAttr-VALUE";
request.setAttribute(requestAttrName, requestAttrValue);
Thymeleaf表达式:
<p th:text="${helloRequestAttr}">request field value</p>
会话域
操作会话域
Servlet中代码:
// ①通过request对象获取session对象
HttpSession session = request.getSession();
// ②存入数据
session.setAttribute("helloSessionAttr", "helloSessionAttr-VALUE");
Thymeleaf表达式:
<p th:text="${session.helloSessionAttr}">这里显示会话域数据</p>
应用域
操作应用域
Servlet中代码:
// ①通过调用父类的方法获取ServletContext对象
ServletContext servletContext = getServletContext();
// ②存入数据
servletContext.setAttribute("helloAppAttr", "helloAppAttr-VALUE");
Thymeleaf表达式:
<p th:text="${application.helloAppAttr}">这里显示应用域数据</p>
# 获取请求参数
具体来说,我们这里探讨的是在页面上(模板页面)获取请求参数。底层机制是:
一个名字一个值
页面代码:
<p th:text="${param.username}">这里替换为请求参数的值</p>
页面显示效果:
一个名字多个值
页面代码:
<p th:text="${param.team}">这里替换为请求参数的值</p>
页面显示效果:
如果想要精确获取某一个值,可以使用数组下标。页面代码:
<p th:text="${param.team[0]}">这里替换为请求参数的值</p>
<p th:text="${param.team[1]}">这里替换为请求参数的值</p>
页面显示效果:
# 内置对象
所谓内置对象其实就是在表达式中可以直接使用的对象。
用法举例:
<h3>表达式的基本内置对象</h3>
<p th:text="${#request.getClass().getName()}">这里显示#request对象的全类名</p>
<p th:text="${#request.getContextPath()}">调用#request对象的getContextPath()方法</p>
<p th:text="${#request.getAttribute('helloRequestAttr')}">调用#request对象的getAttribute()方法,读取属性域</p>
基本思路:
- 如果不清楚这个对象有哪些方法可以使用,那么就通过getClass().getName()获取全类名,再回到Java环境查看这个对象有哪些方法
- 内置对象的方法可以直接调用
- 调用方法时需要传参的也可以直接传入参数
公共内置对象
Servlet中将List集合数据存入请求域:
request.setAttribute("aNotEmptyList", Arrays.asList("aaa","bbb","ccc"));
request.setAttribute("anEmptyList", new ArrayList<>());
页面代码:
<p>#list对象isEmpty方法判断集合整体是否为空aNotEmptyList:<span th:text="${#lists.isEmpty(aNotEmptyList)}">测试#lists</span></p>
<p>#list对象isEmpty方法判断集合整体是否为空anEmptyList:<span th:text="${#lists.isEmpty(anEmptyList)}">测试#lists</span></p>
公共内置对象对应的源码位置:
# 分支与迭代
分支 if、unless、switch
if和unless
让标记了th:if、th:unless的标签根据条件决定是否显示。
e.g.
<table>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
</tr>
<tr th:if="${#lists.isEmpty(employeeList)}">
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
<tr th:if="${not #lists.isEmpty(employeeList)}">
<td colspan="3">有数据!</td>
</tr>
<tr th:unless="${#lists.isEmpty(employeeList)}">
<td colspan="3">有数据!</td>
</tr>
</table>
switch
<h3>测试switch</h3>
<div th:switch="${user.memberLevel}">
<p th:case="level-1">银牌会员</p>
<p th:case="level-2">金牌会员</p>
<p th:case="level-3">白金会员</p>
<p th:case="level-4">钻石会员</p>
</div>
判断表达式
运用于判断表达式中时,关系判断使用 gt / ge / eq / lt / le / ne (即:使用缩写)
gt: great than(大于)>
ge: great equal(大于等于)>=
eq: equal(等于)==
lt: less than(小于)<
le: less equal(小于等于)<=
ne: not equal(不等于)!=
以 th:if 为例
<td th:if="${user} ne null">
<!-- 此处 ne 若是替换成 not equal 或 != , 则会判断错误,无法正常执行 -->
用户名:<font>[[${user.username}]]</font>
</td>
迭代(遍历)
<h3>测试each</h3>
<table>
<thead>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
</tr>
</thead>
<tbody th:if="${#lists.isEmpty(employeeList)}">
<tr>
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(employeeList)}">
<!-- 遍历出来的每一个元素的名字 : ${要遍历的集合} -->
<tr th:each="employee : ${employeeList}">
<td th:text="${employee.empId}">empId</td>
<td th:text="${employee.empName}">empName</td>
<td th:text="${employee.empSalary}">empSalary</td>
</tr>
</tbody>
</table>
在迭代过程中,可以参考下面的说明使用迭代状态:
<h3>测试each</h3>
<table>
<thead>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
<th>迭代状态</th>
</tr>
</thead>
<tbody th:if="${#lists.isEmpty(employeeList)}">
<tr>
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(employeeList)}">
<!-- 遍历出来的每一个元素的名字 : ${要遍历的集合} -->
<tr th:each="employee,empStatus : ${employeeList}">
<td th:text="${employee.empId}">empId</td>
<td th:text="${employee.empName}">empName</td>
<td th:text="${employee.empSalary}">empSalary</td>
<td th:text="${empStatus.count}">count</td>
</tr>
</tbody>
</table>
迭代状态 xxxStat
th:each="xxx, xxxStat : ${user_list}"
其中 xxxStat 是状态变量,如果未设置,默认值是 xxx + Stat
其中的属性有:
- index 0开始
- count 1开始
- size
- current 当前迭代的变量
- even/odd 布尔值,当前循环是否是偶数/奇数
- first 布尔值,是否是第一个
- last 布尔值,是否是最后一个
# 包含其他模板文件
应用场景
抽取各个页面的公共部分:
创建页面的代码片段 th:fragment
使用th:fragment来给这个片段命名:
<div th:fragment="header">
<p>被抽取出来的头部内容</p>
</div>
包含到有需要的页面 th:insert、th:replace、th:include
语法 | 效果 |
---|---|
th:insert | 把目标的代码片段整个插入到当前标签内部 |
th:replace | 用目标的代码替换当前标签 |
th:include | 把目标的代码片段去除最外层标签,然后再插入到当前标签内部 |
页面代码举例:
<!-- 代码片段所在页面的逻辑视图 :: 代码片段的名称 -->
<div id="badBoy" th:insert="segment :: header">
div标签的原始内容
</div>
<div id="worseBoy" th:replace="segment :: header">
div标签的原始内容
</div>
<div id="worstBoy" th:include="segment :: header">
div标签的原始内容
</div>
# 引入 script 资源 th:block
<!-- common-script -->
<th:block th:fragment="common-script">
<!--CDN资源-->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
</th:block>
# 解决页面获取项目路径问题 base标签 【不推荐】
说明,base标签会导致锚问题(无法页面内转跳)
下面添加base方法不建议使用。
因此,给出的方案还是:
- 老老实实用 thymeleaf 的
@{...}
- 优化项目目录结构
- 用 thymeleaf 以外的技术(后端苦逼列不出例子…)
参考;
<base>
标签可以在html页面 <head>
标签中一次配置:
<base th:href="${#request.getContextPath()}+'/'">
解决一切 项目路径 导致的头痛问题
e.g.
-
前端:src / href / …
不加 / , 如:
src="/dist/lib/pjax/jquery.pjax.js"
或者 th… , 如:th:src="@{/dist/lib/pjax/jquery.pjax.js}"
(是的,这样也行。所以之前有th:xxx
的不用一个一个改)
th:href="@{dist/css/animate.css}"
-
fragments
加 / , 如:
<nav th:replace="/fragments/_fragments :: menu(1)"></nav>