我们对HTML的新的期待:既能够正常显示页面,又能在页面中包含动态数据部分。而动态数据单靠HTML本身是无法做到的,所以此时我们需要引入服务器端动态视图模板技术。
M:Model模型
V:View视图
C:Controller控制器
MVC是在表述层开发中运用的一种设计理念。主张把封装数据的『模型』、显示用户界面的『视图』、协调调度的『控制器 分开。
好处:
除了我们熟悉的JSP,还有Velocity、Freemarker、Thymeleaf等视图模板技术。虽然具体语法各不相同,但是它们都有一个共通的特点,就是在固定内容中可以穿插表达式等形式的动态内容。将视图模板中的动态内容转换为对应的Java代码并执行,然后使用计算得到的具体数据替换原来的动态部分。这样整个文件的动态内容就可以作为确定的响应结果返回给浏览器。在这种模式下,前端工程师将前端页面全部开发完成,交给后端程序员加入到项目中。此时不可避免的需要后端程序员根据需要对前端代码进行补充和调整。
前后端分离模式下,前端程序和后端程序使用JSON格式进行交互,所以项目启动时前端工程和后端工程师需要坐在一起开会,商量确定JSON格式的具体细节。然后分头开发。后端工程师在把后端的代码发布到测试服务器前,前端工程师无法调用后端程序拿到真实数据,所以使用Mock.js生成假数据。直到后端工程师开发完成,后端程序发布到了测试服务器上,前端工程师再从Mock.js切换到实际后端代码。
开发工程师有项目需要部署的时候,如果需要交给运维工程操作,那么除了要部署的项目本身,还要编写可以自动化部署项目的Shell脚本,一起交给运维工程师去部署项目。
另外还有一种情况,程序员开发具体模块时需要设计创建本模块的数据库表,本地建表调试确定后,把建表语句发送给运维工程师,在正式环境服务器上创建数据库表。因为正式服务器上通常是不给开发工程师操作权限的。
你以为完成组长交给的任务就可以轻松优雅的下班?图样图森破!产品经理会直接找到你,让你改需求!你90%的精力其实都用来忍住揍他的冲动。
开发工程师将代码提交到版本控制服务器(SVN或Git的代码托管中心),然后根据版本控制服务器上的代码部署到测试服务器上,测试工程师访问测试服务器上的应用进行测试。发现Bug通过Bug管理软件通知开发工程师。
JSP、Freemarker、Velocity等等,它们有一个共同的名字:服务器端模板技术。
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<p th:text="${hello}">Original Valuep>
body>
html>
在Servlet中,将请求转发到一个HTML页面文件时,使用的完整的转发路径就是物理视图。
/pages/user/login_success.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 |
这个jar包可放在web/WEB-INF目录下,也可根据自己需求放在Module外面
物理视图=视图前缀+逻辑视图+视图后缀
<context-param>
<param-name>view-prefixparam-name>
<param-value>/WEB-INF/view/param-value>
context-param>
<context-param>
<param-name>view-suffixparam-name>
<param-value>.htmlparam-value>
context-param>
说明:param-value中设置的前缀、后缀的值不是必须叫这个名字,可以根据实际情况和需求进行修改。
为什么要放在WEB-INF目录下?
原因:WEB-INF目录不允许浏览器直接访问,所以我们的视图模板文件放在这个目录下,是一种保护。以免外界可以随意访问视图模板文件。
访问WEB-INF目录下的页面,都必须通过Servlet转发过来,简单说就是:不经过Servlet访问不了。
这样就方便我们在Servlet中检查当前用户是否有权限访问。
那放在WEB-INF目录下之后,重定向进不去怎么办?
重定向到Servlet,再通过Servlet转发到WEB-INF下。
这个类可以直接复制粘贴即可,将来使用框架后,这些代码都将被取代。
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());
}
}
<a href="/view/TestThymeleafServlet">初步测试Thymeleafa>
要在web.xml中配置下面的代码
<servlet>
<servlet-name>TestThymeleafServletservlet-name>
<servlet-class>com.cdz.thymeleaf.TestThymeleafServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>TestThymeleafServletservlet-name>
<url-pattern>/TestThymeleafServleturl-pattern>
servlet-mapping>
把继承的HttpServlet改成ViewBaseServlet
@WebServlet(name = "TestThymeleafServlet", value = "/TestThymeleafServlet")
public class TestThymeleafServlet extends ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.声明当前请求要前往的视图名称
String viewName = "target";
// 2.调用ViewBaseServlet父类中的解析视图模板的方法
super.processTemplate(viewName, request, response);
}
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<p th:text="@{'/aaa/bbb/ccc'}">Thymeleaf将在这里显示一个解析出来的URL地址p>
<a href="/view/TestThymeleafServlet">初步测试Thymeleafa>
body>
html>
代码示例:
<p th:text="标签体新值">标签体原始值p>
『字面量』是一个经常会遇到的概念,我们可以对照『变量』来理解它的含义。
// a是变量,100是字面量
int a = 100;
System.out.println("a = " + a);
现在在th:text属性中使用的就是『字面量』,它不指代任何其他值。
代码示例:
<input type="text" name="username" th:value="文本框新值" value="文本框旧值" />
语法:任何HTML标签原有的属性,前面加上『th:』就都可以通过Thymeleaf来设定新值。
代码示例:
<p th:text="@{/aaa/bbb/ccc}">标签体原始值p>
经过解析后得到:
/view/aaa/bbb/ccc
所以@{}的作用是在字符串前附加『上下文路径』
这个语法的好处是:实际开发过程中,项目在不同环境部署时,Web应用的名字有可能发生变化。所以上下文路径不能写死。而通过@{}动态获取上下文路径后,不管怎么变都不怕啦!
如果我们直接访问index.html本身,那么index.html是不需要通过Servlet,当然也不经过模板引擎,所以index.html上的Thymeleaf的任何表达式都不会被解析。
解决办法:通过Servlet访问index.html,这样就可以让模板引擎渲染页面了:
进一步的好处:
通过上面的例子我们看到,所有和业务功能相关的请求都能够确保它们通过Servlet来处理,这样就方便我们统一对这些请求进行特定规则的限定。
Servlet代码:
request.setAttribute("reqAttrName", "hello-value");
页面代码:
<p>有转义效果:[[${reqAttrName}]]p>
<p>无转义效果:[(${reqAttrName})]p>
执行效果:
<p>有转义效果:<span>hello-value</span>p>
<p>无转义效果:<span>hello-valuespan>p>
在请求转发的场景下,我们可以借助HttpServletRequest对象内部给我们提供的存储空间,帮助我们携带数据,把数据发送给转发的目标资源。
请求域:HttpServletRequest对象内部给我们提供的存储空间
注:在我们使用的视图是JSP的时候,域对象有4个
- pageContext
- request:请求域
- session:会话域
- application:应用域
所以在JSP的使用背景下,我们可以说域对象有4个,现在使用Thymeleaf了,没有pageContext。
Servlet中代码:
String requestAttrName = "helloRequestAttr";
String requestAttrValue = "helloRequestAttr-VALUE";
request.setAttribute(requestAttrName, requestAttrValue);
Thymeleaf表达式:
<p th:text="${helloRequestAttr}">request field valuep>
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>
页面显示效果:
所谓内置对象其实就是在表达式中可以直接使用的对象。
用法举例:
<h1>表达式的基本内置对象h1>
<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>
基本思路:
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)}">测试#listsspan>p>
<p>#list对象isEmpty方法判断集合整体是否为空anEmptyList:<span th:text="${#lists.isEmpty(anEmptyList)}">测试#listsspan>p>
公共内置对象对应的源码位置:
OGNL:Object-Graph Navigation Language对象-图 导航语言
从根对象触发,通过特定的语法,逐层访问对象的各种属性。
在Thymeleaf环境下,${}中的表达式可以从下列元素开始:
让标记了th:if、th:unless的标签根据条件决定是否显示。
示例的实体类:
public class Employee {
private Integer empId;
private String empName;
private Double empSalary;
示例的Servlet代码:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.创建ArrayList对象并填充
List<Employee> employeeList = new ArrayList<>();
employeeList.add(new Employee(1, "tom", 500.00));
employeeList.add(new Employee(2, "jerry", 600.00));
employeeList.add(new Employee(3, "harry", 700.00));
// 2.将集合数据存入请求域
request.setAttribute("employeeList", employeeList);
// 3.调用父类方法渲染视图
super.processTemplate("list", request, response);
}
示例的HTML代码:
<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>
if配合not关键词和unless配合原表达式效果是一样的,看个人喜好。
<h3>测试switchh3>
<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>
<h3>测试eachh3>
<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}">empIdtd>
<td th:text="${employee.empName}">empNametd>
<td th:text="${employee.empSalary}">empSalarytd>
tr>
tbody>
table>
<h3>测试eachh3>
<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}">empIdtd>
<td th:text="${employee.empName}">empNametd>
<td th:text="${employee.empSalary}">empSalarytd>
<td th:text="${empStatus.count}">counttd>
tr>
tbody>
table>
抽取各个页面的公共部分:
使用th:fragment来给这个片段命名:
<div th:fragment="header">
<p>被抽取出来的头部内容p>
div>
语法 | 效果 |
---|---|
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>