Zulip Onboarding Steps 子系统解析:一次性引导提示的配置、展示与已读机制
2026/9/13 3:56:54 网站建设 项目流程

Zulip Onboarding Steps 子系统解析:一次性引导提示的配置、展示与已读机制

【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip

导读

Zulip 的 Onboarding Steps 是一套轻量级的用户引导机制,用于在用户首次接触某些不易自明(not self-evident)的 UI 元素时,通过一次性横幅(banner)或弹窗(modal)提供上下文说明。本文以 docs/subsystems/onboarding-steps.md 为核心骨架,结合服务端、前端与测试源码,完整讲解如何为 Zulip 配置一个新的引导步骤、服务端如何决定下发哪些步骤、前端如何判断展示并回写已读状态,以及这条链路在源码中的完整实现。读完本文,你将掌握从"注册引导步骤名"到"标记已读"的全流程开发方法,并理解其中涉及的后端模型、事件同步、API 端点和重试机制。

什么是 Onboarding Steps

Onboarding Steps 是 Zulip 中一类一次性(one-time)提示:每个用户只会看到一次,用于引导用户注意到某个重要的 UI 元素或功能入口。当前实现下,引导步骤以banner(横幅)modal(弹窗)两种形态呈现;文档也指出,历史上还曾有过 "hotspots"(热点高亮)这种引导形式,但现已不再提供。

这类引导特别适合 Zulip 这类功能密集的团队协作产品——例如"收件箱视图""最近会话视图""话题已解决"等概念对新手并不直观,通过一次性提示可以显著降低学习成本,而不会像常驻教程那样打扰老用户。

该子系统在前端和后端各有一个核心入口文件:

  • 服务端注册中心:zerver/lib/onboarding_steps.py
  • 前端展示与已读逻辑:web/src/onboarding_steps.ts

核心数据模型与步骤注册中心

持久化模型:OnboardingStep

每个用户的已读步骤持久化在 Django 模型 zerver/models/onboarding_steps.py 中:

class OnboardingStep(models.Model): user = models.ForeignKey(UserProfile, on_delete=CASCADE) onboarding_step = models.CharField(max_length=40) timestamp = models.DateTimeField(default=timezone_now) class Meta: unique_together = ("user", "onboarding_step")

关键约束有两点:

  • onboarding_step字段最长40 个字符,自定义步骤名时需控制长度;
  • (user, onboarding_step)联合唯一,同一用户对同一步骤只保留一条记录,天然保证了"一次性"语义。

步骤的两种类型

在 zerver/lib/onboarding_steps.py 中定义了三个 dataclass:

@dataclass class APIOnboardingStep: type: str name: str @dataclass class OneTimeNotice: name: str def to_dict(self) -> APIOnboardingStep: return APIOnboardingStep(type="one_time_notice", name=self.name) @dataclass class OneTimeAction: name: str def to_dict(self) -> APIOnboardingStep: return APIOnboardingStep(type="one_time_action", name=self.name)
  • OneTimeNotice(一次性提示):对应 banner/modal 类提示,前端仅负责"展示一次"。
  • OneTimeAction(一次性动作):对应需要在展示前主动执行的动作流程(例如自动跳转到欢迎机器人的私信会话)。

两者对外(API / 前端)都序列化为APIOnboardingStep,仅type字段不同("one_time_notice""one_time_action")。前端对应的 schema 校验定义在 web/src/state_data.ts:

const one_time_notice_schema = z.object({ name: z.string(), type: z.literal("one_time_notice"), }); const one_time_action_schema = z.object({ name: z.string(), type: z.literal("one_time_action"), }); export const onboarding_step_schema = z.union([one_time_notice_schema, one_time_action_schema]);

当前已注册的步骤清单

服务端维护了全量步骤注册表(zerver/lib/onboarding_steps.py),ALL_ONBOARDING_STEPS = ONE_TIME_NOTICES + ONE_TIME_ACTIONS

类型name(步骤名)前端消费位置
one_time_noticevisibility_policy_bannerweb/src/compose.ts
one_time_noticeintro_inbox_view_modalweb/src/inbox_ui.ts
one_time_noticeintro_recent_view_modalweb/src/recent_view_ui.ts
one_time_noticefirst_stream_created_bannerweb/src/stream_create.ts
one_time_noticejump_to_conversation_bannerweb/src/compose_notifications.ts
one_time_noticenon_interleaved_view_messages_fadingweb/src/compose_notifications.ts
one_time_noticeinterleaved_view_messages_fadingweb/src/compose_notifications.ts
one_time_noticeintro_resolve_topicweb/src/message_edit.ts
one_time_noticenavigation_tour_videoweb/src/onboarding_steps.ts
one_time_noticeintro_go_to_conversation_tooltipweb/src/compose_recipient.ts
one_time_actionnarrow_to_dm_with_welcome_bot_new_userweb/src/onboarding_steps.ts

可见当前引导覆盖了收件箱视图、最近会话、创建频道、话题已解决、消息淡出提示、导航导览视频等主要新手触点。

三步配置一个新 Onboarding Step

原文档给出了非常简洁的三步流程,这里结合真实源码逐一展开。

Step 1:注册引导步骤名

打开 zerver/lib/onboarding_steps.py,向ONE_TIME_NOTICES列表追加一项:

ONE_TIME_NOTICES: list[OneTimeNotice] = [ ... OneTimeNotice( name="Provide a concise name", ), ]

几个实践要点:

  • 命名要简短:名称会写入OnboardingStep.onboarding_step字段,数据库层面限制为 40 字符;
  • 保持可读性:从现有清单看,命名惯例是"意图 + 形态"(如intro_inbox_view_modal表示"收件箱视图介绍弹窗",first_stream_created_banner表示"首次创建频道横幅");
  • 必须在注册表中存在:后端视图 zerver/views/onboarding_steps.py 会校验提交的步骤名是否存在于ALL_ONBOARDING_STEPS,否则返回错误Unknown onboarding_step: {onboarding_step}
  • 如果这个步骤是一次性动作(如自动跳转到某视图),则追加到ONE_TIME_ACTIONS列表。

Step 2:展示判断与渲染

当承载引导的那个 UI 元素即将出现时,在对应前端模块中读取 web/src/onboarding_steps.ts 导出的集合:

export const ONE_TIME_NOTICES_TO_DISPLAY = new Set<string>();

判断逻辑形如(以收件箱视图弹窗为例,见 web/src/inbox_ui.ts):

if (onboarding_steps.ONE_TIME_NOTICES_TO_DISPLAY.has("intro_inbox_view_modal")) { // 展示 intro_inbox_view_modal,并在展示完成后标记已读 onboarding_steps.post_onboarding_step_as_read("intro_inbox_view_modal"); }

ONE_TIME_NOTICES_TO_DISPLAY是服务端下发、前端维护的待展示集合:只有出现在集合中的步骤名才需要展示。该集合由update_onboarding_steps_to_display在每次拿到服务端数据时重建(web/src/onboarding_steps.ts)——它只把type === "one_time_notice"的步骤放入集合,one_time_action类型不走此集合,而是由initialize直接触发对应动作。

Step 3:标记为已读

提示展示完成后,调用 post_onboarding_step_as_read:

post_onboarding_step_as_read("intro_inbox_view_modal");

该函数会向服务端POST /json/users/me/onboarding_steps提交步骤名,成功后服务端持久化记录;前端随后通过事件更新ONE_TIME_NOTICES_TO_DISPLAY(移除该步骤),用户便不会再看到该提示。

服务端如何决定下发哪些步骤

服务端的核心决策函数是get_next_onboarding_steps(zerver/lib/onboarding_steps.py):

def get_next_onboarding_steps(user: UserProfile) -> list[APIOnboardingStep]: # 若服务端关闭了教程功能,则不发送任何引导步骤 if not settings.TUTORIAL_ENABLED: return [] seen_onboarding_steps: list[str] = list( OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True) ) if settings.NAVIGATION_TOUR_VIDEO_URL is None: # 管理员禁用了导航导览视频,视为已读 seen_onboarding_steps.append("navigation_tour_video") seen_onboarding_steps_set = frozenset(seen_onboarding_steps) onboarding_steps: list[APIOnboardingStep] = [] for onboarding_step in ALL_ONBOARDING_STEPS: if onboarding_step.name in seen_onboarding_steps_set: continue onboarding_steps.append(onboarding_step.to_dict()) return onboarding_steps

逻辑要点:

  1. 总开关TUTORIAL_ENABLED:若为False,直接返回空列表,任何用户都收不到引导;默认值为True(见 zproject/default_settings.py);
  2. 已读过滤:查询该用户所有已读步骤,与全量注册表做差集,只下发未读步骤;
  3. 视频步骤特判:若管理员把NAVIGATION_TOUR_VIDEO_URL配置为None(默认值是官方视频地址,见 zproject/default_settings.py),则navigation_tour_video直接视为已读,不再下发。

下发时机有两个(均定义在 zerver/lib/events.py 与 zerver/actions/onboarding_steps.py):

  • 初始注册(do_events_register:用户加载客户端时,state["onboarding_steps"]携带全部待展示步骤,state["navigation_tour_video_url"]携带视频地址;
  • 实时事件:用户标记某步骤已读后,服务端向该用户推送type="onboarding_steps"事件,payload 为重新计算后的剩余步骤列表,前端据此更新本地集合(web/src/onboarding_steps.ts 及事件处理逻辑)。

标记已读的完整链路

前端:带重试的 POST

post_onboarding_step_as_read内部使用channel.post调用/json/users/me/onboarding_steps,并实现了最多 5 次(MAX_RETRIES = 5)的指数退避重试(web/src/onboarding_steps.ts):

  • 服务端返回400(步骤名非法,几乎不可能发生,因为不是用户输入)时不重试
  • 其他错误使用get_retry_backoff_seconds计算退避时长后setTimeout递归重试。

该函数还支持可选的第二参数schedule_navigation_tour_video_reminder_delay,仅对navigation_tour_video步骤有效(内部有assert校验),用于"稍后观看"场景——延迟若干秒后由欢迎机器人发送一条提醒私信。

服务端:API 端点与动作函数

API 端点为POST /json/users/me/onboarding_steps,由 zerver/views/onboarding_steps.py 处理:

  1. 校验onboarding_step存在于ALL_ONBOARDING_STEPS,否则抛出JsonableError("Unknown onboarding_step: ...")
  2. 若携带schedule_navigation_tour_video_reminder_delay,则校验步骤必须是navigation_tour_video,并通过check_schedule_message调度一条由WELCOME_BOT发送的私信提醒(deliver_at = now + delay);
  3. 调用动作函数do_mark_onboarding_step_as_read落库。

动作函数位于 zerver/actions/onboarding_steps.py:

@transaction.atomic(durable=True) def do_mark_onboarding_step_as_read(user: UserProfile, onboarding_step: str) -> None: OnboardingStep.objects.get_or_create(user=user, onboarding_step=onboarding_step) event = dict( type="onboarding_steps", onboarding_steps=[asdict(step) for step in get_next_onboarding_steps(user)], ) send_event_on_commit(user.realm, event, [user.id])
  • get_or_create保证了幂等性:重复标记不会报错,也不会产生重复记录;
  • 事务提交后通过 Tornado 事件系统向该用户推送更新,前端update_onboarding_steps_to_display重建集合,提示即时消失。

一次性动作(OneTimeAction)与导览视频弹窗

自动跳转到欢迎机器人私信

narrow_to_dm_with_welcome_bot_new_user是一个典型的OneTimeAction(web/src/onboarding_steps.ts):在新用户注册后的首次加载中,若该步骤尚未完成,前端会自动调用post_onboarding_step_as_read将其标记为已读,并判断当前是否处于首页视图——若用户是通过带next参数的链接进入特定视图,则尊重用户意图不跳转;否则自动窄化(narrow)到欢迎机器人的私信会话,引导新用户发出第一条消息。

导航导览视频弹窗

navigation_tour_video是形态最复杂的一个提示(web/src/onboarding_steps.ts):它通过dialog_widget.launch渲染由 web/templates/navigation_tour_video_modal.hbs 生成的弹窗,支持"跳过视频 / 稍后观看 / 看完"三种结束路径:

  • Watch later(稍后观看):点击后以2 * 60 * 60秒(2 小时)为延迟调用post_onboarding_step_as_read("navigation_tour_video", reminder_delay_seconds),服务端据此调度欢迎机器人的提醒私信;
  • 跳过或看完:弹窗关闭时(on_hide)若未点击过"稍后观看",则直接调用post_onboarding_step_as_read("navigation_tour_video")标记已读;
  • 弹窗关闭后还会把焦点显式移回#compose-textarea,避免与消息输入框的焦点竞争导致的不稳定行为。

测试与验证

该子系统的行为由 zerver/tests/test_onboarding_steps.py 覆盖,主要用例包括:

  • 部分已读、部分未读test_some_done_some_not):验证get_next_onboarding_steps只返回未读步骤、顺序与注册表一致,且新用户默认已读visibility_policy_banner;同时验证TUTORIAL_ENABLED=False时返回空列表、NAVIGATION_TOUR_VIDEO_URL=None时排除视频步骤;
  • 全部已读test_all_onboarding_steps_done):遍历ALL_ONBOARDING_STEPS全部标记后,get_next_onboarding_steps返回空列表;
  • API 端点test_onboarding_steps_url_endpoint):直接对/json/users/me/onboarding_steps发 POST 验证落库,并验证非法步骤名返回Unknown onboarding_step: invalid
  • 提醒调度test_schedule_navigation_tour_video_reminder):配合time_machine冻结时间,验证schedule_navigation_tour_video_reminder_delay=30时生成一条发送者为欢迎机器人、内容含 "Welcome to Zulip video" 的定时私信,且scheduled_timestamp精确等于now + 30s

后端主要调用链可概括为:

前端 post_onboarding_step_as_read(name) └─ POST /json/users/me/onboarding_steps └─ zerver/views/onboarding_steps.py: mark_onboarding_step_as_read ├─ 校验 name ∈ ALL_ONBOARDING_STEPS ├─ (可选) check_schedule_message 调度欢迎机器人提醒私信 └─ zerver/actions/onboarding_steps.py: do_mark_onboarding_step_as_read ├─ OnboardingStep.get_or_create(user, name) └─ send_event_on_commit → 事件 onboarding_steps → 前端更新 ONE_TIME_NOTICES_TO_DISPLAY

运维与配置注意事项

  • TUTORIAL_ENABLED(默认True,见 zproject/default_settings.py):服务端总开关,设为False后所有用户均不再收到任何引导步骤;
  • NAVIGATION_TOUR_VIDEO_URL(默认官方视频地址,见 zproject/default_settings.py):设为None可禁用导览视频弹窗,同时该步骤对用户自动视为已读;
  • 新用户默认行为:创建用户时,服务端会自动把visibility_policy_banner标记为已读(zerver/actions/create_user.py),因为该横幅只面向存量用户;若用户注册时选择从已有账户导入设置,则通过copy_onboarding_steps(zerver/lib/onboarding_steps.py 与 zerver/lib/create_user.py)把源账户的全部已读步骤复制过来,避免老用户在新账户上重复看到提示。

总结

Zulip 的 Onboarding Steps 子系统用"注册表 + 一次性记录 + 事件同步"三件套,实现了轻量、幂等、可扩展的用户引导能力:开发一个新提示只需三步——在ONE_TIME_NOTICES注册名称、在 UI 出现处查ONE_TIME_NOTICES_TO_DISPLAY决定展示、展示后调用post_onboarding_step_as_read落库。配合TUTORIAL_ENABLEDNAVIGATION_TOUR_VIDEO_URL两个配置项,运维者可以灵活控制引导内容的开启与裁剪。理解这条从注册到已读的完整链路,也为你阅读 Zulip 其他依赖事件同步的子系统(如通知、未读计数)打下了良好基础。

【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip

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

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

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

立即咨询