Skip to content

Fetch(网络请求) ​

fetch 插件为页面提供一个 fetch,它从 Go 侧发出 HTTP 请求。它接受 标准 fetch 的选项并返回标准的 Response,但请求是从应用的 Go 侧 发出的,因此:

  • 它不受 CORS 限制:任何服务器都会应答,并且每个响应头都可读取,除了 Set-Cookie,而 Response 本来也无法携带它;
  • 它可以设置任何请求头,包括 webview 禁止的那些,例如 Origin、 Cookie、Referer 和 User-Agent;
  • 它的响应体在 Go 读取的同时流式传给页面,中止请求会取消 Go 中的请求。

设置 ​

在 Go 中使用该插件:

go
import (
	"github.com/egoist/mygo"
	"github.com/egoist/mygo/plugins/fetch"
)

func main() {
	mygo.Use(fetch.Plugin)
	// ...
}

并在前端添加它的包:

sh
bun add @mygo-plugins/fetch

在一个未使用其 Go 侧组件的应用中调用它的 fetch,Promise 会以一个 错误拒绝(reject),错误信息提示要调用 mygo.Use。

发起请求 ​

从包中导入 fetch,像调用 webview 自带的 fetch 那样调用它:

ts
import { fetch } from "@mygo-plugins/fetch";

const res = await fetch("https://api.example.com/items", {
  method: "POST",
  headers: {
    authorization: `Bearer ${token}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({ name: "Ada" }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const item = await res.json();

Request 可以代替 URL 使用。请求体与标准 fetch 所接受的一样,例如 字符串、FormData、URLSearchParams、Blob 和 ArrayBuffer,并带上 它本会给出的 Content-Type;它们在请求开始前被整体读取,流也一样。

自带 fetch 的库——许多 API 客户端都是如此——可以使用这个 fetch。 不要把它赋值给 globalThis.fetch:插件会把不由它发出的 URL——协议不是 http 或 https 的那些——交给 webview 的 fetch。

流式响应 ​

响应体是一个 ReadableStream,Go 在服务器发送的同时填充它,这很适合 服务器发送事件(server-sent events)和模型 API 的流式回答:

ts
const res = await fetch("https://api.example.com/events");
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  output.append(value);
}

Go 的读取不会快过页面:当页面还有超过 1 MiB 的响应体尚未读取时,Go 会 等待,因此大型下载不会在页面的内存中堆积。

取消 ​

中止请求的 signal 会在 Go 中取消请求,无论响应是否已经到达:

ts
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 10_000);
try {
  const res = await fetch(url, { signal: ctrl.signal });
  return await res.text();
} finally {
  clearTimeout(timer);
}

Promise 或响应体的读取会以 signal 的 reason 拒绝。取消响应体 (res.body.cancel())、页面导航离开和关闭窗口同样会取消请求。

错误 ​

与 webview 的 fetch 一样,响应不是错误,无论其状态码如何:请检查 res.ok。当请求失败时——例如无法到达服务器、TLS 失败或重定向过多—— Promise 会以 TypeError 拒绝,其消息会说明原因:

ts
try {
  await fetch("https://unreachable.example");
} catch (err) {
  console.log(err.message); // fetch failed: Get "https://unreachable.example": dial tcp: lookup unreachable.example: no such host
}

与 webview 的 fetch 的区别 ​

  • mode、credentials、cache、referrerPolicy、integrity、 keepalive 和 priority 不适用,会被忽略。
  • Cookie 是 Go 侧 http.Client 的,绝不是 webview 的:默认没有,除非 客户端带有保存它们的 Jar(参见选项)。
  • redirect: "manual" 返回重定向响应本身,带有它的状态码和 Location,而不是不透明响应。"error" 在重定向时拒绝,"follow" (默认值)最多跟随 20 次重定向,并设置 res.redirected 和 res.url。
  • 协议不是 http 或 https 的 URL,例如构建版本中应用自己的页面 (mygo://localhost)、data: 和 blob: URL,会交给 webview 的 fetch。相对 URL 相对于页面的 URL 解析:在 mygo dev 期间,它们是 开发服务器的 URL,由 Go 去抓取。

选项 ​

fetch.Plugin 通过系统的代理设置发出请求,与 webview 一样(参见 代理),并允许页面请求任何 URL。fetch.New 则接受选项:

go
jar, _ := cookiejar.New(nil)

mygo.Use(fetch.New(fetch.Options{
	Client: &http.Client{Jar: jar, Timeout: time.Minute},
	Allow: func(r *http.Request) bool {
		return r.URL.Hostname() == "api.example.com"
	},
}))
  • Client 发出请求,因此它的 Transport(代理、TLS 设置)、Jar (跨请求保存的 Cookie)和 Timeout 都会生效,它的 CheckRedirect 限制跟随的重定向。没有提供 Client 时,客户端的传输是 netproxy.Transport():当环境(HTTPS_PROXY、NO_PROXY)指定了 代理时使用环境代理,否则使用系统代理。你自己的客户端保留它自己的 Transport;要使用相同的代理,请给它 netproxy.Transport(),或把 它的 Proxy 设为 netproxy.Proxy。
  • Allow 在 Go 发出每个请求之前看到它,带着它的 URL、方法和请求头, 并决定页面是否可以发出该请求:被它拒绝的请求会以错误拒绝。它只看到 第一个请求,看不到随后跟随的重定向,后者可以由客户端的 CheckRedirect 拒绝。

只有应用自己的页面可以调用该插件,与每个绑定方法一样(参见谁可以 调用);Allow 可以防止一个运行了不该 运行的代码的页面(例如通过跨站脚本漏洞)访问其他服务器。