出差期间,自己按照狂神视频搭建一个小型网站测试前后端交互
前端使用BootStrap框架,Thymeleaf模板渲染引擎
后端使用SpringBoot
1、准备工作
1.1、需要的依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zhou</groupId> <artifactId>SpringBoot-03-Web</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringBoot-03-Web</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
前端资源在https://gitee.com/ENNRIAAA/springboot-learning/repository/archive/master.zip下下载。
后端部分,不使用数据库的情况下仿照数据库做一些操作
1.2、pojo层
实体层,设置部门和员工属性
package com.zhou.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @Name Department * @Description 部门表 * @Author 88534 * @Date 2021/10/9 9:26 */ @Data @AllArgsConstructor @NoArgsConstructor public class Department { private Integer departmentId; private String departmentName; } package com.zhou.pojo; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; /** * @Name Employee * @Description 员工表 * @Author 88534 * @Date 2021/10/9 9:28 */ @Data @NoArgsConstructor public class Employee { private Integer employeeId; private String lastName; private String email; /** * 0:女;1:男 */ private Integer gender; private Department department; private Date birth; public Employee(Integer employeeId, String lastName, String email, Integer gender, Department department) { this.employeeId = employeeId; this.lastName = lastName; this.email = email; this.gender = gender; this.department = department; this.birth = new Date(); } }
1.3、dao层
对数据进行处理,实现增删改查
部门dao
package com.zhou.dao; import com.zhou.pojo.Department; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * @Name DepartmentDao * @Description 部门dao,伪造数据库 * @Author 88534 * @Date 2021/10/9 9:30 */ @Repository public class DepartmentDao { /** * 模拟数据库中的数据 */ private static Map<Integer, Department> departments = null; static { // 创建一个部门表 departments = new HashMap<Integer,Department>(); departments.put(101,new Department(101,"教学部")); departments.put(102,new Department(102,"市场部")); departments.put(103,new Department(103,"教研部")); departments.put(104,new Department(104,"运营部")); departments.put(105,new Department(105,"后勤部")); } /** * 获得所有部门信息 * @return */ public Collection<Department> getDepartments(){ return departments.values(); } /** * 通过id得到部门 * @param id * @return */ public Department getDepartmentById(Integer id){ return departments.get(id); } }
员工dao
package com.zhou.dao; import com.zhou.pojo.Department; import com.zhou.pojo.Employee; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * @Name EmployeeDao * @Description 员工dao * @Author 88534 * @Date 2021/10/9 13:26 */ @Repository public class EmployeeDao { /** * 模拟数据库中的数据 */ private static Map<Integer, Employee> employees = null; /** * 员工有所属的部门 */ @Autowired private DepartmentDao departmentDao; static { // 创建一个员工表 employees = new HashMap<Integer,Employee>(); employees.put(1001,new Employee(1001,"AA","A123456@qq.com",1,new Department(101,"教学部"))); employees.put(1002,new Employee(1002,"BB","B123456@qq.com",0,new Department(102,"市场部"))); employees.put(1003,new Employee(1003,"CC","C123456@qq.com",1,new Department(103,"教研部"))); employees.put(1004,new Employee(1004,"DD","D123456@qq.com",0,new Department(104,"运营部"))); employees.put(1005,new Employee(1005,"EE","E123456@qq.com",1,new Department(105,"后勤部"))); } /** * 主键自增 */ private static Integer initId = 1006; /** * 增加一个员工 * @param employee */ public void save(Employee employee){ if (employee.getEmployeeId() == null){ employee.setEmployeeId(initId++); } employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getDepartmentId())); employees.put(employee.getEmployeeId(),employee); } /** * 查询所有部门信息 * @return */ public Collection<Employee> getAllEmployees(){ return employees.values(); } /** * 通过id查询员工 * @param id * @return */ public Employee getEmployeeById(Integer id){ return employees.get(id); } /** * 通过id删除员工 * @param id */ public void delete(Integer id){ employees.remove(id); } }
系统配置application.properties
# 自定义时间格式
spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
# 关闭模板引擎的缓存
spring.thymeleaf.cache=false
server.servlet.context-path=/zhou
# 配置文件存放的真实路径
spring.messages.basename=i18n.login
使用BootStrap前端框架
所有页面的静态资源都需要使用Thymeleaf接管,以首页为例
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Please sign in</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/signin.css}" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" action="dashboard.html"> <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal">请登录</h1> <label class="sr-only">账号</label> <input type="text" class="form-control" placeholder="账号" required="" autofocus=""> <label class="sr-only">密码</label> <input type="password" class="form-control" placeholder="密码" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> 记住我 </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit">提交</button> <p class="mt-5 mb-3 text-muted">© 2017-2018</p> <a class="btn btn-sm">中文</a> <a class="btn btn-sm">English</a> </form> </body> </html>
3、国际化
3.1、配置i18n文件
internationalization i和n之间隔着18个字母,简写为i18n
类似kubernetes:k8s
实现中英文切换
首先确保在配置file encoding中使用UTF-8支持中文
resource目录下添加i18n,添加配置文件
下载resource bundle插件,可以在下方选择中英文同步编辑
application.properties加上i18n路径
# 配置文件存放的真实路径
spring.messages.basename=i18n.login
3.2、LocaleResolver
需要在项目中进行按钮自动切换,需要自定义一个组件LocaleResolver
package com.zhou.config; import org.springframework.web.servlet.LocaleResolver; import org.thymeleaf.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; /** * @Name MyLocalResolver * @Description 自定义语言转换组件 * @Author 88534 * @Date 2021/10/9 15:42 */ public class MyLocalResolver implements LocaleResolver { /** * 解析请求 * @param request * @return */ @Override public Locale resolveLocale(HttpServletRequest request) { // 获取请求中的语言参数 String language = request.getParameter("l"); // 如果没有就使用默认的 Locale locale = Locale.getDefault(); // 如果请求的链接携带了国际化的参数 if (!StringUtils.isEmpty(language)){ // zh_CN String[] split = language.split("_"); // 语言_国家、地区 locale = new Locale(split[0], split[1]); } return locale; } @Override public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { } }
3.3、@Bean
将组件配置到spring容器@Bean,交给Spring管理
package com.zhou.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @Name MyMvcConfig * @Description 添加自定义配置 * @Author 88534 * @Date 2021/10/5 21:21 */ @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); } /** * 自定义国际化组件 * @return */ @Bean public LocaleResolver localeResolver(){ return new MyLocalResolver(); } }
3.4、前端
前端页面加上#{}和对应的配置
链接上加入传递给后端的参数l
输出效果:可以点击下方链接实现中英文跳转
中文:
英文:
4、登录+拦截器
不符合条件的拦截在登录页面
package com.zhou.interceptor;
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title th:text="#{login.tip}">Please sign in</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/signin.css}" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" action="dashboard.html"> <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1> <label class="sr-only">Username</label> <input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus=""> <label class="sr-only">Password</label> <input type="password" class="form-control" th:placeholder="#{login.password}" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> [[#{login.remember}]] </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button> <p class="mt-5 mb-3 text-muted">© 2021-2022</p> <a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a> <a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a> </form> </body> </html>
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @Name LoginHandlerInterceptor * @Description * @Author 88534 * @Date 2021/10/9 17:20 */ public class LoginHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登录成功之后,应该有用户的session Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser==null){ // 没有登录 request.setAttribute("msg","没有权限,请先登录"); request.getRequestDispatcher("/index.html").forward(request,response); return false; } else { return true; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
前端加入msg的加红告警
<!-- 如果msg不为空则显示消息msg-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
输出结果:
没有输入时
输入密码错误时:
5、员工列表展示
5.1、提取公共页面
顶部栏和侧边栏是各个界面都重复的,可以收入一个commons/commons.html作为公共组件
可以使用th:fragment和th:insert/replace实现打包和插入组件
th:fragment="topBar" <div th:replace="~{commons/commons::topBar}"></div> th:fragment="sideBar" <div th:insert="~{commons/commons::sideBar(active='main.html')}
5.2、高亮转移
高亮部分由一个nav-link active的class标签属性确定,可以通过th:class传递active参数,规定到了哪个页面是哪一个部分高亮
如果要传递参数,可以直接使用()传参,接收判断
<a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}"> <div th:insert="~{commons/commons::sideBar(active='main.html')}"></div> <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}"> <div th:insert="~{commons/commons::sideBar(active='list.html')}"></div>
5.3、员工界面
使用th:each进行遍历
<div class="table-responsive"> <table class="table table-striped table-sm"> <thead> <tr> <th>序号</th> <th>名字</th> <th>电子邮箱</th> <th>性别</th> <th>部门</th> <th>生日</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="emp:${emps}"> <td th:text="${emp.getEmployeeId()}"></td> <td>[[${emp.getLastName()}]]</td> <td th:text="${emp.getEmail()}"></td> <td th:text="${emp.getGender()==0?'女':'男'}"></td> <td th:text="${emp.department.getDepartmentName()}"></td> <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td> <td> <button class="btn btn-sm btn-primary">编辑</button> <button class="btn btn-sm btn-danger">删除</button> </td> </tr> </tbody> </table> </div>
展示效果:
6、添加员工
1、按钮提交
<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a> </h2>
2、前端页面
使用post提交表单
<form th:action="@{/emp}" method="post"> <div class="form-group"> <label>用户名</label> <input type="text" name="lastName" class="form-control" placeholder="admin"> </div> <div class="form-group"> <label>电子邮箱</label> <input type="text" name="email" class="form-control" placeholder="123456@qq.com"> </div> <div class="form-group"> <label>性别</label> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" value="1"> <label class="form-check-label">男</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" value="0"> <label class="form-check-label">女</label> </div> </div> <div class="form-group"> <label>部门</label> <select class="form-control" name="department.departmentId"> <!--controller层接收的是一个对象Employee,前端无法提交,所以需要提交的是其中的一个属性--> <option th:each="department:${departments}" th:text="${department.getDepartmentName()}" th:value="${department.getDepartmentId()}"></option> </select> </div> <div class="form-group"> <label>生日</label> <!-- 日期格式默认为yyyy/MM/dd--> <input type="text" name="birth" class="form-control" placeholder="2001-01-01 00:00:00"> </div> <button type="submit" class="btn btn-primary">添加</button> </form>
3、后端处理
前往add.html页面
使用post方法接收emp
@GetMapping("/emp") public String toAddPage(Model model){ // 查出所有部门的信息 Collection<Department> departments = departmentDao.getDepartments(); model.addAttribute("departments",departments); return "/emp/add"; } @PostMapping("/emp") public String addEmp(Employee employee){ System.out.println("save=>"+employee); // 调用底层业务方法,保存员工信息 employeeDao.save(employee); return "redirect:/emps"; }
显示效果:
其中显示的为默认值,可修改
7、修改、删除员工
1、按钮提交
<a class="btn btn-sm btn-primary" th:href="@{/emp/{id}(id=${emp.getEmployeeId()})}">编辑</a> <a class="btn btn-sm btn-danger" th:href="@{/deleteEmp/{id}(id=${emp.getEmployeeId()})}">删除</a>
2、前端页面
修改员工,提交表单,需要获取对应修改id的信息并展示
<form th:action="@{/updateEmp}" method="post"> <!-- 防止自增,使用隐藏域添加id进入修改--> <input type="hidden" name="employeeId" th:value="${emp.getEmployeeId()}"> <div class="form-group"> <label>用户名</label> <input type="text" th:value="${emp.getLastName()}" name="lastName" class="form-control"> </div> <div class="form-group"> <label>电子邮箱</label> <input type="text" th:value="${emp.getEmail()}" name="email" class="form-control"> </div> <div class="form-group"> <label>性别</label> <div class="form-check form-check-inline"> <input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1"> <label class="form-check-label">男</label> </div> <div class="form-check form-check-inline"> <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0"> <label class="form-check-label">女</label> </div> </div> <div class="form-group"> <label>部门</label> <select class="form-control" name="department.departmentId"> <!-- controller层接收的是一个对象Employee,前端无法提交,所以需要提交的是其中的一个属性--> <option th:selected="${department.getDepartmentId() ==emp.getDepartment().getDepartmentId()}" th:each="department:${departments}" th:text="${department.getDepartmentName()}" th:value="${department.getDepartmentId()}"></option> </select> </div> <div class="form-group"> <label>生日</label> <!-- 日期格式默认为yyyy/MM/dd--> <input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}" type="text" name="birth" class="form-control"> </div> <button type="submit" class="btn btn-primary">修改</button> </form>
3、后端处理
/** * 去员工的修改页面 * 符合RestFUL风格 * @param id * @param model * @return */ @GetMapping("/emp/{id}") public String toUpdateEmp(@PathVariable("id")Integer id, Model model){ // 查出原来的数据 Employee employeeById = employeeDao.getEmployeeById(id); model.addAttribute("emp",employeeById); // 查出所有部门的信息 Collection<Department> departments = departmentDao.getDepartments(); model.addAttribute("departments",departments); return "/emp/update"; } @PostMapping("/updateEmp") public String updateEmp(Employee employee){ employeeDao.save(employee); return "redirect:/emps"; } /** * 删除员工 * @param id * @return */ @GetMapping("/deleteEmp/{id}") public String deleteEmp(@PathVariable("id")Integer id){ employeeDao.delete(id); return "redirect:/emps"; }
输出效果:
删除,直接在原页面上直接去掉
8、404处理及注销处理
spring能智能识别resources/templates/error目录(需自行创建),根据各种不同的错误类型,放入对应的404.html、500.html等页面即可自动完成错误页面跳转。
注销只需要销毁session
<a class="nav-link" th:href="@{/user/logout}">注销</a>
LoginController添加
@RequestMapping("/user/logout") public String logout(HttpSession session){ session.invalidate(); return "redirect:/index"; }
注销后会自动返回登录界面
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构