功能简介
点赞功能,一个博客的灵魂功能之一,你的博客可以没有评论和阅读,但是一定不能没有点赞,因为这是一种最基础的互动方式!所以我要从零开始开发一个健全的点赞功能
功能分析
功能点其实不困难,但是还是有一点复杂的,需要实现点赞和取消点赞,无法重复点赞。还有就是设计Api的时候需要考虑到后台要统计点赞数据。
这几个功能点里,比较难搞的就是用户无法重复点赞了,因为博客这类网站,是不需要用户登录的。也就没法像微博,qq,wechat这样记录用户Uid来判断用户是否点赞。所以现在我了解到的方法有两种。
1.是通过本地浏览器缓存去判断用户是否修改,但是这个是可以被修改的,而且用户清空浏览器缓存就么得了。
2.是通过用户的Ip地址去判断,一般来说。把点过赞的ip地址存到数据库里,每次点赞的时候去数据库里比对。有的话就是点赞过了,没有就没有点赞。我个人倾向这个方法。虽然也是有一定缺陷。不过仔细想想谁会换着代理来给我疯狂点赞呢?
代码实现
后端工作:
数据库设计
比较简洁,但是方便数据查找和统计。
const mongoose = require("mongoose");
const Schema = new mongoose.Schema({
articleId: {
type: mongoose.SchemaTypes.ObjectId,
required: true,
},
ip: {
type: String,
required: true,
},
});
module.exports = mongoose.model("Like", Schema);
Api设计
需要设计三个Api 1. 点赞,是的话去数据库里删除点赞信息,并且把文章的点赞数量减一,反之亦然。 2. 用户是否点赞,这个前端需要用到,如果用户点赞了,那么就在用户刷新或者再次访问的时候改变css样为已点赞的样式 3. 点赞数量,展示当前点赞数量,
注意,如果你使用了nginx反向代理,那么前端你获取的ip可能永远都是127.0.0.1,你需要在设置nginx转发时加上http header以获取真实的用户ip 解决方案
//plugin/like.js
module.exports = (app) => {
let likeDb = require("../model/Like");
let articleDb = require("../model/Article");
//点赞接口
app.post("/api/like", async (req, res) => {
const userIp = req.get("X-Real-IP") || req.get("X-Forwarded-For") || req.ip;//获取真实ip
const articleId = req.body.articleId;
const userLike = await likeDb.findOne({
userIp: userIp,
articleId: articleId,
});
console.log(`用户点赞Ip: ${userIp}`);
if (userLike) {
console.log(`用户取消点赞`);
try {
await likeDb.findOneAndRemove({
articleId: articleId,
userIp: userIp,
});
await articleDb.findByIdAndUpdate(
{ _id: articleId },
{ $inc: { like: -1 } }
);
console.log(articleId, "取消点赞");
res.status(200).send({ msg: "取消点赞" });
} catch (error) {
console.log("删除数据失败!", error);
}
} else {
try {
await likeDb.create({ articleId, userIp });
await articleDb.findByIdAndUpdate(
{ _id: articleId },
{ $inc: { like: 1 } }
);
console.log(articleId, " 点赞成功");
res.status(200).send({ msg: "点赞成功" });
} catch (error) {
console.log("创建数据失败", error);
}
}
});
//判断用户是否点过赞 主要用于在页面加载的时候的时候,来改变界面的css
app.get("/api/like/beenLiked/:id", async (req, res) => {
const userIp = req.get("X-Real-IP") || req.get("X-Forwarded-For") || req.ip;
const articleId = await likeDb.findOne({
userIp: userIp,
articleId: req.params.id,
});
if (articleId) {
res.status(200).send({ code: "200", msg: "已经点赞" });
} else {
res.status(200).send({ code: "400", msg: "还未点赞" });
}
});
//点赞数量
app.get("api/like/likeSum/:id", async (req, res) => {
const articleSum = await likeDb
.findOne({ articleId: req.params.id })
.count();
res.send({ msg: "点赞数量", articleSum: articleSum });
});
};
==获取到用户的ip地址,在express里非常的简单,但是会发现会有一个前缀,去掉它的方法有两种:==
//去掉获取前缀方法
//1使用正则
req.ip.match(/\d+\.\d+\.\d+\.\d+/)
//2指定监听地址为0.0.0.0 (推荐)
app.listen(PORT, "0.0.0.0", () => {
console.log(`服务启动 端口号:${PORT}`);
});前端工作:
Api管理
所有的Api都应该统一管理,like是我新增的Api
import http from "../plugins/http";
//restFullApi
let restGetAll = async (url, query) => {
return await http.get(`rest/${url}`, { params: query });
};
let restGetOne = async (url, params) => {
return await http.get(`rest/${url}/${params}`);
};
let restUpdata = async (url, parmas, data) => {
return await http.put(`rest/${url}/${parmas}`, data);
};
let restPostData = async (url, data) => {
return await http.post(`rest/${url}`, data);
};
let restDeleteOne = async (url, parmas) => {
return await http.delete(`rest/${url}/${parmas}`);
};
//点赞
let like = async articleData => {
return await http.post(`like`, articleData);
};
export { restGetAll, restGetOne, restUpdata, restPostData, restDeleteOne,like };
界面设计
点赞样式,这个不多讲,每个人都有不同。我是从jq上移植过来的,当然你也可以直接参考我的代码GitHub,components下的like组件,记得点个👍。
默认样式:

点赞后:

取消点赞

事件绑定和APi请求
样式不多讲,直接在组件里绑定事件就行。然后在事件里请求Api
<template>
<div>
<div @click="like>写你自己的样式</div>
</div>
</template>
这里这个articleID代表了当前博客的唯一标识,是mongodb自动生成的唯一ID。我需要用它传递到后端哥们,让后端哥们去判断用户是否点赞(后端也是我(#`O′))
<script>
import { like } from "../api/api";
export default {
methods: {
async like() {
let articleData = { articleId: this.articleId }; //打包数据
let res = await like(articleData);
}
}
</script>
阶段性效果
可以看到点赞后,后端返回数据点赞成功

取消点赞后

进阶
整个点赞效果已经基本完善了,但是还有很多地方需要完善。
在刷新后要判断用户是否点赞过,然后去相应的改变界面样式,这需要在界面渲染的实话去请求api判断是否点过赞。
如何防御通过爬虫和机器人快速的点赞对服务器造成攻击。
我已经完成了,你有什么思路吗?欢迎在评论下方留言讨论