- 前端
- UI组件
【免费下载链接】react-joyride
Create guided tours in your apps
本文是 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 |
waiting | run=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') |
skipBeacon | false | 跳过 beacon,直接显示气泡 |
buttons | ['back','close','primary'] | 气泡中的按钮。加'skip'可显示跳过按钮 |
hideOverlay | false | 不显示深色遮罩 |
blockTargetInteraction | false | 阻止高亮元素的点击交互 |
before | - | (data) => Promise<void>,步骤显示前的异步钩子 |
after | - | (data) => void,步骤完成后的即弃钩子 |
skipScroll | false | 不滚动到目标 |
scrollTarget | - | 滚动到该元素而非target |
spotlightTarget | - | 高亮该元素而非target |
spotlightPadding | 10 | 聚光灯周围内边距。数字或{ top, right, bottom, left } |
targetWaitTimeout | 1000 | 等待目标出现的毫秒数。0= 不等待 |
beforeTimeout | 5000 | 等待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) => voiddata对象包含完整导览状态外加事件专属字段;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_hook | before钩子被调用 |
step:before | 找到目标,即将渲染步骤 |
scroll:start | 开始滚动到目标 |
scroll:end | 滚动完成 |
beacon | beacon 显示 |
tooltip | 气泡显示 |
step:after | 用户导航(next/prev/close/skip) |
step:after_hook | after钩子被调用 |
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大 switch | on('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
相关推荐
LaMa 界面与控件样式定制完整指南:改对 7 处配置,交互面板贴合自己的工作流
LaMa 界面与控件样式定制完整指南:改对 7 处配置,交互面板贴合自己的工作流 LaMa 是 WACV 2022 论文开源的大面积掩膜图像修复项目,自带交互式
人工智能计算机视觉深度学习图像处理Hermes WebUI 会话管理指南:创建、分组与备份
Hermes WebUI 会话管理指南:创建、分组与备份 Hermes WebUI 会话管理围绕左侧边栏的会话列表展开:它是一款 Hermes Agent 的自
人工智能AI 应用AI Agent交互助手MCP 服务前端Civitai 引导式导览(Guided Tours)系统实现解析:基于 react-joyride 的产品上手引导架构
Civitai 引导式导览(Guided Tours)系统实现解析:基于 react joyride 的产品上手引导架构 导读 本文深入解析 Civitai 前
后端前端AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考