整体思路分析
1. 文章的渲染2. 点赞事件的绑定与存储
第一个设计URL以及文章样式的设计,渲染
整体规划思路
1. URL的设计1. 样式: ```https://www.cnblogs.com/Liuzhijuan/p/15303535.html
2. 设计: 文章的访问必须是一个链接,由username + article + numbers构成3. 特性: 不能原地跳转,需要设计新的页面
- 页面内容设计
- 左侧和顶部内容通过模板继承的方式取得,不再重复造轮子;继承自同一套样式,URL也不能改变,仍在当前页面;
- 详细设计:首先将home代码全部拷贝到base当中,从base中剪切article文章内容部分的代码,将col-md-9内容替换为模板标签 {% block content%},分别在home-site,以及article中引用,并且提取出home中的文章详情页,首先在home中引用base,然后用模板标签渲染文章详情页,article 和 home均共享base的页面,但home需要以模板标签的方式渲染文章详情页; ```
第一级效果—home-site与article均有左侧和顶部标签
整体设计思路
1. 页面内容设计1. 左侧和顶部内容通过模板继承的方式取得,不再重复造轮子;继承自同一套样式,URL也不能改变,仍在当前页面;2. 详细设计:首先将home代码全部拷贝到base当中,从base中剪切article文章内容部分的代码,将col-md-9内容替换为模板标签 {% block content%},分别在home-site,以及article中引用,并且提取出home中的文章详情页,首先在home中引用base,然后用模板标签渲染文章详情页,article 和 home均共享base的页面,但home需要以模板标签的方式渲染文章详情页;
效果
个人站点

文章详情页

代码
base
<!DOCTYPE html>{% load static %}<html lang="en"><head><meta charset="UTF-8"><title>个人站点表</title><link rel="stylesheet" href="{% static '/static/blog/bs/css/bootstrap.css' %}"><style>* {margin: 0;padding: 0;}.header {width: 100%;height: 60px;background-color: #d3dce6;}.header .title {font-size: 18px;font-weight: 100;line-height: 60px; /*设置上下居中*/color: lightpink;margin-left: 14px;margin-top: -10px;}.backend {float: right;color: darkslategrey;text-decoration: none; /*去掉多余的下划线*/font-size: 16px;margin-left: 10px;margin-top: 10px; /*文字下移*/}.pub_info {margin-top: 10px;color: springgreen;}</style></head><body><div class="header"><div class="content"><p class="title"><span>{{ blog.title }}</span><a href="" class="backend">管理</a></p></div></div><div class="container"><div class="row"><div class="col-md-3"><div class="panel panel-warning"><div class="panel-heading">我的标签</div><div class="panel-body">{% for tag in tag_list %}<p><a href="/{{ username }}/tag/{{ tag.0 }}">{{ tag.0 }}({{ tag.1 }})</a></p>{% endfor %}</div></div><div class="panel panel-danger"><div class="panel-heading">随笔分类</div><div class="panel-body">{% for cate in cate_list %}<p><a href="/{{ username }}/category/{{ cate.0 }}">{{ cate.0 }}({{ cate.1 }})</a></p>{% endfor %}</div></div><div class="panel panel-success"><div class="panel-heading">随笔归档</div><div class="panel-body">{% for ym in year_month %}<p><a href="/{{ username }}/archive/{{ ym.0 }}">{{ ym.0 }}({{ ym.1 }})</a></p>{% endfor %}</div></div></div><div class="col-md-9">{% block content %}{% endblock %}</div></div></div></body></html>
home
{% extends 'base.html' %}{% block content %}<div class="article_list">{% for article in article_list %}<div class="article_item clearfix"><h5><a href="">{{ article.title }}</a></h5><div class="article-desc">{{ article.desc }}</div><div class="small pub_info pull-right"><span>发布于 {{ article.create_time|date:"Y-m-d H:i" }}</span> <span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }}) <span class="glyphicon glyphicon-thumbs-up"></span>评论({{ article.up_count }}) </div></div><hr>{% endfor %}</div>{% endblock %}
article
<!DOCTYPE html>{% extends 'base.html' %}<html lang="en"><head><meta charset="UTF-8"><title>文章详情页</title></head><body></body></html>
第二级效果—内容解耦
整体设计思路
2. 页面解耦的思路1. 将H5代码挪到base模板中;2. 需要解耦的对象: 页面中的顶部栏,左边的标签,分类,时间,顶部栏中的username,管理等文字;3. 解耦的方法: 对于站点和article的H5文件,他们可以直接引用base模板;对于base中的H5文件来说,预留可渲染空间,并且再一次解耦功能,将文章列表,category,tags等以自定义标签的方式存储在单独app下的文件中,以模板标签的方式调用给base;在home文件中私有文章列表;自定义模板中3. 特别注意:1. 由于classification中包含了点击左栏跳转的功能,该效果依赖username,因此必须在自定义模板标签中返回该变量;2. 由模板标签返回给base的blog数据不能影响head区域,因此需要view.py中的函数home_site使用contextz
views.py
def get_classification_data(username):user = models.UserInfo.objects.filter(username=str(username)).first()blog = user.bloguserid = user.nidnid = blog.nid # 用作原地跳转标签匹配cate_list = models.Category.objects.filter(blog__nid=nid).values_list("title").annotate(c=Count("Article_category"))tag_list = models.Tag.objects.values('pk').annotate(c=Count("article")).values_list("title", "c").filter(blog_id=nid)year_month = models.Article.objects.filter(user=nid).extra(select={"y_m_date": "date_format(create_time,'%%Y-%%m')"}).values('y_m_date').annotate(c=Count("nid")).values_list('y_m_date', 'c')return {"blog": blog, "cate_list": cate_list, "tag_list": tag_list,"year_month": year_month}def article_detail(request, username, article_id):user = models.UserInfo.objects.filter(username=str(username)).first()blog = user.blogarticle_obj = models.Article.objects.filter(pk=article_id).first()return render(request, "blog/article_detail.html",locals())
my_tags.py
# -*- coding: utf-8 -*-# @Time : 2021/9/17 15:44# @Author : 41999# @Email : 419997284@qq.com# @File : my_tags.py# @Project : row.jsfrom django import templatefrom blog import modelsfrom django.db.models import Countregister=template.Library()@register.simple_tagdef multi_tag(x, y):return x * y@register.inclusion_tag("blog/classification.html")def get_classification_style(username):user = models.UserInfo.objects.filter(username=str(username)).first()blog = user.bloguserid = user.nidnid = blog.nid # 用作原地跳转标签匹配cate_list = models.Category.objects.filter(blog__nid=nid).values_list("title").annotate(c=Count("Article_category"))tag_list = models.Tag.objects.values('pk').annotate(c=Count("article")).values_list("title", "c").filter(blog_id=nid)year_month = models.Article.objects.filter(user=nid).extra(select={"y_m_date": "date_format(create_time,'%%Y-%%m')"}).values('y_m_date').annotate(c=Count("nid")).values_list('y_m_date', 'c')return {"blog": blog, "cate_list": cate_list, "tag_list": tag_list,"year_month": year_month}
base.html
<!DOCTYPE html>{% load static %}<html lang="en"><head><meta charset="UTF-8"><title>个人站点表</title><link rel="stylesheet" href="{% static '/static/blog/bs/css/bootstrap.css' %}"><style>* {margin: 0;padding: 0;}.header {width: 100%;height: 60px;background-color: #d3dce6;}.header .title {font-size: 18px;font-weight: 100;line-height: 60px; /*设置上下居中*/color: lightpink;margin-left: 14px;margin-top: -10px;}.backend {float: right;color: darkslategrey;text-decoration: none; /*去掉多余的下划线*/font-size: 16px;margin-left: 10px;margin-top: 10px; /*文字下移*/}.pub_info {margin-top: 10px;color: springgreen;}</style></head><body><div class="header"><div class="content"><p class="title"><span>{{ blog.title }}</span><a href="" class="backend">管理</a></p></div></div><div class="container"><div class="row"><div class="col-md-3">{% load my_tags %}{% get_classification_style username %}</div><div class="col-md-9">{% block content %}{% endblock %}</div></div></div></body></html>
article.html
<!DOCTYPE html>{% extends 'base.html' %}<html lang="en"><head><meta charset="UTF-8"><title>文章详情页</title></head><body>{% block content %}<h3 class="text-center">{{ article_obj.title }}</h3>{% endblock %}</body></html>
home_site.html
{% extends 'base.html' %}{% block content %}<div class="article_list">{% for article in article_list %}<div class="article_item clearfix"><h5><a href="">{{ article.title }}</a></h5><div class="article-desc">{{ article.desc }}</div><div class="small pub_info pull-right"><span>发布于 {{ article.create_time|date:"Y-m-d H:i" }}</span> <span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }}) <span class="glyphicon glyphicon-thumbs-up"></span>评论({{ article.up_count }}) </div></div><hr>{% endfor %}</div>{% endblock %}
第三级效果
整体思路
1. 存在的问题1. 如果撰写文章时仅保存字符,那么文章渲染不会有任何效果;2. 如果撰写文章时带入H5标签,django禁止转义,因此只是输出H5标签 + 内容2. 解决方案在模板标签引用时,添加safe标记,格式为 * {{ article_obj.content | safe}} *3. 细节1. 模板标签引用作为逻辑代码,比如循环,content插入,只有一对花括号;而作为数据展示的引用时,则是两对花括号;
现实矛盾

safe的妙用
XSS攻击
原因:提交的文章中包含Js代码,在文章从服务器发送给用户的过程中,可能在浏览器渲染页面时控制用户浏览器;设计上的考量:提交文章时不允许包含标签;
效果

第四级效果–H5与CSS的解耦
整体思路
1. 为什么解耦?1. CSS样式与H5代码脱离,便于样式的CRU操作,关联了下一个部分的推荐图标链接;2. 如何解耦1. 将资源存储在 *static/blog/css/article_detail.css*2. 后缀名记得改为对应格式,另外引用标签应该这样写 *<link rel="stylesheet" href="{% static 'static/blog/css/article_detail.css' %}">*3. CSS引入图片无法适应解耦,模板标签必须包含在H5文件当中,因此CSS样式也需要转移到H5主文件中;
目录分布
1. H5主文件保存的位置1. templates/blog/article_detail.html2. templates/blog/home_site.html2. CSS存放的位置1. static/blog/css/article_detail.css2. static/blog/css/home_site.css
样式构成

第四级效果—点赞行为模板
整体设计思路
1. 点赞记录包含那些要件?点赞本身生成ID,点赞行为是boolean值,文章ID和用户ID;mysql> desc blog_articleupdown;+------------+------------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+------------+------------+------+-----+---------+----------------+| nid | int | NO | PRI | NULL | auto_increment || is_up | tinyint(1) | NO | | NULL | || article_id | int | YES | MUL | NULL | || user_id | int | YES | MUL | NULL | |+------------+------------+------+-----+---------+----------------+4 rows in set (0.00 sec)2. 行为记录方式1. 技术基于Ajax2. 两种行为的差异仅仅在于布尔值,所以为避免重复,将两种行为绑定同一个事件;3. 数据分析点赞人即为当前登录对象;
添加Jquery资源文件

关键逻辑代码
<script>$("#div_digg .action").click(function(){var is_up = $(this).hasClass("diggit")alert(is_up);})</script>
最终实现效果


第五级效果—点赞行为保存与结果显示
整体设计思路
1. 设计原理1. 在原有Ajax的设计基础上,绑定数据传输事件,采用POST请求传输,由于使用Unicode编码。所以需要JSON加载数据进行转义;2. 实现步骤1. 首先在Ajax中指定参数;URL中编写路径,views中处理数据;修改文章表中点赞的静态显示,用模板标签读取数据库;3. 出现Ajax绑定事件失效的问题1. reason: 正则匹配的URL规则对当前URL截胡,导致URL不能调用view视图,进而网页返回4042. 解决的次级错误: 上次静态文件配置修改之后,每一个模板标签引用的资源路径全部都需要删除第一级目录的static3. 第二个错误: 页面标签的图标我直接写进base模板中,再也不会有图标未响应的报错4. 其他收获:1, 明白了数据流,首先URL和VIEW的搭配只是为了响应一个结果,数据操作在VIEW中实现。绑定Ajax主要实现的效果是传递数据,是前端给VIEW通过POST请求传递的数据,VIEW拿到之后会将这些数据通过ORM查询写入数据库,实现动态展现信息。然而Ajax次一级的作用就是调用URL显示调用结果,比如HttpResponse返回的OK。它指定的URL无需原地跳转,即使是新的也没关系;2. 排除错误的方式:在Ajax中可以设置 alert(123)直接在浏览器标签页显示提示,也可以直接在VIEW中使用if request.is_ajax():print('OK')直接打印效果,该字符串将会在页面中显示;3. 前端调用的方式: 比如怎么知道POST提交的数据有没有提交成功呢?可以在浏览器的network中点击对应的响应,上面有POST提交的数据;
第二次思考
1. 首先需要实现的业务逻辑时用户点赞后,将点赞记录存储至数据库;2. 怎么实现这个逻辑?首先为特定H5代码区绑定事件,Ajax功能依赖于jquery,用户点击之后提交特定数据[POST为载体],数据由view视图处理,处理时,通过ORM存储到数据库当中;3. 首先页面渲染,点击操作触发事件,Ajax以POST请求处理为载体,将数据传输给VIEW视图,VIEW首先从post请求中取得数据,然后将数据存储到MySQL数据库;4. 又遇到新的问题:前端提交的user_id信息为空;解决办法:通过ORM查询读取用户ID,数据从article_detail类中取得;5. 业务改进:由于点赞个数显示来自数据库表article,存储点赞时并未更新前者,因此在view视图存储点赞记录的同时,使用ORM查询中的F方法实现自加1;
其他功能设计
前端提交的user_id信息为空

新增传递的数据

前端的响应

后端存储的数据

实现文章表中点赞数的自增加

如何设置和获取CSRF TOKEN
后端设置

前端调试

数据传递中CSRF TOKEN的写法

重复点赞则不存储数据
实现逻辑
1. 后端的提示信息如何传递?response字典 + JsonResponse2. 判断逻辑是什么?使用ORM取得点赞表中本用户,文章ID的记录,若无,则添加点赞记录且修改文章表中的赞或者踩,若有,则修改状态码为false,且返回is_up的值;
后端关键代码[16-27]
obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()response = {"status":True}if not obj:articleupdown = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)queryset = models.Article.objects.filter(pk=article_id)if is_up:queryset.update(up_count=F("up_count") + 1)else:queryset.update(down_count=F("down_count") + 1)else:response["status"] = Falseresponse["handled"] = obj.is_upreturn JsonResponse(response)

重复点赞则出现提示信息
整体设计思路
1. 后端处理数据时,会首先判断是否为第一次操作点赞或者点踩,如果是则分别存储点赞记录和文章表中的赞或者踩的个数,然后给前端返回状态码state为true;2. 如果不是第一次操作点赞或者点踩操作,则后端view向前端返回状态码state为false,同时回传点的是赞还是踩,也就是handle负责记录态度,然后前端绑定div ID: dig_words负责显示,如果点踩则返回“您已反对过”,如果点赞则返回“您已赞同过”;3. 在提示信息展示后,限制时长1秒后消失,也就是用空HTML取代前者;
后端关键代码
if(data.state){} {# 如果状态码为true,说明是第一次操作 #}else{if (data.handled){$("#digg_tips").html("您已经推荐过!")} else{$("#digg_tips").html("您已经反对过!")}setTimeout(function(){$("#digg_tips").html("")}, 1000)}
提示信息不出现的原因:没有指定ID

前端实现效果

点赞或者踩之后数据显示的局部刷新
设计思路
1. render会渲染整个页面,Ajax可以实现局部刷新;2. 矛盾点?当前设计下,进行点赞或者点踩之后,显示不会立即刷新,需要使用浏览器刷新按钮才会更新数据库值,这样做显然浪费时间;3. 更优的解决方案?在Ajax处理逻辑中,也就是第一次点赞成功后,对赞记录的显示,那部分H5代码的值直接加一或者减一,避免了直接读取数据库,但二者的值并不冲突;4. 注意点在提取H5标签中的数字时,需要将字符串类型转换为整数类型,不能直接对数字进行加减,否则会被认为是字符串拼接;
测试效果[方法:点赞后立马点踩,看数字是否变化,或者提示信息出现]

后端关键代码
if(data.state){if(is_up){var val = parseInt($("#digg_count").text());$("#digg_count").text(val + 1);}else{var val = parseInt($("#bury_count").text());$("#bury_count").text(val + 1);}
代码优化[Code optimization]
优化思路
1. 对点赞或者点踩的动态刷新优化[4, 19, 12]从父ID处开始分流,直接指定子ID的span标签,该分流执行的逻辑是从点击点赞或者点踩开始,而不是上一个版本代码后端回传数据从状态值判断开始;执行点赞或者点踩,且成功存储之后Ajax触发事件修改对应行为显示的值;1. 对提示信息的优化 [23,24]这里使用C语言中的逻辑选择运算符 条件 ?值 :值,条件满足显示者的值,条件不满足显示的值;
关键代码
$("#div_digg .action").click(function(){var is_up = $(this).hasClass("diggit");$obj = $(this).children("span");$.ajax({url:"/blog/updown/", {# 需要自行实现该路径,也就是实现点赞行为记录之后返回提示的页面#}type:"post", {# 提交数据,使用PIOST请求#}data:{"csrfmiddlewaretoken":$("[name='csrfmiddlewaretoken']").val(), {# 帮助进行安全性校验 #}"is_up":is_up,"user_id": "{{ user.nid }}","article_id":"{{ article_obj.pk }}", {# 仅需要传递文章ID,点赞人与评论人即为当前文章的登陆者 #}}, {# 哪一个用户,对哪一篇文章,进行什么行为 #}success:function (data){console.log(data);if(data.state){var val = parseInt($obj.text());$obj.text(val + 1);} {# 如果状态码为true,说明是第一次操作 #}else{var val = data.handled?"您已经推荐过!":"您已经反对过!"$("#digg_tips").html(val);setTimeout(function(){$("#digg_tips").html("")}, 1000)}}})})
