手把手教你实现高效可复用的前端引导说明组件

FSD-峻成 交互 阅读 1,694
赞 29 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周搞一个新功能,产品经理说“用户第一次进页面,得知道怎么操作”,于是让我加个引导说明。我一开始想用 tooltip + 遮罩层那种经典方案,但发现要适配移动端、还得支持跳过、顺序控制、元素定位动态变化……折腾了两天,最后用了一个轻量又灵活的方案,亲测有效。

手把手教你实现高效可复用的前端引导说明组件

核心思路很简单:用一个半透明遮罩盖住整个页面,然后在目标元素周围高亮一块区域,同时显示说明文字。关键在于“高亮区域”要能自动跟随目标元素的位置,哪怕它在滚动、动画、或者被其他 JS 操作了位置。

下面是我最终用的代码,直接贴出来,你复制过去就能跑:

<div id="guide-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 9999; pointer-events: none;"></div>
<div id="guide-tooltip" style="display: none; position: absolute; background: white; padding: 12px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 10000; max-width: 280px; pointer-events: auto;">
  <div id="guide-content"></div>
  <button id="next-btn" style="margin-top: 8px; padding: 4px 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">下一步</button>
  <button id="skip-btn" style="margin-top: 8px; margin-left: 8px; padding: 4px 12px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">跳过</button>
</div>
class GuideTour {
  constructor(steps) {
    this.steps = steps;
    this.currentIndex = 0;
    this.overlay = document.getElementById('guide-overlay');
    this.tooltip = document.getElementById('guide-tooltip');
    this.content = document.getElementById('guide-content');
    this.nextBtn = document.getElementById('next-btn');
    this.skipBtn = document.getElementById('skip-btn');

    this.initEvents();
  }

  initEvents() {
    this.nextBtn.addEventListener('click', () => this.next());
    this.skipBtn.addEventListener('click', () => this.end());
  }

  start() {
    this.showStep(this.currentIndex);
  }

  showStep(index) {
    if (index >= this.steps.length) {
      this.end();
      return;
    }

    const step = this.steps[index];
    const target = document.querySelector(step.selector);
    if (!target) {
      console.warn(Guide target not found: ${step.selector});
      this.next(); // 跳过不存在的元素
      return;
    }

    // 获取目标元素位置(考虑滚动)
    const rect = target.getBoundingClientRect();
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;

    // 高亮区域:用 clip-path 在遮罩上挖个洞
    this.overlay.style.display = 'block';
    this.overlay.style.clipPath = polygon(0 0, 100% 0, 100% 100%, 0 100%,
      0 ${rect.top + scrollTop}px,
      ${rect.left + scrollLeft - 10}px ${rect.top + scrollTop}px,
      ${rect.left + scrollLeft - 10}px ${rect.bottom + scrollTop + 10}px,
      ${rect.right + scrollLeft + 10}px ${rect.bottom + scrollTop + 10}px,
      ${rect.right + scrollLeft + 10}px ${rect.top + scrollTop}px,
      ${window.innerWidth}px ${rect.top + scrollTop}px,
      ${window.innerWidth}px ${window.innerHeight}px,
      0 ${window.innerHeight}px);

    // tooltip 位置:放在目标下方,避免遮挡
    const tooltipTop = rect.bottom + scrollTop + 10;
    const tooltipLeft = rect.left + scrollLeft;

    this.content.innerText = step.text;
    this.tooltip.style.display = 'block';
    this.tooltip.style.top = ${tooltipTop}px;
    this.tooltip.style.left = ${Math.max(10, Math.min(tooltipLeft, window.innerWidth - 300))}px;
  }

  next() {
    this.currentIndex++;
    this.showStep(this.currentIndex);
  }

  end() {
    this.overlay.style.display = 'none';
    this.tooltip.style.display = 'none';
    this.currentIndex = 0;
  }
}

// 使用示例
const guide = new GuideTour([
  { selector: '#btn-new', text: '点击这里创建新项目' },
  { selector: '.sidebar-menu', text: '这里是导航菜单,可以切换模块' },
  { selector: '#user-avatar', text: '点头像可以进入个人设置' }
]);

// 在合适时机触发
// guide.start();

踩坑提醒:这三点一定注意

这个方案看起来简单,但我踩了至少三个坑,差点被自己写的代码气死。

  • 滚动问题:最开始我没加 scrollTopscrollLeft,结果页面一滚动,高亮区域就错位了。后来才发现 getBoundingClientRect() 返回的是视口坐标,必须加上滚动偏移才是真实位置。这点一定要注意,尤其是在 SPA 应用里,滚动容器可能不是 window,而是某个 div,那你就得改逻辑了。
  • 元素动态渲染:如果目标元素是异步加载的(比如 Vue/React 组件还没挂载),querySelector 会返回 null。我的做法是加个兜底:如果找不到元素,直接跳过这一步。更健壮的做法是加个 MutationObserver 监听 DOM 变化,但我觉得太重了,简单场景跳过就行。
  • z-index 冲突:有些 UI 框架(比如某些后台模板)喜欢把 modal 的 z-index 设成 999999。你的遮罩层如果只有 9999,根本盖不住。建议直接设成 9999999,虽然有点暴力,但省事。实在不行,就让用户配置 z-index 参数。

这个场景最好用

我试过几种引导方案,这个“遮罩+高亮”最适合功能首次使用的场景。比如用户刚注册完,系统要教他怎么发第一条消息、怎么上传头像。

但如果你要做的是“操作提示”(比如表单必填项标红),那就别用这个了,太重。直接用 tooltip 或 inline 提示更合适。

另外,移动端体验要特别注意:按钮别太小,文字别太多。我见过有人在手机上放三行引导文案,用户根本看不完就关了。建议文案控制在 15 个字以内,按钮至少 44px × 44px。

高级技巧:动态调整高亮形状

上面的 clipPath 是固定矩形,但有时候目标元素是圆形(比如头像),或者不规则形状。这时候你可以动态计算路径。

比如圆形头像,可以这样改:

// 假设 target 是圆形,直径 40px
const centerX = rect.left + rect.width / 2 + scrollLeft;
const centerY = rect.top + rect.height / 2 + scrollTop;
const radius = 25; // 稍微大一点

// 生成圆形 clip-path(用多边形近似)
const points = [];
for (let i = 0; i < 36; i++) {
  const angle = (i * 10) * Math.PI / 180;
  const x = centerX + radius * Math.cos(angle);
  const y = centerY + radius * Math.sin(angle);
  points.push(${x}px ${y}px);
}

this.overlay.style.clipPath = polygon(0 0, 100% 0, 100% 100%, 0 100%, ${points.join(&#039;, &#039;)});

不过说实话,这种场景不多,大多数时候矩形就够了。别为了炫技搞复杂,维护成本太高。

结尾碎碎念

这个引导组件我封装后用了好几个项目,改动不大,基本 copy-paste 就能用。虽然不是最优雅的(比如没用 Web Components),但胜在简单、可控、没依赖。

其实还有更高级的玩法,比如结合 Lottie 做动画指引、用 IntersectionObserver 自动触发、或者记录用户是否完成过引导。这些我后续会单独写一篇,毕竟一篇文章塞太多容易乱。

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如你怎么处理动态内容的?或者有没有轻量级的第三方库推荐?

这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论