Next.js通过Socket.io和redis实现消息推送模块(一)next如何连接socket.io和自定义服务器

2,353 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言:

最近在完成大三的一个课程期末设计,独立完成做了一个博客社区,主要技术栈是:

前端:next.js + mobx + ts + antd;

后台管理系统:vue3.0 + pinia + ts + elementUI

后端:next.js + ts + 阿里云oss存储 + redis

开发的时候遇到了很多坑,后面会通过文章的方式总结自己在开发过程中踩到的坑以及一些小经验。

由于博客社区嘛,像咱们大掘金平台,是有系统消息、点赞消息啥的等信息推送,然后我也想要做一个横幅(就是系统维护中,或者是出了什么问题,想要用户感知到,所以就想做一个系统消息推送),一开始通过websocket来实现,但是next.js这个框架似乎不太支持,而是支持了一个封装websocket的socket.io这个第三方依赖。所以就使用了socket.io来进行消息推送和在线人数统计的操作,希望大家喜欢~

一、开始

需求分析

如果我们要做一个模仿掘金平台的“消息通知模块”,当其他同学评论、关注、点赞我们的时候就会有“小红点”,并且可以进入消息页面,查看这些信息。

image.png

image.png 一开始我以为掘金这个右上角消息通知的“小红点”是实时的,但是好像并不是,而是需要刷新一下才可以看到最新的消息,然后我最近做的博客社区本来是要通过Websocket来实现统计当前全网站在线的用户数量的。但是之前开发过一个微信小程序里面通过websocket实现了用户之间聊天(即时通信),然后想再锻炼一下这方面的能力,所以就打算通过websocket统计在线人数之外,再来实现一个系统消息推送和指定用户推送信息(就是其他人关注、点赞、评论你,你就可以收到这个信息,并且是实时的“小红点”),当然掘金为什么不是实时的“小红点”,肯定是考虑了性能的问题,毕竟websocket即时通讯也不太稳定。

最后的效果是这样的(请看GIF动图): image.png

说明:左边浏览器登录了用户A,我开了另外一个浏览器并且登录了另外一个账号B,B用户评论A用户,然后A用户可以实时的展示发送的内容(因为已经再消息页面了,所以右上角的喇叭没有小红点) image.png

说明:上面就是左边用户A,在【非消息通知页面】,右边浏览器B用户给A用户评论的时候,左边用户接收到信息,并在左边浏览器右上角喇叭那显示“小红点”(ps:还有点缺陷,就是骨架屏,我这几天再添加一下骨架屏)

模仿掘金平台,做了这个消息页面,包括评论消息、点赞消息、关注消息、系统消息,都是实时的

image.png

image.png

image.png

image.png

websocket和socket.io对比

一开始是想通过websocket实现的,但是找了一圈的资料都没看到next.js里面使用websocket的项目,后面才发现原来next.js里面用的是socket.io

那websocket和socket.io有什么区别呢 参考:zhuanlan.zhihu.com/p/346650330 ** websocket是HTML5最新提出的规范,虽然主流的浏览器都已经支持了,但是还是有一些浏览器是不兼容的,所以为了兼容所有的浏览器,SocketIo奖websocket、ajax和其他的通讯方式都封装成了一个统一的通信接口。所以在使用socketio的时候就不用担心兼容的问题了,以为底层回自动选取最佳的通信方式 ** 也就是说:Websocket是SocketIo的一个子集

image.png

二、(后端)Next.js自定义server

在官方文档中www.nextjs.cn/docs/advanc…

比如:在主目录下的server.js文件中下面这段代码,并且我们通过node server.js来启动这个next.js项目


// server.js
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    // Be sure to pass `true` as the second argument to `url.parse`.
    // This tells it to parse the query portion of the URL.
    const parsedUrl = parse(req.url, true)
    const { pathname, query } = parsedUrl

    if (pathname === '/a') {
      app.render(req, res, '/me', query)
    } else if (pathname === '/b') {
      app.render(req, res, '/you', query)
    } else {
      handle(req, res, parsedUrl)
    }
  }).listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})

说明:这段代码的意思是如果用户准备进入localhost:3000/a这个路径的时候,我们会自动让他跳转到localhost:3000/me这个目录,并且把用户请求方式和请求的参数也带过去,同理如果用户跳转到/b目录的话,也就是我们可以通过自定义服务器的方式来控制一部分请求的页面请求和接口请求等操作,但是我们还是要帮助用户的正常请求,所以后面通过 handle(req, res ,paeseUrl) 来正常请求。

正常请求下,我们其实并不需要自定义服务器,但是如果我们要做一个全局的socket.io的话,也就是要监听处理用户的长连接,我们就需要自定义服务器,来处理用户的长连接请求。

那么如何在next.js中自定义服务器,并且使用socket.io呢

三、(后端)Next.js如何使用Socket.io

找了好久的资料,后面找到了官方18年提供的一个示例代码。medium.com/@markcollin… 下面我们来搭建一个入门版本的next.js + socket.io服务器

// 参考medium的代码
const app = require('express')()
const server = require('http').Server(app)
const io = require('socket.io')(server, {cors:true})
const next = require('next')

const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = 3000;
const nextApp = next({ dev, hostname, port })
const nextHandler = nextApp.getRequestHandler()

let socketPort = 3000

io.on('connect', socket => {
  socket.on('clientOnline', () => {
    console.log('用户上线了')
  })
  socket.on('disconnect', () => {
    console.log('用户关闭了')
  })
})

nextApp.prepare().then(() => {
  app.get('*', (req, res) => {
    return nextHandler(req, res)
  })
  app.post('*', (req, res) => {
    return nextHandler(req, res)
  })

  server.listen(socketPort, err => {
    if (err) throw err
    console.log(`socket io ready on http://localhost:${port}`)
  })
})

注意点:

  • 配置socket.io的跨域通过:const io = require('socket.io')(server, {cors:true})
  • socket.io和next.js项目的端口是同一个,我上面都是定义为3000
  • 要保证用户的正常请求不受到影响,通过下面的代码
app.get('*', (req, res) => { return nextHandler(req, res) }) 
app.post('*', (req, res) => { return nextHandler(req, res) })

然后在要使用的地方,加入我们要监听用户当前是否在线,所以我在server.js中定义了两个on事件,一个是disconnect一个是clientOnline,注意:disconnect是默认的,不需要我们在前端发送emit('disconnect'),只要用户那边的socket.io断开了,服务器回默认触发disconnect事件的,但是clinetOnline这个事件就是我们自定义的,就需要我们在前端进行emit('clientOnline')了

问题: 1、在哪个页面连接socket.io 2、如何在用户前端连接socket.io 3、什么时候触发emit('clientOnline')合适

问题一:在哪个页面连接socket.io

因为我们要监听用户在本网站的在线情况嘛,所以肯定是用户只要在每个页面都属于在线,所以我就选择了一个公共组件,也就是在Layout组件中进行,因为我们Layout组件是构建了整个网站的基本框架:

image.png 所以我就选择在这里监听用户是否在线了

问题二:如何在用户前端连接socket.io 下面实现了一个基本连接的方式

import { useEffect } from "react";
import { observer } from "mobx-react-lite"
import io from 'socket.io-client'

var socket : any
const Layout = ({ children } : any) => {
  const clineOnline = () => {
    socket.emit('clientOnline', '用户id')
  }

useEffect(() => {
    if (!socket) {
      socket = io('http://localhost:3000')
    }
    document.addEventListener('visibilitychange',function(){
      var isHidden = document.hidden;
      if(!isHidden){
        clineOnline()
      }
    }
 }, [])
   
return (
    <div>
        Layout container
    </div>
  )
}

export default observer(Layout)

我们在useEffct的时候对socket进行初始化; 注意:通过下面代码主要是怕多次初始化socket这样就会建立多条长连接,但是我们只需要一条,所以就判断当前socket是否为初始化

if (!socket) {
  socket = io('http://localhost:3000')
}

然后在useEffct的时候初始化一个监听器visibilitychange,用来判断当前页面是否在后台,也就是如果当前网站有没有被最小化,如果没有话,说明用户在线,那么就发送一个当前在线的socket信号,如果用户关闭了当前网页的话server.js中的disconnect就会自动触发,用户就下线了。

后面下一节我将分享《next.js中通过socket.io来实现广播通信,并在后台管理系统发布系统消息推送给所有人,比如“当前网站正在维护中”》 如图!:

image.png 并通过Notification的方式直接弹窗来提示用户

右边在后台管理系统中填写系统通知内容

image.png

点击发送之后,用户那边可以实时的显示通知notification弹窗

image.png