文章点赞和浏览数统计实现
前段时间大发哥 TG 频道说要发一个用 CFWorker 实现文章点赞和浏览数统计的功能,很快啊,星期二教程就出来了: Hugo Cloudflare Worker 。
整体方案是使用 CfWorker 实现 api 请求,再用 D1 做数据持久化,效果还是很赞的。
本来我是想照着教程完全一步步来的,但是想到我前文 将博主 PC 上使用的应用信息实时显示到博客 在折腾时搭了一个 API 服务,就不想再去用 CFWorker 了,打算直接在自己服务器上实现这个功能,顺便再实践一下用 Express 搭配数据库工具。
所以打算稍改一下,结构如下:
- 前端照搬
- API 结构实现部分照搬
- 数据库方面结构、读写操作照搬
- 数据库持久化用 LokiJS
前端
前端我这里用的 Alpinejs 做驱动,前文 用 Alpinejs 完成主题切换功能 介绍过这个库,在不使用 Vue 等工具下普通 HTML 的一个替代方案,好用。
- 在模板中渲染的时候设置文章
ID
(这里我因为用的 Ghost 做数据源,所以自带一个 MD5 的 ID,用一个固定唯一值就好) - 设置
x-data
为后面 js 中初始化的post_action
对象 - 通过
x-show
调用initViews
、initLike
实现组件数据初始化 - 通过
@click
绑定like
函数实现点赞
HTML
<!-- 点赞按钮 -->
<div x-data="post_action" class="post_action flex flex-wrap justify-center">
<div class="post_like" x-show="initLike('{{ post.id }}')" data-postid="{{ post.id }}" @click="like('{{ post.id }}')">
{% svg 'like', '0 0 26 26' %}
<div class="post_like_desc">
「 还好我们还有文字... 」
</div>
</div>
</div>
<!-- 文章页脚数据显示 -->
<div>
<a class="flex align-center" href="#" title="Last modified by" rel="noopener">
{% svg 'like' %}
<span class="post_likes" ></span>
</a>
</div>
<div>
<a class="flex align-center" href="#" title="Last modified by" rel="noopener">
{% svg 'views' %}
<span class="post_views" x-data="post_action" x-show="initViews('{{ post.id }}')"></span>
</a>
</div>
页面结构
CSS 部分
之前做那个 PC 上 APP 状态显示的时候使用了一下 CSS 动画,效果还不错,这里再次借鉴一下那个思路,点赞后实现了一个心脏跳动的感觉。
.post_action {
.post_like {
align-items: center;
cursor: pointer;
display: flex;
height: 6rem;
position: relative;
text-align: center;
flex-direction: column;
justify-content: center;
svg {
fill: none;
stroke: var(--body-font-color);
height: 1.5em;
margin-inline-end: 00;
width: 1.5em;
}
/** 点赞按钮的激活效果 **/
&.active svg{
fill: var(--hint-color-danger);
stroke: var(--hint-color-danger);
animation: growAndFade 1s 1;
transform-origin: center;
}
}
.post_like_desc{
font-style: oblique;
padding: 8px 0px;
color: var(--gray-500);
font-size: 14px;
}
}
/** 点赞动画 **/
@keyframes growAndFade {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(2);
opacity: 0.3; /* 这里调整透明度,以符合动画结束立即恢复的需求 */
}
100% {
transform: scale(1);
opacity: 1;
}
}
JS 中 Alpinejs 的设置部分
var apiUrl = "https://yourapidomain.com";
Alpine.data("post_action", () => ({
apiUrl: apiUrl,
// 点赞函数
like: (post_id) => {
// 初始化一些元素变量
var likelist = localStorage.getItem("lieklist") || "";
var likeButton = document.querySelector(".post_like");
var likeText = document.querySelector(".post_likes");
// 每次点赞都去本地数据中匹配一下
// 我这里将用户浏览的数据拼成字符串处理的,以后应该会有性能问题
if (likelist.indexOf(post_id + ",") != -1) {
console.log("你已经点过赞了");
likeButton.classList.add("active");
} else {
// 如果没有就发起POST请求更新数据
fetch(`${apiUrl}/post/${post_id}/like`, { method: "post" })
.then((res) => {
return res.json();
})
.then((data) => {
// 成功了就做一些页面数据同步的工作
console.log("点赞成功" + JSON.stringify(data));
localStorage.setItem("lieklist", likelist + post_id + ",");
likeButton.classList.add("active");
likeText.innerText = data.likes
})
.catch((error) => {
console.error(
"There was a problem with the fetch operation:",
error
);
});
}
},
// 后续的代码逻辑上都差不多,我就不一一写备注了。
initLike: (post_id) => {
var likelist = localStorage.getItem("lieklist") || "";
var likeButton = document.querySelector(".post_like");
var likeText = document.querySelector(".post_likes");
if (likelist.indexOf(post_id + ",") != -1) {
likeButton.classList.add("active");
}
fetch(`${apiUrl}/post/${post_id}/like`)
.then((res) => {
return res.json();
})
.then((data) => {
if (data.likes) {
likeButton.dataset.like = data.likes;
likeText.innerText = data.likes;
}else{
likeButton.dataset.like = 0;
likeText.innerText = 0;
}
}).catch((error) => {
likeButton.dataset.like = 0;
likeText.innerText = 0;
console.error(
"There was a problem with the fetch operation:",
error
);
});
return true;
},
initViews: (post_id) => {
var viewlist = localStorage.getItem("viewlist") || "";
var viewText = document.querySelector(".post_views");
if (viewlist.indexOf(post_id + ",") != -1) {
fetch(`${apiUrl}/post/${post_id}/views`)
.then((res) => {
return res.json();
})
.then((data) => {
if (data.Views) {
viewText.innerText = data.Views;
}else{
viewText.innerText = 0;
}
});
} else {
fetch(`${apiUrl}/post/${post_id}/views`, { method: "post" })
.then((res) => {
return res.json();
})
.then((data) => {
console.log("浏览量" + JSON.stringify(data));
localStorage.setItem("viewlist", viewlist + post_id + ",");
viewText.innerText = data.Views;
})
.catch((error) => {
viewText.innerText = 0;
console.error(
"There was a problem with the fetch operation:",
error
);
});
}
return true;
},
}));
后端
后端的实现其实还是和之前一样,只不过这次多了一个数据库的持久化操作。
数据持久化
因为是个轻量型的服务,我也不想搞什么 MySQL 之类的服务,甚至连 SqLite 也不想用,所以想到之前在使用 Twikoo 的时候,发现它用的一个数据服务是一个以 JSON 为结构的数据库:LokiJS,特性如下,应付这个功能应该绰绰有余了。
- Fast performance NoSQL in-memory database, collections with unique index (1.1M ops/s) and binary-index (500k ops/s)
- Runs in multiple environments (browser, node, nativescript)
- Dynamic Views for fast access of data subsets
- Built-in persistence adapters, and the ability to support user-defined ones
- Changes API
- Joins
yarn add -D lokijs
安装 LokiJS 依赖
LokiJS 基础操作
Loki 的数据操作很简单、方便,会用 JavaScript 就可以,对于我来说上手难度更低。
// LojiJS基础操作
// 新建数据库
var db = new loki("blog.db");
// 添加一个「表」(集合)
db.addCollection("posts");
//通过id查找
var post = posts.findOne({ post_id: post_id });
// 插入数据
posts.insert({
post_id: post_id,
likes: 1,
Views: 0,
});
// 操作更新到数据库中
posts.update(post);
另外,
Loki 中 addCollection
这个操作是会覆盖你原有的数据的,我之前用的时候以为打开自动加载、自动保存就好了,没有判断表存不存在,导致了每次服务器重启都会新添加一个表进去,导致原表数据被覆写。
另外 Express 的接口代码和前端的 JS 代码一样没有做结构整理,图方便全部写了一遍,后期是有很大优化空间的。
// 创建数据库
// 开启自动加载,自动保存
var db = new loki("blog.db", {
autoload: true,
// 数据库加载完毕后回调
autoloadCallback: loadHandler,
autosave: true,
});
let posts;
function loadHandler() {
posts = db.getCollection("posts");
// 如果posts不存在才创建
if (posts === null) {
db.addCollection("posts");
}
}
app.get("/post/:key/like", (req, res) => {
const post_id = req.params.key;
try {
let post = posts.findOne({post_id:post_id})
if(post){
res.status(200).json(post)
}
} catch (e) {
res.status(500).json({ error: e });
}
});
app.post("/post/:key/like", (req, res) => {
const post_id = req.params.key;
try {
let post = posts.findOne({ post_id: post_id });
if (!post) {
posts.insert({
post_id: post_id,
likes: 1,
Views: 0,
});
post = posts.findOne({ post_id: post_id });
} else {
post.likes += 1;
}
posts.update(post);
res.status(200).json(post);
} catch (e) {
res.status(500).json({ error: e });
}
});
app.post("/post/:key/views", (req, res) => {
const post_id = req.params.key;
try {
let post = posts.findOne({ post_id: post_id });
if (!post) {
posts.insert({
post_id: post_id,
likes: 0,
Views: 1,
});
post = posts.findOne({ post_id: post_id });
} else {
post.Views += 1;
}
posts.update(post);
res.status(200).json( post );
} catch (e) {
res.status(500).json({ error: e });
}
});
app.get("/post/:key/views", (req, res) => {
const post_id = req.params.key;
try {
let post = posts.findOne({post_id:post_id})
if(post){
res.status(200).json(post)
}
} catch (e) {
res.status(500).json({ error: e });
}
});
Enjoy~
弄完这些也算是对 NodeJS 发布一个拥有完整功能的网站有了些基础了解,不过这种了解暂时还很片面,在架构、性能、安全性方面肯定还有很多需要学习的地方,之后如果碰到了以上问题了再做学习、优化。
在折腾中学习、进步,这种感觉真棒按。
加入评论