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_notice | visibility_policy_banner | web/src/compose.ts |
| one_time_notice | intro_inbox_view_modal | web/src/inbox_ui.ts |
| one_time_notice | intro_recent_view_modal | web/src/recent_view_ui.ts |
| one_time_notice | first_stream_created_banner | web/src/stream_create.ts |
| one_time_notice | jump_to_conversation_banner | web/src/compose_notifications.ts |
| one_time_notice | non_interleaved_view_messages_fading | web/src/compose_notifications.ts |
| one_time_notice | interleaved_view_messages_fading | web/src/compose_notifications.ts |
| one_time_notice | intro_resolve_topic | web/src/message_edit.ts |
| one_time_notice | navigation_tour_video | web/src/onboarding_steps.ts |
| one_time_notice | intro_go_to_conversation_tooltip | web/src/compose_recipient.ts |
| one_time_action | narrow_to_dm_with_welcome_bot_new_user | web/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逻辑要点:
- 总开关
TUTORIAL_ENABLED:若为False,直接返回空列表,任何用户都收不到引导;默认值为True(见 zproject/default_settings.py); - 已读过滤:查询该用户所有已读步骤,与全量注册表做差集,只下发未读步骤;
- 视频步骤特判:若管理员把
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 处理:
- 校验
onboarding_step存在于ALL_ONBOARDING_STEPS,否则抛出JsonableError("Unknown onboarding_step: ..."); - 若携带
schedule_navigation_tour_video_reminder_delay,则校验步骤必须是navigation_tour_video,并通过check_schedule_message调度一条由WELCOME_BOT发送的私信提醒(deliver_at = now + delay); - 调用动作函数
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_ENABLED、NAVIGATION_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),仅供参考