只需几分钟即可在您的应用中构建类似 Figma 和 Google Docs 的实时状态功能🚀🔥🧑‍💻

总结

了解在您的应用中实现协作功能的挑战和解决方案。我们在本教程中使用 Velt 构建了一个实时“谁在线?”墙。

其特点包括:

  • 实时显示在线用户及他们的光标。
  • 用户身份验证和文档上下文管理。
  • 具有评论和退出选项的可定制用户界面。
  • 本指南为创建引人入胜的协作工具奠定了基础。为了进一步增强它,请考虑添加反应、通知和安全登录方法等功能。

    开始吧🚀!

    现代网络应用程序的协作性越来越强。想象一下,在 Figma 中看到那些彩色光标移动,或者在 Google Docs 中看到那些显示谁在查看文档的个人资料气泡,感觉有多自然。这些在线状态功能已成为任何协作应用程序的必备功能。

    您是否知道,当用户能够看到其他用户实时积极协作时,97% 的用户更有可能继续使用产品?这背后的心理非常有趣 - 我们自然而然地被能够看到其他人与我们一起工作的空间所吸引,即使是在数字环境中。

    Users collaborating in real-time

    从头开始构建 Presence 功能的挑战

    构建在线状态功能乍一看似乎很简单 - 只需跟踪谁在线,对吧?但正如许多开发人员发现的那样,它很快就会变得复杂。以我最近从事的一个项目为例:我们开始使用一个简单的 websocket 连接来显示活跃用户,但当我们需要处理不稳定的连接和浏览器标签时,事情就变得混乱了。

    我们先来看看后端。您需要一个强大的系统来维护跨多个服务器实例的 websocket 连接。以下是使用 Redis 的常见模式:

    // Server-side presence tracking
    const presence = new Map();
    redis.subscribe('presence', (channel, message) => {
       const { userId, status } = JSON.parse(message);
       presence.set(userId, status);
       broadcastPresence();
    });

    前端也带来了自己的挑战。有没有注意到,当你切换标签时,Google Docs 会将你显示为“离开”?实现准确的在线状态意味着要处理各种浏览器事件:

    // Frontend presence detection
    window.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            updatePresence('away');
        } else {
            updatePresence('active');
        }
    });
    User state transitions diagram

    最棘手的部分之一是区分真正离线的用户和暂时断开连接的用户。您可能认为简单的超时就可以了:

    // WARNING: Oversimplified approach
    socket.on('disconnect', () => {
        setTimeout(() => markUserOffline(userId), 30000);
    });

    但现实世界的连接情况则更为复杂。用户可能连接不良,他们的笔记本电脑可能会进入睡眠状态,或者他们可能只是关闭笔记本电脑而没有正确断开连接。更强大的解决方案需要心跳机制和清理功能来处理这些极端情况。

    Network connectivity scenarios

    对于在全球运营的公司,多区域支持又增加了一层复杂性。您的在线状态系统需要以最小的延迟同步不同地理区域的用户状态。这通常涉及在每个区域设置在线状态服务器并实施复杂的状态协调:

    // Multi-region presence sync
    function syncPresenceAcrossRegions(userId, status) {
        const regions = ['us-east', 'eu-west', 'ap-south'];
        regions.forEach(region => {
            if (region !== currentRegion) {
                notifyRegion(region, userId, status);
            }
        });
    }

    好消息是,您不必再从头开始构建所有这些。现代解决方案可以处理这些极端情况,同时让您能够灵活地为用户定制体验。无论您是构建下一个 Figma 还是为应用添加基本协作,了解这些挑战都可以帮助您做出更好的架构决策。

    SDK 为何越来越受欢迎

    开发人员正在转向使用 SDK 来构建状态功能,因为这比从头构建一切更实用。原因如下:

    **1. 节省时间 -** 您无需花费数周时间处理网络断开连接或浏览器选项卡等边缘情况,只需几个小时即可集成状态。

    **2. 经过验证的解决方案 -** 流行的 SDK 已经解决了一些常见问题,例如:

  • 管理不稳定的互联网连接
  • 处理多个浏览器选项卡
  • 用户离开时进行清理
  • 跨服务器同步状态
  • **3. 经济高效 -** 构建和维护自定义存在系统所需的时间和资源通常比使用 SDK 花费更多。

    **SDK 发挥作用的真实示例:**

  • 文档编辑器显示谁正在查看或编辑
  • 显示在线/离线状态的聊天应用程序
  • 设计工具可显示其他用户的工作位置
  • 会议平台显示当前发言者
  • 数据分析平台在仪表板上显示实时协作者
  • 视频编辑软件显示谁在哪个时间线片段上工作
  • 选择构建自定义还是使用 SDK 取决于您的具体需求。如果您需要能够正常工作的基本状态功能,SDK 通常是最佳选择。自定义解决方案主要适用于特殊需求或需要完全控制实施的情况。

    Velt 是用于构建状态功能的最流行的 SDK 之一。从本质上讲,Velt 提供了 Figma、Google Docs 等流行应用中的一整套协作功能。它们处理实时协作功能所需的复杂基础架构。它管理跨用户和会话的 WebSocket 连接、状态同步和状态跟踪。

    它对开发人员特别有用的一点是,它消除了构建实时功能的常见麻烦,例如处理连接中断、管理多个选项卡上的状态以及清理过时的会话。SDK 为常见的状态模式提供了现成的组件,同时仍允许在需要时对状态数据进行低级访问。

    在您的项目中设置 Velt 并添加存在

    让我们构建一个实时的“谁在线?”墙,显示您网站上的活跃用户。我们将使用 Next.js 15 和 TypeScript 以及 Velt 的状态功能。

    它看起来是这样的:

    项目设置

    首先,使用 TypeScript 创建一个新的 Next.js 项目:

    npx create-next-app@latest whos-online --typescript --tailwind --app
    cd whos-online

    安装 Velt SDK:

    npm install @veltdev/react
    npm install --save-dev @veltdev/types

    配置

    前往 Velt 控制台并获取您的 Velt API 密钥。这将用于验证您对 Velt API 的请求。

    然后将此 API 密钥存储在您的 `.env` 文件中:

    NEXT_PUBLIC_VELT_API_KEY=your_api_key

    创建一个新的提供程序组件来初始化 Velt:

    'use client'
    
    import { VeltProvider as BaseVeltProvider } from "@veltdev/react"
    
    export function VeltProvider({ children }: { children: React.ReactNode }) {
      return (
        
          {children}
        
      )
    }

    接下来,在你的根组件中,将你的应用程序包装在 `VeltProvider` 组件中:

    import type { Metadata } from "next";
    import { Geist, Geist_Mono } from "next/font/google";
    import "./globals.css";
    import { VeltProvider } from "./provider/VeltProvider";
    
    const geistSans = Geist({
      variable: "--font-geist-sans",
      subsets: ["latin"],
    });
    
    const geistMono = Geist_Mono({
      variable: "--font-geist-mono",
      subsets: ["latin"],
    });
    
    export const metadata: Metadata = { // Next.js metadata
      title: "Who's Online?",
      description: "A real-time presence feature built with Velt",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
         // VeltProvider is a component that provides the Velt context to the app
          
            
              {children}
            
          
        
      );
    }

    验证用户

    首先,让我们为用户数据创建一个类型:

    export interface UserData { // UserData is an interface that defines the structure of the user data
      userId: string;
      name: string;
      email: string;
      photoUrl?: string;
      color: string;
      textColor: string;
    }

    现在,我们需要一种方法让 Velt 知道用户是谁。有一个 `useVeltClient` 钩子可用于识别用户。它的工作方式如下:

    import { useVeltClient } from '@veltdev/react';
    
    const { client } = useVeltClient();
    
    // Perform authentication
    
    client.identify(user); // here user is the user data that you want to identify the user with

    接下来,我们还需要设置文档上下文。这是用户将与之交互的文档。

    const { client } = useVeltClient();
    
    useEffect(() => {
        if (client) {
            client.setDocument('unique-document-id', {documentName: 'Document Name'});
        }
    }, [client]);

    client.setDocument 方法接受两个参数:

  • 第一个参数是 documentId。这是您要为其设置上下文的文档的唯一标识符。
  • 第二个参数是包含文档元数据的对象。这是一个键值对对象,可用于存储有关文档的任何元数据。
  • 在我们简单的“谁在线?”应用中,我们将要求用户输入他们的姓名和电子邮件,然后使用 Velt 识别用户。

    'use client'
    
    import { useState, useEffect } from 'react';
    import { useVeltClient } from '@veltdev/react';
    import { UserData } from '../types';
    import { User } from '@veltdev/types';
    
    const generateRandomColor = () => { // generateRandomColor is a function that generates a random color
      const hue = Math.floor(Math.random() * 360);
      const pastelSaturation = 70;
      const pastelLightness = 80;
      return `hsl(${hue}, ${pastelSaturation}%, ${pastelLightness}%)`;
    };
    
    const getContrastColor = (backgroundColor: string) => { // getContrastColor is a function that returns a contrasting color for the given background color
      const hsl = backgroundColor.match(/\d+/g)?.map(Number);
      if (!hsl) return '#000000';
    
      const lightness = hsl[2];
      return lightness > 70 ? '#000000' : '#ffffff';
    };
    
    export function UserAuth() { // UserAuth is a component that allows the user to authenticate
      const { client } = useVeltClient();
      const [isAuthenticated, setIsAuthenticated] = useState(() => { // state to check if the user is authenticated
        return !!localStorage.getItem('userData');
      });
    
      useEffect(() => {
        const initializeUser = async () => { // function to set the user data
          const savedUser = localStorage.getItem('userData');
          if (savedUser && client) {
            const userData: UserData = JSON.parse(savedUser);
            await client.identify(userData as User);
            client.setDocument('whos-online-wall', {documentName: 'Who\'s Online?'});
          }
        };
    
        if (client) {
          initializeUser();
        }
      }, [client]);
    
      const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        const name = formData.get('name') as string;
        const email = formData.get('email') as string;
    
        if (!name || !email || !client) return;
    
        const backgroundColor = generateRandomColor();
        const userData: UserData = {
          userId: email,
          name,
          email,
          color: backgroundColor,
          textColor: getContrastColor(backgroundColor)
        };
    
        localStorage.setItem('userData', JSON.stringify(userData));
        await client.identify(userData as User);
        client.setDocument('whos-online-wall', {documentName: 'Who\'s Online?'});
        setIsAuthenticated(true);
      };
    
      if (isAuthenticated) return null; // if the user is authenticated, return null
    
      return ( // actual component
        

    Welcome! 👋 Please introduce yourself

    ); }

    在这里,我们需要用户提供姓名和电子邮件,然后将这些数据存储在本地存储中。同时,我们使用“client.identify”方法通过 Velt 识别用户。

    然后我们使用 `client.setDocument` 方法设置文档上下文。您可以在此处了解有关身份验证的更多信息,并在此处设置文档上下文。

    **让我们看看它在浏览器中是如何工作的:**

    UserAuth

    添加在线墙

    现在我们已经完成了用户身份验证并设置了文档上下文,我们可以将在线墙添加到我们的应用程序中。

    为此,我们将使用“VeltPresence”组件。该组件将自动为我们处理存在跟踪。

    'use client'
    
    import { useVeltClient, VeltPresence } from '@veltdev/react'; // import the VeltPresence component
    
    export function OnlineWall() { // OnlineWall is a component that shows the online wall
      const { client } = useVeltClient();
    
      if (!client) return null; // if the client is not initialized, return null
    
      return (
        
    ); }

    现在加入墙后,您应该可以看到已加入墙的用户的在线墙。

    OnlineWall

    我从三个不同的窗口加入,您可以看到已加入墙的用户。请确保通过两个不同的浏览器配置文件或隐身标签加入,以防止用户之间发生冲突。

    但是,如果我想为墙添加自定义 UI 怎么办?如果我想允许用户离开墙怎么办?

    让我们看看如何做到这一点。

    自定义在线墙并允许用户离开

    我们可以使用 `usePresenceUsers` 钩子自定义在线墙 UI。此钩子返回当前在线的用户列表。然后我们可以使用此列表在我们自己的自定义 UI 中呈现用户。

    为了允许用户离开墙,我们可以使用`client.signOutUser`方法。

    这是墙上的注销按钮:

    'use client'
    
    import { useVeltClient } from '@veltdev/react';
    
    export function LogoutButton() {
      const { client } = useVeltClient();
    
      const handleLogout = async () => {
        if (client) {
          await client.signOutUser(); // this will sign out the user from the current document
          localStorage.removeItem('userData'); // this will remove the user data from local storage
          window.location.reload(); // this will reload the page
        }
      };
    
      const isAuthenticated = !!localStorage.getItem('userData');
      if (!isAuthenticated) return null;
    
      return (
        
      );
    }

    现在让我们更新 `OnlineWall` 组件以使用 `usePresenceUsers` 钩子并更新 UI 以在漂亮的网格中显示用户。

    'use client'
    
    import { useVeltClient, usePresenceUsers } from '@veltdev/react';
    import { motion } from 'framer-motion';
    
    export function OnlineWall() {
      const { client } = useVeltClient();
      const presenceUsers = usePresenceUsers();
    
      if (!client) return null;
    
      // Add loading state check
      if (!presenceUsers) {
        return (
          
    {/* Outer spinning ring with gradient */}
    {/* Inner white circle */}
    {/* Middle spinning ring with gradient */}
    {/* Inner white circle */}
    {/* Center dot with pulse effect */}
    ); } // Get current user data from localStorage const currentUserData = localStorage.getItem('userData'); const currentUser = currentUserData ? JSON.parse(currentUserData) : null; // Sort users to put current user first const sortedUsers = presenceUsers?.sort((a, b) => { if (a.userId === currentUser?.userId) return -1; if (b.userId === currentUser?.userId) return 1; return 0; }); return ( // actual component
    {sortedUsers?.map((user) => { const isCurrentUser = user.userId === currentUser?.userId; return ( {isCurrentUser && (
    You
    )}
    {user.photoUrl ? ( {user.name} ) : (
    {user.name?.charAt(0).toUpperCase()}
    )}

    {user.name}

    {user.email}

    Online now
    ); })}
    ); }

    在这里,我们在一个漂亮的网格中显示用户,并且我们还用“您”的徽章指示当前用户。

    这里我还展示了获取在场用户时的加载状态。这可以使用 `usePresenceUsers` 钩子并检查用户是否为空来完成。

    它看起来是这样的:

    Enhanced Online Wall

    退出登录非常简单,只需单击退出按钮即可。

    看起来怎么样?

    Awesome GIF

    这有多简单?只需几行代码,我们就为应用程序添加了状态功能。

    在线墙上显示光标

    让我们更进一步,在在线墙上显示用户的光标。这意味着无论用户在文档中做什么,我们都会在在线墙上显示用户的光标。

    为此,我们将使用“VeltCursor”组件。此组件将自动为我们处理光标跟踪。这就像将组件添加到根一样简单。

    Velt 光标实现的特别之处在于它不仅跟踪原始 x、y 坐标,还能智能地适应底层内容结构。这意味着即使用户的屏幕尺寸、缩放级别或布局不同(例如响应式设计更改),光标也始终会相对于他们正在交互的内容出现在正确的位置。这种对文档结构的语义理解可确保在所有客户端上实现一致的光标定位。

    这里的根是“OnlineWall”组件。它可以在单独的根组件中更好地处理,但现在让我们保持简单。

    <>
           // just add this to the root 
          
    {sortedUsers?.map((user) => { const isCurrentUser = user.userId === currentUser?.userId; return ( {isCurrentUser && (
    You
    )}
    {user.photoUrl ? ( {user.name} ) : (
    {user.name?.charAt(0).toUpperCase()}
    )}

    {user.name}

    {user.email}

    Online now
    ); })}

    我们将组件包装在片段中,因为我们将“VeltCursor”组件添加到根目录,这样就可以向根目录添加更多组件。

    这可以用更好的方式来维护,但这是一个快速而肮脏的解决方案。

    在我们的在线墙上添加评论

    如果我们想让墙更具互动性,并允许用户在墙上的任何地方发表评论,该怎么办?使用 Velt 可以变得非常简单。

    我们可以使用`VeltComments`和`VeltCommentTool`组件允许用户在墙上的任何地方留下评论。

    工作原理如下:

    <>
          
          
          
    {sortedUsers?.map((user) => { const isCurrentUser = user.userId === currentUser?.userId; return ( {isCurrentUser && (
    You
    )}
    {user.photoUrl ? ( {user.name} ) : (
    {user.name?.charAt(0).toUpperCase()}
    )}

    {user.name}

    {user.email}

    Online now
    ); })}

    让我们看看最终的应用程序是什么样子的:

    太简单了!现在我们的墙更具互动性和吸引力了。

    项目概要

    在这个项目中,我们使用 Velt 构建了一个实时“谁在线?”墙。我们学会了如何:

  • 在我们的项目中设置 Velt
  • 使用 Velt 验证用户身份
  • 为当前用户设置文档上下文
  • 使用 VeltPresence 组件显示在线用户
  • 使用 usePresenceUsers 钩子自定义在线墙 UI
  • 允许用户离开墙并退出
  • 显示在线墙上用户的光标
  • 允许用户在墙上的任何地方发表评论
  • 使用 Velt 的好处

    从头开始构建状态功能通常是一个有趣的周末项目。您可以设置 WebSocket 连接、跟踪用户状态并获得一个在本地运行的基本版本。然后现实来了 - 处理断开连接、跨区域同步和管理浏览器怪癖将这个周末项目变成了数周的调试。

    Velt 处理这些复杂性,同时仍为您提供所需的控制。您无需费心处理 WebSocket 重新连接逻辑或消除存在更新抖动,而是可以专注于打造实际的用户体验。这就像使用经过实战考验的身份验证系统,而不是自己动手加密 - 当然,您可以构建它,但为什么要这样做呢?

    当您的应用发展壮大时,真正的价值就会显现出来。当小型团队协作工具突然需要处理数百个并发用户时,或者当您的仅限美国的应用需要向全球扩展时,您无需重写您的状态基础架构。处理 10 个用户的相同 SDK 将可扩展以处理数千个用户。

    Velt

    **开发人员欣赏的一些实际好处:**

  • 无需维护 WebSocket 服务器和连接逻辑
  • 内置处理网络问题和浏览器选项卡同步
  • 与现有身份验证系统简单集成
  • 自动清理过时的状态数据
  • 想象一下使用 Redis 进行缓存 - 您可以构建自己的缓存系统吗?当然可以。但 Redis 为您提供了一个行之有效的解决方案,让您可以解决实际的业务问题。

    最好的部分是,您不会被锁定在特定的实现中。想要在应用的不同部分以不同的方式显示状态?需要添加自定义状态?SDK 为您提供构建块,同时让您控制一切的外观和行为。

    Velt 的其他功能

    除了状态和实时光标之外,Velt 还提供了一些强大的协作功能:

  • 实时反应——添加实时出现的浮动表情符号和反应
  • 跟随模式 - 让用户跟随彼此的动作和行为。非常适合演示和指导教程
  • Huddles - 在您的应用内创建即时音频/视频会议空间。与您现有的 UI 无缝集成
  • 实时选择 - 实时选择并突出显示文本。非常适合协作编辑
  • 视频播放器同步 - 在用户之间同步视频播放器。非常适合视频会议和演示
  • 在此处查看更多功能

    结论

    在本文中,我们了解了使用 Velt 为您的应用添加在线状态功能是多么容易。我们学习了如何设置 Velt、验证用户身份、设置文档上下文以及使用“VeltPresence”组件显示在线用户。我们还了解了如何使用“usePresenceUsers”钩子自定义在线墙 UI,并允许用户离开墙并退出。

    您可以通过添加更多功能(如实时反应、跟随我模式、聚会、通知等)进一步增强我们的在线墙!

    在我们的墙中,我们没有实现一种安全的用户登录方式。我们将把这留给您作为练习来实现。您可以使用任何社交登录提供商或电子邮件/密码身份验证,并配置 Velt 身份验证提供商以使用它。

    我希望本文能让您很好地了解如何使用 Velt 在您的应用中构建存在功能。

    祝你编码愉快!

    要了解有关 Velt 的更多信息并开始使用,请查看:

  • Velt 文档
  • API 参考
  • 使用案例
  • 示例应用程序
  • **如果你 ❤️ 我的内容!在 Twitter 上关注我**

    检查我使用的 SaaS 工具👉🏼在这里访问!

    我愿意在博客文章和来宾帖子上进行合作🫱🏼‍🫲🏼 📅联系这里