又遇到一个好玩的东西:Github 风格的 reaction 点赞功能。
Memos 上也集成了同样的功能,不过没怎么关注,直到刷到了这个仓库:emaction/emaction.frontend,emaction.frontend 把这个功能很好地实现了。好!为网站添砖加瓦。
实际效果:#功能展示
前端集成
前端集成非常简单,只需要引入 JS:
可以通过CDN引入或者下载到本地
<script type="module" src="https://cdn.jsdelivr.net/gh/emaction/frontend.dist@1.0.11/bundle.js"></script>
然后放置占位标签
<emoji-reaction></emoji-reaction>
就ok了。其他自定义参数参考 #标签自定义参数
后端开发
emaction.frontend 同时提供了后端代码和后端的接口文档,emaction/emaction.backend。
eallion 大佬解析代码推算出了后端的数据库结构,同时提供了 Cloudflare 部署后端的方法:自部署 GitHub 风格的 Reactions 点赞功能 · 大大的小蜗牛
不过 Cloudflare、vercel 这类免费服务虽然造福人类,单速度还是慢了点。根据数据结构和接口文档,Node.js + SQLite 简单实现一个后端并不难。
冻手
环境
系统:Ubuntu 22.04.4 LTS
npm版本:10.2.4
Node.js版本:v18.19.1
环境影响不大,这里只是说明
初始化和依赖安装
mkdir -p emaction-backend/src & cd emaction-backend
npm init -y
npm install express sqlite3
代码
src/database.js:用于初始化数据库和表,以及实现数据库操作:
const sqlite3 = require('sqlite3').verbose();
// 连接到 SQLite 数据库
const db = new sqlite3.Database('emaction.db', (err) => {
if (err) {
console.error(err.message);
}
console.log('Connected to the emaction database.');
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS reactions ( // 创建表
id INTEGER PRIMARY KEY AUTOINCREMENT,
target_id TEXT NOT NULL,
reaction_name TEXT NOT NULL,
count INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
});
});
const getReactionsByTargetId = (targetId, callback) => { // 获取特定 targetId 已收到的所有 reactions
db.all('SELECT reaction_name, count FROM reactions WHERE target_id = ?', [targetId], callback);
};
// 新增/更新一个 reaction
const updateReaction = (targetId, reactionName, diff, callback) => {
db.get('SELECT count FROM reactions WHERE target_id = ? AND reaction_name = ?', [targetId, reactionName], (err, row) => {
if (err) {
return callback(err);
}
if (row) {
const newCount = Math.max(0, row.count + diff);
const now = Date.now();
db.run('UPDATE reactions SET count = ?, updated_at = ? WHERE target_id = ? AND reaction_name = ?', [newCount, now, targetId, reactionName], callback); // 更新现有记录
} else {
const now = Date.now();
db.run('INSERT INTO reactions (target_id, reaction_name, count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [targetId, reactionName, Math.max(0, diff), now, now], callback); // 插入新记录
}
});
};
module.exports = {
getReactionsByTargetId,
updateReaction
};
src/functions.js:功能函数
const validateStringParam = (paramName) => {
return (req, res, next) => {
const param = req.query[paramName];
if (typeof param !== 'string' || param.trim() === '') { // 验证 targetId 和 reaction_name 是否为字符串
return res.status(400).json({ code: 1, msg: `Invalid ${paramName}` });
}
next();
};
};
const validateDiffParam = (req, res, next) => {
const diff = parseInt(req.query.diff);
if (isNaN(diff) || (diff !== 1 && diff !== -1)) { // 验证 diff 是否为 1 或 -1
return res.status(400).json({ code: 1, msg: 'Invalid diff, must be 1 or -1' });
}
next();
};
module.exports = {
validateStringParam,
validateDiffParam
};
src/index.js:后端接口实现
const express = require('express');
const app = express();
const db = require('./database');
const cors = require('cors');
const { validateStringParam, validateDiffParam } = require('./functions');
app.use(cors()); // 默认关闭跨域检查,实际环境自行设置
app.get('/reactions', validateStringParam('targetId'), (req, res) => { // 获取特定 targetId 已收到的所有 reactions
const targetId = req.query.targetId;
db.getReactionsByTargetId(targetId, (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ code: 1, msg: `Database error: ${err.message}` });
}
res.json({ code: 0, msg: 'success', data: { reactionsGot: rows } });
});
});
app.patch('/reaction', validateStringParam('targetId'), validateStringParam('reaction_name'), validateDiffParam, (req, res) => {
const targetId = req.query.targetId;
const reactionName = req.query.reaction_name; // 新增/更新一个 reaction
const diff = parseInt(req.query.diff);
db.updateReaction(targetId, reactionName, diff, (err) => {
if (err) {
console.error(err);
return res.status(500).json({ code: 1, msg: `Database error: ${err.message}` });
}
res.json({ code: 0, msg: 'success' });
});
});
// 启动
const port = process.env.PORT || 9000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
调试运行
运行
node src/index.js
测试接口
获取 reaction 数据
curl http://localhost:9000/reactions?targetId=123
更新 reaction 计数
curl -X PATCH "http://localhost:9000/reaction?targetId=123&reaction_name=like&diff=1"
成功!
前端传入后端接口地址
将后端的接口地址通过标签自定义属性 endpoint 传入。(末尾不能有 ’/
’)
<emoji-reaction endpoint=""http://localhost:9000"></emoji-reaction>
完成!
标签自定义参数
自定义 reaction emoji
通过给标签加上属性值 availableArrayString
来自定义 emoji 列表。格式为 "<emoji>,<emoji_name>;"
<emoji-reaction availableArrayString="👍,thumbs-up;😄,smile-face;🎉,party-popper;😕,confused-face;❤️,red-heart;🚀,rocket;👀,eyes;"></emoji-reaction>
自定义 targetId
emaction 通过 targetId 来区分页面,如果有多个部署需求,需要区分targetId,通过 reacttargetid
属性值来指定
<emoji-reaction reacttargetid="newTargetId" endpoint="https://api-emaction.xxxxxx.workers.dev"></emoji-reaction>
自定义
自定义样式
<style>
.reactions {
--start-smile-border-color: #d0d7de;
--start-smile-border-color-hover: #bbb;
--start-smile-bg-color: #f6f8fa;
--start-smile-svg-fill-color: #656d76;
--reaction-got-not-reacted-bg-color: #fff;
--reaction-got-not-reacted-bg-color-hover: #eaeef2;
--reaction-got-not-reacted-border-color: #d0d7de;
--reaction-got-not-reacted-text-color: #656d76;
--reaction-got-reacted-bg-color: #ddf4ff;
--reaction-got-reacted-bg-color-hover: #b6e3ff;
--reaction-got-reacted-border-color: #0969da;
--reaction-got-reacted-text-color: #0969da;
--reaction-available-popup-bg-color: #fff;
--reaction-available-popup-border-color: #d0d7de;
--reaction-available-popup-box-shadow: #8c959f33 0px 8px 24px 0px;
--reaction-available-emoji-reacted-bg-color: #ddf4ff;
--reaction-available-emoji-bg-color-hover: #f3f4f6;
--reaction-available-emoji-z-index: 100;
--reaction-available-mask-z-index: 80;
}
</style>
<emoji-reaction class="reactions"></emoji-reaction>
链接
https://github.com/emaction/emaction.frontend
https://github.com/emaction/emaction.backend
https://www.eallion.com/self-hosted-github-flavored-reactions/
功能展示
👇👇👇👇👇 返回🔙
👇👇👇👇👇
wl
05 / 07用上了!谢!
From Nginx Proxy Manager 登录出错 Bad gateway