返回

动感十足!微信小程序导航选中功能的华丽实现

前端

前言

最近在进行微信小程序全面升级改版时,遇到一个交互需求:当用户滚动页面时,左侧菜单栏会自动选中当前处于可视区域的菜单项。同时,用户也可以点击左侧菜单栏的任意一项,页面会滚动到相应位置,并选中该菜单项。

起初,我对这个需求感到有点棘手,因为我不知道该怎么做。但是经过一番研究,我发现了几种实现这个功能的方法,特此记录一下,希望对其他小伙伴有所帮助。

实现方案

基于锚点

锚点是指页面中某个特定的位置,可以使用锚点链接直接跳转到该位置。要实现导航选中功能,我们可以给每个菜单项添加一个锚点,然后在滚动页面时,监听页面滚动事件,根据当前滚动位置来确定当前处于可视区域的菜单项,并将其选中。

// 监听页面滚动事件
window.addEventListener('scroll', function() {
  // 获取当前滚动位置
  var scrollTop = window.scrollY;

  // 遍历所有菜单项
  var menuItems = document.querySelectorAll('.menu-item');
  for (var i = 0; i < menuItems.length; i++) {
    // 获取菜单项的锚点位置
    var anchor = menuItems[i].getAttribute('href');

    // 如果菜单项的锚点位置在当前滚动位置之上,则将其选中
    if (scrollTop >= document.querySelector(anchor).offsetTop) {
      menuItems[i].classList.add('active');
    } else {
      menuItems[i].classList.remove('active');
    }
  }
});

基于事件

除了使用锚点,我们还可以使用事件来实现导航选中功能。具体来说,我们可以给每个菜单项添加一个点击事件,当用户点击某个菜单项时,页面会滚动到相应位置,并选中该菜单项。

// 给每个菜单项添加点击事件
var menuItems = document.querySelectorAll('.menu-item');
for (var i = 0; i < menuItems.length; i++) {
  menuItems[i].addEventListener('click', function() {
    // 获取菜单项的锚点位置
    var anchor = this.getAttribute('href');

    // 滚动页面到锚点位置
    window.scrollTo({
      top: document.querySelector(anchor).offsetTop,
      behavior: 'smooth'
    });

    // 选中该菜单项
    this.classList.add('active');

    // 取消其他菜单项的选中状态
    var otherMenuItems = document.querySelectorAll('.menu-item');
    for (var j = 0; j < otherMenuItems.length; j++) {
      if (otherMenuItems[j] !== this) {
        otherMenuItems[j].classList.remove('active');
      }
    }
  });
}

基于状态管理

以上两种方法都需要在滚动或点击事件中对菜单项的选中状态进行更新。如果菜单项的数量很多,那么这种方法可能会导致性能问题。为了解决这个问题,我们可以使用状态管理来实现导航选中功能。

状态管理是指将应用程序的状态存储在中央存储库中,并允许组件从中央存储库中读取和更新状态。在 React 中,我们可以使用 Redux 来进行状态管理。

首先,我们需要定义一个 Redux store,其中包含菜单项的选中状态。然后,我们需要给每个菜单项添加一个组件,该组件负责从 Redux store 中读取菜单项的选中状态,并根据选中状态来渲染菜单项。

最后,我们需要在滚动或点击事件中,使用 Redux actions 来更新 Redux store 中的菜单项选中状态。

代码示例

以下是三种实现方案的代码示例:

基于锚点

<div class="menu">
  <a class="menu-item" href="#section-1">Section 1</a>
  <a class="menu-item" href="#section-2">Section 2</a>
  <a class="menu-item" href="#section-3">Section 3</a>
</div>

<div id="section-1">
  <h1>Section 1</h1>
</div>

<div id="section-2">
  <h1>Section 2</h1>
</div>

<div id="section-3">
  <h1>Section 3</h1>
</div>

// 监听页面滚动事件
window.addEventListener('scroll', function() {
  // 获取当前滚动位置
  var scrollTop = window.scrollY;

  // 遍历所有菜单项
  var menuItems = document.querySelectorAll('.menu-item');
  for (var i = 0; i < menuItems.length; i++) {
    // 获取菜单项的锚点位置
    var anchor = menuItems[i].getAttribute('href');

    // 如果菜单项的锚点位置在当前滚动位置之上,则将其选中
    if (scrollTop >= document.querySelector(anchor).offsetTop) {
      menuItems[i].classList.add('active');
    } else {
      menuItems[i].classList.remove('active');
    }
  }
});

基于事件

<div class="menu">
  <a class="menu-item" href="#section-1">Section 1</a>
  <a class="menu-item" href="#section-2">Section 2</a>
  <a class="menu-item" href="#section-3">Section 3</a>
</div>

<div id="section-1">
  <h1>Section 1</h1>
</div>

<div id="section-2">
  <h1>Section 2</h1>
</div>

<div id="section-3">
  <h1>Section 3</h1>
</div>

// 给每个菜单项添加点击事件
var menuItems = document.querySelectorAll('.menu-item');
for (var i = 0; i < menuItems.length; i++) {
  menuItems[i].addEventListener('click', function() {
    // 获取菜单项的锚点位置
    var anchor = this.getAttribute('href');

    // 滚动页面到锚点位置
    window.scrollTo({
      top: document.querySelector(anchor).offsetTop,
      behavior: 'smooth'
    });

    // 选中该菜单项
    this.classList.add('active');

    // 取消其他菜单项的选中状态
    var otherMenuItems = document.querySelectorAll('.menu-item');
    for (var j = 0; j < otherMenuItems.length; j++) {
      if (otherMenuItems[j] !== this) {
        otherMenuItems[j].classList.remove('active');
      }
    }
  });
}

基于状态管理

// 定义 Redux store
const store = createStore(reducer);

// 定义菜单项的选中状态
const initialState = {
  selectedMenuItem: null
};

// 定义 Redux reducer
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'SELECT_MENU_ITEM':
      return {
        ...state,
        selectedMenuItem: action.payload
      };
    default:
      return state;
  }
}

// 定义菜单项组件
const MenuItem = ({ menuItem }) => {
  const dispatch = useDispatch();

  const handleClick = () => {
    dispatch({
      type: 'SELECT_MENU_ITEM',
      payload: menuItem
    });

    window.scrollTo({
      top: document.querySelector(menuItem.anchor).offsetTop,
      behavior: 'smooth'
    });
  };

  return (
    <li className="menu-item" onClick={handleClick}>
      {menuItem.label}
    </li>
  );
};

// 定义菜单组件
const Menu = () => {
  const menuItems = [
    {
      label: 'Section 1',
      anchor: '#section-1'
    },
    {
      label: 'Section 2',
      anchor: '#section-2'
    },
    {
      label: 'Section 3',
      anchor: '#section-3'
    }
  ];

  return (
    <ul className="menu">
      {menuItems.map((menuItem, index) => (
        <MenuItem key={index} menuItem={menuItem} />
      ))}
    </ul>
  );
};

// 渲染应用程序
ReactDOM.render(
  <