Strapi Guided Tour 全解:架构、状态管理与自定义引导之旅的完整实战指南
Guided Tour(引导之旅)是 Strapi 管理面板中的交互式新手引导系统:它通过锚定在界面元素上的 Popover,一步步引导新用户完成“创建 Schema → 创建并发布内容 → 复制 API Token → 部署到 Strapi Cloud”的关键流程。本文基于 Strapi 官方文档 Guided Tour 并结合 GuidedTour 源码 逐层拆解:读完后你将理解其 Context/Reducer 状态架构、createTour 工厂机制与条件步骤原理,并能独立编写、锚定和测试一条属于自己的 Tour。
架构总览:四个模块各司其职
文档将 Guided Tour 系统描述为一套模块化架构,源码目录 packages/core/admin/admin/src/components/GuidedTour/ 与文档描述一一对应:
| 模块 | 文档职责 | 仓库实现 |
|---|---|---|
Context Provider(Context.tsx) |
全局状态管理与持久化 | Context.tsx 中定义 GuidedTourContext、reducer 与 useGuidedTour Hook |
Tour Factory(Tours.tsx) |
Tour 工厂 createTour 与 GuidedTourTooltip |
Tours.tsx |
Steps(Steps/) |
步骤工厂、可复用步骤组件、各 Tour 专属步骤 | Steps/Step.tsx 及各 Tour 步骤文件 |
Overview Component(Overview.tsx) |
首页 Tour 总览与进度追踪 | Overview.tsx 中的 GuidedTourHomepageOverview |
从源码结构看,当前仓库内置了四条 Tour,注册在 Tours.tsx 的 tours 对象中:
const tours = {
contentTypeBuilder: createTour('contentTypeBuilder', contentTypeBuilderSteps),
contentManager: createTour('contentManager', contentManagerSteps),
apiTokens: createTour('apiTokens', apiTokensSteps),
strapiCloud: createTour('strapiCloud', []),
} as const;
其中前三条对应内容类型构建器(Content Type Builder)、内容管理器(Content Manager)与 API Tokens 设置页的引导,strapiCloud 是一个无步骤的外部链接型 Tour。此外还有一个轻量入口 GuidedTourProvider.tsx,它在 NODE_ENV === 'test' 时自动关闭 Tour,避免测试环境干扰。
核心概念一:Tours 与 Steps
Tour 是引导用户完成特定工作流的步骤集合,每个步骤的内容会以 Popover 形式弹出。定义一条 Tour 的标准写法如下(继承自文档):
const tours = {
contentTypeBuilder: createTour('contentTypeBuilder', [
{
name: 'Introduction',
content: (Step) => (
<Step.Root>
<Step.Title id="tour.title" defaultMessage="Welcome!" />
<Step.Content id="tour.content" defaultMessage="Let's get started." />
<Step.Actions showSkip />
</Step.Root>
),
},
// ... more steps
]),
};
createTour 工厂的源码实现
createTour 的定义在 Tours.tsx,它做三件事:
- 类型推导:通过泛型
const T extends ReadonlyArray<TourStep<string>>把步骤名映射为组件键,使tours.myTour.Introduction具备完整类型提示; - 步骤组件化:对每个 step 生成一个包装组件,内部渲染
GuidedTourTooltip,绑定tourName、步骤下标index、content与when条件;若发现重复的步骤名会直接抛出错误:
if (name in acc) {
throw Error(`The tour: ${tourName} with step: ${step.name} has already been registered`);
}
- 元信息计算:
createTour()会为 tour 对象附加_meta属性,提供totalStepCount(定义的全部步骤数)与displayedStepCount(实际展示给用户的步骤数,即总数减去标记了excludeFromStepCount: true的步骤数)。
Tour 弹层的开启条件
文档未展开、但对理解系统行为至关重要的一点是:Popover 何时真正打开。在 Tours.tsx 的 GuidedTourTooltipImpl 中,开启条件是四个逻辑与的组合:
const isPopoverOpen =
guidedTourMeta?.data?.isFirstSuperAdminUser && // 服务端判定为首位超级管理员
!tourState?.isCompleted && // 该 Tour 尚未完成/跳过
isCurrentStep && // 当前步骤指针正好指向这一步
isStepConditionMet; // when() 条件满足(无 when 时恒为 true)
同时 GuidedTourTooltip 存在一个重要的适用前提:只有当 process.env.NODE_ENV === 'development' 时才渲染引导,否则直接透传 children。也就是说 Guided Tour 是为本地开发环境(strapi develop)下的新手准备的体验;此外当 state.enabled 为 false(用户关闭了全部 Tour)或 state.hidden 为 true(非桌面端视口)时同样不渲染。弹层打开时还会执行两个细节行为:将锚点元素 scrollIntoView 居中,并临时锁定 document.body 滚动(Tours.tsx)。
核心概念二:基于 Context + Reducer 的状态管理
Tour 状态通过 React Context 以 reducer 模式管理。文档给出的状态结构为:
type State = {
tours: Tour; // Tour progress for each tour
enabled: boolean; // Whether tours are globally enabled
completedActions: ExtendedCompletedActions; // User-completed actions
};
对照 Context.tsx,实现中每个 Tour 的进度状态为 { currentStep: number; isCompleted: boolean; tourType?: string },并额外保留了 hidden?: boolean(移动端隐藏标记)。初始状态示例(来自文档):
const initialState = {
tours: {
contentManager: {
currentStep: 0,
isCompleted: false,
},
},
enabled: true,
completedActions: ['didCreateSchema'],
};
Reducer 支持的动作
文档列出的 reducer 动作在 Context.tsx 中全部可以找到,语义如下:
next_step:前进到下一个步骤。实现中会读取guidedTours[name]._meta.totalStepCount,当nextStep >= tourLength时把该 Tour 标记为isCompleted: true(Context.tsx);previous_step:回退一步,且带currentStep <= 0的边界保护;go_to_step:跳转到指定步骤(payload 为{ tourName, step });skip_tour:将单个 Tour 标记为已完成(即跳过);skip_all_tours:将enabled置为 false,全局关闭所有 Tour;reset_all_tours:重置所有 Tour 至初始状态并清空completedActions;set_completed_actions:合并更新用户已完成动作列表,实现上做了去重:[...new Set([...draft.completedActions, ...action.payload])](Context.tsx)。
此外源码中还存在文档未列出的扩展动作,可佐证系统的持续演进:remove_completed_action、set_tour_type(在 Tour 类型切换且未完成时重置到第 0 步,用于 Content Type Builder 的 AI/非 AI 双版本引导)以及 set_hidden(Context.tsx)。
通过 Hook 读写状态
import { useGuidedTour } from './Context';
const MyComponent = () => {
const state = useGuidedTour('MyComponent', (s) => s.state);
const dispatch = useGuidedTour('MyComponent', (s) => s.dispatch);
const currentTour = state.tours.myNewTour;
const isEnabled = state.enabled;
const completedActions = state.completedActions;
// Dispatch actions
const handleNext = () => {
dispatch({ type: 'next_step', payload: 'myNewTour' });
};
};
useGuidedTour 采用选择器(selector)模式订阅,第一个参数是用于调试的调用方名称,第二个参数从 { state, dispatch } 中选取所需切片(实现见 Context.tsx)。
持久化与迁移
Tour 状态通过 usePersistentState Hook 自动持久化到 localStorage,存储键为 STRAPI_GUIDED_TOUR(Context.tsx)。两个实现细节值得注意:
- 写入发生在
useLayoutEffect中(布局阶段),确保在浏览器跟随外部链接(如 Strapi Cloud 文档链接)之前 localStorage 已更新(Context.tsx); - 读取后会先经过
migrateTours迁移函数处理旧版本数据(utils/migrations.ts),保证跨版本升级后状态结构兼容;配套的 reducer.test.ts 与 migrations.test.ts 单测分别验证了 reducer 行为与迁移逻辑。
核心概念三:条件步骤(Conditional Steps)
步骤可以基于 completedActions 有条件地展示:
{
name: 'ConditionalStep',
when: (completedActions) => completedActions.includes('didCreateContent'),
content: (Step) => (/* step content */)
}
when 的取值来源是全局的“已完成动作”集合,其合法值由常量表 utils/constants.ts 约束:
const GUIDED_TOUR_REQUIRED_ACTIONS = {
contentTypeBuilder: {
createSchema: 'didCreateContentTypeSchema',
addField: 'didAddFieldToSchema',
},
contentManager: {
createContent: 'didCreateContent',
},
apiTokens: {
createToken: 'didCreateApiToken',
copyToken: 'didCopyApiToken',
},
strapiCloud: {},
} as const;
CompletedActions 类型即从这张表推导出的字符串字面量联合数组,因此 when 回调的参数是类型安全的。从源码结构看,这套机制让 Tour 具备“感知用户真实操作”的能力:例如用户真的在内容管理器里创建了内容,才会解锁依赖 didCreateContent 的后续步骤。
核心概念四:将步骤排除出步骤计数
并非所有步骤都应计入展示给用户的“Step X of Y”计数。使用 excludeFromStepCount 即可排除:
{
name: 'Welcome',
excludeFromStepCount: true,
content: (Step) => (
<Step.Root>
<Step.Title id="tour.welcome" defaultMessage="Welcome!" />
<Step.Content id="tour.intro" defaultMessage="Let's get started with this tour." />
<Step.Actions showStepCount={false} />
</Step.Root>
)
}
实现上,createTour() 在 reduce 步骤数组时,每遇到一个 excludeFromStepCount 步骤就把 acc._meta.displayedStepCount 减一(Tours.tsx)。因此:
_meta.totalStepCount— Tour 定义的全部步骤数(reducer 判断是否完成时也用它);_meta.displayedStepCount— 总数减去excludeFromStepCount: true的步骤数(“Step X of Y”中的 Y 由 StepCount 组件 读取该值渲染)。
实战指南:创建一条新的 Tour
第 1 步:在 Tours.tsx 中定义 Tour 结构
const myNewTour = createTour('myNewTour', [
{
name: 'Introduction',
content: (Step) => (
<Step.Root>
<Step.Title
id="tours.myNewTour.Introduction.title"
defaultMessage="My New Feature"
/>
<Step.Content
id="tours.myNewTour.Introduction.content"
defaultMessage="This tour will show you how to use this feature."
/>
<Step.Actions showSkip />
</Step.Root>
),
},
{
name: 'MainAction',
content: (Step) => (
<Step.Root side="right" sideOffset={16}>
<Step.Title
id="tours.myNewTour.MainAction.title"
defaultMessage="Main Action"
/>
<Step.Content
id="tours.myNewTour.MainAction.content"
defaultMessage="Click this button to perform the main action."
/>
<Step.Actions />
</Step.Root>
),
},
{
name: 'Finish',
content: (Step) => (
<Step.Root>
<Step.Title
id="tours.myNewTour.Finish.title"
defaultMessage="You're all set!"
/>
<Step.Content
id="tours.myNewTour.Finish.content"
defaultMessage="You've successfully completed this tour."
/>
<Step.Actions showStepCount={false} to="/next-page" />
</Step.Root>
),
when: (completedActions) => completedActions.includes('didCompleteMainAction'),
},
]);
// Add to the tours object
const tours = {
// ... existing tours
myNewTour,
} as const;
由于 createTour 使用 const 泛型推导,新增 Tour 后 tours.myNewTour 的每个步骤名都会获得类型提示;重复步骤名则在运行时抛错,可尽早暴露配置错误。
第 2 步:在组件中包裹锚点元素
Tour 组件用于包裹(wrap)应该作为步骤 Popover 锚点的元素:
import { tours } from '@strapi/admin/strapi-admin';
const MyComponent = () => {
return (
<div>
<tours.myNewTour.Introduction>
<h1>My Feature Title</h1>
</tours.myNewTour.Introduction>
<tours.myNewTour.MainAction>
<Button onClick={handleMainAction}>Main Action</Button>
</tours.myNewTour.MainAction>
</div>
);
};
被包裹元素会成为 Popover.Anchor(Tours.tsx),当前 Tour 走到对应下标且满足全部开启条件时,Popover 即围绕该元素弹出,同时页面渲染一层半透明遮罩(GuidedTourOverlay,z-index 10)聚焦用户注意力。
第 3 步:标记动作完成,驱动条件步骤
当用户真正执行了某个动作后,dispatch set_completed_actions 将其写入全局动作集合,从而解锁依赖该动作的条件步骤:
import { useGuidedTour } from './Context';
const MyComponent = () => {
const dispatch = useGuidedTour('MyComponent', (s) => s.dispatch);
const handleMainAction = () => {
// Perform the action
performMainAction();
// Track the completion
dispatch({
type: 'set_completed_actions',
payload: ['didCompleteSomeAction'],
});
};
};
一个可参考的内置例子:GuidedTourTooltipImpl 自带一个“兜底同步”逻辑——当服务端返回的 schema 元数据中已存在 api:: 前缀的内容类型(比如项目由带种子数据的模板创建)时,会自动 dispatch didCreateContentTypeSchema,使用户不必真的走一遍 CTB 流程也能推进到内容管理器 Tour(Tours.tsx)。
Step 组件 API
每个步骤提供四个主要组件(Root、Title、Content、Actions),均由 Steps/Step.tsx 中的 createStepComponents(tourName) 工厂生成,并内置了国际化与埋点。
Step.Root
Popover 容器,提供定位选项。它包装并接收与 Radix Popover 相同的 props。结合源码默认值(Step.tsx):
<Step.Root
side="top|right|bottom|left" // Popover 方位(默认 top)
align="start|center|end" // 沿方位的相对对齐(默认 center)
sideOffset={number} // 与锚点的偏移距离
withArrow={boolean} // 是否显示箭头(默认 true)
>
内部实现上,Step.Root 基于 @strapi/design-system 的 Popover.Content,固定 360px 宽度的内容容器,并通过 onClick 阻止事件冒泡以避免误触发锚点交互。
Step.Title
带 i18n 支持的步骤标题:
<Step.Title
id="translation.key"
defaultMessage="Default Title"
/>
// Or with custom content:
<Step.Title>
<CustomTitleComponent />
</Step.Title>
实现上二选一:传 id/defaultMessage 时渲染 <FormattedMessage> 包裹的加粗 h1(id 固定为 guided-tour-title,与 Popover 的 aria-labelledby 关联,保证可访问性);传 children 时则直接渲染自定义节点(Step.tsx)。
Step.Content
带 i18n 支持的步骤正文:
<Step.Content
id="translation.key"
defaultMessage="Default content message"
/>
// Or with custom content:
<Step.Content>
<CustomContentComponent />
</Step.Content>
注意一个实现细节:id/defaultMessage 模式下的文本经过 formatMessage 后以 dangerouslySetInnerHTML 渲染,并配有针对 p、ul 的默认排版样式(Step.tsx),因此翻译文案中可使用简单的 HTML 片段。
Step.Actions
内置功能的操作按钮区。默认值以源码为准(Step.tsx):showStepCount 默认 true,showSkip 默认 false,showPrevious 默认 true(且仅在 showSkip 为 false 时生效):
<Step.Actions
showStepCount={boolean} // Show "Step X of Y" (default: true)
showSkip={boolean} // Show skip button (default: false)
to="/path" // Navigate to path on next (optional)
/>
// Or with custom actions:
<Step.Actions>
<CustomActionsComponent />
</Step.Actions>
按钮区的行为逻辑在 DefaultActions 组件中(Step.tsx):
- Skip 按钮:点击后先上报
didSkipGuidedTour埋点,再 dispatchskip_tour; - Next 按钮:若
to提供了路径则渲染为LinkButton(基于NavLink),点击时先走handleNextStep推进状态再导航;点击最后一步的 Next 会额外上报didCompleteGuidedTour; - Previous 按钮:dispatch
previous_step; - 支持通过
onNextStep/onPreviousStep回调完全接管步进逻辑,便于跨页面 Tour 自定义推进方式。
后端集成:useGetGuidedTourMetaQuery
Guided Tour 通过 RTK Query 的 useGetGuidedTourMetaQuery() 与服务端集成,拉取判定引导行为所需的元数据,核心字段包括:
isFirstSuperAdminUser:是否为该实例的首位超级管理员——这是弹层与首页总览组件共同的总开关之一;schemas:当前项目的内容类型元数据,供“已有 Schema 则跳过前置步骤”的兜底同步逻辑使用。
从源码结构看,该查询对应管理端后端的 guided tour meta 路由与控制器,定义在 routes/admin.ts 与 controllers/admin.ts 中。由于 GuidedTourTooltipImpl 在 meta 缺失时不会打开弹层,前端行为与服务端判定是强耦合的。
首页总览组件:GuidedTourHomepageOverview
Overview.tsx 中的 GuidedTourHomepageOverview 是文档架构表中“Homepage tour overview and progress tracking”的落地实现。它的展示门槛与 Tour 弹层一致:isFirstSuperAdminUser 为 true、enabled 为 true、非移动端、且处于 development 环境(Overview.tsx)。界面由两部分构成:
- 进度区:按
completedTours.length / tourNames.length计算完成百分比,渲染 “{completed}% completed” 与进度条;“Close guided tour” 按钮经确认对话框后 dispatchskip_all_tours全局关闭引导; - 任务列表(
TASK_CONTENT,Overview.tsx):列出 Create your schema(/plugins/content-type-builder)、Create and publish content(/content-manager)、Copy an API token(/settings/api-tokens)、Deploy your application to Strapi Cloud(外部链接)四项任务,每项显示 Start/Done 状态。一个细节是任务间存在依赖:除 contentTypeBuilder 外的任务链接,在didCreateContentTypeSchema未完成时处于禁用状态(Overview.tsx)。
E2E 测试验证
Tour 的端到端行为在 Playwright 测试套件 tests/e2e/tests/admin/guided-tour.spec.ts 中验证。开发时需要注意前面提到的适用前提:Guided Tour 仅在 NODE_ENV === 'development' 下渲染,因此 e2e 与本地验证都应基于 strapi develop 启动的实例;GuidedTourProvider 则在 NODE_ENV === 'test' 下强制禁用 Tour(GuidedTourProvider.tsx)。
API Reference:类型速查
以下是文档给出的完整类型定义,可直接作为二次开发时的参照:
// Tour configuration
type TourStep<P extends string> = {
name: P;
content: Content;
when?: (completedActions: ExtendedCompletedActions) => boolean;
excludeFromStepCount?: boolean; // Exclude from "Step X of Y" counting
};
// State management
type State = {
tours: Tour;
enabled: boolean;
completedActions: ExtendedCompletedActions;
};
type Action =
| { type: 'next_step'; payload: ValidTourName }
| { type: 'skip_tour'; payload: ValidTourName }
| { type: 'previous_step'; payload: ValidTourName }
| { type: 'go_to_step'; payload: { tourName: ValidTourName; step: number } }
| { type: 'set_completed_actions'; payload: ExtendedCompletedActions }
| { type: 'skip_all_tours' }
| { type: 'reset_all_tours' };
// Tour metadata
type TourMeta = {
totalStepCount: number;
displayedStepCount: number;
};
// Step components
type Step = {
Root: React.ForwardRefExoticComponent<PopoverContentProps & { withArrow?: boolean }>;
Title: (props: StepProps) => React.ReactNode;
Content: (props: StepProps) => React.ReactNode;
Actions: (props: ActionsProps & { to?: string } & FlexProps) => React.ReactNode;
};
// Tour object includes both steps and metadata
type Tour = Components & { _meta: TourMeta };
对照实现,Action 在 Context.tsx 中还扩展了 remove_completed_action、set_tour_type、set_hidden 三种动作,如需在自定义 Tour 中处理“移动端隐藏”或“动作撤销”场景,可直接复用这些 dispatch 入口。
小结
Guided Tour 是 Strapi 管理面板中一个麻雀虽小五脏俱全的引导系统:createTour 工厂 + GuidedTourTooltip 完成“步骤即锚点”的声明式定义,Context/Reducer 配合 STRAPI_GUIDED_TOUR localStorage 键完成进度持久化,completedActions 机制让步骤能响应真实用户行为,useGetGuidedTourMetaQuery 则把“首位超级管理员”的服务端判定作为整个体验的总开关。基于 05-guided-tour.md 文档与 GuidedTour 源码目录 的对照阅读,你可以在开发环境下为自己的插件或页面快速搭建一条类型安全、可持久化、可条件解锁的引导之旅。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00