引言:
对于某语言不算熟悉的话自创项目是很痛苦的过程,即便笔者是一位掌握java的Android码农,对于java入门也是深感无力,毕竟语言是基础,但框架设计模式却与Android有出入,且学习成本较高,mybatisc,Spring-boot,thleaf,Spring Data JPA,Tomcat,IDEA,MVC,等等。有的似曾相识,有的一脸蒙蔽,笔者正陷入这漩涡当中,本章笔者将对Favorites的源码分析中,整理出完整的项目结构思路以及架构思想,层层剥离,以至融会贯通。
源码链接如下:https://github.com/cloudfavorites/favorites-web
调试提示:
//作者使用thymeleaf拆分模块,如需单独调试请将该head单独粘贴到需要观察的页面。
<head>
<meta charset="utf-8"></meta>
<meta http-equiv="X-UA-Compatible" content="IE=edge"></meta>
<meta name="renderer" content="webkit"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
<meta name="description" content=""></meta>
<meta name="author" content=""></meta>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta>
<link rel="icon" href="/img/icon.ico" type="image/x-icon"/>
<title>云收藏 | 让收藏更简单</title>
<link rel="stylesheet" href="../../static/vendor/fontawesome/css/font-awesome.min.css"/>
<link rel="stylesheet" href="../../static/vendor/simple-line-icons/css/simple-line-icons.css"/>
<link rel="stylesheet" href="../../static/vendor/animate.css/animate.min.css"/>
<link rel="stylesheet" href="../../static/vendor/toastr/toastr.min.css"/>
<link rel="stylesheet" href="../../static/media/css/bootstrap.css"/>
<link rel="stylesheet" href="../../static/media/css/app.css"/>
<link rel="stylesheet" href="../../static/media/css/theme-i.css"/>
</head>
项目架构
-
MVC
理解:负责项目的整体架构 简单说就是Controller 调用Repository和Service 通过thymeleaf来响应界面
学习: -
Maven+Spring-Boot
理解:Maven负责导包,Spring-Boot负责启动程序,找到SpringBootApplication即可,全局唯一。
学习: -
thymeleaf
- 理解:首页的 return “index”;即表示映射到templates文件夹下的index.html
@RequestMapping(value="/index",method=RequestMethod.GET)
@LoggerManage(description="首页")
public String index(Model model){
// IndexCollectorView indexCollectorView = collectorService.getCollectors();
model.addAttribute("collector","");
User user = super.getUser();
if(null != user){
model.addAttribute("user",user);
}
return "index";
}
- 基础:
2.1. https://www.cnblogs.com/ityouknow/p/5833560.html
2.2 https://www.jianshu.com/p/810ace1aeeae
-
Spring Data
理解:绑定bean对象 -
Spring Data JPA
理解:绑定bean对象执行相关操作的工具类
学习:
- 基本操作:https://www.cnblogs.com/zjfjava/p/8456771.html
- 操作手册:https://docs.spring.io/spring-data/jpa/docs/2.0.4.RELEASE/reference/html/
- 复杂查询:http://www.cnblogs.com/dixinyunpan/p/5856884.html
- 例子:
增:
userRepository.save(new User("aa", "aa@126.com", "aa", "aa123456"));
删:
//方式一
@Transactional
void deleteById(Long id);
//方式二
@Transactional
@Modifying
@Query("delete from Collect where favoritesId = ?1")
void deleteByFavoritesId(Long favoritesId);
查:
//方式一 固定写法
User findByUserName(String userName);
User findByUserNameOrEmail(String username, String email);
User findByEmail(String email);
User findById(long id);
//方式二
public String baseSql="select c.id as id,c.title as title, c.type as type,c.url as url,c.logoUrl as logoUrl,c.userId as userId, "
+ "c.remark as remark,c.description as description,c.lastModifyTime as lastModifyTime,c.createTime as createTime, "
+ "u.userName as userName,u.profilePicture as profilePicture,f.id as favoritesId,f.name as favoriteName "
+ "from Collect c,User u,Favorites f WHERE c.userId=u.id and c.favoritesId=f.id and c.isDelete='NO'";
//随便看看根据类别查询收藏
@Query(baseSql+ " and c.type='public' and c.category=?1 ")
Page<CollectView> findExploreViewByCategory(String category,Pageable pageable);
//方式三 联查+分页
public String baseSql="select c.id as id,c.title as title, c.type as type,c.url as url,c.logoUrl as logoUrl,c.userId as userId, "
+ "c.remark as remark,c.description as description,c.lastModifyTime as lastModifyTime,c.createTime as createTime, "
+ "u.userName as userName,u.profilePicture as profilePicture,f.id as favoritesId,f.name as favoriteName "
+ "from Collect c,User u,Favorites f WHERE c.userId=u.id and c.favoritesId=f.id and c.isDelete='NO'";
@Query(baseSql+ " and c.userId=?1 ")
Page<CollectView> findViewByUserId(Long userId,Pageable pageable);
改:
//方式一
@Modifying(clearAutomatically=true)
@Transactional
@Query("update User set passWord=:passWord where email=:email")
int setNewPassword(@Param("passWord") String passWord, @Param("email") String email);
//方式二
@Transactional
@Modifying
@Query("update Collect c set c.type = ?1 where c.id = ?2 and c.userId=?3 ")
int modifyByIdAndUserId(CollectType type, Long id, Long userId);
安全机制
- AOP
- SecurityFilter
- 错误URL提示页面
//在common.js中统一处理
function handleServerResponse() {
if (xmlhttp.readyState == 4) {
//document.getElementById("mainSection").innerHTML =xmlhttp.responseText;
var text = xmlhttp.responseText;
if(text.indexOf("<title>Favorites error Page</title>") >= 0){
window.location.href="/error.html";
}else{
$("#content").html(xmlhttp.responseText);
}
}
}
- 统一错误提示(JSON)
- 密码学
统一外部接口
Session 和Cookie
- Cookie返回给客户端
//保存
Cookie cookie = new Cookie(Const.LOGIN_SESSION_KEY, cookieSign(loginUser.getId().toString()));
cookie.setMaxAge(Const.COOKIE_TIMEOUT);
cookie.setPath("/");
response.addCookie(cookie)
//取值验证
Cookie[] cookies = request.getCookies();
if (cookies != null) {
boolean flag = true;
for (int i = 0; i < cookies.length; i++) {
Cookie cookie = cookies[i];
if (cookie.getName().equals(Const.LOGIN_SESSION_KEY)) {
if (StringUtils.isNotBlank(cookie.getValue())) {
flag = false;
} else {
break;
}
}
}
}
- Session :相当于是SP
protected HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
protected HttpSession getSession() {
return getRequest().getSession();
}
getSession().setAttribute(Const.LOGIN_SESSION_KEY, user);
protected User getUser() {
return (User) getSession().getAttribute(Const.LOGIN_SESSION_KEY);
}
Session与Cookie的 |
---|
cookie数据存放在客户的浏览器上,session数据放在服务器上。 |
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗 考虑到安全应当使用session。 |
session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能 考虑到减轻服务器性能方面,应当使用COOKIE。 |
单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。 |
---------------------建议--------------------- |
将登陆信息等重要信息存放为SESSION |
其他信息如果需要保留,可以放在COOKIE中 |
Spring-Boot注解
- @profile的用法
https://blog.csdn.net/wild46cat/article/details/71189858- - @Server("") 括号内作用
//接口
public interface FeedbackService {
public void saveFeeddback(Feedback feedback,Long userId);
}
//实现一
@Service("feedbackService")
public class FeedbackServiceImpl implements FeedbackService {
@Autowired
private FeedbackRepository feedbackRepository;
@Override
public void saveFeeddback(Feedback feedback,Long userId) {
// 一的方法
}
}
//实现二
@Service("feedbackService2")
public class FeedbackServiceImpl2 implements FeedbackService {
@Autowired
private FeedbackRepository feedbackRepository;
@Override
public void saveFeeddback(Feedback feedback,Long userId) {
// 二的方法
}
}
//一般情况下这样写就可以了
初始化
@RestController
@RequestMapping("/feedback")
public class FeedbackController extends BaseController{
@Autowired
private FeedbackService feedbackService;
}
//出现两个实现时
初始化
@RestController
@RequestMapping("/feedback")
public class FeedbackController extends BaseController{
@Autowired
@Qualifier("feedbackService")
private FeedbackService feedbackService;
@Autowired
@Qualifier("feedbackService2")
private FeedbackService feedbackService2;
}
配置文件
- @Configuration
理解:用于定义配置类,@Bean的对象将被加入(Context)上下文中作为全局变量。
学习: - https://blog.csdn.net/leoxyk/article/details/79800020(基础)
- https://blog.csdn.net/qq_34531925/article/details/78194651(高级)
前端
- html2Map:HtmlUtil
- ajax 与Vue http请求
2.1 点击事件绑定
2.1 httpth:onclick="'login()'"//thymeleaf onclick="login()"//js v-on:click="login"//Vue
注意:这里的Vue的使用方法//Vue Vue.http.options.emulateJSON = true; var loginPage = new Vue({ el: '#loginPage', data: { 'username': '', 'password': '' }, methods: { login: function (event) { var ok = $('#form').parsley().isValid({force: true}); if (!ok) { return; } var datas = { userName: this.username, passWord: this.password }; this.$http.post('/user/login', datas).then(function (response) { if (response.data.rspCode == '000000') { window.open(response.data.data, '_self'); } else { $("#errorMsg").html(response.data.rspMsg); $("#errorMsg").show(); } }, function (response) { console.log('error'); }) } } }) //jquery function login() { var username = document.getElementById("username").value; var password = document.getElementById("password").value; var form = new FormData() form.append("userName", username) form.append("passWord", password) $.ajax({ type: "POST", dataType: "json",//预期服务器返回的数据类型 // contentType: "application/json", 不能有这个,不然java后端无法接受到User的Json对象 contentType: false, // 注意这里应设为false processData: false, url: "/user/login", data: form, success: function (response) { if (response.rspCode == '000000') { window.open(response.data, '_self'); } else { $("#errorMsg").html(response.rspMsg); $("#errorMsg").show(); } }, error: function (response) { console.log('error'); } }); }
测试
直接看源码即可,嘿嘿
打包上线
参考:https://blog.csdn.net/qq_20330595/article/details/83862486#javaweb_38
综合案例——收藏列表
//登录成功后
window.open(response.data, '_self');//打开一个新窗口,并控制其外观
//IndexController控制器页面
@RequestMapping(value="/",method=RequestMethod.GET)
@LoggerManage(description="登陆后首页")
public String home(Model model) {
long size= collectRepository.countByUserIdAndIsDelete(getUserId(),IsDelete.NO);
Config config = configRepository.findByUserId(getUserId());
Favorites favorites = favoritesRepository.findById(Long.parseLong(config.getDefaultFavorties()));
List<String> followList = followRepository.findByUserId(getUserId());
model.addAttribute("config",config);
model.addAttribute("favorites",favorites);
model.addAttribute("size",size);
model.addAttribute("followList",followList);
model.addAttribute("user",getUser());
model.addAttribute("newAtMeCount",noticeRepository.countByUserIdAndTypeAndReaded(getUserId(), "at", "unread"));
model.addAttribute("newCommentMeCount",noticeRepository.countByUserIdAndTypeAndReaded(getUserId(), "comment", "unread"));
model.addAttribute("newPraiseMeCount",noticeRepository.countPraiseByUserIdAndReaded(getUserId(), "unread"));
logger.info("collect size="+size+" userID="+getUserId());
return "home";
}
home.html
//可以看到基本都是 thymeleaf的标签
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="layout">
<head th:include="layout :: htmlhead" th:with="title='favorites'"></head>
<body>
<section layout:fragment="content">
</section>
</body>
<script type='text/javascript'>
</script>
</html>
// layout:decorate="layout"
表示该html为一个子模板,且被layout.html引用
// layout:fragment="content"表示其将会替换的部分
<section layout:fragment="content">
</section>
//layout 是文件地址,如果有文件夹可以这样写 fileName/layout:htmlhead
htmlhead 是指定义的代码片段 如 th:fragment="copy"
//th:with="title='favorites'表示子模板想父布局传递值favorites
//整句的意思是 home.html的content部分替换layout.html的content部分,并修改标题为favorites
th:include="layout :: htmlhead" th:with="title='favorites'
参考:https://blog.csdn.net/u010784959/article/details/81001070
layout.html
<div th:replace="fragments/left :: left">left</div>
<div th:replace="fragments/sidebar :: sidebar">sidebar</div>
<div layout:fragment="content" id="content" ></div>
th:fragment
布局标签,定义一个代码片段,方便其它地方引用<div th:fragment="alert">
th:include
布局标签,替换内容到引入的文件<head th:include="layout :: htmlhead" th:with="title='xx'"></head> />
th:replace
布局标签,替换整个标签到引入的文件<div th:replace="fragments/header :: title"></div>
请注意 locationUrl是common.js的函数,即get访问/standard/my/0 ,回显到home.html
locationUrl('/standard/my/0','home');
function locationUrl(url,activeId){
if(mainActiveId != null && mainActiveId != "" && activeId != null && activeId != ""){
$("#"+mainActiveId).removeAttr("class");
$("#"+activeId).attr("class", "active");
mainActiveId = activeId;
}
goUrl(url,null);
}
var xmlhttp = new getXMLObject();
function goUrl(url,params) {
fixUrl(url,params);
if(xmlhttp) {
//var params = "";
xmlhttp.open("POST",url,true);
xmlhttp.onreadystatechange = handleServerResponse;
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
xmlhttp.send(params);
}
}
//最终将获取到的html(statnder.html)内容赋给id=content(layout.html)的布局
function handleServerResponse() {
if (xmlhttp.readyState == 4) {
//document.getElementById("mainSection").innerHTML =xmlhttp.responseText;
var text = xmlhttp.responseText;
if(text.indexOf("<title>Favorites error Page</title>") >= 0){
window.location.href="/error.html";
}else{
$("#content").html(xmlhttp.responseText);
}
}
}
这里的关键在于layout.html 其管理页面以及立即执行函数写的很巧妙。
home页面收藏,点赞,评论,删除,修改属性
通过上面的分析 我们可以发现statnder.html是被填充到home.html中的,也可说是到layout.html的
<div layout:fragment="content" id="content" ></div>
- 点赞
<a th:id="'like' + ${collect.id}" class="sharing-action-button"
th:style="'display:' + @{(${collect.Praise} ? 'none' : 'inline-block')} + ''"
th:onclick="'changeLike(' + ${collect.id} + ');'">
<span class="fa fa-thumbs-o-up"></span>
<show th:id="'likeS' + ${collect.id}" th:text="|点赞(${collect.praiseCount})|"></show>
</a>
// 在collec.js中调用 可以看出 他在界面底部做了一个隐藏字段,方便检验登录状态,随时跳转登录
function changeLike(id){
var userId = document.getElementById("userId").value;
if(userId != "0"){
$.ajax({
async: false,
type: 'POST',
dataType: 'json',
data:"",
url: '/collect/like/'+id,
error : function(XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest);
console.log(textStatus);
console.log(errorThrown);
},
success: function(like){
if($("#like"+id).is(":hidden")){
$("#like"+id).show();
var praiseCount=parseInt($("#praiseC"+id).val())-1;
$("#praiseC"+id).val(praiseCount);
$("#likeS"+id).html("点赞("+praiseCount+")");
$("#likel"+id).show();
$("#unlike"+id).hide();
$("#unlikel"+id).hide();
}else{
$("#like"+id).hide();
$("#likel"+id).hide();
$("#unlike"+id).show();
$("#unlikel"+id).show();
var praiseCount=parseInt($("#praiseC"+id).val())+1;
$("#praiseC"+id).val(praiseCount);
$("#unlikeS"+id).html("取消点赞("+praiseCount+")");
}
}
});
}else{
window.location.href="/login";
}
}
- 修改
//collect是从HomeController传递过来的
<a href="javascript:void(0);"
class="normal-color-a ng-binding"
th:text="${collect.favoriteName}"
th:onclick="'locationUrl(\'/standard/' + ${collect.favoritesId} + '/'+${collect.userId}+'\',\''+ ${collect.favoritesId} + '\');'">文件加名称</a>
这样还是访问了一次@RequestMapping(value="/standard/{type}/{userId}")
- 评论
步骤:查询是否显示评论,查询评论列表,填充回显填充评论列表,显示评论输入框
<a class="sharing-action-button btn-comment"
href="javascript:void(0);"
th:onclick="'switchComment(' + ${collect.id} + ');'">
<span class="fa fa-comment-o"></span>
<show th:id="'commentS' + ${collect.id}"
th:text="|评论(${collect.commentCount})|">
评论(0)
</show>
</a>
//还是在collect.js中调用
function switchComment(collectId){
var userId = document.getElementById("userId").value;
if(userId != "0"){
if($("#collapse"+collectId).hasClass('in')){
$("#collapse"+collectId).removeClass('in');
}else{
showComment(collectId);
}
}else{
window.location.href="/login";
}
}
function showComment(collectId){
$.ajax({
async: false,
type: 'POST',
dataType: 'json',
data:'',
url: '/comment/list/'+collectId,
error : function(XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest);
console.log(textStatus);
console.log(errorThrown);
},
success: function(comments){
initComment(comments,collectId);
$("#collapse"+collectId).addClass('in');
}
});
}
function initComment(comments,collectId){
var comment='';
$("#commentList"+collectId).html("");
for(var i=0;i<comments.length;i++){
var item ='<div class=\"media bb p\"><small class=\"pull-right text-muted\">'+comments[i].commentTime+'</small>';
item=item+'<div class=\"pull-left\"><img class=\"media-object img-circle thumb32\" src=\"/'+comments[i].profilePicture+ '\" /></div> ';
item=item+'<div class=\"media-body\"> <span class=\"media-heading\"> <p class=\"m0\"> '
item=item+"<a href=\"javascript:void(0);\" onclick=\"locationUrl('/user/" + comments[i].userId + "/0')\">"+comments[i].userName+"</a>";
item=item+'</p> <p class=\"m0 text-muted\">';
if(!isEmpty(comments[i].replyUserName)){
item=item+'回复@'+comments[i].replyUserName+':'+comments[i].content+'<small>';
}else{
item=item+comments[i].content+'<small>';
}
if($("#loginUser").length > 0){
if(comments[i].userId==$("#loginUser").val()){
item=item+"<a href=\"javascript:void(0);\" onclick=\"deleteComment('"+comments[i].id+"','"+collectId+"')\" > 删除</a>";
}else{
item=item+"<a href=\"javascript:void(0);\" onclick=\"replyComment('"+comments[i].userName+"','"+collectId+"')\" class=\"replyComment\" > 回复</a>";
}
}else{
if(comments[i].userId==$("#userId").val()){
item=item+"<a href=\"javascript:void(0);\" onclick=\"deleteComment('"+comments[i].id+"','"+collectId+"')\" > 删除</a>";
}else{
item=item+"<a href=\"javascript:void(0);\" onclick=\"replyComment('"+comments[i].userName+"','"+collectId+"')\" class=\"replyComment\" > 回复</a>";
}
}
item=item+'</small></p></span></div></div>';
comment=comment+item;
}
$("#commentList"+collectId).append(comment);
if($("#loginUserInfo").val()==null||$("#loginUserInfo").val()==''){
$(".replyComment").hide();
}
}
- 修改收藏
//对于下拉菜单不必深究,因为他是bootstrap的插件
//参考:http://www.runoob.com/bootstrap/bootstrap-dropdown-plugin.html
<div class="pull-right dropdown dropdown-list">
<a href="#" data-toggle="dropdown"
class="sharing-more-button"
th:if="${userId == collect.userId}">
<span class="fa fa-angle-down"></span>
</a>
<ul class="dropdown-menu animated bounceIn">
<li>
<div class="list-group">
<a href="javascript:void(0);"
class="list-group-item"
th:onclick="'getCollect(' + ${collect.id} + ',\'\');'">
<div class="media-box">
<div class="pull-left">
<em class="fa fa-pencil-square-o fa-2x fa-fw text-info"></em>
</div>
<div class="media-box-body clearfix">
<p class="m0">修改收藏</p>
<p class="m0 text-muted">
<small>修改收藏的各种属性</small>
</p>
</div>
</div>
</a>
</div>
</li>
</ul>
</div>
//调用collect.js中的getCollect方法
//主要看$('#modal-changeSharing').modal('show');函数显示修改窗口
//该方法调用CollectController的detail函数执行查找
@RequestMapping(value="/detail/{id}")
public Collect detail(@PathVariable("id") long id) {
Collect collect=collectRepository.findById(id);
return collect;
}
function getCollect(id,user){
var userId = document.getElementById("userId").value;
if(userId != "0"){
$.ajax({
async: false,
type: 'POST',
dataType: 'json',
data:"",
url: '/collect/detail/'+id,
error : function(XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest);
console.log(textStatus);
console.log(errorThrown);
},
success: function(collect){
$("#ctitle").val(collect.title);
$("#clogoUrl").val(collect.logoUrl);
$("#cremark").val(collect.remark);
$("#cdescription").val(collect.description);
$("#ccollectId").val(collect.id);
$("#curl").val(collect.url);
$('#modal-changeSharing').modal('show');
if("private" == gconfig.defaultCollectType){
$("#type").prop('checked',true);
}else{
$("#type").prop('checked',false);
}
if("simple"==gconfig.defaultModel){
$("#show2").hide();
$("#show1").show();
$("#model2").hide();
$("#model1").show();
}else{
$("#show1").hide();
$("#show2").show();
$("#model1").hide();
$("#model2").show();
}
if("usercontent" == user){
if($("#userId").val() == $("#loginUser").val()){
$("#favoritesSelect").val(collect.favoritesId);
}else{
$("#favoritesSelect").val(gconfig.defaultFavorties);
}
}else{
if($("#userId").val() == collect.userId){
$("#favoritesSelect").val(collect.favoritesId);
}else{
$("#favoritesSelect").val(gconfig.defaultFavorties);
}
}
$("#newFavorites").val("");
$("#userCheck").val(user);
loadFollows();
}
});
}else{
window.location.href="/login";
}
}
//回显alert.html中的modal-changeSharing部分
//fragments/collect :: collect 理解为在fragments/collect文件下的id为collect的html
//参考:http://www.cnblogs.com/lazio10000/p/5603955.html
<div id="modal-changeSharing" class="modal fade">
<div class="modal-dialog wd-xl">
<div th:replace="fragments/collect :: collect">collect</div>
</div>
</div>
- 删除(收藏夹)
//layout.html中注入了弹窗页面
<div th:replace="fragments/alert :: alert">alert</div>
//在standard.html中data-target="#modal-removeFav"
<div class="pull-right" th:if="${otherPeople == null and type != 'garbage'}">
<span class="title-small icon-folder-alt mr-sm"></span>
<span data-toggle="modal" data-target="#modal-removeFav"
class="title-small clickable">删除</span>
</div>
//对应alert.html中的
<div id="modal-removeFav" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" data-dismiss="modal" class="close">
<span>×</span>
</button>
<h4 class="modal-title">删除收藏夹名</h4>
</div>
<div class="modal-body">
<p>删除收藏夹将删除该收藏下所有收藏,且无法找回,是否继续?</p>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn">取消</button>
<button id="delFavoritesBtn" onclick="delFavorites()" type="button" class="btn btn-danger">删除收藏夹</button>
</div>
</div>
</div>
</div>
- 删除收藏
//standard.html中调用
<a href="javascript:void(0);"
class="list-group-item"
th:onclick="'onCollect(' + ${collect.id} + ',\'\');'">
<div class="media-box">
<div class="pull-left">
<em class="fa fa-trash fa-2x fa-fw text-danger"></em>
</div>
<div class="media-box-body clearfix">
<p class="m0">删除</p>
<p class="m0 text-muted">
<small>该分享会永久删除</small>
</p>
</div>
</div>
</a>
//alert.html弹出提示
<div id="modal-remove" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" data-dismiss="modal" class="close">
<span>×</span>
</button>
<h4 class="modal-title">删除收藏</h4>
</div>
<div class="modal-body">
<p>该收藏将被永久删除,且无法找回,是否继续?</p>
</div>
<div class="modal-footer">
<button id="delCollect" type="button" class="btn btn-primary" onclick="delCollect()" >确定</button>
<button type="button" data-dismiss="modal" class="btn btn-danger">取消</button>
</div>
</div>
</div>
</div>
//collect.js中执行
function delCollect(){
$.ajax({
async: false,
type: 'POST',
dataType: 'json',
data:"",
url: '/collect/delete/'+$("#collectId").val(),
error : function(XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest);
console.log(textStatus);
console.log(errorThrown);
},
success: function(response){
loadFavorites();
if("usercontent" == $("#userCheck").val()){
userLocationUrl($("#forward").val(),"userAll");
loadUserFavorites();
}else{
locationUrl($("#forward").val(),"home");
}
$('#modal-remove').modal('hide');
}
});
}
综合案例——左边导航栏
//可以发现activeId即当前高亮的nav
function locationUrl(url,activeId){
if(mainActiveId != null && mainActiveId != "" && activeId != null && activeId != ""){
$("#"+mainActiveId).removeAttr("class");
$("#"+activeId).attr("class", "active");
mainActiveId = activeId;
}
goUrl(url,null);
}
- 导入界面
//重点 name="htmlFile" filestyle="" type="file" accept="text/html"
<div class="panel">
<div class="panel-heading">请选择浏览器导出的html格式的书签文件</div>
<div class="panel-body">
<div class="form-group">
<input id="fileInput" name="htmlFile" filestyle="" type="file" accept="text/html" data-class-button="btn btn-default" data-class-input="form-control" data-button-text="" class="form-control" tabindex="-1" style="position: absolute; clip: rect(0px 0px 0px 0px);" />
<div class="bootstrap-filestyle input-group">
<input type="text" id="fileInputName" value="" class="form-control " disabled="" />
<span class="group-span-filestyle input-group-btn" tabindex="0">
<label for="fileInput" class="btn btn-default ">
<span class="glyph glyphicon glyphicon-folder-open"></span>
</label>
</span>
</div>
</div>
</div>
</div>
js部分
//立即执行函数
$(function(){
//toast插件
toastr.options = {
'closeButton': true,
'positionClass': 'toast-top-center',
'timeOut': '5000',
};
//jquery的输入监听
$("#fileInput").change(function(){
getFileName("fileInput");
});
var count = 0;
//点击事件
$("#submitBtn").click(function(){
if($("#fileInputName").val()==""){
return;
}
//防重复点击
$("#submitBtn").attr("disabled","disabled");
//ajaxSubmit新的提交方式
$("#importHtmlForm").ajaxSubmit({
type: 'post',
async: true,
url: '/collect/import',
success: function(response){
}
});
if(count == 0){
toastr.success('正在导入到"导入自浏览器"收藏夹,请稍后查看', '操作成功');
loadFavorites();
}
count++;
});
});
- 布局
//在app.css中的响应式布局
<div class="content-wrapper">
导出界面
//表单
<form action="/collect/export" id="exportForm" method="post" onsubmit="return false"/>
//上传
$("#exportBtn").click(function(){
if($("input[name='favoritesId']:checked").length ==0){
return;
}
$("#exportForm").removeAttr("onsubmit");
$("#exportForm").submit();
$("#exportForm").attr("onsubmit","return false");
});
//导出逻辑
@RequestMapping("/export")
@LoggerManage(description="导出收藏夹操作")
public void export(String favoritesId,HttpServletResponse response){
if(StringUtils.isNotBlank(favoritesId)){
try {
String[] ids = favoritesId.split(",");
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String fileName= "favorites_" + date + ".html";
StringBuilder sb = new StringBuilder();
for(String id : ids){
try {
sb = sb.append(collectService.exportToHtml(Long.parseLong(id)));
} catch (Exception e) {
logger.error("异常:",e);
}
}
sb = HtmlUtil.exportHtml("云收藏夹", sb);
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-disposition","attachment; filename=" + fileName);
response.getWriter().print(sb);
} catch (Exception e) {
logger.error("异常:",e);
}
}
}
综合案例——顶部导航栏
缩小扩大左侧抽屉布局
<ul class="nav navbar-nav">
<li>
<a title="缩小/扩大侧栏" class="hidden-xs" data-toggle-state="aside-collapsed"
href="#"> <em class="fa fa-navicon"></em>
</a>
<a title="缩小/扩大侧栏" class="visible-xs sidebar-toggle" data-no-persist="true"
data-toggle-state="aside-toggled" href="#"> <em class="fa fa-navicon"></em>
</a>
</li>
</ul>
对应Logo的变化
<div class="navbar-header">
<a class="navbar-brand" href="/index">
<div class="brand-logo">
<img class="img-responsive" alt="App Logo" th:src="@{/img/logo.png}"/></div>
<div class="brand-logo-collapsed">
<img class="img-responsive" alt="App Logo" th:src="@{/img/logo.png}"/></div>
</a>
</div>
消息通知+弹出层
<li class="dropdown dropdown-list">
<a title="消息通知" data-toggle="dropdown" href="#">
<em class="icon-bell"></em>
<!--<div class="label label-danger" th:if="${newAtMeCount gt 0 or newCommentMeCount gt 0 or newPraiseMeCount gt 0}" th:text="${newAtMeCount}+${newCommentMeCount}+${newPraiseMeCount}"></div>-->
<div class="label label-danger" id="noticeNum"></div>
</a>
<ul class="dropdown-menu animated flipInX">
<li>
<div class="list-group">
<a class="list-group-item" href="javascript:void(0);"
onclick="showNotice('at')">
<div class="media-box">
<div class="pull-left">
<em class="fa fa-at fa-2x text-info"></em>
</div>
<div class="media-box-body clearfix">
<p class="m0">@我的</p>
<p class="m0 text-muted">
<small id="atMeNewNotice"></small>
<input type="hidden" id="newAtMeCount"/>
</p>
</div>
</div>
</a>
<a class="list-group-item" href="javascript:void(0);"
onclick="showNotice('comment')">
<div class="media-box">
<div class="pull-left">
<em class="fa fa-comment fa-2x text-warning"></em>
</div>
<div class="media-box-body clearfix">
<p class="m0">评论我的</p>
<p class="m0 text-muted">
<small id="commentMeNewNotice"></small>
<input type="hidden" id="newCommentMeCount"/>
</p>
</div>
</div>
</a>
<a class="list-group-item" href="javascript:void(0);"
onclick="showNotice('praise')">
<div class="media-box">
<div class="pull-left">
<em class="fa fa-thumbs-up fa-2x text-success"></em>
</div>
<div class="media-box-body clearfix">
<p class="m0">赞我的</p>
<p class="m0 text-muted">
<small id="praiseMeNewNotice"></small>
<input id="newPraiseMeCounts" type="hidden"/>
</p>
</div>
</div>
</a>
<a class="list-group-item" href="javascript:void(0);"
onclick="showNotice('letter')">
<div class="media-box">
<div class="pull-left">
<em class="fa fa-bell fa-2x text-danger"></em>
</div>
<div class="media-box-body clearfix">
<p class="m0">私信我的</p>
<p class="m0 text-muted">
<!--<small>0 新消息</small>-->
<small id="newLetterNotice"></small>
<input id="newLetterNoticeCount" type="hidden"/>
</p>
</div>
</div>
</a>
</div>
</li>
</ul>
</li>
<li>
搜索框
<form class="navbar-form" role="search">
<div class="form-group has-feedback">
<input id="searchKey" type="text" class="form-control" placeholder="输入并且按回车确定..."/>
<div class="fa fa-times form-control-feedback" data-search-dismiss=""></div>
</div>
<button class="hidden btn btn-default" type="button">提交</button>
</form>
搜素逻辑
document.onkeydown = function (e) {
if (!e) e = window.event;//火狐中是 window.event
if ((e.keyCode || e.which) == 13) {
window.event ? window.event.returnValue = false : e.preventDefault();
var key = document.getElementById("searchKey").value;
if (key != '') {
locationUrl('/search/' + key, "");
}
}
}
//HomeControllerk逻辑
@RequestMapping(value="/search/{key}")
@LoggerManage(description="搜索")
public String search(Model model,@RequestParam(value = "page", defaultValue = "0") Integer page,
@RequestParam(value = "size", defaultValue = "20") Integer size, @PathVariable("key") String key) {
Sort sort = new Sort(Direction.DESC, "id");
Pageable pageable = PageRequest.of(page, size,sort);
List<CollectSummary> myCollects=collectService.searchMy(getUserId(),key ,pageable);
List<CollectSummary> otherCollects=collectService.searchOther(getUserId(), key, pageable);
model.addAttribute("myCollects", myCollects);
model.addAttribute("otherCollects", otherCollects);
model.addAttribute("userId", getUserId());
model.addAttribute("mysize", myCollects.size());
model.addAttribute("othersize", otherCollects.size());
model.addAttribute("key", key);
logger.info("search end :"+ getUserId());
return "collect/search";
}
评论 赞 私信 以及上面的搜素统统是通过handleServerResponse这个监听器方法获取并替换#content的,在layout.js中
function handleServerResponse() {
if (xmlhttp.readyState == 4) {
//document.getElementById("mainSection").innerHTML =xmlhttp.responseText;
var text = xmlhttp.responseText;
if(text.indexOf("<title>Favorites error Page</title>") >= 0){
window.location.href="/error.html";
}else{
$("#content").html(xmlhttp.responseText);
}
}
}
layout:decorate=“layout” | 表示被父布局layout.html引用 |
---|---|
th:include=“layout :: htmlhead” th:with=“title=‘favorites’” | layout的th:fragment="htmlhead"必须与th:include="layout :: htmlhead"中的值(htmlhead)对应,但th:include="layout :: htmlhead"非必须 |
layout:fragment=“content” | 在子布局中,一般写自己的布局,用来替换父布局,content为自定义名称,需要与layout.html的layout:fragment="content"相对应 |
综合案例 —— 个人中心
待补充。。
总结:
-
分析了源码之后我们得到了什么
1.1. 基本掌握MVC设计模式
1.2. 基本掌握thymeleaf
1.3. 基本掌握项目的部署与搭建(Tomcat)
1.4. 基本理解项目整体架构
1.5. 基本掌握Spring-boot框架
1.6. 基本掌握spring-data-jpa框架 -
我们应该如何去孵化自己的项目
答:其实这个问题应该问下自己,最终学习的目的是什么,如果只是为了学习而学习是很可怕的,因为没有目的性,我们很难坚持,且没有实战的学习毫无意义。笔者也时常问自己究竟想做什么?就在此刻笔者也没有想清楚,但是秉着全栈的初衷,笔者掌握一门后台语言的想法始终不变,就笔者的学习思路,笔者打算就地取材,直接站在巨人的肩膀上面,修修改改,最终改造成一个笔者满意的个人后台系统,可能其中充满着原作者的代码以及版权声明,不过那只是后话。循序渐进的学习才能真正的掌握一门语言,即便笔者有java基础也绝不可能一步登天,任何人都一样,个中的原因笔者不想解释。原作者的项目是从2016年中写到今年下半年,2年之久的项目,加之其精进的代码风格,笔者本着敬畏之心慢慢阅读,断续的花了将近2周的时间,虽收获颇丰,但碍于对java的认知程度不够,仍未能完全理解。