构建聊天 - 使用 React、Websockets 和 Web-Push 实现浏览器通知 🤯

2025-05-27

构建聊天 - 使用 React、Websockets 和 Web-Push 实现浏览器通知 🤯

喜欢这篇文章吗?
在 X 上关注我:
https://x.com/nevodavid


这篇文章是关于什么的?

我们都接触过网络聊天,比如 Facebook、Instagram、Whatsapp 等等。
简单介绍一下背景:你给一个人或一个群组发消息,他们看到后会回复。简单又复杂。

在本系列的前一篇文章中,我们讨论了Socket.io,如何在 React 应用程序客户端和 Socket.io 服务器之间发送消息,如何在 Web 应用程序中获得活跃用户,以及如何添加大多数现代聊天应用程序中存在的“用户正在输入...”功能。

在最后一篇文章中,我们将扩展聊天应用程序的功能。您将学习如何在用户离线时通过发送桌面通知来保持他们的参与度,以及如何读取消息并将其保存到 JSON 文件中。然而,这并非在聊天应用程序中存储消息的安全方式。您可以自由选择任何数据库来构建您自己的应用程序。

推送通知

如何向用户发送桌面消息

在这里,我将指导您在离线用户有新的聊天消息时向他们发送桌面通知。

聊天

Novu——第一个开源通知基础设施

简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。通知可以是应用内通知(类似 Facebook 的铃铛图标 - Websockets)、电子邮件、短信等等。
如果您能给我们一个 star,我将非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu

诺武

在上一篇文章中,我们创建了ChatFooter一个包含表单的组件,该表单带有一个输入字段和一个发送按钮。由于我们将在用户发送消息后立即发送通知,因此桌面通知功能将存在于此。

聊天

请按照以下步骤操作:

更新ChatFooter.js组件,使其包含一个名为 的函数checkPageStatus,该函数在消息发送到 Socket.io 服务器后运行。该函数接受用户名和用户的消息。



import React, {useState} from 'react'

const ChatFooter = ({socket}) => {
    const [message, setMessage] = useState("")
    const handleTyping = () => socket.emit("typing",`${localStorage.getItem("userName")} is typing`)

    const handleSendMessage = (e) => {
        e.preventDefault()
        if(message.trim() && localStorage.getItem("userName")) {
        socket.emit("message", 
            {
            text: message, 
            name: localStorage.getItem("userName"), 
            id: `${socket.id}${Math.random()}`
            }) 
                //Here it is 👇🏻
        checkPageStatus(message, localStorage.getItem("userName")) 
        }}
        setMessage("")
    }

    //Check PageStatus Function
    const checkPageStatus = () => {

    }

  return (
    <div className='chat__footer'>
        <form className='form' onSubmit={handleSendMessage}>
          <input 
            type="text" 
            placeholder='Write message' 
            className='message' 
            value={message} 
            onChange={e => setMessage(e.target.value)}
            onKeyDown={handleTyping}
            />
            <button className="sendBtn">SEND</button>
        </form>
     </div>
  )
}

export default ChatFooter


Enter fullscreen mode Exit fullscreen mode

ChatFooter通过将checkPageStatus函数移动到文件夹中来整理组件src/utils。创建一个名为 的文件夹utils



cd src
mkdir utils


Enter fullscreen mode Exit fullscreen mode

utils在包含该函数的文件夹中创建一个 JavaScript 文件checkPageStatus



cd utils
touch functions.js


Enter fullscreen mode Exit fullscreen mode

将下面的代码复制到functions.js文件中。



export default function checkPageStatus(message, user){

}


Enter fullscreen mode Exit fullscreen mode

更新ChatFooter组件以包含文件中新创建的函数utils/functions.js



import React, {useState} from 'react'
import checkPageStatus from "../utils/functions"
//....Remaining codes


Enter fullscreen mode Exit fullscreen mode

functions.js您现在可以按如下所示更新文件中的函数:



export default function checkPageStatus(message, user) {
    if(!("Notification" in window)) {
      alert("This browser does not support system notifications!")
    } 
    else if(Notification.permission === "granted") {
      sendNotification(message, user)
    }
    else if(Notification.permission !== "denied") {
       Notification.requestPermission((permission)=> {
          if (permission === "granted") {
            sendNotification(message, user)
          }
       })
    }
}


Enter fullscreen mode Exit fullscreen mode

从上面的代码片段可以看出,  JavaScript 通知 API  用于配置和向用户显示通知。它有三个属性表示其当前状态。它们是:

  • 拒绝 — 不允许通知。
  • 已授予 - 允许通知。
  • 默认 - 用户选择未知,因此浏览器将按通知被禁用的方式运行。(我们对此不感兴趣)

第一个条件语句(if)检查 JavaScript 通知 API 在 Web 浏览器上是否可用,然后警告用户浏览器不支持桌面通知。

第二个条件语句检查是否允许通知,然后调用该sendNotification函数。

最后一个条件语句检查通知是否未被禁用,然后在发送通知之前请求权限状态。

接下来,创建sendNotification上面代码片段中引用的函数。



//utils/functions.js
function sendNotification(message, user) {

}
export default function checkPageStatus(message, user) {
  .....
}


Enter fullscreen mode Exit fullscreen mode

更新sendNotification功能以显示通知的内容。



/*
title - New message from Open Chat
icon - image URL from Flaticon
body - main content of the notification
*/
function sendNotification(message, user) {
    const notification = new Notification("New message from Open Chat", {
      icon: "https://cdn-icons-png.flaticon.com/512/733/733585.png",
      body: `@${user}: ${message}`
    })
    notification.onclick = ()=> function() {
      window.open("http://localhost:3000/chat")
    }
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段代表通知的布局,当点击时,它会将用户重定向到http://localhost:3000/chat

恭喜!💃🏻 我们已经能够在用户发送消息时向其显示桌面通知。在下一节中,您将学习如何向离线用户发送提醒。

💡 离线用户是指当前未浏览网页或未连接到互联网的用户。当他们登录互联网时,将会收到通知。

如何检测用户是否正在浏览你的网页

在本节中,您将学习如何通过  JavaScript 页面可见性 API检测聊天页面上的活跃用户。它允许我们跟踪页面何时最小化、关闭、打开以及用户何时切换到另一个选项卡。

接下来我们来使用API​​给离线用户发送通知。

更新sendNotification功能以仅当用户离线或在另一个选项卡上时发送通知。



function sendNotification(message, user) {
    document.onvisibilitychange = ()=> {
      if(document.hidden) {
        const notification = new Notification("New message from Open Chat", {
          icon: "https://cdn-icons-png.flaticon.com/512/733/733585.png",
          body: `@${user}: ${message}`
        })
        notification.onclick = ()=> function() {
          window.open("http://localhost:3000/chat")
        }
      }
    }  
}


Enter fullscreen mode Exit fullscreen mode

从上面的代码片段来看,document.onvisibilitychange它会检测可见性变化,并document.hidden在发送通知之前检查用户是否位于其他选项卡或浏览器是否已最小化。您可以点击此处了解更多关于不同状态的信息。

接下来,更新checkPageStatus功能以向发件人以外的所有用户发送通知。



export default function checkPageStatus(message, user) {
  if(user !== localStorage.getItem("userName")) {
    if(!("Notification" in window)) {
      alert("This browser does not support system notifications!")
    } else if(Notification.permission === "granted") {
      sendNotification(message, user)
    }else if(Notification.permission !== "denied") {
       Notification.requestPermission((permission)=> {
          if (permission === "granted") {
            sendNotification(message, user)
          }
       })
    }
  }     
}


Enter fullscreen mode Exit fullscreen mode

恭喜!🎉您现在可以向离线用户发送通知。

可选:如何将消息保存到 JSON“数据库”文件

在本节中,您将学习如何将消息保存到 JSON 文件中——为了简单起见。此时,您可以随意使用您选择的任何实时数据库,如果您有兴趣学习如何使用 JSON 文件作为数据库,可以继续阅读。

server/index.js我们将在本文的剩余部分继续引用该文件。



//index.js file
const express = require("express")
const app = express()
const cors = require("cors")
const http = require('http').Server(app);
const PORT = 4000
const socketIO = require('socket.io')(http, {
    cors: {
        origin: "http://localhost:3000"
    }
});

app.use(cors())
let users = []

socketIO.on('connection', (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`)  
    socket.on("message", data => {
      console.log(data)
      socketIO.emit("messageResponse", data)
    })

    socket.on("typing", data => (
      socket.broadcast.emit("typingResponse", data)
    ))

    socket.on("newUser", data => {
      users.push(data)
      socketIO.emit("newUserResponse", users)
    })

    socket.on('disconnect', () => {
      console.log('🔥: A user disconnected');
      users = users.filter(user => user.socketID !== socket.id)
      socketIO.emit("newUserResponse", users)
      socket.disconnect()
    });
});

app.get("/api", (req, res) => {
  res.json({message: "Hello"})
});


http.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});


Enter fullscreen mode Exit fullscreen mode

从 JSON 文件中检索消息

导航到服务器文件夹并创建一个messages.json文件。



cd server
touch messages.json


Enter fullscreen mode Exit fullscreen mode

通过复制以下代码向文件添加一些默认消息 - 一个包含默认消息的数组。



"messages": [
        {
           "text": "Hello!",
           "name": "nevodavid",
           "id": "abcd01" 
        }, {
            "text": "Welcome to my chat application!💃🏻",
           "name": "nevodavid",
           "id": "defg02" 
        }, {
            "text": "You can start chatting!📲",
           "name": "nevodavid",
           "id": "hijk03" 
        }
    ]
}


Enter fullscreen mode Exit fullscreen mode

通过将下面的代码片段添加到文件顶部,将文件导入并读messages.json入文件。server/index.js



const fs = require('fs');
//Gets the messages.json file and parse the file into JavaScript object
const rawData = fs.readFileSync('messages.json');
const messagesData = JSON.parse(rawData);


Enter fullscreen mode Exit fullscreen mode

通过 API 路由呈现消息。



//Returns the JSON file
app.get('/api', (req, res) => {
  res.json(messagesData);
});


Enter fullscreen mode Exit fullscreen mode

现在我们可以通过组件在客户端获取消息ChatPage。每个用户登录聊天应用程序时都会显示默认消息。



import React, { useEffect, useState, useRef} from 'react'
import ChatBar from './ChatBar'
import ChatBody from './ChatBody'
import ChatFooter from './ChatFooter'

const ChatPage = ({socket}) => { 
  const [messages, setMessages] = useState([])
  const [typingStatus, setTypingStatus] = useState("")
  const lastMessageRef = useRef(null);

/**  Previous method via Socket.io */
  // useEffect(()=> {
  //   socket.on("messageResponse", data => setMessages([...messages, data]))
  // }, [socket, messages])

/** Fetching the messages from the API route*/
    useEffect(()=> {
      function fetchMessages() {
        fetch("http://localhost:4000/api")
        .then(response => response.json())
        .then(data => setMessages(data.messages))
      }
      fetchMessages()
  }, [])

 //....remaining code
}

export default ChatPage


Enter fullscreen mode Exit fullscreen mode

将消息保存到 JSON 文件

在上一节中,我们创建了一个messages.json包含默认消息的文件并将该消息显示给用户。

在这里,我将引导您完成messages.json用户从聊天页面发送消息后自动更新文件的操作。

更新服务器上的 Socket.io 消息监听器以包含以下代码:



socket.on("message", data => {
  messagesData["messages"].push(data)
  const stringData = JSON.stringify(messagesData, null, 2)
  fs.writeFile("messages.json", stringData, (err)=> {
    console.error(err)
  })
  socketIO.emit("messageResponse", data)
})


Enter fullscreen mode Exit fullscreen mode

上面的代码片段在用户发送消息后运行。它将新数据添加到messages.json文件中的数组中,并重写它以包含最新更新。

返回聊天页面,发送一条消息,然后刷新浏览器。你的消息将会显示出来。打开messages.json文件即可查看包含新条目的更新文件。

结论

在本文中,您学习了如何向用户发送桌面通知、检测用户当前是否在页面上活动,以及如何读取和更新 JSON 文件。这些功能可以在构建各种应用程序时用于不同的场景。

该项目是您使用Socket.io构建的演示;您可以通过添加身份验证和连接任何支持实时通信的数据库来改进此应用程序。

本教程的源代码可以在这里找到:
https://github.com/novuhq/blog/tree/main/build-a-chat-app-part-two

帮帮我!

如果您觉得这篇文章帮助您更好地理解了 WebSocket!请给我们一个 Star,我会非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu
帮助

感谢您的阅读!

文章来源:https://dev.to/novu/building-a-chat-browser-notifications-with-react-websockets-and-web-push-1h1j
PREV
如何使用 React Native 和 Socket.io 构建最漂亮的 Todolist 🎉
NEXT
如何使用 Three.js 和 React 渲染你自己的 3D 模型