整体思路分析
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.blog
userid = user.nid
nid = 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.blog
article_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.js
from django import template
from blog import models
from django.db.models import Count
register=template.Library()
@register.simple_tag
def 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.blog
userid = user.nid
nid = 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.html
2. templates/blog/home_site.html
2. CSS存放的位置
1. static/blog/css/article_detail.css
2. 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. 技术基于Ajax
2. 两种行为的差异仅仅在于布尔值,所以为避免重复,将两种行为绑定同一个事件;
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视图,进而网页返回404
2. 解决的次级错误: 上次静态文件配置修改之后,每一个模板标签引用的资源路径全部都需要删除第一级目录的static
3. 第二个错误: 页面标签的图标我直接写进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字典 + JsonResponse
2. 判断逻辑是什么?
使用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"] = False
response["handled"] = obj.is_up
return 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)
}
}
})
})