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

引言:

对于某语言不算熟悉的话自创项目是很痛苦的过程,即便笔者是一位掌握java的Android码农,对于java入门也是深感无力,毕竟语言是基础,但框架设计模式却与Android有出入,且学习成本较高,mybatisc,Spring-boot,thleaf,Spring Data JPA,Tomcat,IDEA,MVC,等等。有的似曾相识,有的一脸蒙蔽,笔者正陷入这漩涡当中,本章笔者将对Favorites的源码分析中,整理出完整的项目结构思路以及架构思想,层层剥离,以至融会贯通。

源码链接如下:https://github.com/cloudfavorites/favorites-web
调试提示:
//作者使用thymeleaf拆分模块,如需单独调试请将该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", "[email protected]", "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 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 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("Favorites error Page") >= 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 {

    @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(高级)
前端
  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 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的标签 

  
  
      
// layout:decorate="layout" 表示该html为一个子模板,且被layout.html引用 // layout:fragment="content"表示其将会替换的部分
//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

left
sidebar
th:fragment 布局标签,定义一个代码片段,方便其它地方引用
th:include 布局标签,替换内容到引入的文件 /> th:replace 布局标签,替换整个标签到引入的文件

请注意 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("Favorites error Page") >= 0){
			window.location.href="/error.html";
		}else{
			$("#content").html(xmlhttp.responseText);
		}
	}
}

这里的关键在于layout.html 其管理页面以及立即执行函数写的很巧妙。

home页面收藏,点赞,评论,删除,修改属性

通过上面的分析 我们可以发现statnder.html是被填充到home.html中的,也可说是到layout.html的

 
  1. 点赞
  
 
 // 在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传递过来的
文件加名称                                

这样还是访问了一次@RequestMapping(value="/standard/{type}/{userId}")

  1. 评论
    步骤:查询是否显示评论,查询评论列表,填充回显填充评论列表,显示评论输入框

 //还是在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';
		item=item+'
'; item=item+'

' item=item+""+comments[i].userName+""; item=item+'

'; if(!isEmpty(comments[i].replyUserName)){ item=item+'回复@'+comments[i].replyUserName+':'+comments[i].content+''; }else{ item=item+comments[i].content+''; } if($("#loginUser").length > 0){ if(comments[i].userId==$("#loginUser").val()){ item=item+" 删除"; }else{ item=item+" 回复"; } }else{ if(comments[i].userId==$("#userId").val()){ item=item+" 删除"; }else{ item=item+" 回复"; } } item=item+'

'; 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


//调用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


  1. 删除(收藏夹)
//layout.html中注入了弹窗页面
alert
//在standard.html中data-target="#modal-removeFav"
删除
//对应alert.html中的
  1. 删除收藏
//standard.html中调用

       

删除

该分享会永久删除

//alert.html弹出提示 //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" 

请选择浏览器导出的html格式的书签文件

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中的响应式布局
 

导出界面

//表单
//上传 $("#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); } } }
综合案例——顶部导航栏

缩小扩大左侧抽屉布局

  

对应Logo的变化


消息通知+弹出层

  
  • 搜索框

    
                

    搜素逻辑

     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 myCollects=collectService.searchMy(getUserId(),key ,pageable);
        List 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("Favorites error Page") >= 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的认知程度不够,仍未能完全理解。

    你可能感兴趣的:(JAVA)