基于 Cloudflare Workers & KV 构建一个评论系统——前端篇
本文将继续介绍使用 KV 构建评论系统,主要关注前端开发部分。
Introduction
对于一个嵌入式的评论系统,开发的原则是风格尽量简单、代码不依赖其他组件、尽量不和原页面冲突、引入尽量简单。理想的方法就是一行 div 加一行 js:
<div class="kv-cmt" endpoint="https://xxx.workers.dev">
</div>
<script src="https://xxx.pages.dev/script.js"></script>
然后所有 css 和组件都通过 js 加载。然后获取用 fetch 而非 axios,虽然 Vue 做内容绑定很方便,不过太重了,考虑直接生成一些原生的代码。
Development
实际开发中也会遇到一些问题,例如老生常谈的 CORS,如何有效地加载评论,以及如何修饰 css。CORS 其实主要是后端处理,需要在 OPTION、GET、POST response 上加上对应 header,加载评论,这里采用了一种比较有意思的方法,即 append
每一个 comment 元素时都加上和后端查询出来的 key 相对应 id 值,例如 kv-1
(此处 kv-
前缀主要是为了避免和其他元素冲突),然后对于子元素,例如 key 为 1|1
,这样就可以直接去掉最后一个数字,然后 document.getElementById
把父元素取出来, append
即可。最开始是准备用嵌套数组的方式,但是发现这个方法更好。CSS 方面,参考 waline 尽可能用淡一点的颜色、透明度调低,加个简单的缩进就算能用了。毕竟不是做 UI。
JS 代码如下所示,如果需要动态加载可以参考。
// 加载CSS样式表
function load_css() {
const cssUrl = './style.css'
const head = document.head || document.getElementsByTagName('head')[0]
const style = document.createElement('link')
style.rel = 'stylesheet'
style.href = cssUrl
head.appendChild(style)
}
load_css()
var kv_comment_parent = null
// 获取评论容器和API端点
const container = document.querySelector('.kv-cmt');
const endpoint = container.getAttribute('endpoint')
//添加评论列表容器
const commentsContainer = document.createElement('div');
commentsContainer.classList.add('kv-comments');
commentsContainer.id = 'kv';
container.appendChild(commentsContainer);
//添加新评论容器
const newCommentContainer = document.createElement('div');
newCommentContainer.classList.add('kv-newcomment');
newCommentContainer.innerHTML = `
<textarea name="kv-comment-value" id="kv-comment-value" placeholder="Comment here" cols="30" rows="10"></textarea>
<div class="kv-toolbar">
<span>New Comment / </span><span style="display: none;">New Reply / </span><span>BY </span><input type="text" name="kv-comment-user" id="kv-comment-user" value="匿名">
<button id="kv-comment-submit" >submit</button>
</div>
`;
container.appendChild(newCommentContainer);
const replyLabel = '<span class="kv-reply">reply</span>';
//创建表示单个评论的HTML元素
function createCommentElement(comment) {
const commentElem = document.createElement('div');
commentElem.id = `kv-${comment.key}`;
commentElem.classList.add('kv-comment');
commentElem.innerHTML = `
<div class="kv-body">
<div class="kv-stat">BY ${comment.user} / AT ${new Date(comment.time).toLocaleString()} ${replyLabel}</div>
<div class="kv-content">${comment.content}</div>
<hr class="kv-hr">
</div>
`;
return commentElem;
}
//获取评论列表
fetch(`${endpoint}/?url=${window.location.href}`)
.then(response => response.json())
.then(comments => {
//遍历JSON数组并为每个评论创建HTML元素
comments.forEach(comment => {
const commentElem = createCommentElement(comment);
if (!comment.key.includes('|')) {
commentsContainer.appendChild(commentElem);
} else {
const id = comment.key;
const parentId = id.substring(0, id.lastIndexOf('|')); // 获取父级评论的 ID
const parentComment = document.getElementById(`kv-${parentId}`); // 查找元素
parentComment.appendChild(commentElem)
}
});
});
//为“回复”标签添加事件监听器
commentsContainer.addEventListener('click', event => {
if (event.target.classList.contains('kv-reply')) {
const commentElem = event.target.closest('.kv-comment');
kv_comment_parent = commentElem.id.split('-')[1]
const toolbar = newCommentContainer.querySelector('.kv-toolbar');
toolbar.children[0].style.display = 'none';
toolbar.children[1].style.display = '';
}
});
//为提交增加监听器
const submitBtn = document.getElementById('kv-comment-submit');
submitBtn.addEventListener('click', async () => {
const commentValue = document.getElementById('kv-comment-value').value;
const commentUser = document.getElementById('kv-comment-user').value;
// 构造请求体
const requestBody = {
url: window.location.href,
content: commentValue,
user: commentUser,
parent: kv_comment_parent, // 假设已知父级评论的 ID
};
try {
const response = await fetch(`${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody),
});
if (response.ok) {
const responseBody = await response.json();
alert(responseBody.message);
} else {
console.error(`Failed to post comment. Status code: ${response.status}`);
}
} catch (error) {
console.error(`Failed to post comment. Error: ${error}`);
}
});
Conclusion
其实这一套我一直想加一点人机验证,防止刷评论、刷接口访问。用 CF 的托管质询,似乎不好处理 POST;用 Turnsite,似乎没地方加组件。不过类似这种数据应该被攻击了问题也不大。另外就是这个 KV 的更新速率真的是,,一言难尽。基本上是不能做到即时反馈。不清楚 CF 自己是怎么做数据库更新的。
最后修改于 2023-06-20