【前端小技巧】实现详情页滚动位置记忆,提升用户体验

前端小技巧:

在前端开发中,我们常常会遇到这样的需求:当用户在一个页面中浏览了一部分内容,跳转到其他详情页后,再回来时希望能回到之前浏览的位置。今天就通过一段有趣的代码,给大家详细讲解如何实现这个功能,让你的网站更具用户友好性。

一、代码实现效果

想象一下,你在一个电商网站浏览商品列表,列表很长,你好不容易找到了心仪商品的详情介绍部分。这时,你突然想看看其他商品的类似介绍,点击跳转过去后,当你再次回到这个商品详情页,页面自动回到了你之前看到的位置,是不是很方便?我们今天要实现的就是这样的效果。在我们的示例代码中,页面有一个导航栏,点击不同的导航项(比如“内容 1”“内容 2”等),会加载不同的内容,并且能记住每次跳转前的滚动位置。

二、JavaScript功能实现:核心逻辑大揭秘

  1. 配置选项:为功能设定规则
    代码一开始定义了一个config对象,它就像是整个功能的规则手册。里面定义了一些重要的参数,比如localStorageKey,它指定了在本地存储中用来保存滚动位置数据的键名。animation对象设置了滚动动画的时长duration和缓动函数easing,就像控制汽车行驶的速度和加速度一样,让滚动动画看起来更自然。content对象则规定了初始加载的内容数量initialLoad和懒加载的阈值lazyLoadThreshold。还有一个maxSaved,它限制了最多可以保存多少个滚动位置数据,防止本地存储被过多的数据占用。
const config = {
    localStorageKey:'scrollPositions',
    animation: {
        duration: 150,
        easing: 'easeInOutQuad'
    },
    content: {
        initialLoad: 100,
        lazyLoadThreshold: 50
    },
    maxSaved: 4
};
  1. 缓动函数映射:让滚动更丝滑
    easingFunctions对象定义了不同的缓动函数,比如easeInOutQuadlinear。缓动函数决定了滚动动画的速度变化方式,easeInOutQuad会让滚动开始和结束时速度慢一些,中间快一些,就像汽车启动和刹车时的平稳过渡,而linear则是匀速滚动。这些函数就像给滚动动画注入了不同的“灵魂”,让用户的体验更加丰富。
const easingFunctions = {
    easeInOutQuad: t => t < 0.5? 2 * t * t : -1 + (4 - 2 * t) * t,
    linear: t => t
};
  1. 获取当前内容ID:定位内容的钥匙
    getCurrentId函数的作用是从当前页面的URL哈希值中获取内容的ID。比如当URL是example.com#id=1时,这个函数就能把1提取出来。它就像一把钥匙,帮助我们找到对应的内容和其相关的滚动位置数据。
const getCurrentId = () => {
    const match = window.location.hash.match(/id=(\d+)/);
    return match? match[1] : null;
};
  1. 防抖函数:避免频繁操作
    debounce函数是一个很有用的工具。它可以防止某个函数在短时间内被频繁调用。在我们的场景中,当用户滚动页面时,saveScrollPosition函数会被触发来保存滚动位置,但如果不做处理,每次滚动一点点都会触发保存操作,这会很消耗性能。通过debounce函数,我们设置了一个等待时间,只有当用户停止滚动一段时间(这里是250毫秒)后,才会真正执行保存操作,就像你打字时,输入法不会每次你按一个键就立刻联想,而是等你停顿一下再联想,这样既保证了功能实现,又提高了性能。
const debounce = (func, wait) => {
    let timeout;
    return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
    };
};
  1. 获取/保存滚动位置数据:数据的存储与读取
    scrollData对象负责处理滚动位置数据的获取和保存。get方法尝试从本地存储中读取数据,如果读取失败,会在控制台给出警告信息。save方法则把数据保存到本地存储,但在保存前会检查数据数量,如果超过了config.maxSaved设置的数量,会按照访问时间对数据进行排序,只保留最近访问的几个数据,就像整理书架,把不常用的书清理掉,只保留最常看的。
const scrollData = {
    get: () => {
        try {
            return JSON.parse(localStorage.getItem(config.localStorageKey)) || {};
        } catch (e) {
            console.warn('读取localStorage失败:', e);
            return {};
        }
    },
    save: (data) => {
        try {
            const entries = Object.entries(data);
            if (entries.length > config.maxSaved) {
                const sorted = entries.sort((a, b) => b[1].accessTime - a[1].accessTime);
                data = Object.fromEntries(sorted.slice(0, config.maxSaved));
            }
            localStorage.setItem(config.localStorageKey, JSON.stringify(data));
        } catch (e) {
            console.warn('保存到localStorage失败:', e);
        }
    }
};
  1. 记录滚动位置:时刻记录你的足迹
    saveScrollPosition函数通过debounce处理后,在用户滚动页面时,会获取当前内容的ID,并且检查当前滚动位置与之前保存的位置是否有较大差异(这里设置为超过5像素)。如果有差异或者之前没有保存过该ID的位置,就把当前滚动位置和访问时间保存到本地存储中,就像一个贴心的小秘书,时刻记录着你在页面上的“足迹”。
const saveScrollPosition = debounce(() => {
    const id = getCurrentId();
    if (id) {
        const data = scrollData.get();
        const currentPosition = window.scrollY;
        if (!data[id] || Math.abs((data[id].position || 0) - currentPosition) > 5) {
            data[id] = {
                position: currentPosition,
                accessTime: Date.now()
            };
            scrollData.save(data);
        }
    }
}, 250);
  1. 恢复滚动位置:回到你上次停留的地方
    restoreScrollPosition函数负责在页面加载或切换内容时,恢复之前保存的滚动位置。它从本地存储中获取对应ID的滚动位置数据,如果有数据,就通过动画效果将页面滚动到指定位置。动画效果通过requestAnimationFrame和缓动函数来实现,让滚动过程更加流畅自然,就像带你穿越回上次浏览的地方。
const restoreScrollPosition = (id) => {
    const { position } = scrollData.get()[id] || {};
    if (position == null) return window.scrollTo(0, 0);
    
    const start = window.scrollY;
    const distance = position - start;
    const easing = easingFunctions[config.animation.easing] || easingFunctions.linear;
    let startTime = null;

    const animate = (currentTime) => {
        startTime = startTime || currentTime;
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / config.animation.duration, 1);
        
        window.scrollTo(0, start + distance * easing(progress));
        if (progress < 1) requestAnimationFrame(animate);
    };
    
    requestAnimationFrame(animate);
};
  1. 加载内容:呈现精彩内容
    loadContent函数是内容加载的核心。当点击导航链接或者页面初始化时,它会被调用。首先,它在content区域显示“加载中…”的提示信息,然后通过window.location.hash设置当前内容的ID,同时触发saveScrollPosition保存当前可能的滚动位置。接着,通过setTimeout模拟数据加载过程(这里设置为150毫秒),加载完成后,生成对应的内容并显示在content区域,最后调用restoreScrollPosition恢复滚动位置,让用户看到熟悉的页面位置。
const loadContent = (id) => {
    const contentDiv = document.getElementById('content');
    contentDiv.innerHTML = '<div class="loading">加载中...</div>';
    
    window.location.hash = `id=${id}`;
    saveScrollPosition();
    
    setTimeout(() => {
        const generateContent = (title) => {
            let html = `<h1>${title}</h1>`;
            for (let i = 0; i < config.content.initialLoad; i++) {
                html += `<p>这是 ${title} 的长页面,滚动试试。</p>`;
            }
            return html;
        };
        
        contentDiv.innerHTML = generateContent(`内容 ${id}`) || '<h1>未找到内容</h1>';
        restoreScrollPosition(id);
    }, 150);
};
  1. 初始化与导航点击事件:启动与交互
    在页面加载完成时,通过window.addEventListener('load', () => {})来初始化一些操作。它添加了一个滚动事件监听器,当页面滚动时触发saveScrollPosition记录滚动位置,并且调用loadContent加载初始内容(如果URL中没有指定ID,则加载“内容 1”)。

对于导航栏的点击事件,通过document.querySelectorAll('nav a[data-id]').forEach(link => {})遍历所有带有data-id属性的导航链接,为每个链接添加点击事件处理函数。当点击链接时,阻止默认的链接跳转行为(因为我们要用自己的方式加载内容),然后调用loadContent加载对应的内容,就像为每个导航按钮安装了一个智能开关,控制着内容的展示和滚动位置的管理。

window.addEventListener('load', () => {
    window.addEventListener('scroll', saveScrollPosition);
    loadContent(getCurrentId() || '1');
});

document.querySelectorAll('nav a[data-id]').forEach(link => {
    link.addEventListener('click', (e) => {
        e.preventDefault();
        loadContent(link.dataset.id);
    });
});

三、完整的HTML代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>实现跳转不同的详情页滚动条位置记录功能</title>
  <style>
    body {
      height: 2000px;
      padding: 0;
      margin: 0;
    }

    #scrollBox {
      position: relative;
    }

    nav {
      height: 60px;
    }

    nav ul {
      list-style-type: none;
      padding: 0;
      margin: 0;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      background-color: #f2f2f2;
      z-index: 1;
      height: 60px;
      line-height: 60px
    }

    nav ul li {
      display: inline;
      margin: 0 20px;
    }

    nav ul li a {
      text-decoration: none;
      cursor: pointer;
    }

    #content {
      padding: 20px;
    }

    .loading {
      text-align: center;
      padding: 50px;
      font-size: 18px;
      color: #666;
    }
  </style>
</head>
<body>
  <div id="app">
    <div id="scrollBox">
      <nav>
        <ul>
          <li><a href="#" data-id="1">内容 1</a></li>
          <li><a href="#" data-id="2">内容 2</a></li>
          <li><a href="#" data-id="3">内容 3</a></li>
          <li><a href="#" data-id="4">内容 4</a></li>
          <li><a href="#" data-id="5">内容 5</a></li>
        </ul>
      </nav>
      <div id="content"></div>
    </div>
  </div>

  <script>
    // 配置选项
    const config = {
      localStorageKey: 'scrollPositions', // 本地存储中用于保存滚动位置的键名
      animation: {
        duration: 150, // 滚动动画持续时间,单位毫秒
        easing: 'easeInOutQuad' // 动画缓动函数类型
      },
      content: {
        initialLoad: 100, // 初始加载的内容段落数量
        lazyLoadThreshold: 50 // 懒加载触发阈值,距离底部多少像素时加载更多内容
      },
      maxSaved: 4 // 最多保存的滚动位置记录数量
    };

    // 缓动函数映射
    const easingFunctions = {
      easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
      linear: t => t
    };

    // 获取当前内容ID
    const getCurrentId = () => {
      const match = window.location.hash.match(/id=(\d+)/);
      return match ? match[1] : null;
    };

    // 防抖函数
    const debounce = (func, wait) => {
      let timeout;
      return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
      };
    };

    // 获取/保存滚动位置数据
    const scrollData = {
      get: () => {
        try {
          return JSON.parse(localStorage.getItem(config.localStorageKey)) || {};
        } catch (e) {
          console.warn('读取localStorage失败:', e);
          return {};
        }
      },
      save: (data) => {
        try {
          const entries = Object.entries(data);
          if (entries.length > config.maxSaved) {
            const sorted = entries.sort((a, b) => b[1].accessTime - a[1].accessTime);
            data = Object.fromEntries(sorted.slice(0, config.maxSaved));
          }
          localStorage.setItem(config.localStorageKey, JSON.stringify(data));
        } catch (e) {
          console.warn('保存到localStorage失败:', e);
        }
      }
    };

    // 记录滚动位置
    const saveScrollPosition = debounce(() => {
      const id = getCurrentId();
      if (id) {
        const data = scrollData.get();
        const currentPosition = window.scrollY;
        if (!data[id] || Math.abs((data[id].position || 0) - currentPosition) > 5) {
          data[id] = {
            position: currentPosition,
            accessTime: Date.now()
          };
          scrollData.save(data);
        }
      }
    }, 250);

    // 恢复滚动位置
    const restoreScrollPosition = (id) => {
      const { position } = scrollData.get()[id] || {};
      if (position == null) return window.scrollTo(0, 0);
      
      const start = window.scrollY;
      const distance = position - start;
      const easing = easingFunctions[config.animation.easing] || easingFunctions.linear;
      let startTime = null;

      const animate = (currentTime) => {
        startTime = startTime || currentTime;
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / config.animation.duration, 1);
        
        window.scrollTo(0, start + distance * easing(progress));
        if (progress < 1) requestAnimationFrame(animate);
      };
      
      requestAnimationFrame(animate);
    };

    // 加载内容
    const loadContent = (id) => {
      const contentDiv = document.getElementById('content');
      contentDiv.innerHTML = '<div class="loading">加载中...</div>';
      
      window.location.hash = `id=${id}`;
      saveScrollPosition();
      
      setTimeout(() => {
        const generateContent = (title) => {
          let html = `<h1>${title}</h1>`;
          for (let i = 0; i < config.content.initialLoad; i++) {
            html += `<p>这是 ${title} 的长页面,滚动试试。</p>`;
          }
          return html;
        };
        
        contentDiv.innerHTML = generateContent(`内容 ${id}`) || '<h1>未找到内容</h1>';
        restoreScrollPosition(id);
      }, 150);
    };

    // 初始化
    window.addEventListener('load', () => {
      window.addEventListener('scroll', saveScrollPosition);
      loadContent(getCurrentId() || '1');
    });

    // 导航点击事件
    document.querySelectorAll('nav a[data-id]').forEach(link => {
      link.addEventListener('click', (e) => {
        e.preventDefault();
        loadContent(link.dataset.id);
      });
    });
  </script>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要暴富啊

感谢老铁们的打赏!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值