React 新手引导库开箱评测

3,802 阅读7分钟

对于新上线的页面,往往会加上一个新手引导功能,告诉用户应该如何使用,如下图所示:

那如何实现这个新手引导的功能呢?我的需求只有两个:

  • 支持 React 技术栈
  • 能够自定义布局和样式

经过一番调研,发现一些 star 比较多的库,其中一类是通用性技术,但有各种框架(包括 React)的 wrapper,例如:

还有一种是纯 React 技术栈的解决方案,例如:

接下来分别对这些库进行踩坑,并在最后给出自己比较推荐的新手引导库。

intro.js-react

示例代码和 demo 效果可以参考:codesandbox.io/embed/o2A4g…

使用方法比较简单,只需要设置一个 steps 数组,明确每一步的引导分别挂在哪个元素上、气泡展现的方向和气泡内容即可:

const steps = [
  {
    element: '.contact-form',
    position: 'bottom',
    intro: (
      <>
        <div className="guide-cover">
          <img src="xxx.png" alt="引导图片" />
        </div>
        <div className="guide-title">引导标题</div>
        <div className="guide-content">引导内容</div>
      </>
    ),
  },
]

虽然引导内容可以自定义,但是引导的布局不好调整,官方只提供了几个可用的选项,例如:

const options = {
  showBullets: false,
  hidePrev: true,
  skipLabel: '跳过',
  prevLabel: '上一步',
  nextLabel: '下一步',
  doneLabel: '完成',
  showStepNumbers: true,
  stepNumbersOfLabel: '/',
}

这里面最令人无法接受的是不能自定义上一步和下一步按钮,只能改一些文案,如果想替换原始的 DOM,并绑定自己的事件时,似乎无能为力。无奈只能硬着头皮看了一下源码,都是原生 JS 写的,直接操作 DOM 创建各种元素:

所以确实不太好自定义布局,这是这个库不好的地方,但好的地方就是它没有任何其他依赖,一个很纯粹的包,集成进去的代码量不大。但是有个非常不好的地方在于它的 License 不是 MIT 的,而是 AGPLv3 协议,好家伙,这个协议的杀伤力非常大:

AGPL v3协议:除非获得商业授权,否则无论以何种方式修改或者使用代码,都需要开源。

如果你是商业产品,强烈建议不要用 intro.js,否则会坑了公司,综合推荐指数:⭐️⭐️

react-shepherd

这个库也是封装的 stepherd ,我看了一会就弃坑了,因为配置非常复杂(不知道你看了下面的部分配置项之后何种感受,我反正晕了):

const steps = [
  {
    id: 'intro',
    attachTo: { element: '.first-element', on: 'bottom' },
    beforeShowPromise: function () {
      return new Promise(function (resolve) {
        setTimeout(function () {
          window.scrollTo(0, 0);
          resolve();
        }, 500);
      });
    },
    buttons: [
      {
        classes: 'shepherd-button-secondary',
        text: 'Exit',
        type: 'cancel'
      },
      {
        classes: 'shepherd-button-primary',
        text: 'Back',
        type: 'back'
      },
      {
        classes: 'shepherd-button-primary',
        text: 'Next',
        type: 'next'
      }
    ],
    classes: 'custom-class-name-1 custom-class-name-2',
    highlightClass: 'highlight',
    scrollTo: false,
    cancelIcon: {
      enabled: true,
    },
    title: 'Welcome to React-Shepherd!',
    text: ['React-Shepherd is a JavaScript library for guiding users through your React app.'],
    when: {
      show: () => {
        console.log('show step');
      },
      hide: () => {
        console.log('hide step');
      }
    }
  },
  // ...
];

配置复杂一点我也就忍了,但是跑起来的效果那叫一个丑啊:

老土的标题区,只能设置字符串数组的内容区,还有那 bootstrap 风格的巨大按钮区,让人怎么看都不舒服。鉴于配置复杂时间宝贵,建议大家不要选择这个库,尽早弃坑,综合推荐指数:⭐️

reactour

这个库是专门为 React 框架设计的,定制化的能力比较强,可以参考 demosandbox文档。基础使用方法和 intro.js 比较类似,也是先定义 steps:

const steps = [
  {
    selector: '.first',
    position: 'bottom' as any,
    content: "字符串或 jsx 都行"
  }
]

使用的时候,需要套一个 TourProvider 在外面:

export default function ({ children }) {
  return <TourProvider {...props}>{children}</TourProvider>
}

属性也挺多的,需要耐心看一下文档,例如:

const props = {
  steps,
  defaultOpen: true,
  showBadge: false,
  showNavigation: true,
  showDots: false,
  className: 'user-guide',
  styles: {
    maskArea: (base) => ({ ...base, rx: 10 }),
  },
  padding: { mask: 0 },
  onClickMask: () => {},
  ContentComponent,
}

有一点需要注意,它的默认遮罩的高亮区域是用 svg 做的,不能用 borderRadius 控制圆角,要用 rx 来控制才行:

配置项中的 ContentComponent 用于自定义气泡内容,是个函数组件,非常灵活,可以重写全部的布局,例如:

function ContentComponent(props) {
  const { currentStep, steps, setIsOpen, setCurrentStep } = props
  return (
    <div className="user-guide-content">
      // 这里可以让用户自定义气泡布局
    </div>
  )
}

这是比上面两个库好很多的地方:UI 的自定义能力!不过比较坑的一点是:引导气泡默认没有箭头效果,需要自己写,好在写个箭头并不复杂,简单的代码如下:

.user-guide-content {
  padding: 15px;
  min-width: 300px;
  
  &::after {
    content: '';
    width: 0;
    height: 0;
    position: absolute;
  }
  &.bottom-arrow::after {
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
    border-bottom: 12px solid white;
    left: 22px;
    top: -10px;
  }
  &.right-arrow::after {
    border-top: 8px solid transparent;
    border-bottom: 8px solid transparent;
    border-right: 12px solid white;
    top: 22px;
    left: -10px;
  }
  &.left-arrow::after {
    border-top: 8px solid transparent;
    border-bottom: 8px solid transparent;
    border-left: 12px solid white;
    top: 22px;
    right: -10px;
  }
  &.top-arrow::after {
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
    border-top: 12px solid white;
    left: 22px;
    bottom: -10px;
  }
}

不过上面的简单效果不适用于页面缩放的自适应场景,在 steps 里面会设置气泡默认的方向,但是当缩放的时候,由于空间不足的原因会改变气泡的相对位置,这个时候箭头的位置也要做相应的调整才行,否则会出现下面的效果:

其实解决起来也很简单,可以参考我给作者提的这个 issue ,利用 css variable 来动态设置宿主的样式,然后在 ::after伪类里面引用变量。总而言之,这个库还是非常不错的,持续维护,作者响应 issue 很积极,综合推荐指数:⭐️⭐️⭐️⭐️,最后附上完整的示例代码:

import './Reactour.scss'
import { TourProvider } from '@reactour/tour'
import Layout from './Layout'

const steps = [
  {
    selector: '.contact-form',
    position: 'right' as any,
    content: (
      <>
        <div className="guide-cover">
          <img src="/write.jpeg" />
        </div>
        <div className="guide-title">简约而不失优雅的表单</div>
        <div className="guide-content">请在左侧表单区域填写您的姓名、邮箱、标题和内容,输入完成后点击提交按钮即可。</div>
      </>
    ),
  },
  {
    selector: '.map',
    position: 'left',
    content: (
      <>
        <div className="guide-title">谷歌地图快速定位</div>
        <div className="guide-content">双击鼠标可快速放大地图,您可以通过拖动地图来选择您的位置。</div>
      </>
    ),
  },
  {
    selector: '.street',
    position: 'left',
    content: (
      <>
        <div className="guide-title">联系我们</div>
        <div className="guide-content">详细的街道、邮箱和电话</div>
      </>
    ),
  },
]

const stepArrowDirections = ['right', 'left', 'left']

function ContentComponent(props) {
  const { currentStep, steps, setIsOpen, setCurrentStep } = props
  const isLastStep = currentStep === steps.length - 1
  const content = steps[currentStep].content
  const close = () => {
    setIsOpen(false)
    localStorage.setItem('skip_user_guide', 'true')
  }
  return (
    <div className={`user-guide-content ${stepArrowDirections[currentStep]}-arrow`}>
      {typeof content === 'function' ? content({ ...props }) : content}
      <div className="guide-navigation">
        <div className="guide-prev" onClick={() => close()}>
          {`${'跳过'} (${currentStep + 1}/${steps.length})`}
        </div>
        <div
          className="guide-next"
          onClick={() => {
            if (isLastStep) {
              close()
            } else {
              setCurrentStep((s) => s + 1)
            }
          }}
        >
          {isLastStep ? '完成' : '下一步'}
        </div>
      </div>
    </div>
  )
}

const root = document.getElementById('root')

const props = {
  steps,
  defaultOpen: !localStorage.getItem('skip_user_guide'),
  afterOpen: () => (root!.style.overflow = 'hidden'),
  beforeClose: () => (root!.style.overflow = 'auto'),
  showBadge: false,
  showNavigation: true,
  showDots: false,
  ContentComponent,
  className: 'user-guide',
  styles: {
    maskArea: (base) => ({ ...base, rx: 5 }),
  },
  onClickMask: () => {},
}

export default function () {
  return (
    <TourProvider {...props}>
      <Layout />
    </TourProvider>
  )
}

react-joyride

这个库也有非常强的 UI 定制能力,配置项也不复杂,跟 reactour 的思想非常接近,基本上可以满足所有项目的需求了,而且默认自带气泡箭头,如下图所示:

综合推荐综合指数:⭐️⭐️⭐️⭐️⭐️,话不多说,直接附上完整示例代码:

import './Joyride.scss'
import Joyride from 'react-joyride'
import Layout from './Layout'

const locale = {
  back: '上一步',
  next: '下一步',
  skip: '跳过',
  close: '下一步',
  last: '完成',
}
const steps = [
  {
    target: '.contact-form',
    placement: 'right' as any,
    disableBeacon: true,
    hideCloseButton: true,
    locale,
    spotlightClicks: true,
    content: (
      <>
        <div className="guide-cover">
          <img src="/write.jpeg" />
        </div>
        <div className="guide-title">简约而不失优雅的表单</div>
        <div className="guide-content">请在左侧表单区域填写您的姓名、邮箱、标题和内容,输入完成后点击提交按钮即可。</div>
      </>
    ),
  },
  {
    target: '.map',
    placement: 'left' as any,
    disableBeacon: true,
    hideCloseButton: true,
    locale,
    content: (
      <>
        <div className="guide-title">谷歌地图快速定位</div>
        <div className="guide-content">双击鼠标可快速放大地图,您可以通过拖动地图来选择您的位置。</div>
      </>
    ),
  },
  {
    target: '.street',
    placement: 'top' as any,
    disableBeacon: true,
    hideCloseButton: true,
    locale,
    content: (
      <>
        <div className="guide-title">联系我们</div>
        <div className="guide-content">详细的街道、邮箱和电话</div>
      </>
    ),
  },
]
const Tooltip = ({ continuous, index, step, backProps, closeProps, primaryProps, tooltipProps }) => (
  <div className="tooltip-body" {...tooltipProps}>
    {step.title && <div className="tooltip-title">{step.title}</div>}
    <div className="tooltip-content">{step.content}</div>
    <div className="tooltip-footer guide-navigation">
      <div className="guide-prev" {...backProps}>
        {index > 0 ? '上一步' : ''}
      </div>
      {index + 1 === steps.length ? (
        <div className="guide-next" {...closeProps}>完成</div>
      ) : (
        <div className="guide-next" {...primaryProps}>
          下一步({`${index + 1}/${steps.length}`})
        </div>
      )}
    </div>
  </div>
)

export default function () {
  return (
    <div className="app">
      <Joyride showProgress continuous={true} tooltipComponent={Tooltip} steps={steps} />
      <Layout />
    </div>
  )
}

结论

React 新手引导库开箱评测的结果如下(仅代表个人意见,仅供参考,如有冒犯,还望海涵):

框架名称License复杂性扩展性推荐指数
intro.js 的封装 ( intro.js-react )原库:AGPLv3,封装库:MIT简单不易扩展⭐️⭐️
stepherd 的封装 ( react-shepherd )原库:MIT,封装库:MIT复杂不易扩展⭐️
reactourMIT中等易扩展⭐️⭐️⭐️⭐️
react-joyrideMIT简单易扩展⭐️⭐️⭐️⭐️⭐