幻灯片js入门:用原生 JavaScript 实现图片轮播

AI提示词4分钟前发布 jdkjadmin
0 00
幻灯片js入门:用原生 JavaScript 实现图片轮播

幻灯片js入门:用原生 JavaScript 实现图片轮播

图片轮播常见于商品详情、活动展示和内容推荐区域。它看起来只是“点一下切换一张图”,实际还涉及布局、状态管理、键盘操作、屏幕阅读器、自动播放和窄屏适配等问题。本文从一个不依赖第三方库的轮播组件开始,使用原生 HTML、CSS 和 JavaScript 完成切换,并说明哪些情况下适合采用轮播,以及容易忽略的边界。

一、什么情况下适合使用轮播

轮播适用于一组内容确实需要占据同一块有限空间,而且用户能够方便地查看前后内容的场景,例如产品的多张展示图、活动页的几条重点信息,或一组可手动浏览的案例。它也可以用来呈现图片以外的内容:轮播的核心是切换一组幻灯片,并不限定每张幻灯片必须是图片。

但轮播并不是节省空间的万能办法。用户可能看不到后续内容,自动切换还可能让阅读被打断。如果内容都很重要,且用户需要比较或快速浏览,网格、列表或分页往往更合适。设计前先确认:内容是否适合顺序浏览?用户能否发现还有其他页?不使用自动播放是否更清晰?

二、实现思路:状态、轨道和控制按钮

下面的示例使用一个横向排列的“轨道”承载多张幻灯片。JavaScript 保存当前索引,每次切换时更新轨道的水平位移,同时更新上一张、下一张按钮的状态,以及圆点导航的选中状态。示例用有颜色的内容块代替图片,便于直接复制运行,也避免将组件逻辑绑定到某一种内容上。

把以下代码放进一个 HTML 文件即可运行。示例没有自动播放:用户可以通过上、下一张按钮或圆点导航切换内容。样式和脚本均限定在组件的 .carousel 类下,便于嵌入已有页面。

1. HTML 结构

<section class="carousel" aria-label="精选内容轮播">
  <div class="carousel__viewport">
    <div class="carousel__track">
      <article class="carousel__slide">
        <div class="slide-card slide-card--one">
          <p>第 1 张 / 共 3 张</p>
          <h2>认识轮播组件</h2>
          <p>每张幻灯片可以承载不同类型的内容。</p>
        </div>
      </article>
      <article class="carousel__slide">
        <div class="slide-card slide-card--two">
          <p>第 2 张 / 共 3 张</p>
          <h2>用按钮切换</h2>
          <p>用户可以主动控制浏览顺序。</p>
        </div>
      </article>
      <article class="carousel__slide">
        <div class="slide-card slide-card--three">
          <p>第 3 张 / 共 3 张</p>
          <h2>按需扩展</h2>
          <p>可以替换内容,也可以加入自动播放。</p>
        </div>
      </article>
    </div>
  </div>

  <div class="carousel__controls">
    <button class="carousel__prev" type="button" aria-label="上一张">
      上一张
    </button>
    <div class="carousel__dots" aria-label="选择幻灯片"></div>
    <button class="carousel__next" type="button" aria-label="下一张">
      下一张
    </button>
  </div>
</section>

按钮显式使用 type="button",避免组件放进表单后意外触发表单提交。外层区域有可访问名称,控制按钮也有明确标签。若页面上有多个轮播,应为每个轮播提供不同的区域名称,并确保每个组件的按钮和圆点只作用于自身。

2. CSS 样式

.carousel {
  max-width: 720px;
  margin: 2rem auto;
  font-family: system-ui, sans-serif;
}

.carousel__viewport {
  overflow: hidden;
  border-radius: 12px;
}

.carousel__track {
  display: flex;
  transition: transform 300ms ease;
  will-change: transform;
}

.carousel__slide {
  flex: 0 0 100%;
  min-width: 0;
}

.slide-card {
  box-sizing: border-box;
  min-height: 260px;
  padding: 2rem;
  color: #fff;
}

.slide-card--one {
  background: #2457a7;
}

.slide-card--two {
  background: #7838a8;
}

.slide-card--three {
  background: #147560;
}

.carousel__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}

.carousel button {
  padding: 0.55rem 0.8rem;
  font: inherit;
  cursor: pointer;
}

.carousel__dots {
  display: flex;
  gap: 0.4rem;
}

.carousel__dot {
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #ddd;
}

.carousel__dot[aria-current="true"] {
  background: #2457a7;
}

.carousel button:focus-visible {
  outline: 3px solid #f0a000;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .carousel__track {
    transition: none;
  }
}

overflow: hidden 让视口只显示轨道的一部分;每张幻灯片占轨道宽度的百分之百;切换时再移动轨道。这里的百分比位移是相对于轨道自身的尺寸计算的,所以无论视口多宽,单次移动一张幻灯片都使用 100%。如果把幻灯片改成有间距的布局,不能直接假设每张仍正好占 100%,需要重新计算步长。

3. JavaScript 切换逻辑

document.querySelectorAll(".carousel").forEach((carousel) => {
  const track = carousel.querySelector(".carousel__track");
  const slides = Array.from(
    carousel.querySelectorAll(".carousel__slide")
  );
  const prevButton = carousel.querySelector(".carousel__prev");
  const nextButton = carousel.querySelector(".carousel__next");
  const dotsContainer = carousel.querySelector(".carousel__dots");

  if (!track || slides.length === 0 || !prevButton ||
      !nextButton || !dotsContainer) {
    return;
  }

  let currentIndex = 0;

  const dots = slides.map((slide, index) => {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "carousel__dot";
    button.setAttribute("aria-label", `显示第 ${index + 1} 张`);
    button.addEventListener("click", () => showSlide(index));
    dotsContainer.append(button);
    return button;
  });

  function showSlide(index) {
    currentIndex = (index + slides.length) % slides.length;

    track.style.transform =
      `translateX(-${currentIndex * 100}%)`;

    slides.forEach((slide, slideIndex) => {
      slide.setAttribute(
        "aria-hidden",
        String(slideIndex !== currentIndex)
      );
    });

    dots.forEach((dot, dotIndex) => {
      if (dotIndex === currentIndex) {
        dot.setAttribute("aria-current", "true");
      } else {
        dot.removeAttribute("aria-current");
      }
    });
  }

  prevButton.addEventListener("click", () => {
    showSlide(currentIndex - 1);
  });

  nextButton.addEventListener("click", () => {
    showSlide(currentIndex + 1);
  });

  showSlide(0);
});

showSlide() 是组件的状态更新入口。它将传入索引折回有效范围:比如只有三张时,从最后一张继续向后会回到第一张;从第一张向前则回到最后一张。这是循环轮播的常见行为。如果产品要求到达边界后停止,应改为限制索引,并在首尾禁用相应按钮,而不是使用取模。

使用 querySelectorAll(".carousel") 初始化每个组件,可以在同一页面上放置多个轮播,而不让一个轮播的按钮控制另一个。初始化时也检查必要节点和幻灯片数量:缺少标记时直接结束,避免继续访问不存在的元素。项目中还可以把这类结构错误记录到开发日志,帮助及时发现模板问题。

三、边界情况与无障碍注意事项

索引和空数据

示例在没有幻灯片时退出,不会对零取模;有一张幻灯片时,前后切换仍会停留在唯一一张上。真实项目应明确空数据时的展示方案,例如隐藏整个组件、显示空状态,或由服务端不输出组件标记。不要只假定数据永远非空。

示例使用了循环切换。若改成非循环模式,索引应限制在 0 到 slides.length - 1 之间,并同步设置按钮的 disabled。按钮禁用后会退出正常键盘操作序列,因此不要把它作为唯一的状态提示;必要时也应让界面清楚显示当前位置。

键盘与焦点

原生 button 默认支持键盘操作,用户可以用 Tab 聚焦控制按钮,再用 Enter 或空格激活。保持清晰的焦点样式,不要为了视觉效果移除轮廓后不提供替代样式。若进一步添加左右方向键控制,应只在焦点位于轮播区域内时响应,并避免干扰输入框、页面滚动和其他组件。

示例为非当前幻灯片设置了 aria-hidden="true",让辅助技术不把它当作当前可见内容。但这并不会自动阻止隐藏幻灯片里的链接或按钮获得键盘焦点。如果幻灯片包含可交互元素,应额外处理非当前页的焦点可达性:例如在支持的浏览器环境中使用 inert,或在隐藏时妥善管理其中控件的 Tab 顺序,并在显示时恢复。不要在用户焦点仍位于某张幻灯片内部时,直接把该内容从可访问状态中移除。

示例没有把整条轨道设成持续播报的实时区域。手动切换后,屏幕阅读器用户仍需要获得足够的当前页信息;实际项目可根据内容设计适当的状态提示,避免每次自动切换都造成连续播报。圆点的可访问名称还可进一步包含标题或总页数,使当前位置更容易理解。

图片尺寸、加载和响应式布局

替换成图片内容时,应给图片确定的宽高或宽高比,避免资源加载后组件高度突然变化;同时选择合适的裁切策略,让不同尺寸素材的显示结果符合设计预期。重要内容不要只依靠图片本身表达,图片的替代文本要与用途相符;纯装饰内容则不应重复朗读无意义描述。

视口通常会随容器变化。示例用百分比布局适配宽度,但幻灯片内部的文字、按钮和内容仍需检查小屏表现。若组件宽度可能被侧栏、弹窗或网格约束,建议在这些真实容器尺寸下测试,而不只看浏览器窗口全宽时的效果。

四、自动播放:需要明确的暂停规则

自动播放不应是默认必需品。它可能在用户阅读时切走内容,也会给需要更长阅读时间的用户造成困难。若产品确实需要自动切换,应提供清晰的暂停或停止控制,支持用户通过键盘操作;用户与轮播交互、鼠标悬停或焦点进入轮播时,应按设计暂停,并在页面进入后台时停止计时。还要遵循用户的减少动态效果偏好。

一个基础的定时器写法如下。它展示的是如何保存和清理计时器,不是完整的无障碍自动播放方案:

let timerId = null;

function startAutoPlay() {
  if (timerId !== null || slides.length < 2) return;

  timerId = window.setInterval(() => {
    showSlide(currentIndex + 1);
  }, 5000);
}

function stopAutoPlay() {
  if (timerId === null) return;

  window.clearInterval(timerId);
  timerId = null;
}

这里的 5000 表示间隔为五秒,并不代表适用于所有内容的推荐值。较短间隔可能来不及阅读,较长间隔也不能替代暂停控制。添加自动播放时,还要为组件卸载或页面局部更新清理定时器;否则旧计时器可能继续运行,造成重复切换或资源泄漏。可以通过 visibilitychange 在页面隐藏时暂停,并在合适时机恢复,但恢复前应检查用户是否已经主动暂停。

CSS 已用 prefers-reduced-motion 关闭位移动画。若 JavaScript 也负责自动播放,应读取同一偏好,并且不要仅仅取消动画、仍持续自动切换。具体行为应以用户可控制、状态可理解为目标。

五、常见错误

  • 把内容数量写死。新增或删除幻灯片后,圆点数量、索引边界和控制状态都应根据实际节点计算。示例从幻灯片节点生成圆点,避免数量不一致。
  • 只改变图片,不更新状态。圆点的选中状态、可访问名称、可见内容和轮播索引必须保持一致。应集中在一个切换函数中更新,减少遗漏。
  • 把百分比位移理解错。translateX(-100%) 相对于设置变换的元素自身尺寸,不一定是视口宽度。示例移动整条轨道,而轨道的宽度随幻灯片数量增长,因此每次移动一个视口宽度恰好对应一张幻灯片。若更换布局模型,要重新验证这项关系。
  • 在图片加载前后改变布局。如果没有预留尺寸,加载完成后页面可能跳动。给内容预留稳定空间,并对慢网速和加载失败做测试。
  • 只测试鼠标。还要检查键盘焦点、屏幕阅读器可见状态、触屏点击目标以及移动设备上的内容高度。
  • 忽略重复初始化。如果脚本可能被执行多次,事件监听器和自动播放定时器可能重复。应保证初始化只执行一次,或提供明确的销毁与重新初始化机制。

六、安全边界:数据如何进入页面

上面的示例只读取页面中的固定结构,并用 createElement() 创建按钮、用 textContent 或属性 API 写入文本和状态。轮播本身不需要执行字符串代码,也不需要用 innerHTML 拼接数据。若幻灯片内容来自用户输入或外部接口,应把它视为不可信数据:插入普通文本时使用 textContent;不要将未经处理的内容直接拼进 HTML 字符串,也不要把未经校验的值放进事件处理代码。

如果轮播需要依据数据创建链接,还要校验目标 URL 的协议和来源,避免把不可信的 javascript: 等地址当作可点击链接。HTML 转义只能解决特定上下文中的编码问题,不等于对所有 URL、脚本或样式输入都安全。图片资源地址也应遵守站点的内容策略和来源限制。组件中的 aria-label 与可见文本同样可能来自数据,需按其输出上下文安全处理。

七、上线前检查清单

  • 零张、一张、多张幻灯片时都能正确工作;切换边界符合产品要求。
  • 一页有多个轮播时,各自的控制器不会互相影响。
  • 触屏、键盘和鼠标都能操作,焦点清晰可见。
  • 非当前页中的链接或按钮不会意外进入 Tab 顺序。
  • 图片加载、加载失败、窄屏和长文本不会造成布局错乱。
  • 如启用自动播放,用户能暂停,后台页面会停止不必要的计时,减少动态效果偏好也得到尊重。
  • 动态内容不会通过不安全的 HTML 字符串直接插入页面。

原生 JavaScript 轮播的核心并不复杂:根据当前索引移动轨道,再让视觉状态、控件状态与辅助技术感知到的状态保持一致。真正需要花时间的是明确交互边界,处理内容变化和键盘可达性,并决定自动播放是否有必要。先把这些行为定义清楚,再扩展图片加载、手势或分页功能,组件才更容易维护。

© 版权声明

相关文章