首页
/ Strapi Guided Tour 全解:架构、状态管理与自定义引导之旅的完整实战指南

Strapi Guided Tour 全解:架构、状态管理与自定义引导之旅的完整实战指南

2026-09-05 11:00:25作者:柏廷章Berta

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 工厂 createTourGuidedTourTooltip Tours.tsx
Steps(Steps/ 步骤工厂、可复用步骤组件、各 Tour 专属步骤 Steps/Step.tsx 及各 Tour 步骤文件
Overview Component(Overview.tsx 首页 Tour 总览与进度追踪 Overview.tsx 中的 GuidedTourHomepageOverview

从源码结构看,当前仓库内置了四条 Tour,注册在 Tours.tsxtours 对象中:

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,它做三件事:

  1. 类型推导:通过泛型 const T extends ReadonlyArray<TourStep<string>> 把步骤名映射为组件键,使 tours.myTour.Introduction 具备完整类型提示;
  2. 步骤组件化:对每个 step 生成一个包装组件,内部渲染 GuidedTourTooltip,绑定 tourName、步骤下标 indexcontentwhen 条件;若发现重复的步骤名会直接抛出错误:
if (name in acc) {
  throw Error(`The tour: ${tourName} with step: ${step.name} has already been registered`);
}
  1. 元信息计算createTour() 会为 tour 对象附加 _meta 属性,提供 totalStepCount(定义的全部步骤数)与 displayedStepCount(实际展示给用户的步骤数,即总数减去标记了 excludeFromStepCount: true 的步骤数)。

Tour 弹层的开启条件

文档未展开、但对理解系统行为至关重要的一点是:Popover 何时真正打开。在 Tours.tsxGuidedTourTooltipImpl 中,开启条件是四个逻辑与的组合:

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: trueContext.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_actionset_tour_type(在 Tour 类型切换且未完成时重置到第 0 步,用于 Content Type Builder 的 AI/非 AI 双版本引导)以及 set_hiddenContext.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_TOURContext.tsx)。两个实现细节值得注意:

  • 写入发生在 useLayoutEffect 中(布局阶段),确保在浏览器跟随外部链接(如 Strapi Cloud 文档链接)之前 localStorage 已更新(Context.tsx);
  • 读取后会先经过 migrateTours 迁移函数处理旧版本数据(utils/migrations.ts),保证跨版本升级后状态结构兼容;配套的 reducer.test.tsmigrations.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.AnchorTours.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

每个步骤提供四个主要组件(RootTitleContentActions),均由 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-systemPopover.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 渲染,并配有针对 pul 的默认排版样式(Step.tsx),因此翻译文案中可使用简单的 HTML 片段。

Step.Actions

内置功能的操作按钮区。默认值以源码为准(Step.tsx):showStepCount 默认 trueshowSkip 默认 falseshowPrevious 默认 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 埋点,再 dispatch skip_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.tscontrollers/admin.ts 中。由于 GuidedTourTooltipImpl 在 meta 缺失时不会打开弹层,前端行为与服务端判定是强耦合的。

首页总览组件:GuidedTourHomepageOverview

Overview.tsx 中的 GuidedTourHomepageOverview 是文档架构表中“Homepage tour overview and progress tracking”的落地实现。它的展示门槛与 Tour 弹层一致:isFirstSuperAdminUser 为 true、enabled 为 true、非移动端、且处于 development 环境(Overview.tsx)。界面由两部分构成:

  1. 进度区:按 completedTours.length / tourNames.length 计算完成百分比,渲染 “{completed}% completed” 与进度条;“Close guided tour” 按钮经确认对话框后 dispatch skip_all_tours 全局关闭引导;
  2. 任务列表TASK_CONTENTOverview.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 };

对照实现,ActionContext.tsx 中还扩展了 remove_completed_actionset_tour_typeset_hidden 三种动作,如需在自定义 Tour 中处理“移动端隐藏”或“动作撤销”场景,可直接复用这些 dispatch 入口。

小结

Guided Tour 是 Strapi 管理面板中一个麻雀虽小五脏俱全的引导系统:createTour 工厂 + GuidedTourTooltip 完成“步骤即锚点”的声明式定义,Context/Reducer 配合 STRAPI_GUIDED_TOUR localStorage 键完成进度持久化,completedActions 机制让步骤能响应真实用户行为,useGetGuidedTourMetaQuery 则把“首位超级管理员”的服务端判定作为整个体验的总开关。基于 05-guided-tour.md 文档与 GuidedTour 源码目录 的对照阅读,你可以在开发环境下为自己的插件或页面快速搭建一条类型安全、可持久化、可条件解锁的引导之旅。

登录后查看全文
热门项目推荐
相关项目推荐