JavaScript 中使用fetch的AbortController和取消功能:如何在Go 實現

2023/05/14閱讀時間約 13 分鐘

有多種情況下我們需要從API取消請求:
  1. 如果我們正在建立一個支付系統,並且請求一直進行,但使用者希望立即取消這筆訂單。這就是為什麼開發者會建立這個功能的原因。
  2. 從電子商務購買商品。使用者意外關閉了頁面,無法立即返回上一頁。
  3. 訂閱取消。
......等等
這將對對這個系統沒有信心的使用者產生影響。因此,開發者需要認真對待這個問題。

我使用Vue和Go建立了一個簡單的前端系統和後端系統的示範。

後端部分

mkdir cancellation-demo && cd cancellation-demo
建立資料夾,稱為 `cancellation-demo`
go mod init callcellation-demo && \ 
touch main.go && \
go get github.com/gin-gonic/gin
從go module和main.go開始。導入gin套件。
以下是main.go的內容。建立一個簡單的Hello World。
package main

import (
"net/http"
"github.com/gin-gonic/gin"
)

func CancelHandler(ctx *gin.Context) {
ctx.String(http.StatusOK, "hello world")
}

func main() {
router := gin.Default()
router.GET("/", EchoHandler)
router.Run()
}
我們的目標是在收到請求後等待3秒,然後發送一個文字回應"Hello World"。如果使用者在3秒內取消了請求,我們將停止處理並打印結果,告知系統管理員該請求已被取消
以下範例
func CancelHandler(ctx *gin.Context) {
ctx.String(http.StatusOK, "hello world")
}
我們改寫成
func CancelHandler(ctx *gin.Context) {
responseResult := make(chan string)

go func() {
time.Sleep(3 * time.Second)
responseResult <- "hello world"
}()

for {
select {
case result := <-responseResult:
ctx.String(http.StatusOK, result)
return
case <-ctx.Request.Context().Done():
log.Println("abort triggerred.Cancellation happen")
ctx.AbortWithStatus(http.StatusNoContent)
return
}
}
}
讓我來解釋這部分:
我們需要建立一個goroutine和channel來傳送資料,並使用CancelHandler接收channel和ctx.Context().Done()來傳送chan類型。
這有助於我們檢查是否在請求被取消時收到中止訊號。這個方法將由中止訊號觸發。
因為我們需要建立一個在不同端口上監聽的前端專案,這導致了跨來源資源共享(CORS)的問題。因此,我們需要建立一個中間件(Middleware)來解決這個問題。
func CorsMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Writer.Header().Set("Access-Control-allow-Credentials", "true")
ctx.Writer.Header().Set("Access-Control-allow-Method", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
ctx.Writer.Header().Set("Access-Control-allow-Headers", "Authorization, Accept,Accept-Encoding, Content-Type,Content-Length, Cache-Control, Origin, X-CSRF-Token, X-Requested-With")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(http.StatusNoContent)
 return
}
ctx.Next()
}
}
最後,我們的後端專案的部分就會看起來像這樣
package main

import (
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"
)

func CorsMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Writer.Header().Set("Access-Control-allow-Credentials", "true")
ctx.Writer.Header().Set("Access-Control-allow-Method", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
ctx.Writer.Header().Set("Access-Control-allow-Headers", "Authorization, Accept,Accept-Encoding, Content-Type,Content-Length, Cache-Control, Origin, X-CSRF-Token, X-Requested-With")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(http.StatusNoContent)
return
}
ctx.Next()
}
}

func CancelHandler(ctx *gin.Context) {
responseResult := make(chan string)

go func() {
time.Sleep(3 * time.Second)
responseResult <- "hello world"
}()

for {
select {
case result := <-responseResult:
ctx.String(http.StatusOK, result)
return
case <-ctx.Request.Context().Done():
log.Println("abort triggered")
ctx.AbortWithStatus(http.StatusNoContent)
return
}
}

}

func main() {
router := gin.Default()
router.Use(CorsMiddleware())
router.GET("/", CancelHandler)
router.Run()
}

現在換前端的部分

我們前端使用 vue 作為我們的前端框架
pnpm create vite --template vue-ts web && \
cd web && \
pnpm install
注意:對了,我本人個人是喜歡用 pnpm 作為我的 package manager,
不代表不能用 npm

然後我們將建立一個簡單的使用AbortController的fetch範例。這部分是向http://localhost:8080發送請求的部分。切記我們的後端專案在端口8080上監聽。
import { ref } from 'vue';
const abortReference = ref(new AbortController());

const getRequest = async () => {
  abortReference.value = new AbortController();
  try {
   const response = await fetch('http://localhost:8080', {
       method: 'GET',
      signal: abortReference.value.signal,
  })
   const mainContent = await response.text();
    console.log(mainContent);
  } catch{
   console.error('api error');
  }
}
取消功能的部分
const quitRequest = () => {
console.log('cancel request');
abortReference.value.abort();
}
現在我們把兩個方法取代原本專案的 app.vue
<script setup lang="ts">
import { ref } from 'vue';
const abortReference = ref(new AbortController());

const getRequest = async () => {
abortReference.value = new AbortController();
  try {
  const response = await fetch('http://localhost:8080', {
    method: 'GET',
     signal: abortReference.value.signal,
})
  const mainContent = await response.text();
  console.log(mainContent);
} catch{
  console.error('api error');
}
}

const quitRequest = () => {
console.log('cancel request');
abortReference.value.abort();
}

</script>

<template>
<button @click="getRequest">
click to request
</button>
<button @click="quitRequest">
Abort Request
</button>
</template>
這樣就大功告成了,
pnpm dev
然後打開瀏覽器我們會看到的結果
如果有任何問題歡迎跟我討論,感謝你得閱讀
以下是我的 medium 帳號
還有我發表的英文版原文 ,如果對我的創作有興趣歡迎追蹤捧場
1會員
3內容數
留言0
查看全部
發表第一個留言支持創作者!