Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • AI

KY使用教程

bajiu
前端

2025-06-16 20:13:00

react 项目更加倾向于使用原生的fetch请求方式,而 ky 正是底层使用fetch的api做请求。github星数是8.2K,源码地址:https://github.com/sindresorhus/ky ,

ky简介

ky是一个基于 fetchAPI 封装的请求库,兼容浏览器和node,类似于axios,但是axios的实现是在浏览器端是基于XMLHttpRequests而在node端则是基于原生的http模块.

安装与快速上⼿

这么装:

npm i ky         # 或 pnpm add ky / bun add ky

这么用:

import ky from 'ky';

// GET 并直接解析 JSON
const list = await ky.get('/api/todos').json<Todo[]>();

// POST JSON,自动加 Content-Type 与 Accept
await ky.post('/api/todos', {json: {title: 'Write doc'}});

// 40 ms 超时+3 次指数退避重试
await ky.get('/slow', {timeout: 40_000, retry: 3});

核心 API ⼀览

  • ky(input, options?): 基础函数,等同于 ky.get() 默认 GET
  • ky.get/post/put/patch/delete/head(): 常⽤ HTTP 动词静态⽅法
  • ky.extend(defaultOptions): 返回新的 Ky 实例,⽤于共享 headers、hooks 等
  • ky.create(defaultOptions): 功能同 extend,但更贴近 SDK 场景,常与多个后端域名并存
  • .json() / .text() / .arrayBuffer() / .blob(): 给 Response 动态挂载的 链式解析器,失败时抛 HTTPError
  • Options : json、form、searchParams、headers、timeout、retry、hooks、onDownloadProgress

解析器 必须在 链式 调⽤:ky.get(...).json();否则你将得到原始 Response 对象。

Hook 体系(Ky 的灵魂)

  • beforeRequest:触发时机-请求发送前 , 场景:统一加签名 / Token / 追踪 ID
  • afterResponse:触发时机-响应收到后 , 场景:401 刷新 Token 并重放、全局业务错误包装
  • beforeRetry:触发时机-即将重试前 , 场景:记录日志、动态调整间隔
  • beforeError:触发时机-抛出异常前 , 场景:统一错误格式化、打点

刷新 Token 模板:

const api = ky.create({
  hooks: {
    afterResponse: [
      async (req, opts, res) => {
        if (res.status === 401) {
          const token = await ky.post('/refresh').text();
          // 重放原请求
          return ky(req, {...opts, headers: {...opts.headers, Authorization: `Bearer ${token}`}});
        }
        return res;
      }
    ]
  }
});

使用场景:

源码

Ky 代码总量约 1.4 k 行 TypeScript,阅读⻔槛低。

source/
 ├─ index.ts        // 默认导出函数,包装 createKy()
 ├─ core/
 │   ├─ Ky.ts       // Ky 实例类,负责请求构造 & Hook 管理
 │   ├─ normalize.ts// 规范化 options / URL 处理
 │   └─ utils.ts    // 深度合并、退避算法、type guards
 └─ errors.ts       // HTTPError、TimeoutError 等定义

调用流程:

  1. index.ts 暴露 ky 函数,内部 return new Ky(input, options).request().

  2. Ky.request()

  • 组装 Request -> 触发 beforeRequest
  • fetch 真正发起请求
  • 正常返回或触发内部重试
  • 将 Response 克隆后串行执⾏ afterResponse
  • 对 response.ok 做短路:不 OK 抛 HTTPError
  • ⽤ augmentResponse 给 Response 动态挂载 .json() 等解析器
  1. 错误路径:beforeError -> 抛出(可能已被包装)

举个栗子

src/api.ts

import ky from 'ky';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const globalAny: any = globalThis;

const api = ky.create({
  prefixUrl: 'http://localhost:8989',
  timeout: 10_000,
  hooks: {
    beforeRequest: [
      request => {
        if (globalAny.token) {
          request.headers.set('Authorization', `Bearer ${globalAny.token}`);
        }
      }
    ],
    afterResponse: [
      async (request, options, response) => {
        if (response.status === 401 && !request.headers.get('x-auto-retry')) {
          console.log('Token expired, refreshing...');
          const newToken: string = await ky.post('http://localhost:4000/refresh').text();
          globalAny.token = newToken;

          return ky(request, {
            ...options,
            headers: {
              ...Object.fromEntries(request.headers),
              Authorization: `Bearer ${newToken}`,
              'x-auto-retry': 'true'
            }
          });
        }
        return response;
      }
    ]
  }
});

export default api;

src/client.ts

import api from './api.js';

(async () => {
  try {
    console.log('--- GET /posts');
    const posts = await api.get('posts').json<any[]>();
    console.table(posts);

    console.log('--- POST /posts');
    const newPost = await api
      .post('posts', { json: { title: 'Using Ky demo', body: 'Everything is typed!' } })
      .json();
    console.log('Created:', newPost);

    console.log('--- GET /protected (should trigger 401 -> refresh -> retry)');
    const secret = await api.get('protected').json();
    console.log(secret);

    console.log('Demo finished');
  } catch (err: any) {
    if (err.response) {
      console.error('HTTP error', err.response.status);
    } else {
      console.error(err);
    }
  }
})();

服务端代码如下:

import express from 'express';
import cors from 'cors';

const app = express();
const PORT = process.env.PORT || 8989;

app.use(cors());
app.use(express.json());

let POSTS = [{ id: 1, title: 'Hello Ky', body: 'First post' }];
let TOKEN = 'initial-token';

app.get('/posts', (_req, res) => {
  res.json(POSTS);
});

app.post('/posts', (req, res) => {
  const post = { id: Date.now(), ...req.body };
  POSTS.push(post);
  res.status(201).json(post);
});

app.get('/protected', (req, res) => {
  const auth = req.headers['authorization'];
  if (auth === `Bearer ${TOKEN}`) {
    res.json({ secret: 'You have accessed a protected resource!' });
  } else {
    res.status(401).json({ message: 'Token expired or missing' });
  }
});

app.post('/refresh', (_req, res) => {
  TOKEN = 'token-' + Date.now();
  console.log('Issued new token:', TOKEN);
  res.send(TOKEN);
});

app.listen(PORT, () => {
  console.log(`Mock API running at http://localhost:${PORT}`);
});
上一篇

go mod 基础

下一篇

U-Net简介

©2025 By bajiu.