技术教程Qexo 说说 API 适配 AnZhiYu 主题
Cheymin前言
AnZhiYu 主题自带的 essay(即刻短文)功能是基于本地 source/_data/essay.yml 静态渲染的,每次发说说都要修改配置文件重新构建部署,非常麻烦。而 Qexo 作为 Hexo 博客的在线管理后台,提供了 /pub/talks/ 公开 API,可在后台随时发布说说,无需重新构建。
本教程讲解如何用前端 JS 动态拉取 Qexo 说说 API,并适配 AnZhiYu 主题的 essay 时间线样式。
前置条件
- 首先 你得有Qexo
- 其次 你得有Qexo
- 最后 你得有Qexo
一、Qexo 说说 API 说明
Qexo 提供的公开 API 端点:
1
| GET https://你的Qexo域名/pub/talks/?limit=每页条数&page=页码
|
返回数据结构(JSON):
1 2 3 4 5 6 7 8 9 10 11
| { "status": true, "count": 17, "data": [ { "content": "<p>说说正文HTML</p><img src=\"https://...\">", "time": "1690982400", "tags": ["生活", "随笔"] } ] }
|
| 字段 | 类型 | 说明 |
|---|
status | boolean | 请求是否成功 |
count | number | 说说总条数 |
data | array | 当前页说说数组 |
data[].content | string | 说说正文(HTML,可能含<p>/<br>/<img>) |
data[].time | string | Unix 时间戳(秒) |
data[].tags | array | 标签数组 |
二、修改 essay 页面模板
编辑 themes/anzhiyu/layout/includes/page/essay.pug,将原来的静态渲染区域替换为动态容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| if site.data.essay each i in site.data.essay #essay_page .author-content.author-content-item.essayPage.single(style = i.top_background ? `background: url(${i.top_background}) left 28% / cover no-repeat;` : "") .card-content .author-content-item-tips=i.title span.author-content-item-title=i.subTitle .content-bottom .tips=i.tips .banner-button-group a.banner-button(href=i.buttonLink) i.anzhiyufont.anzhiyu-icon-arrow-circle-right span.banner-button-text=i.buttonText #bber section.timeline.page-1 ul#waterfall.list #bber-tips(style='color: var(--anzhiyu-secondtext);') ="- 加载中... -" #essay-load-more-wrap(style='text-align: center; margin-top: 1.5rem;') button#essay-load-more(style='padding: 8px 24px; border-radius: 20px; border: var(--style-border-always); background: var(--anzhiyu-card-bg); color: var(--anzhiyu-fontcolor); cursor: pointer; font-size: 14px; transition: all .3s;') 加载更多 script(data-cfasync="false", src=url_for('/js/essay_qexo.js') + '?v=4')
|
关键点:
ul#waterfall.list 是动态渲染容器,初始为空#bber-tips 显示加载状态#essay-load-more 是分页加载按钮data-cfasync="false" 防止 Cloudflare Rocket Loader 干扰脚本执行顺序?v=4 是版本号,更新脚本后递增强制浏览器刷新缓存
三、编写动态拉取脚本
在 source/js/essay_qexo.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 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
|
(function () { var QEXO_API = "https://你的Qexo域名/pub/talks/"; var PAGE_SIZE = 9; var currentPage = 1; var totalCount = 0; var loadedCount = 0; var loading = false;
function escapeAttr(s) { return String(s).replace(/"/g, """).replace(/'/g, "'"); }
// 时间戳格式化 function formatDate(ts) { var d = new Date(parseInt(ts, 10) * 1000); var Y = d.getFullYear(); var M = ("0" + (d.getMonth() + 1)).slice(-2); var D = ("0" + d.getDate()).slice(-2); return Y + "/" + M + "/" + D; }
// 构建单条说说的 HTML function buildItemHTML(item) { var tmp = document.createElement("div"); tmp.innerHTML = item.content || "";
// 提取所有图片 src (含 data-src 兼容懒加载) var imgList = []; var imgs = tmp.querySelectorAll("img"); for (var i = 0; i < imgs.length; i++) { var src = imgs[i].getAttribute("src") || imgs[i].getAttribute("data-src") || ""; if (src) imgList.push(src); }
// 移除图片节点, 保留文本结构 while (imgs.length > 0) { imgs[0].parentNode.removeChild(imgs[0]); imgs = tmp.querySelectorAll("img"); } // 清理空 p 标签 var emptyP = tmp.querySelectorAll("p"); for (var j = 0; j < emptyP.length; j++) { if (!emptyP[j].textContent.trim() && emptyP[j].children.length === 0) { emptyP[j].parentNode.removeChild(emptyP[j]); } } var contentHTML = tmp.innerHTML.trim();
var html = '<div class="bber-content">'; if (contentHTML) { html += '<div class="datacont">' + contentHTML + '</div>'; } if (imgList.length > 0) { html += '<div class="bber-container-img">'; imgList.forEach(function (src) { html += '<a class="bber-content-img" href="' + escapeAttr(src) + '" target="_blank" data-fancybox="gallery" data-caption="">' + '<img src="' + escapeAttr(src) + '" alt=""></a>'; }); for (var k = imgList.length; k < 4; k++) { html += '<div class="bber-content-noimg"></div>'; } html += '</div>'; } html += '</div><hr>';
// 底部信息 html += '<div class="bber-bottom"><div class="bber-info"><div class="bber-info-time">'; html += '<i class="anzhiyufont anzhiyu-icon-clock"></i>'; var dt = new Date(parseInt(item.time, 10) * 1000).toISOString(); html += '<time class="datatime" datetime="' + dt + '">' + formatDate(item.time) + '</time>'; html += '</div>'; if (item.tags && item.tags.length) { item.tags.forEach(function (tag) { html += '<span class="bber-info-from"><i class="anzhiyufont anzhiyu-icon-fw-fire"></i><span>' + escapeAttr(tag) + '</span></span>'; }); } html += '</div></div>'; return html; }
function loadPage(page) { if (loading) return Promise.resolve(); loading = true; var btn = document.getElementById("essay-load-more"); if (btn) { btn.textContent = "加载中..."; btn.disabled = true; }
var url = QEXO_API + "?limit=" + PAGE_SIZE + "&page=" + page; return fetch(url) .then(function (r) { return r.json(); }) .then(function (res) { if (!res || !res.status || !res.data || !res.data.length) { if (page === 1) updateTips(); loading = false; if (btn) btn.disabled = false; return; } totalCount = res.count || res.data.length; var waterfall = document.getElementById("waterfall"); if (!waterfall) { loading = false; return; }
if (page === 1) waterfall.innerHTML = "";
res.data.forEach(function (item) { var li = document.createElement("li"); li.className = "bber-item"; li.innerHTML = buildItemHTML(item); waterfall.appendChild(li); });
loadedCount += res.data.length; waterfall.classList.add("show"); // 主题 CSS 默认 opacity:0 updateTips(); updateLoadMoreBtn(); loading = false; if (btn) btn.disabled = false; }) .catch(function (e) { console.error("说说加载失败:", e); var tips = document.getElementById("bber-tips"); if (tips && page === 1) tips.textContent = "- 说说加载失败 -"; loading = false; if (btn) { btn.disabled = false; btn.textContent = "加载失败, 点击重试"; } }); }
function init() { currentPage = 1; loadedCount = 0; totalCount = 0; loadPage(currentPage); bindLoadMore(); }
function bindLoadMore() { var btn = document.getElementById("essay-load-more"); if (!btn || btn.dataset.bound) return; btn.dataset.bound = "1"; btn.addEventListener("click", function () { if (loading) return; currentPage++; loadPage(currentPage); }); }
if (document.readyState !== "loading") init(); else document.addEventListener("DOMContentLoaded", init);
// pjax 兼容 document.addEventListener("pjax:complete", function () { setTimeout(init, 50); }); })();
|
四、关键适配点解析
1. CSS 类名对应
脚本渲染的 HTML 必须严格匹配主题 essay 页面的 CSS 类名:
| 类名 | 作用 |
|---|
li.bber-item | 单条说说卡片容器 |
.bber-content | 内容区(文字+图片) |
.datacont | 文字内容 |
.bber-container-img | 图片容器 |
.bber-content-img | 单张图片(带 Fancybox) |
.bber-content-noimg | 空位占位(凑齐 4 格) |
.bber-bottom | 底部信息栏 |
.bber-info-time | 时间 |
.bber-info-from | 标签 |
2. 显示控制:.show 类
主题 CSS 默认 #waterfall 是 opacity: 0 隐藏的,必须加 .show 类才会显示:
1
| waterfall.classList.add("show");
|
3. 三列布局对齐
主题 CSS 中 #waterfall 用 flex 布局,li.bber-item 宽度为 calc((100% - 2 * 1rem) / 3),间距用 gap: 1rem 控制。
坑点:如果给 li.bber-item 加 margin-right: 1rem,会导致第三列被挤到下一行。必须用父容器 gap 控制间距,不要给子项加 margin。
4. 图片处理逻辑
Qexo 说说的 content 字段是 HTML,图片可能混在文本中。脚本逻辑:
- 用临时
div 解析 HTML - 提取所有
img 的 src(兼容 data-src 懒加载) - 从 DOM 中移除图片节点
- 清理图片原来所在的空
<p> 标签 - 图片单独放入
.bber-container-img 容器 - 不足 4 张时用
.bber-content-noimg 占位凑齐网格
5. Pjax 兼容
主题开启 Pjax 后页面切换不会刷新,需监听 pjax:complete 事件重新初始化:
1 2 3
| document.addEventListener("pjax:complete", function () { setTimeout(init, 50); });
|
6. Cloudflare Rocket Loader 兼容
如果博客套了 Cloudflare CDN 且开启了 Rocket Loader,会改变脚本加载顺序导致报错。引用脚本时加 data-cfasync="false" 排除 Rocket Loader 处理:
1
| script(data-cfasync="false", src=url_for('/js/essay_qexo.js') + '?v=4')
|
五、首页即刻说说适配(看自己需求)
AnZhiYu 主题首页顶部有个”即刻短文”滚动条组件,默认从 source/_data/essay.yml 读取静态数据。
改为动态拉取最新1条
编辑 source/js/home_essay_bar.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
| (function () { var QEXO_API = "https://你的Qexo域名/pub/talks/?limit=1";
function render() { var bberTalk = document.getElementById("bber-talk"); if (!bberTalk) return;
fetch(QEXO_API) .then(function (r) { return r.json(); }) .then(function (res) { if (!res || !res.status || !res.data || !res.data.length) return; bberTalk.innerHTML = ""; var item = res.data[0]; var a = document.createElement("a"); a.className = "li-style swiper-slide"; a.href = "javascript:void(0);"; var tmp = document.createElement("div"); tmp.innerHTML = item.content || ""; var text = (tmp.textContent || "").trim(); if (tmp.querySelector("img")) text += " [图片]"; if (!text) text = "..."; a.textContent = text; bberTalk.appendChild(a); }) .catch(function (e) { console.error("首页说说加载失败:", e); }); }
if (document.readyState !== "loading") render(); else document.addEventListener("DOMContentLoaded", render);
document.addEventListener("pjax:complete", function () { setTimeout(render, 50); }); })();
|
关闭循环播放
编辑 themes/anzhiyu/source/js/utils.js,找到 initIndexEssay 函数,关闭 swiper 的 loop 和 autoplay:
1 2 3 4 5 6 7
| let essay_bar_swiper = new Swiper(".essay_bar_swiper_container", { passiveListeners: true, direction: "vertical", loop: false, autoplay: false, mousewheel: false, });
|
六、essay.yml 配置
即使说说用动态拉取,source/_data/essay.yml 仍然需要保留(控制顶部 banner 展示):
1 2 3 4 5 6 7 8 9
| - title: 即刻短文 subTitle: 咸鱼的日常生活。 tips: 随时随地,分享生活 buttonText: 关于我 buttonLink: /about/ limit: 50 home_essay: true top_background: https://api.yppp.net/pc.php essay_list: []
|
八、总结
通过本教程,你可以:
- 摆脱静态构建:在 Qexo 后台随时发说说,无需重新部署博客
- 完美适配主题:渲染样式与原版 essay 页面完全一致
- 分页加载:首次填满页面,按需加载更多
- Pjax 兼容:页面切换无刷新
- CDN 兼容:规避 Cloudflare Rocket Loader 干扰
核心思路就是:保留主题的页面骨架和 CSS,只把数据源从本地 yml 换成 Qexo API。这个思路同样适用于其他 Hexo 主题(如 Butterfly、Volantis)的说说/短文功能适配 可以丢给AI这个文章作参考