在使用 Waline 官方组件时,通过增加count参数改变评论显示数量,具体操作因使用场景(如博客框架类型)而异,以下为常见场景下的教程:
基于 Hexo 框架(以集成 Next 主题为例)
- 找到配置文件:在 Hexo 根目录下的node_modules/@waline中找到default.yaml文件,这里面包含了 Waline 的所有设置选项。同时,为了方便管理,通常会将需要修改的选项手动写入到 Next 主题下的_config.yml文件中。
- 增加或修改count参数:打开_config.yml文件,找到waline配置部分,如果没有则添加。在其中增加count参数并设置期望的值,比如设置为显示 20 条评论:
1 2 3 4 5 6 7 8 9 10 11 12 13
| waline: enable: true # 是否开启 serverurl: waline-server-ecru.vercel.app # waline服务端地址 locale: placeholder: 疑义相与析,畅所欲言,不登录也没关系哒 # 评论框的默认文字 avatar: mm # 头像风格 meta: (nick,mail) # 自定义评论框上面的三个输入框的内容 pagesize: 20 # 这里将评论数量修改为20 lang: zh-cn # 语言,可选值: en,zh-cn visitor: true # 文章阅读统计 comment_count: true # 如果为false,评论数量只会在当前评论页面显示,主页则不显示 requiredfields: (nick) # 设置用户评论时必填的信息,(nick,mail):(nick)|(nick,mail) liburl: https:
|
https://waline_server/comment?type=recent
但是这样有个严重的问题,就是通过上面的这个地址每次只能抓取最近的10条评论。
所以我有了另外一个思路,就是抓取每个页面里的评论内容,生成一个总的文件,这样就能直接通过统计这个文件里的评论内容来整理出访客记录了。
抓取评论数据的代码:命名为fetch.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| const fs = require('fs').promises; const fetch = require('node-fetch'); const { JSDOM } = require('jsdom');
const SITEMAP_URL = “your sitemap"; const WALINE_SERVER = "your comment server";
// 从 URL 提取路径 function urlToAbbrlinkPath(url) { try { const u = new URL(url); return u.pathname.match(/^\/[a-z0-9]+\/$/)?.[0] || null; } catch { return null; } }
// 获取单个页面的评论 async function fetchCommentsForPath(path) { const pageSize = 100; let page = 1; let allComments = [];
while (true) { const res = await fetch(`${WALINE_SERVER}/comment?type=comment&page=${page}&pageSize=${pageSize}&path=${encodeURIComponent(path)}`); const data = await res.json(); if (!data || !data.data || data.data.length === 0) break; // 为每条评论添加 path 信息 const commentsWithPath = data.data.map(comment => ({ ...comment, path })); allComments.push(...commentsWithPath); if (data.data.length < pageSize) break; page++; }
return allComments; }
// 获取评论的回复 async function fetchRepliesForComment(comment) { const pageSize = 100; let page = 1; let replies = [];
while (true) { const res = await fetch(`${WALINE_SERVER}/comment?type=reply&page=${page}&pageSize=${pageSize}&parent=${comment._id}`); const data = await res.json(); if (!data || !data.data || data.data.length === 0) break; replies.push(...data.data); if (data.data.length < pageSize) break; page++; }
return replies; }
// 解析 sitemap 获取所有页面路径 async function fetchSitemapPaths() { const res = await fetch(SITEMAP_URL); const xml = await res.text(); const dom = new JSDOM(xml, { contentType: "application/xml" }); const urls = Array.from(dom.window.document.querySelectorAll('url > loc')) .map(url => url.textContent.trim()); return urls .map(urlToAbbrlinkPath) .filter(Boolean) .filter((path, index, self) => self.indexOf(path) === index); // 去重 }
// 主函数:抓取所有页面的评论及回复 async function fetchAllComments() { const paths = await fetchSitemapPaths(); let allComments = []; const concurrency = 5; // 并发请求数量
for (let i = 0; i < paths.length; i += concurrency) { const batchPaths = paths.slice(i, i + concurrency); console.log(`处理第 ${i+1}-${Math.min(i+concurrency, paths.length)} 条路径(共 ${paths.length} 条)`); const batchResults = await Promise.all( batchPaths.map(async path => { try { const comments = await fetchCommentsForPath(path); console.log(`成功获取 ${path} 的 ${comments.length} 条评论`); return comments; } catch (error) { console.warn(`跳过路径 ${path}: ${error.message}`); return []; } }) );
allComments.push(...batchResults.flat()); }
// 为每条评论获取回复 console.log("开始获取评论回复..."); const commentsWithReplies = await Promise.all( allComments.map(async comment => { const replies = await fetchRepliesForComment(comment); return { ...comment, replies }; }) );
return commentsWithReplies; }
// 保存评论数据到 JSON 文件 async function main() { try { console.log("开始抓取所有评论..."); const comments = await fetchAllComments(); console.log(`共抓取到 ${comments.length} 条评论`); await fs.writeFile('source/comments.json', JSON.stringify(comments, null, 2)); console.log("评论数据已保存到 comments.json"); } catch (error) { console.error("抓取评论失败:", error); process.exit(1); } }
main();
|
访客列表页面
在页面上添加下面的代码,就能识别到抓取的全部评论
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
| <div id="visitors" class="visitors-container">加载中…</div>
<style> .visitors-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: clamp(8px, 3vw, 16px); margin-top: 20px; padding: 0 10px; } .visitor { display: flex; flex-direction: column; align-items: center; width: 100%; font-size: 14px; text-align: center; background-color: var(--background-hover, rgba(255 255 255 / 0.05)); padding: 6px; border-radius: 8px; transition: all 0.3s ease; box-sizing: border-box; } .visitor:hover { transform: translateY(-3px); background-color: var(--background-hover, rgba(255 255 255 / 0.1)); } .visitor img { width: 48px; height: 48px; border-radius: 50%; border: 1px solid var(--border-color, #555); transition: border-color 0.3s ease; object-fit: cover; } .visitor-name { margin-top: 6px; font-weight: bold; color: var(--text-color, #eee); transition: color 0.3s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; } .visitor-count { font-size: 12px; color: var(--text-secondary-color, #bbb); transition: color 0.3s ease; } @media (max-width: 576px) { .visitors-container { grid-template-columns: repeat(auto-fill, minmax(65px, 1fr)); gap: 8px; } .visitor img { width: 40px; height: 40px; } .visitor-name { font-size: 12px; } .visitor-count { font-size: 10px; } } .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: skeleton-loading 1.5s infinite; } @keyframes skeleton-loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } </style>
<script> const WALINE_SERVER = "https://your server"; const visitorsElement = document.getElementById("visitors"); // 显示骨架屏 function showSkeleton() { let skeletonHTML = ''; for (let i = 0; i < 12; i++) { skeletonHTML += ` <div class="visitor"> <div class="skeleton" style="width: 48px; height: 48px; border-radius: 50%;"></div> <div class="skeleton" style="width: 80%; height: 16px; margin-top: 8px; border-radius: 4px;"></div> <div class="skeleton" style="width: 60%; height: 12px; margin-top: 4px; border-radius: 4px;"></div> </div> `; } visitorsElement.innerHTML = skeletonHTML; } // 初始显示骨架屏 showSkeleton(); fetch(`/comments.json?v=${Date.now()}`) // 添加时间戳防止缓存 .then(res => { if (!res.ok) throw new Error(`HTTP错误,状态码: ${res.status}`); return res.json(); }) .then(comments => { if (!Array.isArray(comments) || comments.length === 0) { throw new Error('评论数据为空'); } const visitors = {};
comments.forEach(comment => { const key = comment.mail || comment.nick; if (!visitors[key]) { visitors[key] = { nick: comment.nick || '匿名用户', avatar: comment.avatar || 'https://picsum.photos/48/48', count: 0, link: comment.link || '#' }; } visitors[key].count++; });
const sorted = Object.values(visitors).sort((a, b) => b.count - a.count); if (sorted.length === 0) { throw new Error('没有找到评论用户'); } const html = sorted.map(v => ` <div class="visitor"> <a href="${v.link}" target="_blank" rel="noopener noreferrer"> <img src="${v.avatar}" alt="${v.nick}" onerror="this.src='https://picsum.photos/48/48?random=${Math.random()}'" /> </a> <div class="visitor-name">${v.nick}</div> <div class="visitor-count">${v.count} 评论</div> </div> `).join(''); visitorsElement.innerHTML = html; }) .catch(error => { console.error('加载访客评论失败:', error); visitorsElement.innerHTML = ` <div style="width:100%;text-align:center;padding:20px;color:var(--text-secondary-color);"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12.01" y2="16"></line> </svg> <p style="margin-top:10px;">加载评论数据失败,请稍后再试</p> <p style="font-size:12px;margin-top:5px;">错误: ${error.message}</p> </div> `; }); </script>
|
设置github action 定时自动抓取
因为这个需要手动执行fetch.js才能拉取每个页面的评论,所以最好添加到githuba ction的workflow里面,让它定期获取。
我的github action现在每小时都会获取一遍notion上的文章,就直接把获取评论数据的命令也加进去,每个小时也会顺便拉取一次评论数据。
我唯一需要担心的就是随着时间越来越长,抓取的时间会增加。。