后端应返回结构化JSON数据,如{"items":[...],"has_more":true,"next_offset":20},而非HTML片段,以保障SEO、安全与语义;前端用insertAdjacentHTML等原生方法追加DOM。

点击加载更多时后端该返回什么数据
后端接口不能返回完整 HTML 片段(比如带 <div>...</div> 的字符串),而应返回结构化数据,通常是 JSON。前端拿到后自己拼 DOM 或用模板渲染。否则容易破坏页面语义、影响 SEO、增加 XSS 风险。
推荐返回格式示例:
{
"items": [
{"id": 101, "title": "文章标题一", "excerpt": "摘要内容..."},
{"id": 102, "title": "文章标题二", "excerpt": "摘要内容..."}
],
"has_more": true,
"next_offset": 20
}
-
items是本次要插入的新条目数组,字段与首屏保持一致 -
has_more控制“加载更多”按钮是否继续显示 -
next_offset或next_page用于下一次请求的分页参数,避免前端靠current_page + 1硬算(防跳页、防重复)
前端用 fetch 请求 + insertAdjacentHTML 追加最轻量
不依赖框架时,直接用原生 JS 实现最可控。关键不是“怎么发请求”,而是“怎么把新内容塞进老容器里且不闪、不重绘整页”。
- 用
insertAdjacentHTML('beforeend', htmlString)替代innerHTML += ...,避免重新解析已有 DOM - 每次请求前禁用按钮,响应后恢复,防止连点触发多次请求
- 建议给容器加一个 loading 状态类(如
is-loading),配合 CSS 显示骨架或禁用样式 - 错误时要显式提示,并保留按钮可点击,别让页面“卡死”
简短示例:
async function loadMore() {
const btn = document.querySelector('#load-more');
const container = document.querySelector('#list-container');
btn.disabled = true;
btn.textContent = '加载中...';
try {
const res = await fetch(`/api/posts?offset=${offset}`);
const data = await res.json();
data.items.forEach(item => {
const el = document.createElement('article');
el.innerHTML = `<h3>${item.title}</h3><p>${item.excerpt}</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill9972" title="Wechat Article Html"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20e695c2374.jpg" alt="Wechat Article Html" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill9972" title="Wechat Article Html" class="overflowclass">Wechat Article Html</a>
<p class="overflowclass">将 Markdown 文章转换为微信公众号兼容的纯 HTML 格式。当用户要求"转成微信格式"、"生成公众号文章 HTML"、"排版到微信草稿箱"、"微信粘贴格式"时触发。输出带内联样式的纯 HTML,微信编辑器可直接渲染,无需额外适配。支持标题、段落、列表、粗体、行内代码、表格、引用块、代码块、配图的完整转换。</p>
</div>
<a rel="nofollow" href="/xiazai/skill9972" title="Wechat Article Html" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>`;
container.appendChild(el);
});
if (data.has_more) {
offset = data.next_offset;
} else {
btn.remove(); // 或 btn.style.display = 'none'
}
} catch (err) {
alert('加载失败,请重试');
} finally {
btn.disabled = false;
btn.textContent = '加载更多';
}
}
滚动到底部自动加载 vs 点击按钮:别混用同一套逻辑
用户明确点击“加载更多”时,行为是确定的;而滚动触底是隐式动作,两者触发时机、失败反馈、加载状态展示方式都不同。
- 滚动监听需节流(
throttle),否则高频触发请求;点击按钮天然有交互边界,不用额外节流 - 滚动加载失败时,用户可能已经划走了,得靠顶部 banner 提示;点击失败则可聚焦按钮并高亮,用户能立刻感知
- 如果同时支持两种方式,
offset或page状态必须统一管理,否则容易出现“点一次、滚一次,结果跳两页” - 移动端触摸滚动的
scroll事件兼容性差,iOS Safari 有延迟,点击按钮更稳
服务端分页参数用 offset/limit 还是 cursor?
对简单列表(如博客归档、商品列表),offset/limit 足够,但要注意性能陷阱。
-
OFFSET 10000 LIMIT 20在 MySQL 中会扫描前 10000 行再丢弃,数据量大时明显变慢 - 如果列表支持排序(如按时间倒序),且数据实时写入,
offset可能漏项或重复(新数据插入导致偏移错位) -
cursor方案(如传上一页最后一条的created_at和id)更稳定,但要求后端有唯一递增字段或复合游标逻辑 - 初期用
offset/limit没问题,但上线后观察慢查询日志,一旦OFFSET > 5000就该切 cursor
真实项目里,最容易被忽略的是“加载更多”按钮的 DOM 生命周期——它可能被 JS 动态删掉,也可能被 Vue/React 重新渲染导致事件丢失。所以绑定事件别只靠 onclick="loadMore()",要用事件委托或框架生命周期钩子接管。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










