glsk-toolkit
v1.0.5
Published
一个兼容Web端与小程序端的工具包,包含HTTP请求、签名算法、拦截器等功能
Maintainers
Readme
glsk-toolkit
一个兼容Web端与小程序端的工具包,包含HTTP请求、签名算法、拦截器等功能
特性
- 🌐 跨平台兼容:同时支持Web端和小程序端 和 uniapp
- 🔐 内置签名算法:支持自动签名和自定义签名函数
- 🛡️ 拦截器支持:请求和响应拦截器
- 🔄 自动重试:支持请求失败自动重试
- 🚫 防重复提交:内置防重复提交机制,避免用户重复点击造成的重复请求
- 📊 错误监控:集成Sentry错误上报
- 🎯 TypeScript支持:完整的类型定义
- 🚀 轻量级:无额外依赖,体积小巧
- 🔧 灵活配置:支持全局配置和单次请求配置
- 🛡️ 安全特性:支持白名单验证和参数签名
- 📱 小程序优化:针对微信小程序等环境优化
- 🛠️ 工具集合:未来将扩展更多实用工具函数
安装
npm install glsk-toolkit使用方法
基本使用
import { Request } from 'glsk-toolkit';
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret'
});
// GET 请求
request.get('/users').then(response => {
console.log(response.data);
});
// POST 请求
request.post('/users', {
name: 'John',
email: '[email protected]'
}).then(response => {
console.log(response.data);
});简单GET请求(simpleGet)
如果只需要发送简单的GET请求而不需要签名和其他复杂配置,可以使用导出的simpleGet函数:
import { simpleGet } from 'glsk-toolkit';
// 发送简单的GET请求
simpleGet('https://api.example.com/data')
.then(response => {
console.log(response);
})
.catch(error => {
console.error('请求失败:', error);
});
// 使用async/await
async function fetchData() {
try {
const data = await simpleGet('https://jsonplaceholder.typicode.com/posts/1');
console.log('获取到的数据:', data);
} catch (error) {
console.error('请求失败:', error);
}
}注意: simpleGet函数是一个轻量级的GET请求工具,不包含签名、拦截器等高级功能。如果需要这些功能,请使用完整的Request类。
使用 Defaults 类async/await
import { Request } from 'glsk-toolkit';
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret'
});
async function fetchUsers() {
try {
const response = await request.get('/users');
console.log(response.data);
} catch (error) {
console.error('Error fetching users:', error);
}
}
fetchUsers();使用 Defaults 类管理配置
import { Request, Defaults } from 'glsk-toolkit';
// 创建全局默认配置
const globalDefaults = new Defaults({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Authorization': 'Bearer global-token',
'X-App-Version': '1.0.0'
},
appId: 'your_app_id',
appSecret: 'your_app_secret',
retry: {
retries: 2,
retryDelay: 500,
shouldResetTimeout: true
}
});
// 创建使用全局配置的请求实例
const request = new Request(globalDefaults);
// 使用 merge 方法创建特定请求的配置
const specificConfig = request.defaults.merge({
timeout: 3000,
headers: {
'X-Request-ID': 'specific-request-123'
},
params: {
lang: 'zh-CN'
}
});
// 使用特定配置发送请求
request.get('/users', specificConfig).then(response => {
console.log(response.data);
});其他使用示例
import { Request } from 'glsk-toolkit';
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret'
});
// 使用 async/await
async function fetchUsers() {
try {
const response = await request.get('/users');
console.log(response.data);
} catch (error) {
console.error('请求失败:', error.message);
}
}
fetchUsers();拦截器
// 请求拦截器
request.interceptors.request.use(
config => {
// 在发送请求之前做些什么
return config;
},
error => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
response => {
// 对响应数据做点什么
return response;
},
error => {
// 对响应错误做点什么
return Promise.reject(error);
}
);事件监听
glsk-toolkit 支持事件监听机制,可以监听请求的各个阶段:
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret',
// 事件配置
events: {
// 请求发送前触发
request: (requestData, config) => {
console.log('request事件触发', requestData, config);
},
// 收到响应时触发(无论成功或失败)
response: (responseData, config) => {
console.log('response事件触发', responseData, config);
},
// 请求成功时触发(业务状态码为成功值)
success: (data, config) => {
console.log('success事件触发', data, config);
},
// 请求失败时触发(业务状态码为失败值)
error: (errorData, config) => {
console.log('error事件触发', errorData, config);
},
// 需要登出时触发(业务状态码为登出值)
logout: (data, config) => {
console.log('logout事件触发', data, config);
// 可以在这里处理登出逻辑
},
// 重复请求被阻止时触发
duplicate: (duplicateData, config) => {
console.log('duplicate事件触发', duplicateData, config);
// 可以在这里处理重复请求的逻辑,如显示提示信息
}
}
});
// 发送请求时会自动触发相应的事件
request.get('/users').then(response => {
console.log(response.data);
});事件触发时机:
request: 请求发送前response: 收到响应时(无论成功失败)success: 业务逻辑成功时(code等于code_success_value)error: 业务逻辑失败时(code不等于code_success_value且不是登出状态)logout: 需要登出时(code或errId等于errId_logout_value)duplicate: 重复请求被阻止时(启用重复防护且检测到重复请求)
数组参数处理
glsk-toolkit 支持发送包含数组的请求参数:
// GET 请求中使用数组参数(查询参数)
request.get('/posts', {
params: {
userId: [1, 2, 3] // 默认转换为 userId=1&userId=2&userId=3
}
}).then(response => {
console.log(response.data);
});
// POST 请求中使用数组参数(请求体)
request.post('/posts', {
title: '我的帖子',
body: '帖子内容',
userId: 1,
tags: ['JavaScript', 'TypeScript', 'Fetch'] // 数组会被自动转换为 JSON
}).then(response => {
console.log(response.data);
});自定义签名函数
glsk-toolkit 支持自定义签名函数,允许用户实现自己的签名算法:
// 导入默认签名函数(可选)
import { generateSignature } from 'glsk-toolkit';
// 自定义签名函数示例
const customSignatureFunc = ({ appId, appSecret, params, method, noSignAttr }) => {
// 用户自定义的签名算法
const nonce = 'custom-nonce';
const timestamp = Date.now().toString();
// 实现自定义签名逻辑
const sign = 'custom-signature';
// 返回头部字段(与 SDK 约定一致)
return {
'X-App-Id': appId,
'X-Nonce': nonce,
'X-Timestamp': timestamp,
'X-Sign': sign
// 可选:若需要也可返回 'X-SessionId'
};
};
// 使用自定义签名函数
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret',
signatureFunc: customSignatureFunc // 使用自定义签名函数
});
// 发起请求
request.get('/users').then(response => {
console.log(response.data);
});防重复提交
glsk-toolkit 内置了防重复提交机制,可以有效防止用户重复点击造成的重复请求:
// 启用防重复提交(使用请求URL作为标识)
const response = await request.post('/api/submit-form', {
name: '张三',
email: '[email protected]'
}, {
duplicatePrevention: true // 启用防重复提交
});
// 禁用防重复提交
const response = await request.post('/api/submit-form', {
name: '张三',
email: '[email protected]'
}, {
duplicatePrevention: false // 禁用防重复提交
});防重复提交工作原理
- 启用防重复提交:当
duplicatePrevention设置为true时,系统会使用请求URL作为唯一标识 - 重复检测:如果相同URL的请求正在进行中,新的请求会被阻止并抛出
DuplicateRequestError错误 - 自动清理:请求完成(成功或失败)后,该URL的防重复标识会被自动清除
- 事件触发:重复请求被阻止时会触发
duplicate事件
处理重复请求错误
try {
const response = await request.post('/api/submit-form', {
name: '张三',
email: '[email protected]'
}, {
duplicatePrevention: true
});
console.log('提交成功:', response.data);
} catch (error) {
if (error.isDuplicateError) {
console.log('请求正在进行中,请勿重复提交');
// 可以显示用户友好的提示信息
} else {
console.error('提交失败:', error.message);
}
}监听重复请求事件
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret',
events: {
duplicate: (url, config) => {
console.log('检测到重复请求:', url);
// 可以在这里显示用户提示,如 Toast 消息
showToast('请求正在进行中,请勿重复操作');
}
}
});防重复提交管理器
您也可以直接使用防重复提交管理器进行更精细的控制:
import { duplicatePreventionManager } from 'glsk-toolkit';
// 检查是否存在重复请求
const isDuplicate = duplicatePreventionManager.checkDuplicate(true, '/api/submit-form');
// 手动注册请求
const requestPromise = fetch('/api/submit-form');
duplicatePreventionManager.registerRequest(true, '/api/submit-form', requestPromise);
// 清除所有防重复记录
duplicatePreventionManager.clearAll();
// 获取当前活跃请求数量
const activeCount = duplicatePreventionManager.getActiveRequestCount();
重试配置
SDK 支持在单次请求中配置重试参数。当请求配置中包含 retry 对象时,将自动启用重试功能。
基本使用
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret'
});
// 启用重试的请求
try {
const response = await request.post('/api/submit', {
data: 'some data'
}, {
retry: { // 配置重试参数
retries: 3, // 最大重试次数
retryDelay: 1000, // 重试延迟时间(ms)
shouldResetTimeout: true
}
});
console.log('请求成功:', response.data);
} catch (error) {
console.error('请求最终失败:', error.message);
}
// 不启用重试的请求(默认行为)
try {
const response = await request.get('/api/quick-check');
console.log('快速检查结果:', response.data);
} catch (error) {
console.error('请求失败,不重试:', error.message);
}自定义重试条件
// 自定义重试条件的请求
try {
const response = await request.post('/api/upload', formData, {
retry: {
retries: 2,
retryDelay: 500,
retryCondition: (error) => {
// 只有网络错误或5xx服务器错误才重试
return error.code === 'NETWORK_ERROR' ||
(error.response && error.response.status >= 500);
}
}
});
console.log('上传成功:', response.data);
} catch (error) {
console.error('上传失败:', error.message);
}结合事件监听
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret',
events: {
request: (url, config) => {
if (config.retry) {
console.log('🔄 启用重试的请求:', url);
}
},
error: (error, config) => {
if (config.retry) {
console.log('❌ 重试请求失败:', error.message);
}
}
}
});
### 取消请求(简化版)
glsk-toolkit 现在支持简化版的请求取消方式,请求方法直接返回带有 `abort()` 方法的对象:
```javascript
// 1. 发起请求
const requestPromise = request.get('/users');
// 2. 立即取消请求
requestPromise.abort();
// 3. 处理结果
requestPromise
.then(response => {
// 处理响应(不会执行,因为请求已被取消)
})
.catch(error => {
if (error.message === '请求已被取消') {
console.log('请求已被取消');
}
});在 async/await 中使用取消功能
// 发起请求
const requestPromise = request.get('/users');
// 立即取消请求
requestPromise.abort();
try {
// 等待请求结果
const response = await requestPromise;
console.log(response.data);
} catch (error) {
if (error.message === '请求已被取消') {
console.log('请求已被取消');
}
}取消请求(标准版)
您也可以使用标准的 AbortController 方式:
// 1. 创建 AbortController 实例
const controller = new AbortController();
// 2. 发起请求时传入 signal
request.get('/users', {
signal: controller.signal
}).then(response => {
// 处理响应
}).catch(error => {
if (error.message === '请求已被取消') {
console.log('请求已被取消');
}
});
// 3. 取消请求
controller.abort();批量请求的选择性取消
// 发起多个请求
const request1 = request.get('/users');
const request2 = request.get('/posts');
const request3 = request.get('/albums');
// 取消其中一个请求
request2.abort();
// 处理各个请求的结果
request1.then(response => console.log('用户数据:', response.data));
request2.catch(error => {
if (error.message === '请求已被取消') {
console.log('帖子请求被取消');
}
});
request3.then(response => console.log('相册数据:', response.data));小程序中使用
在小程序中使用方式与 Web 端完全一致:
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret'
});
request.get('/users').then(response => {
console.log(response.data);
});Sentry 错误上报
glsk-toolkit 集成了 Sentry 错误上报功能,可以自动捕获网络错误和业务逻辑错误:
import { Request, reportBusinessError } from 'glsk-toolkit';
// 创建请求实例时配置 Sentry
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'your_app_id',
appSecret: 'your_app_secret',
// Sentry 配置
sentry: {
enable: true, // 是否启用Sentry错误上报
dsn: 'https://[email protected]/project-id',
options: {
environment: 'production',
release: 'v1.0.0'
}
}
});
// 网络错误自动上报
try {
await request.get('/api/users');
} catch (error) {
console.error('请求失败:', error.message);
// 错误已自动上报到 Sentry,无需手动处理
}
// 业务逻辑错误自动上报
// 假设 API 返回: { code: 40001, message: "用户未授权", data: null }
const response = await request.get('/api/user/profile');
// 虽然 HTTP 状态码是 200,但业务错误码非零
// 系统会自动检测并上报到 Sentry
// 手动上报业务错误
reportBusinessError(
'支付处理错误',
{ code: 50001, message: '服务暂时不可用' },
{ requestContext: { orderId: 'order123' } },
{ level: 'error', tags: { module: 'payment' } }
);更多详细信息,请参阅 Sentry 错误上报文档。
API
Request 构造函数
const request = new Request({
baseURL: '', // 基础URL
timeout: 5000, // 超时时间(毫秒)
appId: '', // 应用ID(用于签名)
appSecret: '', // 应用密钥(用于签名)
headers: {}, // 默认请求头
signatureFunc: null, // 自定义签名函数
sentry: { // Sentry 错误上报配置
enable: false, // 是否启用Sentry错误上报,默认为false
dsn: '', // Sentry DSN
options: {} // Sentry 初始化选项
},
security: { // 安全配置
enable: false, // 是否启用白名单功能,默认为false
address: undefined // 自定义白名单地址,默认使用内置地址
},
disableBusinessErrorReport: false // 是否禁用业务错误自动上报
});请求方法
所有请求方法都返回带有 abort() 方法的 Promise 对象:
request.get(url[, config])request.post(url[, data[, config]])request.put(url[, data[, config]])request.delete(url[, config])request.patch(url[, data[, config]])request.request(config)
响应处理配置说明
code_key: 指定后端返回数据中状态码的字段名,可以是字符串或字符串数组code_success_value: 指定表示成功的状态码值data_key: 指定后端返回数据中实际数据的字段名errId: 指定错误ID的字段名errId_logout_value: 指定需要登出的错误ID值
示例:
// 1) 在单次请求中配置响应处理参数
await request.get('/api/user/info', {
response: {
code_key: ['code', 'status'], // 按顺序匹配第一个存在的字段
code_success_value: '0', // 比较时内部会转成字符串
data_key: 'data',
errId: 'errId',
errId_logout_value: '21'
}
});
// 2) 在构造实例或 Defaults 中设置全局响应处理参数
const req = new Request({
baseURL: 'https://api.example.com',
appId: 'id',
appSecret: 'secret',
response: {
code_key: 'code',
code_success_value: '0',
data_key: 'data',
errId: 'errId',
errId_logout_value: '21'
}
});
// 若需要仅本次返回完整响应,可在单次请求设置 returnAll 覆盖:
await req.get('/api/list', { returnAll: true });会话ID(sessionId)
- 作用:当后端接口需要会话标识时,可为“单次请求”或“初始化全局配置”开启并自动注入会话ID。
- 开启方式:支持两种方式:
- 初始化传入:在创建 Request 时通过
new Request({ session: { requiresSession: true, sessionParamName, sessionId } })配置为默认开启; - 单次请求开启:在该次请求的
config.session.requiresSession设为true。
- 初始化传入:在创建 Request 时通过
- 提供方式(三选一):
- 直接字符串:
sessionId: 'abc123' - 同步函数:
sessionId: () => localStorage.getItem('sid') || '' - 异步函数/Promise:
sessionId: async () => await getSid()或sessionId: fetchSidPromise
- 直接字符串:
- 注入位置:SDK 会将会话ID按
sessionParamName注入到请求的params中(包括 POST/PUT/PATCH),并参与签名归一化处理;如需使其不参与签名,可结合noSignAttr排除对应字段名。 - 并发与缓存:同一 Request 实例内会共享并发获取过程,成功后会缓存本次获取的会话ID;首次获取失败会抛错并清理共享等待,下次请求会重新尝试。
- 覆盖规则:单次请求的
session会与初始化的session合并:{ ...init.session, ...config.session },并以单次请求配置为优先(例如不同的sessionParamName以单次请求为准)。
示例(初始化传入,全局开启):
const request = new Request({
baseURL: 'https://api.example.com',
appId: 'id',
appSecret: 'secret',
session: {
requiresSession: true,
sessionParamName: 'sessionId',
sessionId: () => localStorage.getItem('sessionId') || ''
}
});
// 会按初始化的 session 配置自动注入 sid
await request.post('/api/order/submit', { amount: 100 });示例(单次请求覆盖并注入自定义参数名 sid):
const request = new Request({ baseURL: 'https://api.example.com', appId: 'id', appSecret: 'secret', session: { requiresSession: true, sessionId: () => localStorage.getItem('sid') || '' } });
await request.post('/api/order/submit', { amount: 100 }, {
session: {
requiresSession: true,
sessionParamName: 'sid'
}
});Tips:
- 若
requiresSession为true但未提供sessionId获取方式,或获取到的值为空/非字符串,会抛出错误。 - 若后端不要求每个请求都携带会话ID,请勿在全局默认配置中启用,会影响所有请求;建议仅在需要的单次请求中配置。
noSignAttr(单次请求签名排除)
- 作用:为“单次请求”指定不参与签名的参数名列表,避免某些业务参数(如
token、traceId、sessionId等)进入签名字符串。 - 传参方式:在该次请求的
config.noSignAttr传入字符串数组。 - 规则说明:
- 列表中的字段会在签名字符串构建时被忽略;字段名需与归一化后的键一致(GET 的查询键、POST/PUT/PATCH 归并后的
params/data键)。 - 建议只排除业务参数,勿排除系统参数(如
X-App-Id、X-Nonce、X-Timestamp),否则可能导致服务端验签失败。 - 建议仅在方法调用处传入,避免在全局默认配置中设置,防止影响所有请求。
- 列表中的字段会在签名字符串构建时被忽略;字段名需与归一化后的键一致(GET 的查询键、POST/PUT/PATCH 归并后的
示例:
// 从签名中排除 token 与会话参数(默认名为 sessionId,或自定义为 sid)
await request.get('/api/secure', {
params: { a: 1, token: 't123' },
noSignAttr: ['token', 'sessionId']
});
// 若自定义了会话参数名为 'sid',并希望其不参与签名:
await request.post('/api/profile', { name: 'Jack' }, {
session: { requiresSession: true, sessionId: () => getSid(), sessionParamName: 'sid' },
noSignAttr: ['sid']
});签名函数接口
自定义签名函数需要符合以下接口:
interface ISignatrueOptionsParams {
params: Record<string, any>;
data: Record<string, any>;
}
interface SignatureOptions {
appId: string;
appSecret: string;
// 归一化后的参数集合:分别给出 params 与 data,对应本次请求的最终值
// 若该次请求开启了会话(requiresSession=true),则含注入的 sessionId(使用 sessionParamName 指定键名)
params?: ISignatrueOptionsParams;
// 单次请求签名排除列表:在构建签名字符串时需忽略这些键
noSignAttr?: string[];
// 本次请求方法(GET/POST/PUT/PATCH/DELETE),影响参数归一化策略
method?: string;
// 可选:会话ID(通常无需显式传入)
sessionId?: string;
}
interface SignatureResult {
"X-App-Id": string;
"X-Nonce": string;
"X-Timestamp": string;
"X-Sign": string;
"X-SessionId"?: string;
}
function signatureFunc(options: SignatureOptions): SignatureResult;说明:
- 框架会先对参数进行归一化处理(含
processParamsAsJson规则与sessionId注入),再将params与可选的noSignAttr一并传入签名函数。 - 归一化细则:数组/对象统一转为 JSON 字符串;字符串来自
params时不再额外JSON.stringify,来自data时会JSON.stringify;当params与data存在同名键时,优先采用params的值。 - 自定义签名函数内部应确保:对
options.params中的键进行排序、拼接等逻辑时,忽略options.noSignAttr中的键,以保证与服务端验签一致。 returnAll: 若为true,处理器会直接返回后端原始业务体(config.response 仍然会触发 logout 事件但不会截取 data 字段)。code_key支持字符串或字符串数组:为数组时将按顺序查找第一个在响应中存在的字段作为业务状态码字段。code_success_value与实际业务码比较时采用宽松等值(内部转为字符串比较),例如0与'0'视为一致。errId_logout_value的判断同时支持两种情况:当code等于该值或当errId等于该值,均视为需要登出。
