引言:

对于某语言不算熟悉的话自创项目是很痛苦的过程,即便笔者是一位掌握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

  1. 理解:首页的 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";}
  1. 基础:
    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对象执行相关操作的工具类
    学习:

  1. 基本操作:https://www.cnblogs.com/zjfjava/p/8456771.html
  2. 操作手册:https://docs.spring.io/spring-data/jpa/docs/2.0.4.RELEASE/reference/html/
  3. 复杂查询:http://www.cnblogs.com/dixinyunpan/p/5856884.html
  4. 例子:

增:

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);
安全机制
  1. AOP
  2. SecurityFilter
  3. 错误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);}}
}
  1. 统一错误提示(JSON)
  2. 密码学
统一外部接口
Session 和Cookie
  1. 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;}}}
}
  1. 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注解
  1. 参考:https://www.cnblogs.com/ldy-blogs/p/8550406.html
  • @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 {@Autowiredprivate FeedbackRepository feedbackRepository;@Overridepublic void saveFeeddback(Feedback feedback,Long userId) {// 一的方法}
}//实现二
@Service("feedbackService2")
public class FeedbackServiceImpl2 implements FeedbackService {@Autowiredprivate FeedbackRepository feedbackRepository;@Overridepublic void saveFeeddback(Feedback feedback,Long userId) {// 二的方法}
}
//一般情况下这样写就可以了
初始化
@RestController
@RequestMapping("/feedback")
public class FeedbackController extends BaseController{@Autowiredprivate 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(高级)
前端
  1. html2Map:HtmlUtil
  2. ajax 与Vue http请求
    2.1 点击事件绑定

     th:onclick="'login()'"//thymeleafonclick="login()"//jsv-on:click="login"//Vue
    

    2.1 http
    注意:这里的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');})}}
    })
    //jqueryfunction 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, // 注意这里应设为falseprocessData: 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部分,并修改标题为favoritesth: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>
  1. 点赞
  <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";}
}
  1. 修改
//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}")

  1. 评论
    步骤:查询是否显示评论,查询评论列表,填充回显填充评论列表,显示评论输入框
<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();}
}
  1. 修改收藏
//对于下拉菜单不必深究,因为他是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>
  1. 删除(收藏夹)
//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>&times;</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>
  1. 删除收藏
//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>&times;</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);
}
  1. 导入界面
//重点 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++;});});
  1. 布局
//在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.eventif ((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.1. 基本掌握MVC设计模式
    1.2. 基本掌握thymeleaf
    1.3. 基本掌握项目的部署与搭建(Tomcat)
    1.4. 基本理解项目整体架构
    1.5. 基本掌握Spring-boot框架
    1.6. 基本掌握spring-data-jpa框架

  2. 我们应该如何去孵化自己的项目
    答:其实这个问题应该问下自己,最终学习的目的是什么,如果只是为了学习而学习是很可怕的,因为没有目的性,我们很难坚持,且没有实战的学习毫无意义。笔者也时常问自己究竟想做什么?就在此刻笔者也没有想清楚,但是秉着全栈的初衷,笔者掌握一门后台语言的想法始终不变,就笔者的学习思路,笔者打算就地取材,直接站在巨人的肩膀上面,修修改改,最终改造成一个笔者满意的个人后台系统,可能其中充满着原作者的代码以及版权声明,不过那只是后话。循序渐进的学习才能真正的掌握一门语言,即便笔者有java基础也绝不可能一步登天,任何人都一样,个中的原因笔者不想解释。原作者的项目是从2016年中写到今年下半年,2年之久的项目,加之其精进的代码风格,笔者本着敬畏之心慢慢阅读,断续的花了将近2周的时间,虽收获颇丰,但碍于对java的认知程度不够,仍未能完全理解。

基于‘纯洁的微笑’开源项目 — Favorites 源码分析相关推荐

  1. Go实现的5G核心网开源项目free5gc源码分析系列 | Gopher Daily (2021.01.08) ʕ◔ϖ◔ʔ

    每日一谚:"Abstractions should be discovered, not created." Go技术新闻 Go实现的5G核心网开源项目free5gc源码分析系列 ...

  2. BT开源项目Snark源码分析

    BT开源项目Snark源码分析 Snark是国外一个开源Java的项目,实现了BitTorrent协议,通过分析此项目的源程序,可以更利于我们更加深入的了解当前流行的BT软件的原理,进而可以指导我们的 ...

  3. go开源项目influxdb-relay源码分析(一)

    influxdb-relay项目地址: https://github.com/influxdata/influxdb-relay,主要作为负载均衡节点,写入多个influxdb节点,起到高可用效果. ...

  4. 鸿蒙开源源码,基于鸿蒙系统开源项目OpenHarmony源码静态分析

    #ifndef __scc #define __scc(X) ((long) (X)) // 转为long类型 typedef long syscall_arg_t; #endif #define _ ...

  5. 今年我读了四个开源项目的源码,来分享下心得

    今年来看了 RocketMQ.Kafka.Dubbo .Tomcat 的源码,之前也有读者询问过如何读源码,索性就来分享一下. 其实还看了一点点 Linux.Redis.jdk8,这几个阅读的目的和上 ...

  6. 开源项目实例源码_今年我读了四个开源项目的源码,来分享下心得

    今年来看了 RocketMQ.Kafka.Dubbo .Tomcat 的源码,之前也有读者询问过如何读源码,索性就来分享一下. 其实还看了一点点 Linux.Redis.jdk8,这几个阅读的目的和上 ...

  7. 【机器人学】机器人开源项目KDL源码学习:(5)KDL如何求解几何雅克比矩阵

    这篇文章试图说清楚两件事:1. 几何雅克比矩阵的本质:2. KDL如何求解机械臂的几何雅克比矩阵. 一.几何雅克比矩阵的本质 机械臂的关节空间的速度可以映射到执行器末端在操作空间的速度,这种映射可以通 ...

  8. 开源项目Telegram源码 Telegram for Android Source

    背景介绍 Telegram 是一款跨平台的即时通信软件,它的客户端是自由及开放源代码软件.用户可以相互交换加密与自毁消息,发送照片.影片等所有类型文件.官方提供手机版.桌面版和网页版等多种平台客户端. ...

  9. php,mysql 项目--商城--源码分析。

    php项目–商城–源码分析. 此商城网站源码下载地址:https://github.com/jeremywong1992/nuomi_shop php项目---商城--源码分析 网站目录下包含六个文件 ...

最新文章

  1. 图神经网络(AliGraph)在阿里巴巴的发展与应用
  2. 无法打开文件“opencv_world410d.obj”
  3. 《3D数学基础系列视频》1.1向量基本概念
  4. 如何升级xcode 中的cocos2dx 到v2.2.2以上版本
  5. VC++开发一个简易图片浏览器 - 含目录浏览功能
  6. 奥巴马女儿要上哈佛了!从小给女儿定下了五条规矩! 2017-08-07 07:35 哈佛/美国 转载授权请回复“转载“ 文:益美传媒|编辑:Angela 奥巴马曾说自己最骄傲的一件事,就是即使在长
  7. python opencv 4.1.0 cv2.convertScaleAbs()函数 (通过线性变换将数据转换成8位[uint8])(用于Intel Realsense D435显示depth图像)
  8. linux如何在a休息时运行b,Linux下如何优雅地修正命令?
  9. 前端学习(2775):方式2进行路由跳转
  10. 凸优化有关的数值线性代数知识四:分块消元与Schur补
  11. HTTP headers 详解 headers 常用属性
  12. 【C#】 .NET Reflector 安装与学习
  13. openfoam一些报错的原因(持续更新)
  14. 公众号文章阅读量数据导出
  15. 国产麒麟系统忘记密码重置办法(5步解决)
  16. 如何设置HttpClient请求的Content-Type标头?
  17. 如何将PDF删除水印?PDF删除水印的方法
  18. Flutter组件学习(13)层叠布局Stack、Positioned
  19. tup,list,dict,set的创建
  20. 看这玩意复习你还会挂科?《数据结构篇》

热门文章

  1. PTA-使用函数验证哥德巴赫猜想
  2. Win7出现0xc000000e错误的解决办法
  3. java随机生成验证码图片
  4. javaScript变量提升以及函数提升
  5. 金杉号:现在做什么生意项目可能实现财富自由的目标
  6. FFmpeg源代码:avformat_write_header/av_write_frame/av_interleaved_write_frame/av_write_trailer
  7. 在跨境电商领域,国际短信的优势你了解多少?
  8. PhoneME简介(翻译)
  9. 看我如何黑进邻居的电视
  10. Android应用“安豆苗”让你用手指滑动图片将图片实时分享到另一部手机