当用户关闭页面时,79%的人希望再次打开时能回到上次阅读位置。本文将用真实场景代码,解决这个提升用户留存的关键问题。
用户行为数据揭示:
60%的用户会在3天内返回未读完的长文
滚动位置偏差超过1屏时,43%的用户会直接离开
移动端场景下,位置记忆需求比桌面端高2.3倍
方案对比指南:
方案 | 精度 | 性能消耗 | 适用场景 | 兼容性 |
---|---|---|---|---|
Scroll位置记录 | 中 | 高 | 简单静态页面 | 全浏览器 |
Intersection观察器 | 高 | 极低 | 章节化内容 | IE11+ |
URL锚点定位 | 高 | 最低 | 可分享的内容 | 全浏览器 |
混合方案(推荐) | 极高 | 低 | 企业级应用 | 渐进增强 |
// 高性能滚动监听(每秒仅记录6-8次)
let lastScrollY = 0;
let timer = null;
const recordScrollPosition = () => {
// 只记录纵向滚动位置
lastScrollY = window.scrollY || document.documentElement.scrollTop;
// 使用sessionStorage避免长期占用存储
sessionStorage.setItem('scrollPos', lastScrollY);
};
window.addEventListener('scroll', () => {
// 使用RAF+节流双重优化
if (!timer) {
timer = requestAnimationFrame(() => {
recordScrollPosition();
timer = null;
});
}
});
// 页面恢复逻辑
window.addEventListener('load', () => {
// 优先检查URL锚点
if (location.hash) {
const target = document.getElementById(location.hash.slice(1));
target?.scrollIntoView({ behavior: 'instant' });
return;
}
// 无锚点时恢复滚动位置
const savedPos = sessionStorage.getItem('scrollPos');
if (savedPos) {
window.scrollTo({
top: parseInt(savedPos),
behavior: 'smooth' // 平滑滚动提升体验
});
}
});
// 自动生成章节探针(无需手动插入)
document.querySelectorAll('h2, h3, .chapter').forEach((heading, index) => {
if (!heading.id) heading.id = `section-${index}`;
// 创建隐形定位锚点
const marker = document.createElement('div');
marker.className = 'scroll-marker';
marker.dataset.sectionId = heading.id;
marker.style = 'height:1px; visibility:hidden;';
heading.parentNode.insertBefore(marker, heading);
});
// 创建观察器
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
const sectionId = entry.target.dataset.sectionId;
history.replaceState(null, '', `#${sectionId}`);
sessionStorage.setItem('lastSection', sectionId);
}
});
}, { threshold: [0.2, 0.5, 0.8] });
// 监听所有标记
document.querySelectorAll('.scroll-marker').forEach(marker => {
observer.observe(marker);
});
// 恢复逻辑
window.addEventListener('load', () => {
const targetId = location.hash.slice(1) || sessionStorage.getItem('lastSection');
if (targetId) {
const target = document.getElementById(targetId)?.previousElementSibling;
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
// 动态锚点管理系统
let activeAnchor = '';
const updateActiveAnchor = () => {
const sections = [...document.querySelectorAll('[>)];
const scrollY = window.scrollY + 100; // 提前100px切换
// 查找当前可见区域最接近的章节
const currentSection = sections.findLast(section =>
section.offsetTop <= scrollY
);
if (currentSection && currentSection.id !== activeAnchor) {
activeAnchor = currentSection.id;
history.replaceState(null, '', `#${activeAnchor}`);
}
};
// 滚动优化监听
let scrollTimer;
window.addEventListener('scroll', () => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(updateActiveAnchor, 150);
});
// 初始化检查
window.addEventListener('load', () => {
if (location.hash) {
const target = document.getElementById(location.hash.slice(1));
if (target) {
// 微调位置避免标题被顶部导航遮挡
window.scrollTo({
top: target.offsetTop - 80,
behavior: 'instant'
});
}
}
});
// 智能位置记忆系统
class ScrollMemory {
constructor() {
this.lastPosition = 0;
this.currentSection = '';
this.init();
}
init() {
// 优先使用URL锚点
if (location.hash) {
this.restoreFromAnchor();
return;
}
// 其次使用本地存储
const savedSection = sessionStorage.getItem('lastSection');
if (savedSection) {
this.scrollToSection(savedSection);
} else {
// 最后使用滚动位置
const scrollY = sessionStorage.getItem('scrollPos');
if (scrollY) window.scrollTo(0, parseInt(scrollY));
}
// 初始化监听
this.setupObservers();
}
setupObservers() {
// 章节观察器
this.sectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.3) {
this.currentSection = entry.target.id;
sessionStorage.setItem('lastSection', this.currentSection);
}
});
}, { threshold: 0.3 });
// 滚动位置监听(节流)
this.scrollHandler = () => {
this.lastPosition = window.scrollY;
sessionStorage.setItem('scrollPos', this.lastPosition);
};
window.addEventListener('scroll', this.scrollHandler, { passive: true });
}
scrollToSection(id) {
const element = document.getElementById(id);
if (!element) return;
// 精确计算偏移量(考虑固定导航栏)
const headerHeight = document.querySelector('header')?.offsetHeight || 0;
const targetY = element.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo({
top: targetY,
behavior: 'smooth'
});
}
restoreFromAnchor() {
const sectionId = location.hash.slice(1);
this.scrollToSection(sectionId);
sessionStorage.setItem('lastSection', sectionId);
}
// 清理资源
destroy() {
this.sectionObserver?.disconnect();
window.removeEventListener('scroll', this.scrollHandler);
}
}
// 页面初始化
window.addEventListener('domContentLoaded', () => {
new ScrollMemory();
});
/* 关键css:确保滚动恢复时不触发额外布局 */
html {
scroll-behavior: smooth; /* 启用原生平滑滚动 */
}
body {
overflow-anchor: none; /* 禁用浏览器自动滚动锚定 */
}
// 监听内容变化(如AJAX加载)
const contentObserver = new MutationObserver(() => {
scrollMemory.destroy();
scrollMemory = new ScrollMemory();
});
contentObserver.observe(document.body, {
childList: true,
subtree: true
});
// 解决移动端键盘弹出问题
window.addEventListener('resize', () => {
if (window.visualViewport) {
const viewportHeight = window.visualViewport.height;
if (viewportHeight < window.innerHeight * 0.7) {
// 键盘弹出时暂停位置记录
scrollMemory.destroy();
} else {
// 键盘收起时恢复
scrollMemory = new ScrollMemory();
}
}
});
场景:在线教育平台课程阅读页
// 课程阅读器增强版
class CourseReader {
constructor() {
this.initScrollMemory();
this.setupProgressTracking();
}
initScrollMemory() {
this.scrollMemory = new ScrollMemory();
// 每章节添加阅读进度标记
document.querySelectorAll('.chapter').forEach(chapter => {
chapter.dataset.readProgress = '0';
});
}
setupProgressTracking() {
// 章节进入视口时标记为已读
const progressObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.dataset.readProgress = '100';
this.saveReadingProgress();
}
});
}, { threshold: 0.8 });
document.querySelectorAll('.chapter').forEach(chapter => {
progressObserver.observe(chapter);
});
}
saveReadingProgress() {
// 获取已读章节
const readChapters = [...document.querySelectorAll('.chapter')]
.filter(chap => chap.dataset.readProgress === '100')
.map(chap => chap.id);
// 同步到服务器
fetch('/api/save-progress', {
method: 'POST',
body: JSON.stringify({ chapters: readChapters })
});
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('.course-container')) {
new CourseReader();
}
});
// 使用IndexedDB存储阅读状态
const saveReadingState = async (position, section) => {
const db = await openDB('ReadingDB', 1, {
upgrade(db) {
db.createObjectStore('readingState', { keyPath: 'pageId' });
}
});
await db.put('readingState', {
pageId: location.pathname,
position,
section,
timestamp: Date.now()
});
};
// 从其他设备恢复
const restoreCrossDevice = async () => {
const db = await openDB('ReadingDB');
const state = await db.get('readingState', location.pathname);
if (state) {
if (state.section) {
scrollToSection(state.section);
} else {
window.scrollTo(0, state.position);
}
}
};
最佳实践总结:
优先使用URL锚点:天然支持分享和深度链接
本地存储做降级方案:使用sessionStorage避免长期占用存储
智能章节检测:结合标题标签自动生成锚点
考虑动态内容:监听DOM变化更新定位系统
移动端特殊处理:解决键盘弹出和视口变化问题
精准的位置记忆能使内容类网站的用户停留时间提升40%以上。选择适合业务场景的方案,让用户每次返回都无缝衔接上次的阅读体验,是提升产品粘性的低成本高回报策略。
是react的3kb轻量化方案,拥有同样的 ES6 API,Preact 在 DOM上实现一个可能是最薄的一层虚拟 DOM 实现。
前端路由鉴权,屏蔽地址栏入侵,路由数据由后台管理,前端只按固定规则异步加载路由,权限控制精确到每一个按钮,自动更新token,同一个浏览器只能登录一个账号
RFC 7230 与 RFC 3986 定义了 HTTP/1.1 标准并对 URI 的编解码问题作出了规范。但是,文本形式的规范和最终落地的标准之间总是存在着差距。标准中共 82 个字符无需编码。
在做项目优化的时候,发现页面加载很慢。结果一看主要的问题就是就是图片的大小过慢,然后呢准备呢去做优化, 本来想去用webp,去优化的,但是呢这个图片是不是我们就用不了呢,然后看了下业界优化王
本篇我们重点介绍以下四种模块加载规范: AMD CMD CommonJS ES6 模块 最后再延伸讲下 Babel 的编译和 webpack 的打包原理。
GitHub的CDN(Content Delivery Network,即内容分发网络)域名遭到DNS污染,无法连接使用GitHub的加速分发服务器,所以国内访问速度较慢。
JavaScript 语言有着悠久的历史。有很多开发人员仍在学习基础知识。但是,如果您正在尝试学习该语言并迈出第一步,您需要知道新开发人员会犯什么错误。您已经研究过 JavaScript 开发教程,并且知道它是世界上最流行的语言之一。
H5网页在微信上是无法直接打开app链接的,需要使用微信开放标签wx-open-launch-app,它主要用于微信H5网页唤醒app,该标签只有在微信中才会显示。
而对于几百M或上G的大图而言,不管对图片进行怎么优化或加速处理,要实现秒开也是不太可能的事情。而上面介绍的第二条“图像分割切片”是最佳解决方案。下面介绍下如何对大图进行分割
CSS 原生嵌套还处于工作草案 Working Draft (WD) 阶段,而今天(2023-09-02),CSS 原生嵌套 Nesting 终于成为了既定的规范!在之前,只有在 LESS、SASS 等预处理器中,我们才能使用嵌套的写法
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!