假设你的博客已经顺利部署到了线上。你写了很多好文章,和粉丝们互动并感受成就感。

现在你想更进一步,努力提高文章质量,使其更受读者欢迎,打造圈内一流博客。问题是该如何判断一篇文章是“受欢迎的”?靠浏览量是个方法,但是并不能区分出内容花拳绣腿的标题党。靠评论数也是个好方法,但个人博客通常读者不多,好文章零评论是很正常的。

这时候“点赞”功能就显得重要了。如果大部分读者都给了一个赞,那就表明文章确实还不错。

动手之前的思考

点赞功能可不简单,实现途径非常的多。别急着动手,耐心思考:我们的博客到底需要什么样的点赞?

首先,点赞是否要求用户必须登录?要求登录的好处是可以精确的记录是哪些用户、对哪些文章点过赞(多对多关系),以便进行细致的数据分析。坏处是登录这个要求很笨重,会屏蔽掉大部分的游客用户。博主倾向于不要求用户登录,毕竟小站通常用户就不多,提高参与度才是点赞最核心的任务。

如果某天你的小站火了,就把要求用户登录的交互功能让给“收藏”吧!

其次,用户是否可以重复点赞?很多视频平台的用户可以对某个喜欢的女主播疯狂点赞,以表达自己非常非常的喜欢。这对用户较多的平台是没问题的,因为用户数量多了之后,你点几百个赞也只是九牛一毛。但博客网站这样做很容易造成某些文章点赞为零,某些文章点赞数又出奇的高。显然这不代表文章质量的差异。

好了,目前我们的策略是不要求用户登录,也不允许用户重复点赞。下一个问题是,在哪里记录用户的点赞相关的数据呢?点赞数量毫无疑问要保存在数据库里,以便随时取出数据并呈现出来。

但问题是校验用户是否已点赞的记录保存在哪?在数据库中记录用户的IP地址是个方法,但你得处理好记录IP和记录登录用户的关系,稍微有点麻烦。另外每次用户的点赞都需要向后端发送校验请求,增加了服务器的负担。

既然数据保存在后端数据库里不好,那能不能保存在浏览器端呢?答案是可以的,并且有 CookieLocalStorage 都可以让你保存数据。它两的主要区别如下:

特性 Cookie LocalStorage
生命周期 可设置失效时间,默认是关闭浏览器后失效 除非被清除,否则永久保存
存储空间 4K左右 一般为5MB
与服务器通信 每次都会携带在HTTP头中 不参与服务器的通信
易用性 源生接口不友好 源生接口可以接受

比较下来会发现 LocalStorage 可以永久保存数据,存储空间大,也不参与服务器通信,很适合点赞的需求。由于数据保存在浏览器中,所以也不需要区分用户有没有登录了:实际上每次请求点赞时,校验的是当前这个浏览器是否已经点过赞了,而不是用户!

可能你会反驳说,那要是用户换一个浏览器不就可以重复点赞了吗,更何况浏览器端的数据是非常容易篡改的。但这又有什么关系呢?点赞数据并不需要非常精确,随他去吧。

所有的现代浏览器都支持 LocalStorage 功能。如果你还在用 IE6 ,赶紧考虑升级浏览器吧。

总结一下,我们的点赞功能如下:

  • 不要求用户登录
  • 不允许重复点赞
  • 点赞数保存在服务器数据库中
  • 点赞校验数据保存在浏览器的 LocalStorage 中

当用户点赞时,前端脚本会在 LocalStorage 里校验是否已经点过赞了;如未点过赞,才会向服务器发送点赞请求,并记录数据。

想清楚需求,难题就迎刃而解了。接下来就是代码实现。

需要说明的是,以上分析并不代表其他方法不好,仅仅是在博客小站的环境下,博主觉得合适的技术路径而已。如果你心中住着另一个哈姆雷特,请想办法去实现它。

代码实现

准备工作

本章的重点工作在前端,因此先把简单的后端代码写了,权当热身。

有的读者听到前端就觉得头疼。你的痛苦我明白,但也是必不可少的。光写 Python 是做不出漂亮网站的。

由于点赞数需要保存在数据库中,因此修改文章模型是必须的了:

article/models.py

...
# 文章模型
class ArticlePost(models.Model):
    ...
    # 新增点赞数统计
    likes = models.PositiveIntegerField(default=0)
    ...

迁移数据:

(env) > python manage.py makemigrations
(env) > python manage.py migrate

继续用类视图:

article/views.py

...
# 点赞数 +1
class IncreaseLikesView(View):
    def post(self, request, *args, **kwargs):
        article = ArticlePost.objects.get(id=kwargs.get('id'))
        article.likes += 1
        article.save()
        return HttpResponse('success')

功能是让点赞数增加1个,并且返回 success 。至于为什么是 success 后面再讲。

最后就是路由了:

article/urls.py

...
urlpatterns = [
    ...
    # 点赞 +1
    path(
        'increase-likes/<int:id>/', 
        views.IncreaseLikesView.as_view(), 
        name='increase_likes'
    ),
]

很简单吧。剩下的就是专心写前端代码了。

JS与Ajax

由于校验数据保存在浏览器中,因此前端的工作较多。

先把完整代码贴出来(讲解在后面):

templates/article/detail.html

...

<!-- 已有代码,文章正文 -->
<div class="col-12">
    <p>{{ article.body|safe }}</p>
</div>

<!-- 新增点赞按钮 -->
<div style="text-align:center;" class="mt-4">
    <button class="btn btn-outline-danger"
            type="button"
            onclick="validate_is_like(
                     '{% url 'article:increase_likes' article.id %}',
                     {{ article.id }},
                     {{ article.likes }}
                     )"
            >
        <span>点赞</span>
        <span>
            <i class="fas fa-heart"></i>
        </span>
        <span id="likes_number">
            {{ article.likes }}
        </span>
    </button>
</div>

...
{% block script %}
...

<!-- 以下均为新代码 -->

<!-- csrf token -->
<script src="{% static 'csrf.js' %}"></script>
<script>
    // 点赞功能主函数
    function validate_is_like(url, id, likes) {
        // 取出 LocalStorage 中的数据
        let storage = window.localStorage;
        const storage_str_data = storage.getItem("my_blog_data");
        let storage_json_data = JSON.parse(storage_str_data);
        // 若数据不存在,则创建空字典
        if (!storage_json_data) {
            storage_json_data = {}
        };
        // 检查当前文章是否已点赞。是则 status = true
        const status = check_status(storage_json_data, id);
        if (status) {
            layer.msg('已经点过赞了哟~');
            // 点过赞则立即退出函数
            return;
        } else {
            // 用 Jquery 找到点赞数量,并 +1
            $('span#likes_number').text(likes + 1).css('color', '#dc3545');
        }
        // 用 ajax 向后端发送 post 请求
        $.post(
            url,
            // post 只是为了做 csrf 校验,因此数据为空
            {},
            function(result) {
                if (result === 'success') {
                    // 尝试修改点赞数据
                    try {
                        storage_json_data[id] = true;
                    } catch (e) {
                        window.localStorage.clear();
                    };
                    // 将字典转换为字符串,以便存储到 LocalStorage
                    const d = JSON.stringify(storage_json_data);
                    // 尝试存储点赞数据到 LocalStorage
                    try {
                        storage.setItem("my_blog_data", d);
                    } catch (e) {
                        // code 22 错误表示 LocalStorage 空间满了
                        if (e.code === 22) {
                            window.localStorage.clear();
                            storage.setItem("my_blog_data", d);
                        }
                    };
                } else {
                    layer.msg("与服务器通信失败..过一会儿再试试呗~");
                }

            }
        );
    };

    // 辅助点赞主函数,验证点赞状态
    function check_status(data, id) {
        // 尝试查询点赞状态
        try {
            if (id in data && data[id]) {
                return true;
            } else {
                return false;
            }
        } catch (e) {
            window.localStorage.clear();
            return false;
        };
    };
</script>
{% endblock script %}

代码内容很多,接下来拆分讲解。

<!-- 新增点赞代码 -->
<div style="text-align:center;" class="mt-4">
    <button class="btn btn-outline-danger"
            type="button"
            onclick="validate_is_like(
                     '{% url 'article:increase_likes' article.id %}',
                     {{ article.id }},
                     {{ article.likes }}
                     )"
            >
        <span>点赞</span>
        <span>
            <i class="fas fa-heart"></i>
        </span>
        <span id="likes_number">
            {{ article.likes }}
        </span>
    </button>
</div>

上面的 HTML 代码功能很简单,提供一个点赞的按钮,点击此按钮时会触发叫做validate_is_like的 JavaScript 函数。特别需要注意的是 '{% url 'article:increase_likes' article.id %}' 都是用的单引号,这里千万不能用双引号,原因请读者思考一下。

<script src="{% static 'csrf.js' %}"></script>

还记得csrf.js吗?我们在多级评论章节已经将它引入了,目的是让 Ajax 也能通过 csrf 校验。如果还没有这个文件的请点击链接下载。

接下来就是占据最多版面的函数validate_is_like(),我们来拆分里面的内容。

// 取出 LocalStorage 中的数据
let storage = window.localStorage;
const storage_str_data = storage.getItem("my_blog_data");
let storage_json_data = JSON.parse(storage_str_data);
// 若数据不存在,则创建空字典
if (!storage_json_data) {
    storage_json_data = {}
};

浏览器里面, window 对象指当前的浏览器窗口。它也是当前页面的顶层对象(即最高一层的对象),所有其他对象都是它的下属,localStorage 也是如此。

要校验数据,首先必须取出数据。这里用localStorage.getItem()接口取出了数据。

虽然 LocalStorage 的存储方式为标准的键值对类型(类似Python的字典),但是很怪的是存储的值只支持字符串类型。所以这里要用 JSON.parse() 将字符串还原为对象。

用户第一次点赞时,LocalStorage 中肯定是没有任何数据的,所以 if 语句的作用是创建一个空的字典待用。

// 检查当前文章是否已点赞。是则 status = true
const status = check_status(storage_json_data, id);
if (status) {
    layer.msg('已经点过赞了哟~');
    // 点过赞则立即退出函数
    return;
} else {
    // 用 Jquery 找到点赞数量,并 +1
    $('span#likes_number').text(likes + 1).css('color', '#dc3545');
}

接下来马上调用函数 check_status 检查用户是否已经对本文点过赞了。如果点过了,就弹窗提示,并且用 return 马上终止 validate_is_like 函数,后面的代码就不执行了;如果还没点过,就让按钮的点赞数 +1。

但这时候其实后台数据库的点赞数并没有更新。接着往下看。

// 用 ajax 向后端发送 post 请求
$.post(
    url,
    // post 只是为了做 csrf 校验,因此数据为空
    {},
    function(result) {
        if (result === 'success') {
            // 尝试修改点赞数据
            try {
                storage_json_data[id] = true;
            } catch (e) {
                window.localStorage.clear();
            };

            const d = JSON.stringify(storage_json_data);
            // 尝试存储点赞数据到 LocalStorage
            try {
                storage.setItem("my_blog_data", d);
            } catch (e) {
                // code 22 错误表示 LocalStorage 空间满了
                if (e.code === 22) {
                    window.localStorage.clear();
                    storage.setItem("my_blog_data", d);
                }
            };
        } else {
            layer.msg("与服务器通信失败..过一会儿再试试呗~");
        }

    }
);

这里开始尝试与后端通信并更新点赞数。整块代码被 $.post() 包裹,它其实就是 Ajax 的 post 请求。function(result) {...} 是请求成功时才执行的回调函数,参数 result 是后端的返回值。如果通信成功,则尝试将点赞的校验数据保存到 LocalStorage 中。期间发生任何错误(特别是 LocalStorage 存储已满的错误),都会清除 LocalStorage 中的所有数据,以便后续的数据记录。

可以看出,博主采用的数据结构比较简单,像这样:

{
    2: true,
    31: true
    ...
}

键代表文章的 id,布尔值代表点赞的状态。上面数据的意思是 id 为 2 和 31 的文章已经点过赞了。读者以后可能会希望文章、评论以及其他内容都可以点赞,那就需要设计更加复杂的数据结构。

// 辅助点赞主函数,验证点赞状态
function check_status(data, id) {
    // 尝试查询点赞状态
    try {
        if (id in data && data[id]) {
            return true;
        } else {
            return false;
        }
    } catch (e) {
        window.localStorage.clear();
        return false;
    };
};

至于 check_status() 函数就很简单了,作用是查询是否已经点过赞了,是则返回 true,否则返回 false。

整个 JavaScript 脚本就完成了。

调试接口

读者在调试时可能会出现各种问题,请按 Ctrl + Shift + I 打开浏览器控制台的 Console 界面,利用以下命令 debug:

  • localStorage:查看 LocalStorage 的数据
  • localStorage.clear():清除所有数据
  • localStorage.getItem():获取某个数据
  • localStorage.setItem():保存某个数据

测试

代码讲完了,接下来就打开文章详情页面测试一下:

点击点赞按钮,点赞数 +1;再次点击点赞按钮,点赞数不会增加,并且会弹窗提示用户已经点过了。

你可以随意尝试关闭页面或浏览器,保存的点赞校验数据是不会消失的。

这样就完成了一个简单的点赞功能。

当然还可以继续往下优化:

  • 没点赞的爱心应该显示为灰色,点过的显示为红色,这样才人性化
  • 最好再来一点酷炫的点赞动画,或者提示性文字
  • 要不要给被点赞人发一条通知信息呢?
  • ...

教程篇幅有限,不打算再深入下去了,就当做读者朋友的课后作业吧,要用心完成哦。给你点赞!

第一条的提示:初始加载页面时,爱心统一显示为灰色,然后调用 JavaScript 脚本比对 LocalStorage 中的数据,灵活运用 Jquery ,将点过赞的爱心颜色修改为红色。

总结

我们的博客项目现在拥有了层次分明的用户交互结构:浏览量数据最轻巧,评价文章类型的受欢迎度;点赞数据比较平衡,评价文章内容的受欢迎度;评论数据最笨重,但价值也最高。读者以后在开发功能的时候,也要像这样把核心需求想清楚才行。

另一个需要提出的是,只有非敏感、不重要的数据才保存在 LocalStorage,不要对它太过依赖。

再一次提醒,教程为了便于讲解,代码文件已经变得越来越庞大。请在适当的时候把它分割成多个更小的组件,方便维护和重用。



杜赛
190 声望68 粉丝

啥都不会的菜鸟一个。