Skip to content

react

1 post with the tag “react”

在 React 项目中优雅实现新用户引导:HagiCode 的 driver.js 实践

在 React 项目中优雅实现新用户引导:HagiCode 的 driver.js 实践

Section titled “在 React 项目中优雅实现新用户引导:HagiCode 的 driver.js 实践”

当用户第一次打开你的产品时,他们真的知道该从哪里开始吗?这篇文章聊聊我们在 HagiCode 项目里用 driver.js 做新用户引导的那些事儿,也算是抛砖引玉罢了。

你有没有遇到过这样的场景:新用户注册了你的产品,打开页面后一脸茫然,东张西望,不知道该点哪里、该做什么。作为开发者,我们总以为用户会”自己探索”,毕竟人的好奇心是无限的嘛。可现实是——大部分用户会在几分钟内因为找不到入口而悄悄离开,就像故事开始得突然,结束得也自然。

新用户引导是解决这个问题的重要手段,只是实现起来也不那么简单。一个好的引导系统需要:

  • 能够精准定位页面元素并高亮显示
  • 支持多步骤引导流程
  • 能够记住用户的选择(完成/跳过)
  • 不影响页面性能和正常交互
  • 代码结构清晰,易于维护

在开发 HagiCode 的过程中,我们也遇到了同样的挑战。HagiCode 是一个 AI 代码助手项目,核心工作流是”用户创建提案 → AI 生成计划 → 用户审核 → AI 执行”这样一套 OpenSpec 流程。对于第一次接触这个概念的用户来说,这套流程是全新的,必须有一个好的引导来帮助他们快速上手。毕竟,新事物总是需要一点时间的。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个基于 Claude 的 AI 代码助手,通过 OpenSpec 工作流帮助开发者更高效地完成代码任务。你可以在 GitHub 上查看我们的开源代码。

在技术选型阶段,我们评估了几个主流的引导库,怎么说呢,每个都有自己的特点:

  • Intro.js:功能强大但体积较大,样式定制相对复杂
  • Shepherd.js:API 设计很好,但对于我们的场景来说有点”重”
  • driver.js:轻量、简洁、API 直观,且支持 React 生态

最终我们选择了 driver.js,其实也没什么特别的理由,主要基于以下几点考虑:

  1. 轻量级:核心库体积小,不会显著增加打包体积
  2. API 简洁:配置项清晰直观,上手快
  3. 灵活性:支持自定义定位、样式和交互行为
  4. 动态导入:可以按需加载,不影响首屏性能

选型这件事,其实没有最好的,只有最合适的罢了。

driver.js 的配置非常直观,以下是 HagiCode 项目中的核心配置:

import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';
const newConversationDriver = driver({
allowClose: true, // 允许用户关闭引导
animate: true, // 启用动画效果
overlayClickBehavior: 'close', // 点击遮罩层关闭引导
disableActiveInteraction: false, // 保持元素可交互
showProgress: false, // 不显示进度条(我们有自定义进度管理)
steps: guideSteps // 引导步骤数组
});

这些配置背后的考虑是:

  • allowClose: true - 尊重用户选择,不强制完成引导,毕竟强扭的瓜不甜
  • disableActiveInteraction: false - 某些步骤需要用户实际操作(如输入文字),所以不能禁用交互
  • overlayClickBehavior: 'close' - 给用户一个快速的退出方式

引导状态的持久化是关键——我们不希望每次刷新页面都重新引导,那样挺烦人的。HagiCode 使用 localStorage 来管理引导状态:

export type GuideState = 'pending' | 'dismissed' | 'completed';
export interface UserGuideState {
session: GuideState;
detailGuides: Record<string, GuideState>;
}
// 读取状态
export const getUserGuideState = (): UserGuideState => {
const state = localStorage.getItem('userGuideState');
return state ? JSON.parse(state) : { session: 'pending', detailGuides: {} };
};
// 更新状态
export const setUserGuideState = (state: UserGuideState) => {
localStorage.setItem('userGuideState', JSON.stringify(state));
};

我们定义了三种状态:

  • pending:引导进行中,用户还未完成或跳过
  • dismissed:用户主动关闭了引导
  • completed:用户完成了所有步骤

对于提案详情页的引导,我们还支持更细粒度的状态追踪(通过 detailGuides 字典),因为一个提案可能会经历多个阶段(草稿、审核、执行完成),每个阶段都需要不同的引导。毕竟,事情的状态总是在变化的。

driver.js 使用 CSS 选择器来定位目标元素。HagiCode 采用了一个约定:使用 data-guide 自定义属性来标记引导目标:

const steps = [
{
element: '[data-guide="launch"]',
popover: {
title: '开始新对话',
description: '点击这里创建一个新的对话会话...'
}
}
];

在组件中这样使用:

<button data-guide="launch" onClick={handleLaunch}>
新建对话
</button>

这种做法的好处是:

  • 避免与业务样式类名冲突
  • 语义清晰,一眼就能看出这个元素与引导相关
  • 便于统一管理和维护

因为引导功能只在特定场景下才需要(比如新用户第一次访问),我们采用动态导入来优化初始加载性能:

const initNewUserGuide = async () => {
// 动态导入 driver.js
const { driver } = await import('driver.js');
await import('driver.js/dist/driver.css');
// 初始化引导
const newConversationDriver = driver({
// ...配置
});
newConversationDriver.drive();
};

这样 driver.js 及其样式文件只会在需要时才加载,不会影响首屏性能。毕竟,谁愿意为暂时用不到的东西付出等待的代价呢?

HagiCode 实现了两条引导路径,覆盖了用户的核心使用场景。

这条引导帮助用户完成从创建对话到提交第一个完整提案的整个流程:

  1. launch - 启动引导,介绍”新建对话”按钮
  2. compose - 引导用户在输入框中输入请求
  3. send - 引导点击发送按钮
  4. proposal-launch-readme - 引导创建 README 提案
  5. proposal-compose-readme - 引导编辑 README 请求内容
  6. proposal-submit-readme - 引导提交 README 提案
  7. proposal-launch-agents - 引导创建 AGENTS.md 提案
  8. proposal-compose-agents - 引导编辑 AGENTS.md 请求
  9. proposal-submit-agents - 引导提交 AGENTS.md 提案
  10. proposal-wait - 说明 AI 正在处理,请稍候

这条引导的设计思路是:通过两个实际的提案创建任务(README 和 AGENTS.md),让用户亲手体验 HagiCode 的核心工作流。毕竟,纸上得来终觉浅,绝知此事要躬行。

下面这几张图,对应的就是会话引导里的几个关键节点:

会话引导:从创建普通会话开始

会话引导的第一步,先把用户带到“新建普通会话”的入口上。

会话引导:输入第一句请求

接着引导用户在输入框里写下第一句请求,降低第一次开口的门槛。

会话引导:发送第一条消息

输入完成后,再明确提示用户发送第一条消息,让操作路径更连贯。

会话引导:等待会话列表继续执行

当两个提案都创建完成后,引导会回到会话列表,让用户知道接下来只需要等待系统继续执行和刷新。

当用户进入提案详情页时,根据提案的当前状态触发对应的引导:

  1. drafting(草稿阶段)- 引导用户查看 AI 生成的计划
  2. reviewing(审核阶段)- 引导用户执行计划
  3. executionCompleted(完成阶段)- 引导用户归档计划

这条引导的特点是状态驱动——根据提案的实际状态动态决定显示哪个引导步骤。事物总是在变化,引导也应该跟着变化才是。

下面这张图展示的是提案详情页在“起草阶段”的引导状态:

提案详情引导:起草阶段先生成规划

在这个阶段,引导会把用户注意力聚焦到“生成规划”这个关键动作上,避免第一次进入详情页时不知道该先做什么。

在 React 应用中,引导目标元素可能还没渲染完成(比如等待异步数据加载)。为了处理这种情况,HagiCode 实现了一个重试机制:

const waitForElement = (selector: string, maxRetries = 10, interval = 100) => {
let retries = 0;
return new Promise<HTMLElement>((resolve, reject) => {
const checkElement = () => {
const element = document.querySelector(selector) as HTMLElement;
if (element) {
resolve(element);
} else if (retries < maxRetries) {
retries++;
setTimeout(checkElement, interval);
} else {
reject(new Error(`Element not found: ${selector}`));
}
};
checkElement();
});
};

在初始化引导前调用这个函数,确保目标元素已经存在。有时候,多等待一下也是值得的。

基于 HagiCode 的实践经验,这里分享几个关键的最佳实践:

不要强制用户完成引导。有些用户是探索型的,他们更喜欢自己摸索。提供清晰的”跳过”按钮,并记住用户的选择,下次不再打扰。毕竟,美的事物或人,不一定要占有,只要她还是美的,自己好好看着她的美就好了。

每个引导步骤应该聚焦于单一目标:

  • Title:简短清晰,不超过 10 个字
  • Description:直击要点,告诉用户”这是啥”和”为啥要用”

避免长篇大论的说明——用户在引导阶段的注意力是很有限的。话说多了,反而没人愿意看。

使用稳定的、不频繁变化的元素标记方式。data-guide 自定义属性是一个好选择,避免依赖 class 名或 DOM 结构,因为这些很容易在重构中变化。代码总是在变化的,但有些东西应该尽量保持稳定。

HagiCode 为引导功能编写了完整的测试用例:

describe('NewUserConversationGuide', () => {
it('应该正确初始化引导状态', () => {
const state = getUserGuideState();
expect(state.session).toBe('pending');
});
it('应该正确更新引导状态', () => {
setUserGuideState({ session: 'completed', detailGuides: {} });
const state = getUserGuideState();
expect(state.session).toBe('completed');
});
});

测试可以确保在重构代码时不会不小心破坏引导功能。毕竟,谁也不希望改点代码就把之前的功能搞坏了。

  • 使用动态导入延迟加载引导库
  • 避免在用户已经完成引导后仍然初始化引导逻辑
  • 考虑引导动画的性能影响,低端设备上可以关闭动画

性能这东西,就像生活一样,该省的地方还是要省的。

新用户引导是提升产品用户体验的重要环节。在 HagiCode 项目中,我们使用 driver.js 构建了一套完整的引导系统,覆盖了从会话创建到提案执行的整个工作流。

通过本文的分享,我们希望传达的核心观点是:

  1. 技术选型要匹配需求:driver.js 不是最强的,但对我们来说是最合适的
  2. 状态管理很关键:用 localStorage 持久化引导状态,避免重复打扰用户
  3. 引导设计要聚焦:每个步骤解决一个问题,不要贪多
  4. 代码结构要清晰:分离引导配置、状态管理和 UI 逻辑,便于维护

如果你正在为自己的项目添加新用户引导功能,希望本文的实践经验能对你有所帮助。其实技术这东西,也没什么神秘的,多尝试,多总结,慢慢就好了…

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。 本内容采用人工智能辅助协作,最终内容由作者审核并确认。