题外话:
此次文章的输出动力来源,是拉勾教育的大数据开发训练营课程(没有推销的意思,也没有广告费,纯粹是记录一下这个过程,陪伴我走过来的一些重要的人或事)
虽然我的学习进度并不理想,但是好在他们提供的线上班级学习氛围很给力,除了给我一种推动力,在节奏跟不上想要give up的时候,也会有热情的同学、助教、班主任及时的督促和鼓励
我会继续保持学习的状态,也会尽力跟上课程进度,提高学习效率
另外要补充一点:该系统内容为我自己根据课程学习阶段、内容吸收情况以及自己的现有编程能力开发
涉及到的技术点:
【前端】:H5、CSS3、jQuery、Vue、laydate日期控件、Font Awesome图标
(未使用UI框架)
【交互】:Axios、Ajax、JSON
【后端】:普通JavaWeb项目、Tomcat、Servlet、三层架构模式、Druid连接池、MySQL
(未使用maven工程、ssm框架、无spring相关技术、微服务技术、无redis、消息队列技术等)
【开发工具】:VS Code、IDEA、SQLyog、浏览器
补充:
- 了解MVC与三层架构可以参考这篇文章 MVC与三层架构
- VSCode中无需手动刷新,代码自动生效需要安装Live Server扩展插件
- 界面布局未使用UI框架,采用Flex响应式布局和浮动布局(登录界面支持完全响应式,首页和添加学生页面未完全实现响应式布局)
- 此文主要用于练习,代码规范程度有限,如果有童鞋对代码有疑问,欢迎探讨
实现的目标:
- 支持学生登录
- 支持查看学生列表
- 支持添加学生
详细功能包括:
- 用户登录相关
(1)用户注册入口
(2)账号校验(账号是否符合规则、账号是否重复校验)
(3)账号密码校验(密码是否符合规则)- 学生列表界面/首页
(1)数据即时更新
(2)支持手动刷新
(3)首页布局设计
(4)header和footer展示关键信息和入口- 添加学生相关
(1)账号是否重复校验
(2)账号格式校验
(3)密码格式校验
(4)评分数值校验
(5)必填项校验
(6)关闭添加窗口
完整代码下载:
一、界面开发(VS Code)
- 用户登录界面,
- 用户列表页/系统首页,
- 添加用户页/用户注册页
- 草图原型如下
- 注意:
- 在绘制草图或界面过程中,或者开始画原型之前,需要考虑用户需要保存哪些信息,比如像界面中展示的:账号、姓名、昵称、出生日期、性别等,因为是学生信息管理系统,还可以补充:入学年份、所在学院、所选专业、综合评分等信息
- 另外,这里没有考虑修改密码的界面和实现,只是作为练习Demo,真正的企业系统肯定是需要的。
- 开发结果如下:
-
登录界面
-
系统首页
-
添加学生界面
-
二、后端结构搭建(IDEA)
-
创建Java web项目
-
根据三层架构思想创建包目录
-
编写视图层框架 ,处理前端请求
-
web下创建LoginServlet、RegisterServlet、CommonServlet
@WebServlet("/loginServlet") public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String result = "false"; String userId = req.getParameter("userid"); String password = req.getParameter("password"); if(userId.equals("admin") && password.equals("1")){ result = "true"; } System.out.println("【登录服务】/loginServlet result = " + result); resp.getWriter().write(result); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("--------登录请求进入post方法--------"); doGet(req,resp); } }
@WebServlet("/registerServlet") public class RegisterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String result = "true"; String userId = req.getParameter("userid"); String password = req.getParameter("password"); String userName = req.getParameter("username"); String nickName = req.getParameter("nickname"); String birth = req.getParameter("birth"); String gender = req.getParameter("gender"); String grade = req.getParameter("grade"); System.out.println("userId = " + userId); System.out.println("password = " + password); System.out.println("userName = " + userName); System.out.println("nickName = " + nickName); System.out.println("birth = " + birth); System.out.println("gender = " + gender); System.out.println("grade = " + grade); System.out.println("【注册服务】/registerServlet result = " + result); resp.getWriter().write(result); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("--------注册请求进入post方法--------"); doGet(req,resp); } }
@WebServlet("/commonServlet") public class CommonServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String result = "false"; String userId = req.getParameter("userid"); if(userId.equals("admin")){ result = "true"; } System.out.println("【公共服务】/commonServlet result = " + result); resp.getWriter().write(result); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("--------公用请求进入post方法--------"); doGet(req,resp); } }
-
将前端页面代码拷贝到项目的web目录下
-
配置WEB-INF下的web.xml文件,添加启动项配置
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 设置默认启动的首页 --> <welcome-file-list> <welcome-file>homework/login.html</welcome-file> </welcome-file-list> </web-app>
-
三、添加前端校验(VS Code)
为元素添加事件有俩种方式:事件绑定、事件派发
- 事件绑定
<input type="button" value="点我试试" id="btn1" onclick="fn()">
function fn(){
alert("事件绑定!");
}
- 事件派发
<head>
<script>
$(function(){
$("#btn2").click(function(){
alert("事件派发");
})
})
</script>
</head>
<input type="button" value="点我试试" id="btn2">
我主要使用的事件派发,也就意味着基本所有的校验都是在js文件中实现,很少改动之前写好的HTML代码
-
登录页面校验 login.js(代码从完整代码中获取)
- 账号输入框失去焦点事件:发送ajax请求CommonServlet,判断账号是否存在
- 账号输入框获取焦点事件:隐藏账号警告信息
- 密码输入框获取焦点事件:隐藏密码警告信息
- 登录按钮点击事件:发送ajax请求,判断账号密码是否错误
- 注册链接点击事件:展示添加用户界面
-
系统首页校验 welcome.js(代码从完整代码中获取)
- 退出按钮点击事件:跳转到登录页面
- 添加按钮点击事件:展示添加用户界面
- 刷新按钮点击事件:刷新列表页数据
-
添加用户界面校验 add-stu.js(代码从完整代码中获取)
- 账号输入框失焦事件:判断是否为空;是否格式正确;账号是否重复
- 账号输入框聚焦事件:隐藏账号警告信息
- 密码输入框失焦事件:判断是否为空;是否格式正确
- 密码输入框聚焦事件:隐藏占位符;隐藏密码警告信息
- 姓名输入框失焦事件:判断是否为空
- 姓名输入框聚焦事件:隐藏姓名警告信息
- 出生日期失焦事件:判断是否为空
- 出生日期聚焦事件:隐藏日期警告信息
- 评分输入框失焦事件:判断评分是否格式正确
- 评分输入框聚焦事件:隐藏评分警告信息
- 提交按钮点击事件:ajax请求RegisterServlet,判断是否注册成功;刷新列表页
- 关闭按钮点击事件:隐藏添加用户界面
踩坑1:
添加用户界面是登录和首页的公共页面,所以单独拎出来一个add-stu.html,但是嵌套公共HTML的时候,加载公共html自己的js代码存在不生效问题。如下代码:
<body>
</body>
<script>
window.onload = function(){
console.log("----页面资源已经全部加载完毕----")
$("#addStu").load("add-stu.html")
}
</script>
<script src="js/add-stu.js"></script>
经过分析,是因为login.html或welcome.html中通过jquery的load函数加载公共html的方式属于动态加载,浏览器解析的时候会先加载静态资源,包括add-stu.js,然后才加载add-stu.html,也就导致出现了js代码中打印出来的公共页面的DOM元素不存在的情况
解决方式是:js文件也采用动态加载,在加载add-stu.html之后动态加载add-stu.js,代码如下:
<body>
</body>
<script>
window.onload = function(){
console.log("----页面资源已经全部加载完毕----")
$("#addStu").load("add-stu.html")
// jQuery.getScript()该函数用于动态加载JS文件,并在全局作用域下执行文件中的JS代码。
// 该函数可以加载跨域的JS文件。请注意,该函数是通过异步方式加载数据的。
// 该函数属于全局jQuery对象。
$.getScript("js/login.js",function(){
console.log("公用页面js加载完毕")
})
}
</script>
踩坑2:
随着引入的外部js、css等外部资源变多,导致添加界面add-stu.html的性别单选框莫名其妙失效,排查和测试一段时间后,仍然未找到具体原因,原先代码如下:
<div class="option-div">
<label>
<input type="radio" name="r-gender" checked=true value="1">
<div class="r-gender-div" >男</div>
</label>
<label>
<input type="radio" name="r-gender" value="0">
<div class="r-gender-div" >女</div>
</label>
</div>
最后采用jquery监听事件解决,修改后代码如下:
<div class="option-div">
<label>
<input type="radio" name="r-gender" checked=true class="r-gender-input" value="1">
<div class="r-gender-div" index="0">男</div>
</label>
<label>
<input type="radio" name="r-gender" class="r-gender-input" value="0">
<div class="r-gender-div" index="1">女</div>
</label>
</div>
//20210123 解决性别单选框交互失效问题
$(".r-gender-div").on("click",function(){
// console.log($(this).prop("index")) //prop获取自定义属性失败,提示undefined
//console.log($(this).attr("index"))
//获取单选项的序号
var index = $(this).attr("index")
var gender_checked_dom = $(".r-gender-input")[index] //获取的是DOM元素
//console.log(gender_checked_dom)
//$(gender_checked_dom).prop("checked",true);
$(gender_checked_dom).attr("checked",true);
})
四、后端功能实现(IDEA)
数据库准备工作(SQLyog)
- 创建数据库webdb(从完整代码中的resources目录获取)
- 创建用户表tuser(同上)
Java代码开发(IDEA)
因为第二部分已经搭好了架子,这部分相对容易一些
- domain包
- 创建用户/学生的bean对象User
- 创建一个响应统一返回的Java对象ResultData
- 其中涉及到code字段,创建一个枚举类CodeEnum
- 其中涉及到的status字段,创建一个枚举类StatusEnum
- util包
- 创建Druid连接池工具类DruidUtils,方便dao层进行操作数据库
- 在项目根目录下创建一个resources目录,右键该目录,设置Mark Directory As 属性为Resources Root
- 在resources目录下创建一个数据库连接信息配置文件druid.properties(名字及后缀可以自定义,DruidUtils工具类中匹配上就行,建议用druid.properties命名,见名知意)
- dao包
- 创建用户信息持久化层接口UserDao,定义4个方法
- 根据账号查询用户数量 findUserCountByUserId
- 根据账号和密码查询用户 findUserByUserIdAndPwd
- 添加用户 insertUserByUser
- 查询所有用户 findAllUsers
- 创建一个子包impl
- impl包下创建UserDao接口的实现类UserDaoImpl,调用DruidUtils操作数据库,实现上述4个方法
- 创建用户信息持久化层接口UserDao,定义4个方法
- service包
- 用户服务层接口UserService,定义4个方法
- 查询该账号是否存在 isExistStudentOfUserId
- 验证账号密码是否正确 isCorrectOfUserIdAndPwd
- 添加用户 addUser
- 查询所有用户 findAllUsers
- 创建一个子包impl
- impl包下创建UserService实现类UserServiceImpl,重写UserService接口的4个方法
- isExistStudentOfUserId,调用dao层的实现类UserDaoImpl方法 findUserCountByUserId
- isCorrectOfUserIdAndPwd,调用dao层的实现类UserDaoImpl方法findUserByUserIdAndPwd
- addUser,调用dao层的实现类UserDaoImpl方法insertUserByUser
- findAllUsers,调用dao层的实现类UserDaoImpl方法findAllUsers
- 用户服务层接口UserService,定义4个方法
- web包
- 创建查询账号处理器CommonServlet,调用UserService的方法 isExistStudentOfUserId
- 创建用户登录请求处理器LoginServlet,调用UserService的方法 isCorrectOfUserIdAndPwd
- 创建添加用户处理器RegisterServlet,调用UserService的方法 addUser
- 创建查询用户列表处理器UserListServlet,调用UserService的方法 findAllUsers
- 创建一个子包filter
- filter包下创建编码过滤器 EncodeFilter,重写doFilter方法,统一设置请求和响应的编码格式
- 配置WEB-INF的web.xml,添加过滤器拦截路径
五、前后端联调(IDEA)
- 调整前端代码中的ajax请求,axios请求的回调函数中对响应数据的获取和判断
- 后端Java代码中根据联调情况,添加相应的日志打印或者服务器采用debug方式运行,打断点调试后端代码
六、相关参考资料(浏览器)
- 关于登录界面,80%的代码都是参考该文章进行实现的
html+css实现漂亮的透明登录页面,HTML实现炫酷登录页面
- 关于响应式布局的学习,参考此文章
HTML+CSS十分钟实现响应式布局页面,响应式布局实战教程
- 嵌套HTML后内部HTML的js失效问题参考(坑)
使用jquery的load方法加载公共html页面,但是引入的公共html的js却不生效
- Ajax的回调函数中使用return返回内容,但是外部函数接收到的确实undefined(坑)
Ajax回调函数中return不生效问题
- jar依赖包在idea中添加依赖后,启动tomcat报错问题(注意必须要放到tomcat的lib目录下)
java.lang.ClassNotFoundException: com.alibaba.druid.support.http.WebStatFilter异常解决
- com.alibaba.druid.pool.DruidDataSource.error {dataSource-1} init error
java.sql.SQLException: com.mysql.cj.jdbc.Driver 问题- 我这里是因为没有添加mysql 8.0连接驱动的jar包到tomcat目录下,添加上即可。
- 如果之前可以连接和访问,之后出现报错的话,可以参考下方文章
(已解决)解决问题:dataSource init error java.sql.SQLException: com.mysql.cj.jdbc.Driver和一些mysql连接jar包的问题
- idea中在dao层写sql语句时。表名和字段名报红,但是不影响程序运行,可以正常访问
- 如果看着不舒服,想解决idea报红问题,可以在idea中打开database选项(打开方法咨询度娘),连接到本地的数据库,这样idea就可以识别表和字段名是否正确,只有在不正确的情况下才会报红
- jdbc插入mysql的date类型字段时会报错
Data truncation: Incorrect datetime value: ‘Fri Jan 22 17:53:31 CST 2021’ for column ‘create_time’ at row 1
参考解决方案:java插入数据到mysql的datetime类型字段
- ajax通过post方式访问servlet时,发现浏览器控制台总会多一个get请求,地址栏也会变化
http://localhost:8080/homework/welcome.html?r-userid=test7&r-password=777777&r-username=%E6%B5%8B%E8%AF%957&r-nickname=&r-birth=2021-01-26&r-gender=1&r-grade=
- 因为form默认的提交方式是get,需要禁止默认提交操作
$("form").on("click",function(){ event.preventDefault(); })
- IDEA中写js函数时,提示错误,版本过低,解决方法如下
method definition shorthands are not supported by current JavaScript version
- tomcat的web目录下新添加的js文件在浏览器一直没有生效,不管怎么清缓存刷新都不行
IDEA中删除tomcat资源的输出目录(我这里是out目录),然后重启tomcat,便会全部加载,查看本地out目录路径方法如下图:
- 登录之后在首页获取到登录成功后存入cookie的值,报错$.cookie is not a function
参考解决方法
- 在一个页面中,body下面是2个平级的div,那么如何在俩个div中都挂载上vue对象(因为body不允许作为vue的挂载点)
解决方法
- 在俩个平级的div外面加一层div,新的div作为挂载点(缺点是:添加div后,可能要重新调整界面样式)
- 只用其中一个div作为挂载点,然后在vue的的某个方法内,使用jquery或js给另一个div的元素传参或赋值(我采用)
- 取消使用vue,全部使用jquey或js实现(缺点:渲染表格比较麻烦,还得拼接标签)
- -20210126 更新代码
添加登录过滤器,session过期或丢失后,再点击添加/刷新按钮,会自动进入登录状态拦截器,检查登录状态,发现没有session后,自动跳转到登录页面
涉及到的技术:
- 后端:Filter、Servlet、Session
- 前端:ajax回调函数、axios回调函数、Cookie
问题总结
-
访问/action/loginServlet后登录,该servlet向Cookie中保存了登陆用户的姓名userName,但是当页面自动跳转到首页后,地址栏一闪而过立马就重定向到登录页面了
【问题分析】:经过排查,发现是cookie的path不同导致的,登录后存储到cookie的path没有设置默认是/action,但是访问首页后,加载的userListServlet的路径没有/action,导致首页中cookie不可用,此> 时会判断cookie不存在默认跳转到登录页面
【问题解决】:在登录后存储cookie之前,主动设置cookie的path为 “/”,而不是默认的"/action"。关于cookie的path问题介绍可以参考:Cookie的路径设置(很重要)
cookie.setPath("/") //这样可以解决问题,但不是最好的方式
-
ajax发出请求,如果被过滤器拦截后,会进入ajax的error回调函数中,而不是success回调函数
另外success回调函数和error回调函数的参数顺序,以及从参数中获取请求头的方式之前不熟悉,记录如下:
success函数:
success:function(response,status,xhr){ ... }
error函数:
error:function(xhr,status,error){ var REDIRECT = xhr.getResponseHeader("REDIRECT"); if (REDIRECT == "REDIRECT"){ location.href = xhr.getResponseHeader("CONTENTPATH") }else{ alert("检查登录状态接口异常!") } }
-
axios发送的请求被过滤器拦截后,也会进入error回调函数,取出响应头的方式记录如下:
axios.get("/action/userListServlet", {params: {}}).then( function (response) { t.userList = response.data.data }, function (error) { //20210126 add //axios的请求被拦截后,会进入error方法 //直接通过.headers.contentpath的方式获取到headers的重定向路径,没有提供专门获取响应头的函数 console.log("error.response.headers.contentpath = " + error.response.headers.contentpath) if(error.response.headers.redirect == "REDIRECT"){ location.href = error.response.headers.contentpath } } )
- axios的拦截器,以及ajax回调函数执行完必然执行的的complete函数,也尝试使用,发现可能是因为系统设计的不规范,使用起来会出现很多新问题,最终没有采用