NextJS connect to backend Go api for buffering response
如果是 ASP 或 PHP 類的技術,可以透過設定 response 的 timeout 時間,再加上 flush (或類似機制)達成。這樣的處理用在需要較長執行時間的 api (或頁面)的情境相當適合,但如果是 nextjs 前端搭配 golang 後端,就沒有類似的機制,需要改用其他方式。
<!--more-->
# Golang WebSocket
以 fiber framework 為例, golang api backend 會長得類似這樣
```go
import {
"github.com/gofiber/c..."
}
func main() {
app := fiber.New(fiber.Config{})
app.Get("/ws/resetfolder", websocket.New(handleResetFolder))
}
func handleResetFolder(c *websocket.Conn) {
if err := ResetFolder(c); err != nil {
log.Println("Error resetting folder:", err)
c.Close()
}
}
func ResetFolder(c *websocket.Conn) error {
// folder is defined in somewhere else
if err := os.RemoveAll(folder); err != nil {
return fmt.Errorf("error removing folder %s: %v", folder, err)
}
outputMessage("■", c)
outputMessage("<br/><br/>finished!", c)
c.Close()
return nil
}
```
nextjs 則需要建立 websocket connection
```javascript
const [bWSClosed, setWSClosed] = useState(false);
const [bWSOpenedOnce, setWSOpened] = useState(false);
let wsHost = process.env.NEXT_PUBLIC_API_URL;
if (wsHost?.startsWith("http://")) {
wsHost = "ws" + wsHost?.substring(wsHost.indexOf(":"));
} else if (wsHost?.startsWith("https://")) {
wsHost = "wss" + wsHost?.substring(wsHost.indexOf(":"));
}
useEffect(() => {
const socket = new WebSocket(wsHost + 'ws/resetfolder');
socket.onopen = () => {
setWSClosed(false);
setWSOpened(true);
};
socket.onmessage = (event) => {
prevMessageRef.current = prevMessageRef.current + event.data;
setMessage(prevMessageRef.current);
};
socket.onclose = () => {
setWSClosed(true);
}
return () => {
socket.close();
};
}, []);
```
# Golang Progressive Response
Go lang 裏頭,是使用 Stream 方式來做 progressive response,所以跟上面的 WebSocket 不同,這裡的 prResetFolder 這個 handler 的參數跟一般 api 呼叫一樣,都還是一個 *fiber.Ctx。
而且在 function 內可以看到 `c.Context().SetBodyStreamWriter()`,直接用一個 streamwriter 來當作 body 內容輸出使用。
且裏頭還有設定一個 heart beat ticker,避免太久沒有 response 資料被browser client當作 idle 太久而斷線。
```go
func main() {
app := fiber.New(fiber.Config{})
app.Get("/pr/resetfolder", prResetFolder)
}
func prResetFolder(c *fiber.Ctx) error {
c.Set("Content-Type", "text/plain")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
folder := filepath.Join(dirResult, strconv.Itoa(report_year))
ticker := time.NewTicker(1 * time.Minute) // Send heartbeat every 1 min
defer ticker.Stop()
done := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
fmt.Fprintf(w, ".") // Heartbeat to prevent idle
w.Flush()
case <-done:
return
}
}
}()
if err := os.RemoveAll(folder); err != nil {
fmt.Printf("error removing folder %s: %v\r\n", folder, err)
}
fmt.Fprintf(w, "■")
w.Flush()
close(done) // Stop ticker when work is done
fmt.Fprintf(w, "<br/><br/>finished!")
w.Flush()
})
return nil
}
```
NextJS 部分會變成
```javascript
const [bWSClosed, setWSClosed] = useState(false);
const [bWSOpenedOnce, setWSOpened] = useState(false);
useEffect(() => {
const fetchProgress = async () => {
const response = await fetch(process.env.NEXT_PUBLIC_API_URL + 'pr/resetfolder');
const reader = response?.body?.getReader();
const decoder = new TextDecoder();
let progressData = '';
while (true) {
const result = await reader?.read();
if (!result) break;
const { done, value } = result;
if (done) {
setWSClosed(true);
setWSOpened(true);
break;
} else {
console.log("not done");
}
progressData += decoder.decode(value, { stream: true });
prevMessageRef.current = progressData;
setMessage(prevMessageRef.current);
}
};
fetchProgress();
}, []);
```
# 實際測試與心得
因為 websocket 建立的連線跟實際 tcp/ip socke不完全一樣,所以當這個backend api 執行時間需要很久的時候,在瀏覽器這邊確實會發生以為連線已經中斷的情況。解決方式就是要自己加上程式碼,讓 backend 這邊可以定時(或者每做完一些事情)輸出一些資料去給前端。
使用 stream writer 方式加上了 ticker ,理論上就可以避免上述情況發生。但我自己實際上使用時,有時還是會發生這樣的情況,但已經比用 websocket 好很多。
Like my work? Don't forget to support and clap, let me know that you are with me on the road of creation. Keep this enthusiasm together!