第 08 章:Web API
第 08 章:Web API
8.1 Web 标准 API 概述
Deno 内置了大量 WHATWG/W3C Web 标准 API,这意味着你在浏览器中使用过的 API 大多可以直接在 Deno 中使用。
内置 Web API 一览
| API | 说明 | 浏览器兼容 |
|---|---|---|
fetch | HTTP 请求 | ✅ |
Request / Response | HTTP 请求/响应对象 | ✅ |
Headers | HTTP 头部操作 | ✅ |
URL / URLSearchParams | URL 解析 | ✅ |
FormData | 表单数据 | ✅ |
Blob | 二进制大对象 | ✅ |
File | 文件对象 | ✅ |
WebSocket | WebSocket 客户端 | ✅ |
TextEncoder / TextDecoder | 文本编码 | ✅ |
AbortController | 请求取消 | ✅ |
ReadableStream / WritableStream | 流 API | ✅ |
crypto | 加密 API | ✅ |
structuredClone | 深拷贝 | ✅ |
console | 控制台输出 | ✅ |
setTimeout / setInterval | 定时器 | ✅ |
queueMicrotask | 微任务 | ✅ |
8.2 fetch — HTTP 请求
基本 GET 请求
// GET 请求
const response = await fetch("https://api.github.com/users/denoland");
// 检查状态码
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
// 解析 JSON
const user = await response.json();
console.log(user.login);
console.log(user.public_repos);
// 获取响应头
console.log("Content-Type:", response.headers.get("content-type"));
console.log("Rate Limit:", response.headers.get("x-ratelimit-remaining"));
POST 请求
// 发送 JSON 数据
const response = await fetch("https://httpbin.org/post", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer token123",
},
body: JSON.stringify({
name: "Alice",
email: "[email protected]",
}),
});
const result = await response.json();
console.log(result);
请求超时控制
// 使用 AbortController 控制超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 秒超时
try {
const response = await fetch("https://api.example.com/data", {
signal: controller.signal,
});
clearTimeout(timeoutId);
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === "AbortError") {
console.error("请求超时");
}
}
流式响应
// 流式读取大文件
const response = await fetch("https://example.com/large-file.bin");
// 方法 1:使用 arrayBuffer
const buffer = await response.arrayBuffer();
await Deno.writeFile("file.bin", new Uint8Array(buffer));
// 方法 2:使用流(内存友好)
const reader = response.body?.getReader();
const file = await Deno.open("file.bin", { write: true, create: true });
while (true) {
const { done, value } = await reader!.read();
if (done) break;
await file.write(value);
}
file.close();
上传文件
// 上传文件
const fileData = await Deno.readFile("photo.jpg");
const formData = new FormData();
formData.append("file", new Blob([fileData]), "photo.jpg");
formData.append("description", "我的照片");
const response = await fetch("https://api.example.com/upload", {
method: "POST",
body: formData,
});
console.log("上传结果:", await response.json());
8.3 WebSocket — 实时通信
WebSocket 客户端
// 连接 WebSocket 服务器
const ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = () => {
console.log("已连接");
ws.send(JSON.stringify({ type: "greeting", message: "Hello!" }));
};
ws.onmessage = (event) => {
console.log("收到消息:", event.data);
};
ws.onerror = (error) => {
console.error("WebSocket 错误:", error);
};
ws.onclose = () => {
console.log("连接已关闭");
};
使用事件监听方式
const ws = new WebSocket("ws://localhost:8080/ws");
ws.addEventListener("open", () => {
console.log("连接建立");
});
ws.addEventListener("message", (event) => {
if (typeof event.data === "string") {
console.log("文本消息:", event.data);
} else {
console.log("二进制消息:", event.data);
}
});
ws.addEventListener("close", (event) => {
console.log(`连接关闭:code=${event.code}, reason=${event.reason}`);
});
// 发送消息
ws.send("Hello Server");
// 关闭连接
ws.close(1000, "正常关闭");
8.4 URL 与 URLSearchParams
URL 解析
const url = new URL("https://example.com:8080/path?query=value&key=123#section");
console.log(url.protocol); // https:
console.log(url.hostname); // example.com
console.log(url.port); // 8080
console.log(url.pathname); // /path
console.log(url.search); // ?query=value&key=123
console.log(url.hash); // #section
console.log(url.origin); // https://example.com:8080
URLSearchParams
// 从字符串创建
const params = new URLSearchParams("?name=Alice&age=30&city=北京");
console.log(params.get("name")); // Alice
console.log(params.get("age")); // 30
console.log(params.has("email")); // false
// 添加参数
params.append("email", "[email protected]");
// 删除参数
params.delete("age");
// 遍历参数
for (const [key, value] of params) {
console.log(`${key}: ${value}`);
}
// 转为字符串
console.log(params.toString()); // name=Alice&city=%E5%8C%97%E4%BA%AC&email=alice%40example.com
构建 URL
// 动态构建 API URL
function buildApiUrl(base: string, path: string, params: Record<string, string>): string {
const url = new URL(path, base);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
return url.toString();
}
const apiUrl = buildApiUrl("https://api.example.com", "/v1/users", {
page: "1",
limit: "10",
sort: "name",
});
// https://api.example.com/v1/users?page=1&limit=10&sort=name
8.5 FormData
// 创建 FormData
const form = new FormData();
form.append("username", "alice");
form.append("email", "[email protected]");
// 添加文件
const fileContent = await Deno.readFile("document.pdf");
form.append("document", new Blob([fileContent], { type: "application/pdf" }), "document.pdf");
// 遍历表单数据
for (const [key, value] of form.entries()) {
if (value instanceof File) {
console.log(`${key}: 文件=${value.name}, 大小=${value.size}`);
} else {
console.log(`${key}: ${value}`);
}
}
// 发送表单数据
const response = await fetch("https://api.example.com/register", {
method: "POST",
body: form,
});
8.6 Blob 与 File
// 创建 Blob
const textBlob = new Blob(["Hello, World!"], { type: "text/plain" });
const jsonBlob = new Blob([JSON.stringify({ key: "value" })], { type: "application/json" });
// 读取 Blob 内容
const text = await textBlob.text();
console.log(text); // Hello, World!
const arrayBuffer = await jsonBlob.arrayBuffer();
console.log(arrayBuffer.byteLength);
// 创建 File
const file = new File(["文件内容"], "test.txt", {
type: "text/plain",
lastModified: Date.now(),
});
console.log(file.name); // test.txt
console.log(file.size); // 文件大小
console.log(file.type); // text/plain
console.log(file.lastModified); // 修改时间
8.7 AbortController
// 场景 1:取消 fetch 请求
const controller = new AbortController();
fetch("https://api.example.com/data", { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === "AbortError") {
console.log("请求被取消");
}
});
// 3 秒后取消
setTimeout(() => controller.abort(), 3000);
// 场景 2:多个请求共享取消信号
const controller2 = new AbortController();
const promise1 = fetch("/api/users", { signal: controller2.signal });
const promise2 = fetch("/api/posts", { signal: controller2.signal });
const promise3 = fetch("/api/comments", { signal: controller2.signal });
// 任何一个失败就取消全部
Promise.allSettled([promise1, promise2, promise3]).then(() => {
console.log("所有请求完成");
});
// 场景 3:超时控制
function fetchWithTimeout(url: string, ms: number) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}
try {
const res = await fetchWithTimeout("https://api.example.com", 5000);
console.log("请求成功", res.status);
} catch {
console.log("请求超时");
}
8.8 Streams API
ReadableStream
// 创建自定义 ReadableStream
function createCounter(): ReadableStream<number> {
let count = 0;
return new ReadableStream({
pull(controller) {
if (count < 10) {
controller.enqueue(count++);
} else {
controller.close();
}
},
});
}
// 使用
const stream = createCounter();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // 0, 1, 2, ..., 9
}
TransformStream
// 创建转换流:将文本转为大写
const uppercase = new TransformStream<string, string>({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
});
// 使用
const response = await fetch("https://example.com/text");
const readable = response.body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(uppercase);
const reader = readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value);
}
流式文件处理
// 流式复制大文件
async function streamCopy(src: string, dest: string) {
const srcFile = await Deno.open(src, { read: true });
const destFile = await Deno.open(dest, { write: true, create: true });
await srcFile.readable.pipeTo(destFile.writable);
// pipeTo 会自动关闭流
}
await streamCopy("large-input.dat", "large-output.dat");
8.9 crypto API
// 生成 UUID
const uuid = crypto.randomUUID();
console.log(uuid); // 550e8400-e29b-41d4-a716-446655440000
// 生成随机字节
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
console.log(bytes);
// SHA-256 哈希
const data = new TextEncoder().encode("Hello, Deno!");
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
console.log("SHA-256:", hashHex);
8.10 structuredClone — 深拷贝
// 深拷贝对象
const original = {
name: "Alice",
nested: { age: 30, hobbies: ["reading", "coding"] },
date: new Date(),
};
const cloned = structuredClone(original);
cloned.nested.hobbies.push("gaming");
console.log(original.nested.hobbies); // ["reading", "coding"] - 未被修改
console.log(cloned.nested.hobbies); // ["reading", "coding", "gaming"]
// 拷贝复杂数据结构
const data = new Map([["key1", { value: 1 }], ["key2", { value: 2 }]]);
const clonedData = structuredClone(data);
8.11 实战场景
场景:API 客户端封装
class ApiClient {
#baseUrl: string;
#token?: string;
constructor(baseUrl: string, token?: string) {
this.#baseUrl = baseUrl;
this.#token = token;
}
async get<T>(path: string, params?: Record<string, string>): Promise<T> {
const url = new URL(path, this.#baseUrl);
if (params) {
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, v);
}
}
const response = await fetch(url.toString(), {
headers: {
...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
"Accept": "application/json",
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
}
async post<T>(path: string, body: unknown): Promise<T> {
const response = await fetch(new URL(path, this.#baseUrl).toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json() as Promise<T>;
}
}
// 使用
const api = new ApiClient("https://api.example.com", "my-token");
const users = await api.get<User[]>("/v1/users", { page: "1" });
const newUser = await api.post<User>("/v1/users", { name: "Bob" });
8.12 本章小结
| API | 用途 | 常用场景 |
|---|---|---|
fetch | HTTP 请求 | API 调用、数据获取 |
WebSocket | 实时通信 | 聊天、实时推送 |
URL / URLSearchParams | URL 处理 | 路由、参数解析 |
FormData | 表单数据 | 文件上传 |
AbortController | 请求取消 | 超时控制、组件卸载取消 |
Streams | 流处理 | 大文件处理、实时数据 |
crypto | 加密 | 哈希、UUID、随机数 |
structuredClone | 深拷贝 | 状态管理、数据隔离 |
📖 扩展阅读
下一章:第 09 章:文件 I/O → 深入了解 Deno 的文件读写与目录操作。