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

引言:

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

//接口
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;
}
配置文件
前端
  1. html2Map:HtmlUtil
  2. ajax 与Vue http请求
    2.1 点击事件绑定
     th:onclick="'login()'"//thymeleaf
     onclick="login()"//js
     v-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');
                })
            }
        }
    })
    //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>
  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.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.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的认知程度不够,仍未能完全理解。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值