登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go 接浏览器 Beacon API:页面关闭时上报请求、Content-Type 与失败兜底

来源:17golang原创

时间:2026-08-11 13:42:44 140浏览 收藏

页面刚切走,最后一次停留时长却总是少一截,问题往往不在 Go 接口,而在浏览器还没来得及把普通 fetch 发完。适合这种场景的是 Beacon API:前端在 visibilitychange 或页面离开前,把一小段数据交给浏览器异步发送;Go 服务端只需按一次普通 POST 接收并快速返回。

要点速览

  • navigator.sendBeacon() 只负责小型异步 POST,不适合需要读取响应体的业务请求。
  • JSON Blob 会带来 Content-Type: application/json,Go 端不要用表单解析器代替 JSON 解码。
  • 服务端应先限制请求体,再校验事件名和时间字段,成功后返回 204。
  • Beacon 不可用、数据过大或必须控制请求方法时,再切到 fetch(..., {keepalive:true})

页面关闭时,普通 fetch 为什么会丢最后一条事件

一个阅读页会在用户离开时上报 article_leave,字段包括文章编号、进入时间和停留秒数。最直观的写法是监听 beforeunload,然后调用普通 fetch。但卸载阶段的页面生命周期很短,浏览器可以中止未完成的异步任务;移动端切到后台、系统回收页面时,这个窗口还会更窄。

这里先不要把问题归因到 Go 超时。即使 /v1/events/leave 在本机 2 毫秒返回,页面也可能在请求发出前就被回收。更稳妥的触发点是 visibilitychange:文档进入 hidden 状态时,尽早把最后一小段数据交给浏览器。

Beacon API 的职责边界:交给浏览器发,不等业务响应

navigator.sendBeacon(url, data) 的返回值只表示数据是否被浏览器接受进发送队列,不代表 Go 接口已经返回 2xx。它适合离开页面统计、诊断事件和小型状态同步,不能拿来做“服务端返回优惠券编号后再更新页面”这类需要响应体的流程。

下面的页面代码只发送一次事件,并用布尔值防止 visibilitychange 重复触发:

let sent = false;

function reportLeave() {
  if (sent) return;
  sent = true;

  const body = JSON.stringify({
    event: "article_leave",
    article_id: "go-beacon-101",
    duration_sec: 37,
    occurred_at: new Date().toISOString()
  });

  const blob = new Blob([body], { type: "application/json" });
  const queued = navigator.sendBeacon("/v1/events/leave", blob);
  if (!queued) {
    sent = false;
    fallbackReport(body);
  }
}

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") reportLeave();
});

sendBeacon 使用 POST,调用方不能像普通请求那样读取响应体,也不应把它当成可靠消息队列。数据要短,事件也要允许重复或迟到;这些约束会直接影响 Go 端的校验和去重设计。

页面进入 hidden 状态后,Beacon 将 article_leave 小事件交给浏览器队列并发送到 Go 接口的工程证据插画

Go 接收 JSON Beacon:Content-Type 和 204 要先处理对

前端传入的是 JSON Blob,Go 端应从 r.Body 解码,而不是调用 r.ParseForm() 后读取表单字段。接口还应拒绝过大的请求,避免离开页面接口被当成任意文件入口。

package main

import (
    "encoding/json"
    "io"
    "net/http"
    "time"
)

type leaveEvent struct {
    Event      string    `json:"event"`
    ArticleID  string    `json:"article_id"`
    Duration   int       `json:"duration_sec"`
    OccurredAt time.Time `json:"occurred_at"`
}

func leaveHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }
    if r.ContentLength > 64*1024 {
        http.Error(w, "payload too large", http.StatusRequestEntityTooLarge)
        return
    }

    r.Body = http.MaxBytesReader(w, r.Body, 64*1024)
    defer r.Body.Close()

    var event leaveEvent
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&event); err != nil {
        http.Error(w, "invalid json", http.StatusBadRequest)
        return
    }
    if event.Event != "article_leave" || event.ArticleID == "" || event.Duration 

真实项目里可以把最后一段替换成消息队列或批量写入。示例用 io.Discard 只是让代码能独立编译;它不会持久化事件,验收时要把“收到请求”和“写入存储”分成两个指标。

返回 204 的意义是告诉客户端“请求处理完成,但没有响应正文”。不要在这个接口里拼一段 JSON 结果,Beacon 调用方本来就不会读取它。

Go net/http 处理 Beacon POST:先限体积和校验 JSON,再快速返回 204 的请求处理路径

一次真实验收:看请求头、状态码和服务端日志

先把 Go 服务跑在 HTTPS 页面可访问的地址上,再在浏览器开发者工具的 Network 面板中触发页面隐藏。重点看三件事:请求方法是否为 POST,Content-Type 是否为 application/json,响应状态是否为 204。

POST /v1/events/leave HTTP/1.1
Content-Type: application/json
Content-Length: 112

{"event":"article_leave","article_id":"go-beacon-101","duration_sec":37}
HTTP/1.1 204 No Content

如果 Network 里没有请求,先查页面是否在 HTTP 环境、浏览器是否支持 Beacon,以及 sendBeacon 返回值是不是 false。如果请求到了但 Go 返回 400,优先检查 JSON 字段、时间格式和请求体限制,不要先放宽校验。

Beacon 不够用时,keepalive fetch 怎么做兜底

需要自定义请求头、使用其他方法、读取响应,或者 Beacon 队列接收失败时,可以尝试 fetchkeepalive: true。它同样受请求体大小和浏览器实现限制,不能因为加了这个开关就把几 MB 的日志塞进页面关闭事件。

function fallbackReport(body) {
  void fetch("/v1/events/leave", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body,
    keepalive: true,
    credentials: "same-origin"
  }).catch(() => {
    // 离开页面阶段不要弹窗;下次进入页面时可从本地短暂重试。
  });
}

如果事件必须一条不丢,Beacon 也不是答案。应在页面仍然可交互时写入本地待发送记录,由正常请求、重试和服务端幂等键完成可靠投递;离开页面只做最后一次“尽力发送”。

常见问题

Beacon 能发送 GET 请求吗?

不能。sendBeacon 的接口语义是异步 POST;必须改用 GET 或读取响应时,应重新设计请求,不要把 Beacon 当成通用 HTTP 客户端。

为什么 Go 端读取不到表单里的字段?

JSON Blob 不是表单编码。读取 r.Body 并用 encoding/json 解码,同时检查 Content-Type 和请求体上限。

sendBeacon 返回 true 就代表数据已经入库了吗?

不是。true 只表示浏览器接受了发送请求;服务端仍可能返回 4xx、5xx 或在写入存储前失败,所以要用服务端接收计数和业务落库计数分别监控。

Beacon 适合上传完整操作日志吗?

不适合。它面向小型、异步、无需读取响应的请求。较大的日志应在页面活跃时分批发送,并给每条事件加幂等标识。

把页面离开上报做成一个小而可靠的接口

这套组合的关键不是把页面卸载事件“强行拦住”,而是把职责拆开:浏览器负责尽早排队,Go 负责快速校验和接收,后续存储负责幂等与重试。用 Network 面板确认 POST、JSON 和 204,再用服务端指标确认接收与落库,基本就能定位问题是在浏览器队列、网络,还是业务存储。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>