# fastify-api-client **Repository Path**: xmecard/fastify-api-client ## Basic Information - **Project Name**: fastify-api-client - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-16 - **Last Updated**: 2026-07-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # @fastify-api/client 浏览器端 HMAC 签名 + JWT 鉴权客户端,配合 [`@fastify-api/api`](https://gitee.com/xmecard/fastify-api) 使用。 ## 安装 ```bash npm install git+ssh://git@gitee.com/xmecard/fastify-api-client.git ``` ## 快速开始 ```typescript import { createApiClient, ApiError } from '@fastify-api/client'; const credential = { appKey: 'app-demo-001', appSecret: 'sk-xxx' }; const client = createApiClient({ baseUrl: 'http://127.0.0.1:3001', getCredential: () => credential, refreshCredential: async () => { const newCred = await fetch('/api/credential/refresh').then(r => r.json()); credential = newCred; return newCred; }, // JWT 鉴权 getToken: () => localStorage.getItem('token'), onLoginRequired: () => { window.location.href = '/login'; }, // 错误分发 toast: (msg) => Toast.show(msg), alert: (msg) => alert(msg), }); // 200 直接返回 body,非 200 抛 ApiError try { const profile = await client.get('/api/app/profile'); console.log(profile); // Profile 类型,无需 .body } catch (e) { if (e instanceof ApiError) { console.error(`API ${e.statusCode}: ${e.message}`); } } // POST / PUT / DELETE 同理 const result = await client.post('/api/app/echo', { msg: 'hello' }); // 上传文件 const file = document.querySelector('input[type=file]').files[0]; const uploadResult = await client.upload('/api/app/upload', { fileName: file.name }, file); // 下载文件(仍返回 Response) const blobRes = await client.blob('/api/app/download'); if (blobRes.status === 200) { const text = await blobRes.body.text(); } ``` ## 错误处理 非 200 状态码统一抛 `ApiError`: ```typescript class ApiError extends Error { statusCode: number; // HTTP 状态码 message: string; // 优先取 body.message → body.error → "HTTP {code}" code?: string; // 服务端返回的 code(如 'login', 'retry', 'error') body?: unknown; // 原始响应体 } ``` ### 错误分发(toast / alert / 自定义) 每个请求可指定错误处理方式,默认 `'toast'`: ```typescript // toast 模式(默认)→ options.toast(message) await client.get('/api/data'); // alert 模式 → options.alert(message) await client.get('/api/data', 'alert'); // 自定义函数 await client.post('/api/submit', data, (msg) => { console.error('提交失败', msg); }); ``` 创建 client 时需提供 `toast` / `alert` 实现: ```typescript const client = createApiClient({ toast: (msg) => Toast.show(msg), alert: (msg) => alert(msg), // ... }); ``` ## 自动刷新凭证 服务端返回 `401 { code: 'retry' }` 时自动调用 `refreshCredential()` 获取新凭证并重试。多个并发 401 请求共享同一次刷新,不会重复调用。 ```typescript refreshCredential: async () => { const resp = await fetch('/api/credential/refresh'); const newCred = await resp.json(); sessionStorage.setItem('credential', JSON.stringify(newCred)); return newCred; // 必须返回 Credential 对象 } ``` ## 登录跳转 服务端返回 `401 { code: 'login' }` 时自动调用 `onLoginRequired()`,同时抛出 `ApiError`。 ```typescript onLoginRequired: () => { window.location.href = '/login'; } ``` ## JWT 鉴权 配置 `getToken` 后,每次请求自动带 `Authorization: Bearer ` 头。`skipAuthPaths` 中的路由跳过 HMAC 签名(如登录接口)。 ```typescript const client = createApiClient({ getToken: () => localStorage.getItem('token'), onLoginRequired: () => { window.location.href = '/login'; }, skipAuthPaths: ['/api/auth/login'], // ... }); ``` ## API ### `createApiClient(options) → ApiClient` | 参数 | 类型 | 说明 | |------|------|------| | `getCredential` | `() => Credential` | 获取当前 HMAC 凭证 | | `refreshCredential` | `() => Promise` | 刷新凭证并返回新凭证 | | `getToken` | `() => string \| null` | 获取 JWT token,null 表示未登录 | | `onLoginRequired` | `() => void` | 收到 401 code=login 时触发 | | `skipAuthPaths` | `string[]` | 跳过 HMAC 签名的路由列表 | | `baseUrl` | `string` | 基础 URL,拼接在所有 api 路径前 | | `windowMs` | `number` | 时间窗口(毫秒),默认 5 分钟 | | `logger` | `boolean \| Logger` | 启用日志 | | `toast` | `(msg: string) => void` | toast 错误处理器 | | `alert` | `(msg: string) => void` | alert 错误处理器 | ### `ApiClient` | 方法 | 签名 | 返回 | |------|------|------| | `get` | `(api: string, error?: ErrorHandler) => Promise` | 200 返回 body,非 200 抛 ApiError | | `post` | `(api: string, data?: unknown, error?: ErrorHandler) => Promise` | 同上 | | `put` | `(api: string, data?: unknown, error?: ErrorHandler) => Promise` | 同上 | | `delete` | `(api: string, error?: ErrorHandler) => Promise` | 同上 | | `upload` | `(api: string, jsonBody: Record, file: File, error?: ErrorHandler) => Promise` | 同上 | | `blob` | `(path: string) => Promise>` | 返回 `{ status, body: Blob }` | ### 类型 ```typescript type ErrorHandler = 'toast' | 'alert' | ((message: string) => void); interface Credential { appKey: string; appSecret: string; } type Response = { status: number; body: T }; ``` ## 日志 `logger: true` 启用 console.log 输出,或传入自定义 Logger 对象。 ```typescript const client = createApiClient({ logger: true, ... }); ``` 输出格式: ``` [api-client] REQ {"method":"GET","url":"...","body":{...}} [api-client] RES {"method":"GET","url":"...","status":200,"body":{...}} [api-client] ERR {"method":"GET","url":"...","error":"NetworkError"} ``` ```typescript import type { Logger } from '@fastify-api/client'; const customLogger: Logger = { log(msg: string) { yourLogger.info(msg); }, }; const client = createApiClient({ logger: customLogger, ... }); ``` ## 签名算法 每次请求客户端自动计算: ``` stringToSign = METHOD\nPATH\ntimestamp\nnonce\nbody signature = HMAC-SHA256(appSecret, stringToSign) → Base64 ``` 通过 `Web Crypto API` 实现,无需额外依赖。 ## 构建 ```bash npm run build # tsc → dist/ ```