对于某语言不算熟悉的话自创项目是很痛苦的过程,即便笔者是一位掌握java的Android码农,对于java入门也是深感无力,毕竟语言是基础,但框架设计模式却与Android有出入,且学习成本较高,mybatisc,Spring-boot,thleaf,Spring Data JPA,Tomcat,IDEA,MVC,等等。有的似曾相识,有的一脸蒙蔽,笔者正陷入这漩涡当中,本章笔者将对Favorites的源码分析中,整理出完整的项目结构思路以及架构思想,层层剥离,以至融会贯通。
//作者使用thymeleaf拆分模块,如需单独调试请将该head单独粘贴到需要观察的页面。
云收藏 | 让收藏更简单
MVC
理解:负责项目的整体架构 简单说就是Controller 调用Repository和Service 通过thymeleaf来响应界面
学习:
Maven+Spring-Boot
理解:Maven负责导包,Spring-Boot负责启动程序,找到SpringBootApplication即可,全局唯一。
学习:
thymeleaf
@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";
}
Spring Data
理解:绑定bean对象
Spring Data JPA
理解:绑定bean对象执行相关操作的工具类
学习:
增:
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);
//在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);
}
}
}
//保存
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;
}
}
}
}
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中 |
//接口
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;
}
th:onclick="'login()'"//thymeleaf
onclick="login()"//js
v-on:click="login"//Vue
2.1 http//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的
- 点赞
// 在collec.js中调用 可以看出 他在界面底部做了一个隐藏字段,方便检验登录状态,随时跳转登录
function changeLike(id){
var userId = document.getElementById("userId").value;
if(userId != "0"){
$.ajax({
async: false,
type: 'POST',
dataType: 'json',
data:"",
url: '/collect/like/'+id,
error : function(XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest);
console.log(textStatus);
console.log(errorThrown);
},
success: function(like){
if($("#like"+id).is(":hidden")){
$("#like"+id).show();
var praiseCount=parseInt($("#praiseC"+id).val())-1;
$("#praiseC"+id).val(praiseCount);
$("#likeS"+id).html("点赞("+praiseCount+")");
$("#likel"+id).show();
$("#unlike"+id).hide();
$("#unlikel"+id).hide();
}else{
$("#like"+id).hide();
$("#likel"+id).hide();
$("#unlike"+id).show();
$("#unlikel"+id).show();
var praiseCount=parseInt($("#praiseC"+id).val())+1;
$("#praiseC"+id).val(praiseCount);
$("#unlikeS"+id).html("取消点赞("+praiseCount+")");
}
}
});
}else{
window.location.href="/login";
}
}
- 修改
//collect是从HomeController传递过来的
文件加名称
这样还是访问了一次@RequestMapping(value="/standard/{type}/{userId}")
- 评论
步骤:查询是否显示评论,查询评论列表,填充回显填充评论列表,显示评论输入框
评论(0)
//还是在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();
}
}
//对于下拉菜单不必深究,因为他是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
collect
//layout.html中注入了弹窗页面
alert
//在standard.html中data-target="#modal-removeFav"
删除
//对应alert.html中的
//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);
}
//重点 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++;
});
});
//在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. 基本掌握MVC设计模式
1.2. 基本掌握thymeleaf
1.3. 基本掌握项目的部署与搭建(Tomcat)
1.4. 基本理解项目整体架构
1.5. 基本掌握Spring-boot框架
1.6. 基本掌握spring-data-jpa框架
-
我们应该如何去孵化自己的项目
答:其实这个问题应该问下自己,最终学习的目的是什么,如果只是为了学习而学习是很可怕的,因为没有目的性,我们很难坚持,且没有实战的学习毫无意义。笔者也时常问自己究竟想做什么?就在此刻笔者也没有想清楚,但是秉着全栈的初衷,笔者掌握一门后台语言的想法始终不变,就笔者的学习思路,笔者打算就地取材,直接站在巨人的肩膀上面,修修改改,最终改造成一个笔者满意的个人后台系统,可能其中充满着原作者的代码以及版权声明,不过那只是后话。循序渐进的学习才能真正的掌握一门语言,即便笔者有java基础也绝不可能一步登天,任何人都一样,个中的原因笔者不想解释。原作者的项目是从2016年中写到今年下半年,2年之久的项目,加之其精进的代码风格,笔者本着敬畏之心慢慢阅读,断续的花了将近2周的时间,虽收获颇丰,但碍于对java的认知程度不够,仍未能完全理解。