大多数应用程序使用 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将发送数据到前台。
项目源码点击标题