后端应返回结构化JSON数据,如{"items":[...],"has_more":true,"next_offset":20},而非HTML片段,以保障SEO、安全与语义;前端用insertAdjacentHTML等原生方法追加DOM。
点击加载更多时后端该返回什么数据
后端接口不能返回完整 HTML 片段(比如带
的字符串),而应返回结构化数据,通常是 JSON。前端拿到后自己拼 DOM 或用模板渲染。否则容易破坏页面语义、影响 SEO、增加 XSS 风险。
推荐返回格式示例:
是本次要插入的新条目数组,字段与首屏保持一致
控制“加载更多”按钮是否继续显示
或
用于下一次请求的分页参数,避免前端靠
硬算(防跳页、防重复)
前端用
请求 +
追加最轻量
不依赖框架时,直接用原生 JS 实现最可控。关键不是“怎么发请求”,而是“怎么把新内容塞进老容器里且不闪、不重绘整页”。
用
替代
,避免重新解析已有 DOM
每次请求前禁用按钮,响应后恢复,防止连点触发多次请求
建议给容器加一个 loading 状态类(如
),配合 CSS 显示骨架或禁用样式
错误时要显式提示,并保留按钮可点击,别让页面“卡死”
简短示例:
立即学习
“
前端免费学习笔记(深入)
”;
使用HTML,CSS,JavaScript开发Android应用程序 英文文字pdf版附源文件
如果你了解HTML,CSS和JavaScript,您已经拥有所需的工具开发Android应用程序。本动手本书展示了如何使用这些开源web标准设计和建造,可适应任何Android设备的应用程序 - 无需使用Java。您将学习如何创建一个在您选择的平台的Android友好的网络应用程序,然后转换与自由PhoneGap框架到一个原生的Android应用程序。了解为什么设备无关的移动应用是未来的潮流,并开始构建应用程序,提供更
下载
滚动到底部自动加载 vs 点击按钮:别混用同一套逻辑
用户明确点击“加载更多”时,行为是确定的;而滚动触底是隐式动作,两者触发时机、失败反馈、加载状态展示方式都不同。
滚动监听需节流(
),否则高频触发请求;点击按钮天然有交互边界,不用额外节流
滚动加载失败时,用户可能已经划走了,得靠顶部 banner 提示;点击失败则可聚焦按钮并高亮,用户能立刻感知
如果同时支持两种方式,
或
状态必须统一管理,否则容易出现“点一次、滚一次,结果跳两页”
移动端触摸滚动的
事件兼容性差,iOS Safari 有延迟,点击按钮更稳
服务端分页参数用
还是
?
对简单列表(如博客归档、商品列表),
足够,但要注意性能陷阱。
在 MySQL 中会扫描前 10000 行再丢弃,数据量大时明显变慢
如果列表支持排序(如按时间倒序),且数据实时写入,
可能漏项或重复(新数据插入导致偏移错位)
方案(如传上一页最后一条的
和
)更稳定,但要求后端有唯一递增字段或复合游标逻辑
初期用
没问题,但上线后观察慢查询日志,一旦
就该切 cursor
真实项目里,最容易被忽略的是“加载更多”按钮的 DOM 生命周期——它可能被 JS 动态删掉,也可能被 Vue/React 重新渲染导致事件丢失。所以绑定事件别只靠
,要用事件委托或框架生命周期钩子接管。
...{
"items": [
{"id": 101, "title": "文章标题一", "excerpt": "摘要内容..."},
{"id": 102, "title": "文章标题二", "excerpt": "摘要内容..."}
],
"has_more": true,
"next_offset": 20
}itemshas_morenext_offsetnext_pagecurrent_page + 1fetchinsertAdjacentHTMLinsertAdjacentHTML('beforeend', htmlString)innerHTML += ...is-loadingasync 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 = `${item.title}
${item.excerpt}
`;
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 = '加载更多';
}
}throttleoffsetpagescrolloffset/limitcursoroffset/limitOFFSET 10000 LIMIT 20offsetcursorcreated_atidoffset/limitOFFSET > 5000onclick="loadMore()"