WebSockets 与 NextJS 和 Golang 结合使用的案例源码


大多数应用程序使用 CRUD(创建/读取/更新/删除)API——前端将更改发送到后端,但反之亦然。

CRUD 应用程序允许您将更改发送到服务器,并允许其他用户请求这些更改。

在实时应用程序中,所有客户端都与后端保持持久的 WebSocket 连接,并在更新发生时接收更新,而不需要等待用户刷新页面。

架构图:

这种架构意味着您可以在创建任何 websocket 之前立即查看网页内容(使用 NextJS 进行服务器端呈现),同时通过直接连接到 Go 服务器来保持更新以了解更改。
这也意味着我们可以将所有实际代码保留在 Go 中,并避免在后端使用 Javascript 或 Typescript,同时仍然能够使用 React。

从这个现成代码开始:

git clone https://github.com/webappio/golang-nextjs-example.git

main函数:

func main() {
    handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        var resp []byte
        if req.URL.Path == "/handler-initial-data" {
            resp = []byte(`{"text": "initial"}`)
        } else if req.URL.Path == "/handler" {
            time.Sleep(time.Second) //TODO HACK: sleep a second to check everything is working properly

            resp = []byte(`{"text": "updated"}`)
            
            //this line is required because the browser will check permissions to avoid getting hacked
            rw.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
        } else {
            rw.WriteHeader(http.StatusNotFound)
            return
        }

        rw.Header().Set("Content-Type", "application/json")
        rw.Header().Set("Content-Length", fmt.Sprint(len(resp)))
        rw.Write(resp)
    })

    log.Println("Server is available at http://localhost:8000")
    log.Fatal(http.ListenAndServe(":8000", handler))
}

现在我们有两个端点,http://localhost:8000/handler-initial-dataNextJS 最初将发送给浏览器的内容(作为 HTML,由于服务器端呈现),然后http://localhost:8000/handler是我们的应用程序将从中提取数据的内容。

在前端添加一个异步更新
接下来,更新前端,在index.js中引用我们的/handler-initial-data和/handler路径。

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import {useEffect, useState} from "react";

export async function getServerSideProps() {
    const initialData = await fetch("http://localhost:8000/handler-initial-data").then(x => x.json());
    return {props: {data: initialData}}
}

export default function Home(props) {
    const [data, setData] = useState(props.data);
    useEffect(() => {
        fetch("http://localhost:8000/handler")
            .then(x => x.json())
            .then(x => setData(x));
    }, [])
    return (
        <div className={styles.container}>
            <Head>
                <title>OSS Docs</title>
                <meta name="description" content="Fast like SSR, Powerful like WebSockets"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>

            <main className={styles.main}>
                <h1 className={styles.title}>
                    {props.title || "Untitled Document"}
                </h1>
                <div>Data is: {JSON.stringify(data)}</div>
            </main>
        </div>
    )
}

如果你用go run main.go运行后端,用npm run dev运行前端,你最初应该看到Data is:{"text": "initial"},然后一秒钟后看到数据是。{"text": "uped"}- 所发生的操作是。

  • 你的浏览器向NextJS询问网站的HTML内容
  • NextJS调用getServerSideProps()和函数Home(props)来生成该HTML,导致HTTP请求到http://localhost:8000/handler-initial-data
  • 你的浏览器收到HTML和JavaScript,并在本地运行函数Home
  • 一秒钟后,useEffect钩子完成了从你的浏览器中的/handler获取数据,并更新数据,然后在本地改变你的网站

添加一个websocket
为了建立像Google Docs这样的东西,我们希望能够将我们的修改发送到后台,同时也能接收其他人的修改,因为他们进来了。这意味着我们需要双向通信,这正是WebSocket的作用。

Go并没有自带WebSocket库,所以我们来安装一个。

# NOTE: if you have a GitHub or GitLab repository, change this line 
user@computer:my-project/services/backend$ go mod init github.com/webappio/golang-nextjs-example

user@computer:my-project/services/backend$ go get github.com/gobwas/ws

并在/handler处更新我们的后端以处理WebSockets。

if req.URL.Path == "/handler" {
    conn, _, _, err := ws.UpgradeHTTP(req, rw)
    if err != nil {
        log.Println("Error with WebSocket: ", err)
        rw.WriteHeader(http.StatusMethodNotAllowed)
        return
    }
    go func() {
        defer conn.Close()

        time.Sleep(time.Second) //TODO HACK: sleep a second to check everything is working properly
        err = wsutil.WriteServerMessage(conn, ws.OpText, []byte(`{"text": "from-websocket"}`))
        if err != nil {
            log.Println("Error writing WebSocket data: ", err)
            return
        }
    }()
    return
}

将已安装的库添加到我们的导入中。

import (
    "fmt"
    "github.com/gobwas/ws"
    "github.com/gobwas/ws/wsutil"
    "log"
    "net/http"
    "time"
)

最后,我们可以改变我们的前端,以使用WebSocket而不是fetch调用。

export default function Home(props) {
    const [data, setData] = useState(props.data);
    const [ws, setWS] = useState(null);
    useEffect(() => {
        const newWS = new WebSocket("ws://localhost:8000/handler")
        newWS.onerror = err => console.error(err);
        newWS.onopen = () => setWS(newWS);
        newWS.onmessage = msg => setData(JSON.parse(msg.data));
    }, [])
    return (
        <div className={styles.container}>
            <Head>
                <title>OSS Docs</title>
                <meta name="description" content="Fast like SSR, Powerful like WebSockets"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>

            <main className={styles.main}>
                <h1 className={styles.title}>
                    {props.title || "Untitled Document"}
                </h1>
                <div>Data is: {JSON.stringify(data)}</div>
            </main>
        </div>
    )
}

重新启动后台(ctrl + c,向上箭头,回车)后,你应该看到文本迅速从Data is:{"text": "initial"}变为Data is:{"text": "from-websocket"}。- 与我们的获取实现结果相同,但现在我们有两个新方法。
  • 我们前端的ws.send(...)将向我们的后端发送数据,而
  • 后台的wsutil.WriteServerMessage将发送数据到前台。

项目源码点击标题