Files
ele-HeiXiu/electron/main/net-request.ts
YoungestSongMo ae733016bb feat: 日志模块 + netRequest 封装 + PROJECT.md 更新
日志模块(文件持久化 + 事件总线联动 + 脱敏)
    新增:
      - shared/types/logging.ts         — LogLevel / LogCategory / LogEntry 类型
      - electron/main/logger.ts         — 主进程日志核心(JSON Lines / 每日轮转 / 7天清理)
      - electron/main/log-ipc.ts        — 渲染进程日志 IPC 通道接收
      - src/utils/logger.ts             — 渲染进程日志器(IPC 转发 / DEV 控制台 / 事件总线钩子)
      - shared/utils/sanitize.ts        — 脱敏工具(ID哈希 / 邮箱 / 手机 / 名称 / Token)
    修改:
      - electron/main.ts                — initLogger + flushLogger + uncaughtException
      - src/main.tsx                    — renderer 全局错误捕获
      - src/utils/event-bus.ts          — setEventLogListener 钩子,emit 自动审计
      - src/services/request.ts         — 拦截器结构化错误分级记录
      - electron/main/updater.ts        — 14处 console → logger 迁移
      - src/components/AppProvider.tsx   — 登录/登出/自动登录 auth 事件日志
      - shared/constants/ipc-channels.ts — 新增 LOG_MESSAGE 通道

  netRequest 封装(Electron net.request 的 axios 风格 API)
    新增:
      - electron/main/net-request.ts     — netRequest.get/post/put/del 命名空间
    修改:
      - electron/main/updater.ts         — fetchUpdateInfo 改用 netRequest(22→12行)

  文档更新
    - PROJECT.md                       — 目录结构 / 状态管理 / 日志系统 / IPC 章节同步现状
    - TODO.md                          — 增量更新改造 + 日志模块待办
    - UpdateA.md                       — API blockmap 字段 / 数据库字段说明
2026-06-04 11:03:25 +08:00

209 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// NetRequest — Electron net.request 的 axios 风格封装
//
// 用法:
// import { request, get, post } from './net-request';
// const { data } = await get<UpdateCheckResult>('/api/v1/update/check', {
// baseURL: 'https://www.heixiu.com',
// params: { platform: 'win32', version: '1.0.0' },
// });
//
// 设计:
// - 底层使用 Electron net.request不引入额外依赖
// - Promise 封装,支持 async/await
// - JSON 自动解析、查询参数自动拼接
// - 流式下载场景仍直接用 net.request本模块不封装 stream
// ============================================================
import { net } from 'electron';
// ---------- 类型 ----------
export interface RequestConfig {
/** 请求方法(默认 GET */
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
/** 基础 URL可选方便切换环境 */
baseURL?: string;
/** 查询参数(自动拼接 ?key=value&... */
params?: Record<string, string | number | undefined>;
/** 请求头 */
headers?: Record<string, string>;
/** 请求体(对象自动 JSON.stringify字符串原样发送 */
data?: unknown;
/** 响应类型(默认 json设置 text 跳过解析) */
responseType?: 'json' | 'text';
/** 超时时间(毫秒,默认 30s */
timeout?: number;
}
export interface RequestResponse<T = unknown> {
/** HTTP 状态码 */
status: number;
/** 状态文本(如 "OK" */
statusText: string;
/** 响应头 */
headers: Record<string, string | string[]>;
/** 响应体 */
data: T;
}
/** 网络请求错误 */
export class RequestError extends Error {
status?: number;
constructor(message: string, status?: number) {
super(message);
this.name = 'NetRequestError';
this.status = status;
}
}
// ---------- 工具 ----------
const DEFAULT_TIMEOUT = 30_000;
/** 拼接 URLbaseURL + path + query */
function buildUrl(url: string, config: RequestConfig): string {
let fullUrl = url;
if (config.baseURL) {
// url 以 / 开头时为绝对路径,否则拼接
const base = config.baseURL.replace(/\/+$/, '');
const path = url.startsWith('/') ? url : `/${url}`;
fullUrl = `${base}${path}`;
}
if (config.params) {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(config.params)) {
if (value !== undefined) {
search.append(key, String(value));
}
}
const qs = search.toString();
if (qs) {
fullUrl += (fullUrl.includes('?') ? '&' : '?') + qs;
}
}
return fullUrl;
}
/** 序列化请求体 */
function serializeBody(data: unknown): { body: string; contentType: string } {
if (typeof data === 'string') {
return { body: data, contentType: 'text/plain' };
}
return { body: JSON.stringify(data), contentType: 'application/json' };
}
// ---------- 核心 ----------
/**
* 发起请求axios 风格 API
*
* 流式下载场景请直接使用 Electron net.request本模块不封装
*/
export async function request<T = unknown>(
url: string,
config: RequestConfig = {},
): Promise<RequestResponse<T>> {
const fullUrl = buildUrl(url, config);
const method = (config.method || 'GET').toUpperCase();
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
return new Promise((resolve, reject) => {
const req = net.request({
method,
url: fullUrl,
});
// 超时处理
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
req.abort();
reject(new RequestError(`请求超时 (${timeout}ms)`));
}, timeout);
// 请求头
if (config.headers) {
for (const [key, value] of Object.entries(config.headers)) {
req.setHeader(key, value);
}
}
// 请求体
if (config.data !== undefined) {
const { body, contentType } = serializeBody(config.data);
req.setHeader('Content-Type', contentType);
req.write(body);
}
req.on('response', (response) => {
clearTimeout(timer);
const status = response.statusCode;
const statusText = response.statusMessage ?? '';
// 读取响应头
const headers: Record<string, string | string[]> = {};
// Electron IncomingMessage.headers 是 Record<string, string[]>
const rawHeaders = response.headers;
if (rawHeaders) {
for (const [key, value] of Object.entries(rawHeaders)) {
headers[key] = value.length === 1 ? value[0] : value;
}
}
// 读取响应体
let body = '';
response.on('data', (chunk: Buffer) => (body += chunk.toString()));
response.on('end', () => {
try {
const asJson = config.responseType !== 'text';
const data = asJson ? JSON.parse(body) : body;
resolve({ status, statusText, headers, data: data as T });
} catch {
// JSON 解析失败 → 返回原始文本
resolve({ status, statusText, headers, data: body as T });
}
});
response.on('error', (err) => {
clearTimeout(timer);
reject(new RequestError(`响应读取失败: ${err.message}`));
});
});
req.on('error', (err) => {
clearTimeout(timer);
if (timedOut) return;
reject(new RequestError(`网络请求失败: ${err.message}`));
});
req.end();
});
}
// ---------- 默认导出:类 axios 命名空间 ----------
export const netRequest = {
request,
get<T>(url: string, config?: Omit<RequestConfig, 'method' | 'data'>) {
return request<T>(url, { ...config, method: 'GET' });
},
post<T>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'data'>) {
return request<T>(url, { ...config, method: 'POST', data });
},
put<T>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'data'>) {
return request<T>(url, { ...config, method: 'PUT', data });
},
del<T>(url: string, config?: Omit<RequestConfig, 'method' | 'data'>) {
return request<T>(url, { ...config, method: 'DELETE' });
},
};