☰
React Joyride v3 引导式导览完整实战指南:useJoyride Hook、Step 配置、事件系统与自定义组件
2026/9/27 9:04:42 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】react-joyride

Create guided tours in your apps

项目地址:https://gitcode.com/gh_mirrors/re/react-joyride
点击查看免费下载

本文是 React Joyride v3(当前仓库 react-joyride 的核心能力)的完整技术指南,覆盖useJoyride()Hook 与<Joyride>组件两套公开 API、Step 与 Options 的完整配置项、Tour Status 与 Step Lifecycle 双状态机、事件系统、程序化 Controls、三种样式定制层级,以及常见问题排障方案。读完本文,你将能在自己的 React 应用中独立实现、配置、调试一条从启动到结束的引导式导览(onboarding tour / product tour / walkthrough)。

说明:本仓库中skills/react-joyride/SKILL.md及其references/目录(api-props-options.md、api-step-state-controls.md、api-events-components.md、patterns.md)是官方技能文档的主体,本文以它为骨架,并用 src 下的源码实现做印证与扩充。

快速开始:两套公开 API

React Joyride v3 只导出命名导出(没有 default export),提供两个入口:

  • useJoyride()Hook —— 官方推荐;
  • <Joyride>组件 —— 声明式用法,且内置 SSR 保护。

方式一:使用useJoyride()Hook(推荐)

import { useJoyride, STATUS, Status } from 'react-joyride'; function App() { const { Tour } = useJoyride({ continuous: true, run: true, steps: [ { target: '.my-element', content: 'This is the first step', title: 'Welcome' }, { target: '#sidebar', content: 'Navigate here', placement: 'right' }, ], onEvent: (data) => { if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) { // Tour ended } }, }); return <div>{Tour}{/* rest of app */}</div>; }

方式二:使用<Joyride>组件

import { Joyride, STATUS, Status } from 'react-joyride'; function App() { return ( <Joyride continuous run={true} steps={[ { target: '.my-element', content: 'First step' }, { target: '#sidebar', content: 'Second step' }, ]} onEvent={(data) => { if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) { // Tour ended } }} /> ); }

Hook 的返回值结构为{ controls, failures, on, state, step, Tour },其中Tour是一个 ReactElement,必须在你的 JSX 中渲染出来,导览才会出现。从源码看,<Joyride>组件本身只是useJoyride(props)的一层薄封装(见 src/index.tsx):它先用canUseDOM()判断是否处于浏览器环境,SSR 或预渲染阶段直接返回null,因此服务端渲染场景下组件更安全。

核心概念:双状态机

一次导览由两个正交的状态维度驱动,理解它们是排查所有问题的基础。

Tour Status:整场导览的宏观状态

idle -> ready -> waiting -> running <-> paused -> finished | skipped
状态含义
idle没有加载任何步骤(steps 为空)
ready步骤已加载,等待run: true
waitingrun=true但步骤还在异步加载中(步骤到达后自动转入running)
running导览正在运行
paused导览暂停(受控模式下停在 COMPLETE,或调用stop()后)
finished/skipped导览结束(完成或跳过)

实现印证:在 src/modules/store.ts 的Store构造函数中,初始状态即根据steps.length决定STATUS.READY或STATUS.IDLE;applyTransitions(src/modules/store.ts)实现了waiting -> running的自动迁移——只要waiting状态下 size 大于 0,就自动置为RUNNING。

Step Lifecycle:单个步骤的微观阶段

init -> ready -> beacon_before -> beacon -> tooltip_before -> tooltip -> complete
  • *_before阶段:滚动与定位在此发生;
  • beacon:显示脉冲指示点(当continuous导航中、设置了skipBeacon、或placement: 'center'时跳过);
  • tooltip:气泡提示可见且可交互。

Step 配置

每个 Step 必填target和content,其余字段均可选:

{ target: '.my-element', // CSS selector, HTMLElement, React ref, or () => HTMLElement content: 'Step body text', // ReactNode title: 'Optional title', // ReactNode placement: 'bottom', // Default. Also: top, left, right, *-start, *-end, auto, center id: 'unique-id', // Optional identifier data: { custom: 'data' }, // Attached to event callbacks }

Target 的四种写法

// CSS selector { target: '.sidebar-nav' } // HTMLElement { target: document.getElementById('my-el') } // React ref const ref = useRef(null); { target: ref } // Function(每次生命周期重新求值) { target: () => document.querySelector('.dynamic-element') }

从类型定义看,StepTarget就是这四种联合(见 api-step-state-controls.md),函数形式适合动态出现的元素,因为它在每次生命周期阶段都会被重新求值。

常用 Step 选项(可逐步骤覆盖)

Option默认值说明
placement'bottom'气泡位置。'center'为模态风格(需配合target: 'body')
skipBeaconfalse跳过 beacon,直接显示气泡
buttons['back','close','primary']气泡中的按钮。加'skip'可显示跳过按钮
hideOverlayfalse不显示深色遮罩
blockTargetInteractionfalse阻止高亮元素的点击交互
before-(data) => Promise<void>,步骤显示前的异步钩子
after-(data) => void,步骤完成后的即弃钩子
skipScrollfalse不滚动到目标
scrollTarget-滚动到该元素而非target
spotlightTarget-高亮该元素而非target
spotlightPadding10聚光灯周围内边距。数字或{ top, right, bottom, left }
targetWaitTimeout1000等待目标出现的毫秒数。0= 不等待
beforeTimeout5000等待before钩子的毫秒数。0= 无超时

所有 Options 字段既可通过全局optionsprop 设置,也可在单个 step 内覆盖,step 级覆盖全局。该合并逻辑在 src/modules/step.ts 的getMergedStep中实现:defaultOptions(见 src/defaults.ts)→ 全局props.options→ 当前 step 自身字段,逐层 deepMerge,最终产出StepMerged(所有 Options 字段均已填充默认值的归一化步骤,事件回调和自定义组件 props 里拿到的就是它)。

非受控 vs 受控

非受控模式(默认,强烈推荐)

导览内部自主管理步骤导航,适合绝大多数场景。库会替你处理异步过渡:

  • 某个步骤需要等待 UI 变化(下拉展开、数据加载、动画完成)时,用before钩子——导览会等 Promise resolve 后再显示该步骤;
  • 目标元素尚未挂载到 DOM 时,targetWaitTimeout(默认 1000ms)会轮询等待它出现;
  • 这两种情况都不需要受控模式。
const { Tour } = useJoyride({ continuous: true, run: isRunning, steps: [ { target: '.nav', content: 'Navigation' }, { target: '.dropdown-item', content: 'Inside the dropdown', before: () => { // 打开下拉并等待动画 —— 导览会自动等待 openDropdown(); return new Promise(resolve => setTimeout(resolve, 300)); }, after: () => closeDropdown(), // 步骤结束后清理(fire-and-forget) }, { target: '.main-content', content: 'Main content' }, ], onEvent: (data) => { if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) { setIsRunning(false); } }, });

受控模式(配合stepIndex)—— 谨慎使用

仅当父组件确实需要在外部管理步骤索引时才使用(例如与 URL 参数同步、外部状态机、或before/after钩子搞不定的复杂多组件协调)。

const [stepIndex, setStepIndex] = useState(0); const [run, setRun] = useState(true); const { Tour } = useJoyride({ continuous: true, run, stepIndex, // 传入 stepIndex 即进入受控模式 steps, onEvent: (data) => { const { action, index, status, type } = data; if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(status)) { setRun(false); return; } if (type === 'step:after' || type === 'error:target_not_found') { setStepIndex(index + (action === 'prev' ? -1 : 1)); } }, });

受控模式规则:

  • go()和reset()被禁用(会打印警告日志);
  • 你必须在事件回调里更新stepIndex;
  • 导览在 COMPLETE 处暂停,必须由你推进;
  • 除非有强理由需要外部索引管理,否则优先用非受控模式 +before/after钩子。

实现印证:Store构造时通过is.number(stepIndex)判断是否受控(src/modules/store.ts),受控模式下initialStepIndex会被忽略并打印提示;useControls中go/reset都会先检查controlled,若为受控模式则仅打日志并直接返回(src/hooks/useControls.ts)。受控模式更新索引时还会经过updateState的resolvedIndex逻辑——受控且未强制索引时,外部传入的index变更不会覆盖内部状态(src/modules/store.ts)。

事件系统

onEvent回调

onEvent: (data: EventData, controls: Controls) => void

data对象包含完整导览状态外加事件专属字段;controls对象让你能程序化控制导览。EventData的完整结构(含type判别字段、error、scroll、waiting等)见 api-events-components.md,其基础载荷TourData同时也会传给before/after钩子,包含action、index、lifecycle、origin、size、status、step、controlled等字段。

每个步骤的事件顺序

事件触发时机
tour:start导览开始
step:before_hookbefore钩子被调用
step:before找到目标,即将渲染步骤
scroll:start开始滚动到目标
scroll:end滚动完成
beaconbeacon 显示
tooltip气泡显示
step:after用户导航(next/prev/close/skip)
step:after_hookafter钩子被调用
tour:end导览完成或跳过
tour:status状态变更(stop/reset 时)
error:target_not_found未找到目标元素
error通用错误

对应的事件类型常量EVENTS在 src/literals/index.ts 中定义,与文档完全一致。事件订阅在Store里通过Map<string, Set<EventHandler>>管理,同一事件可挂多个订阅者,且单个订阅者抛错不会影响其他订阅者或导览本身(fire-and-forget,见 src/modules/store.ts)。

用on()订阅特定事件

const { on, Tour } = useJoyride({ ... }); useEffect(() => { const unsubscribe = on('tooltip', (data, controls) => { analytics.track('tour_step_viewed', { step: data.index }); }); return unsubscribe; }, [on]);

on()返回取消订阅函数,适合在 useEffect 中做埋点、日志等横切关注点,而无需在onEvent里写一大串 switch。

Controls:程序化控制

useJoyride()返回值或onEvent第二参数均可拿到Controls(完整类型见 api-step-state-controls.md):

方法说明
next()前进到下一步
prev()回到上一步
close(origin?)关闭当前步骤并前进
skip(origin?)整体跳过导览
start(index?)启动导览(可指定起始索引)
stop(advance?)停止(暂停)导览
go(index)跳转到指定步骤(仅非受控)
reset(restart?)重置导览(仅非受控;restart=true时同时重新启动)
open()打开当前步骤的气泡(跳过 beacon)
info()获取当前状态快照

实现层面,useControls(src/hooks/useControls.ts)每次方法调用都是对store.updateState(...)的一次补丁式状态提交:例如next()用getUpdatedIndex(index + 1, size)做边界夹取(Math.min(Math.max(nextIndex, 0), size)),并把lifecycle置为COMPLETE、重置positioned/scrolling/waiting;stop(advance)若advance=true会先前进一个索引再置PAUSED;skip()直接把状态置为STATUS.SKIPPED。所有方法在非RUNNING状态下调用都会静默返回,避免非法操作。

样式与主题:三层定制

从简单到完全控制,共三层:

第一层:颜色选项(最简单)

options: { primaryColor: '#1976d2', // 按钮和 beacon backgroundColor: '#1a1a2e', // 气泡背景 textColor: '#ffffff', // 气泡文字 overlayColor: 'rgba(0,0,0,0.7)', // 遮罩背景 arrowColor: '#1a1a2e', // 箭头(与背景一致) }

第二层:styles 覆盖

styles: { tooltip: { borderRadius: 12 }, buttonPrimary: { backgroundColor: '#1976d2' }, buttonBack: { color: '#666' }, spotlight: { borderRadius: 8 }, }

完整的样式键共 19 个:arrow、beacon、beaconInner、beaconOuter、beaconWrapper、buttonBack、buttonClose、buttonPrimary、buttonSkip、floater、loader、overlay、tooltip、tooltipContainer、tooltipContent、tooltipFooter、tooltipFooterSpacer、tooltipTitle(类型定义见 api-props-options.md)。Styles接受PartialDeep<Styles>,只需覆盖需要的键。

第三层:自定义组件(完全控制)

见下一节。

自定义组件

任何 UI 部件都能通过 props 替换,每个组件都接收带有步骤数据与按钮处理函数的 render props。

自定义 Tooltip

import type { TooltipRenderProps } from 'react-joyride'; function MyTooltip({ backProps, index, primaryProps, size, skipProps, step, tooltipProps }: TooltipRenderProps) { return ( <div {...tooltipProps} style={{ background: '#fff', padding: 16, borderRadius: 8, width: step.width }}> {step.title && <h3>{step.title}</h3>} <div>{step.content}</div> <div> {index > 0 && <button {...backProps}>Back</button>} <button {...primaryProps}>Next</button> </div> </div> ); } // 用法 <Joyride tooltipComponent={MyTooltip} ... />

重要:必须在容器上展开tooltipProps(它会设置role="dialog"与aria-modal);按钮 props(backProps、primaryProps、closeProps、skipProps)必须展开在对应按钮上,动作处理才会正确。

自定义 Beacon

必须渲染一个<span>(因为它被放进<button>包裹层里)。接收BeaconRenderProps:{ continuous, index, isLastStep, size, step }。

自定义 Arrow

接收ArrowRenderProps:{ base, placement, size }。

自定义 Loader

接收LoaderRenderProps:{ step }。设置为null可完全禁用 loader。

完整自定义 Tooltip 模式

配合step.buttons和isLastStep可以做更完整的产品级气泡(完整示例见 patterns.md):

function CustomTooltip({ backProps, closeProps, index, isLastStep, primaryProps, size, skipProps, step, tooltipProps }: TooltipRenderProps) { return ( <div {...tooltipProps} style={{ background: '#fff', borderRadius: 8, maxWidth: 400, padding: 20, width: step.width }}> {step.title && <h3 style={{ margin: '0 0 8px' }}>{step.title}</h3>} <div>{step.content}</div> <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}> {step.buttons.includes('skip') && !isLastStep && ( <button {...skipProps} type="button">Skip</button> )} <div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}> {index > 0 && <button {...backProps} type="button">Back</button>} <button {...primaryProps} type="button"> {isLastStep ? 'Done' : `Next (${index + 1}/${size})`} </button> </div> </div> </div> ); }

问题 → 解决方案速查

我需要……用什么
在步骤间等待异步 UI 变化(下拉、动画、数据加载)before钩子返回 Promise —— 而不是受控模式
在外部控制步骤导航(URL 同步、外部状态机)受控模式 +stepIndex—— 但先试试before/after钩子
追踪哪些步骤失败了(目标缺失、钩子报错)useJoyride()返回的failures数组
监听特定事件而不写onEvent大 switchon('event:type', handler)
展示居中的模态风格步骤target: 'body'+placement: 'center'

failures数组的元素是{ reason: 'before_hook' | 'target_not_found', step: StepMerged },并在start()/reset()时清空(src/hooks/useTourEngine.ts 中addFailure/clearFailures的实现印证)。

常见坑与调试

第一步:开启debug: true

debugprop 是最强大的排障工具,它会把完整生命周期流转、状态变更、事件发射打到控制台,精确告诉你导览卡在哪个阶段、触发了什么 action。

<Joyride debug={true} ... /> // 或 useJoyride({ debug: true, ... })

控制台输出会显示导览到达了哪个生命周期阶段、哪些 action 正在触发、以及卡在哪里。

导览不启动

  • 确认设置了run={true};
  • 确认steps数组非空,且每个 step 都有合法的target和content;
  • SSR 场景:用<Joyride>组件(自动保护 DOM 访问),或检查typeof window !== 'undefined'。

实现印证:导览启动时validateSteps会逐条校验 step 是否为对象且存在target,不合法时打印target is missing from the step之类的警告并拒绝启动(src/modules/step.ts)。

找不到目标

  • 在控制台测试选择器:document.querySelector('.your-selector');
  • 元素必须可见(不能是display: none、visibility: hidden或零尺寸);
  • 元素晚挂载时,调大targetWaitTimeout(默认 1000ms);
  • 设targetWaitTimeout: 0完全跳过等待;
  • 非受控模式下目标缺失会自动前进;受控模式下需处理error:target_not_found事件。

气泡不出现 / 遮罩闪烁

  • 加debug: true看控制台到达了哪个生命周期阶段;
  • 确认目标元素在视口内或可滚动到;
  • 检查祖先元素是否有overflow: hidden裁切了目标;
  • 使用 portal 或 modal 时,目标可能不可达(可用portalElementprop 指定渲染容器)。

受控模式卡住

  • 先问自己:真的需要受控模式吗?多数异步需求用非受控模式 +before/after钩子就能解决;
  • 若必须受控:type === 'step:after'时必须在onEvent中更新stepIndex;
  • 同时处理前进(action !== 'prev')和后退(action === 'prev')两种方向;
  • 也要处理error:target_not_found以跳过坏步骤;
  • 受控模式下go()和reset()不可用。

before 钩子超时

  • 默认beforeTimeout为 5000ms,异步操作更久就调大它;
  • 设beforeTimeout: 0表示无超时;
  • 等待期间,超过loaderDelay(300ms)后会出现 loader。

滚动问题

  • 用scrollTarget滚动到不同于气泡目标的元素;
  • 调整scrollOffset(默认 20px)应对固定头部或固定元素;
  • 单步设skipScroll: true禁用自动滚动;
  • scrollToFirstStep默认false,第一步在屏幕外时设为true。

center 居中放置

  • 用placement: 'center'+target: 'body'实现模态风格居中气泡;
  • center 放置会自动隐藏 beacon 和箭头。

导入规范

// v3 只有命名导出(没有 default export) import { Joyride, useJoyride } from 'react-joyride'; // 类型安全比较用的常量 import { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from 'react-joyride'; // 类型 import type { Step, Props, EventData, Controls, TooltipRenderProps } from 'react-joyride';

注意:ORIGIN常量包含button_back、button_close、button_primary、button_skip、keyboard、overlay六个取值(src/literals/index.ts),可用于事件追踪中判断用户通过哪个入口触发的动作。

常用实战模式

异步数据加载后再展示步骤

{ target: '.user-profile', content: 'Here is your profile data', before: async () => { await fetchUserProfile(); // 导览会等这个 Promise resolve }, beforeTimeout: 10000, // 允许最长 10s }

步骤完成后的埋点

{ target: '.feature', content: 'Check out this feature', after: (data) => { // fire-and-forget,不阻塞导览 analytics.track('tour_step_completed', { stepIndex: data.index, action: data.action, }); }, }

仅前进时延迟(后退不延迟)

{ target: '.sidebar', content: 'The sidebar', before: ({ action }) => { const ms = action === ACTIONS.PREV ? 0 : 300; return new Promise(resolve => setTimeout(resolve, ms)); }, }

动态步骤(按角色/特性开关构建)

const [steps, setSteps] = useState<Step[]>([]); useEffect(() => { const dynamicSteps: Step[] = [ { target: '.dashboard', content: 'Welcome to your dashboard' }, ]; if (user.isAdmin) { dynamicSteps.push({ target: '.admin-panel', content: 'Admin controls are here' }); } if (featureFlags.newSearch) { dynamicSteps.push({ target: '.search-bar', content: 'Try the new search' }); } setSteps(dynamicSteps); }, [user, featureFlags]); return <Joyride run={steps.length > 0} steps={steps} continuous />;

用 React ref 作目标

const sidebarRef = useRef<HTMLDivElement>(null); const buttonRef = useRef<HTMLButtonElement>(null); const steps: Step[] = [ { target: sidebarRef, content: 'Navigation sidebar' }, { target: buttonRef, content: 'Click here to create' }, { target: '.css-selector', content: 'Mix refs with selectors' }, ]; return ( <div> <Joyride steps={steps} run continuous /> <div ref={sidebarRef}>Sidebar</div> <button ref={buttonRef}>Create</button> <span className="css-selector">Other element</span> </div> );

重新开始 / 续播导览

const [run, setRun] = useState(false); const [initialStepIndex, setInitialStepIndex] = useState(0); const handleStart = () => { setInitialStepIndex(0); setRun(true); }; const handleResume = (fromStep: number) => { setInitialStepIndex(fromStep); setRun(true); }; return ( <div> <Joyride run={run} initialStepIndex={initialStepIndex} steps={steps} continuous onEvent={(data) => { if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) { setRun(false); } }} /> <button onClick={handleStart}>Start Tour</button> <button onClick={() => handleResume(3)}>Resume from Step 4</button> </div> );

模态风格居中步骤

{ target: 'body', placement: 'center', content: ( <div> <h2>Welcome!</h2> <p>This appears as a centered modal overlay.</p> </div> ), // center 放置会自动隐藏 beacon 和箭头 }

完整 API 参考索引

本文是 React Joyride v3 的实战入门与排障指南。若要查阅完整 API 细节,仓库内还有四份结构化参考文档可继续深读:

  • api-props-options.md —— 完整 Props、Options(全部 30+ 字段及默认值)、Locale、FloatingOptions、Styles 类型;
  • api-step-state-controls.md —— Step、StepMerged、StepTarget、State、Controls(全部 10 个方法)、UseJoyrideReturn、StepFailure;
  • api-events-components.md —— 全部 13 种事件类型、ACTIONS/LIFECYCLE/STATUS/ORIGIN 常量、EventData、自定义组件 render props;
  • patterns.md —— 完整可运行示例:受控模式、before/after 钩子、自定义气泡、事件订阅、动态步骤。

仓库中 website/src/app/demos 目录下还有覆盖 overview、carousel、chat、controlled、custom-components、modal、multi-route、scroll 等场景的真实示例页面(对应 e2e 下的端到端测试),可直接对照学习;src 下的组件、hooks 与 modules 是这套 API 的全部实现源码。

  • 前端
  • UI组件

【免费下载链接】react-joyride

Create guided tours in your apps

项目地址:https://gitcode.com/gh_mirrors/re/react-joyride
点击查看免费下载

相关推荐

上一篇:VPP API完全指南:从基础调用到高级功能实现
下一篇:C++类型推导完全指南:如何用auto关键字简化现代C++编程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询