Files
simapi-vue/src/simapi.core.ts
T

335 lines
11 KiB
TypeScript
Raw Normal View History

2026-04-08 01:07:10 +08:00
/**
* SimApi Core — 纯 TypeScript 核心,无框架依赖
*
* 支持所有 JS/TS 环境(Node.js、浏览器、小程序等)。
*
* @example
* import { SimApiCore } from '@simcu/simapi'
* const api = new SimApiCore()
* api.setEndpoints({ default: 'https://api.example.com' })
* const res = await api.query('/users/list', { page: 1 })
*/
import {
2026-05-03 23:11:55 +08:00
type SimApiVersions,
type SimApiAuthConfig,
type SimApiApiConfig,
type SimApiOptions,
type SimApiBaseResponse,
2026-04-08 01:07:10 +08:00
} from './types'
export type {
2026-05-03 23:11:55 +08:00
SimApiVersions,
SimApiAuthConfig,
SimApiApiConfig,
SimApiOptions,
SimApiBaseResponse,
2026-04-08 01:07:10 +08:00
} from './types'
2026-04-08 01:33:23 +08:00
declare const SimApiVersion: string;
2026-04-23 20:16:38 +08:00
declare const AppVersion: string;
2026-05-03 23:11:55 +08:00
2026-04-08 01:07:10 +08:00
// ── Helper: Fetch with Timeout ────────────────────────────────────────
function fetchWithTimeout(
2026-05-03 23:11:55 +08:00
url: string,
options: RequestInit,
timeout: number = 10000
2026-04-08 01:07:10 +08:00
): Promise<Response> {
2026-05-03 23:11:55 +08:00
return Promise.race([
fetch(url, options),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
),
])
2026-04-08 01:07:10 +08:00
}
// ── Helper: Fetch POST with JSON body ────────────────────────────────────
async function fetchPost<T = any>(
2026-05-03 23:11:55 +08:00
url: string,
body: any,
headers: Record<string, string>,
timeout: number
2026-04-08 01:07:10 +08:00
): Promise<SimApiBaseResponse<T>> {
2026-05-03 23:11:55 +08:00
const options: RequestInit = {
method: 'POST',
headers: headers as HeadersInit,
body: body instanceof FormData ? body : JSON.stringify(body),
credentials: 'omit', // 从不发送 Cookie
2026-04-08 01:07:10 +08:00
}
2026-05-03 23:11:55 +08:00
const response = await fetchWithTimeout(url, options, timeout)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw {
status: response.status,
statusText: response.statusText,
data: errorData,
message: `HTTP ${response.status}: ${response.statusText}`,
}
}
return response.json()
2026-04-08 01:07:10 +08:00
}
// ── SimApiCore ────────────────────────────────────────
export class SimApiCore {
private debug: boolean = true
private auth: SimApiAuthConfig = {
2026-05-03 23:11:55 +08:00
token_name: 'simapi-auth-token',
2026-05-04 23:28:35 +08:00
check_url: '/user/info',
2026-05-03 23:11:55 +08:00
logout_url: '/auth/logout',
login_url: '/auth/login',
2026-04-08 01:07:10 +08:00
}
2026-08-10 21:01:21 +08:00
private webConfig: Map<string, Record<string, object>> = new Map();
2026-04-08 01:07:10 +08:00
private api: SimApiApiConfig = {
2026-05-03 23:11:55 +08:00
endpoints: {default: ''},
defaultEndpoint: 'default',
businessCallback: {
2026-05-04 16:57:08 +08:00
401: () => this.removeToken(),
2026-05-03 23:11:55 +08:00
common: () => {
},
},
responseCallback: {
success: (response: any) => response,
error: (_err: any) => {
},
},
timeout: 10000,
2026-04-08 01:07:10 +08:00
}
2026-05-03 23:11:55 +08:00
constructor(options?: SimApiOptions) {
if (options) {
this.configure(options)
2026-04-08 01:07:10 +08:00
}
}
2026-05-03 23:11:55 +08:00
/**
2026-05-04 19:56:48 +08:00
* 从 JSON 文件加载配置
2026-05-03 23:11:55 +08:00
*
2026-05-04 19:56:48 +08:00
* 部署时替换 config.json 即可切换环境,无需重新构建。
*
* @param file - 配置文件路径,默认 'config.json'
* @example
* await api.loadFromFile()
* await api.loadFromFile('/env/prod.json')
2026-05-03 23:11:55 +08:00
*/
2026-05-04 19:56:48 +08:00
async loadFromFile(file: string = 'config.json'): Promise<void> {
try {
const resp = await fetch(file)
if (!resp.ok) {
this.logDebug(`配置文件 ${file} 加载失败: HTTP ${resp.status}`)
return
}
const config = await resp.json()
if (config.debug !== undefined) {
this.debug = config.debug
}
if (config.endpoints) {
2026-05-04 23:28:35 +08:00
this.api.endpoints = {...this.api.endpoints, ...config.endpoints}
2026-05-04 19:56:48 +08:00
}
if (config.defaultEndpoint) {
this.api.defaultEndpoint = config.defaultEndpoint
}
} catch (err: any) {
this.logDebug(`配置文件 ${file} 加载失败: ${err.message}`)
2026-05-03 23:11:55 +08:00
}
2026-04-08 01:07:10 +08:00
}
2026-05-03 23:11:55 +08:00
configure(options: SimApiOptions): void {
if (options.debug !== undefined) {
this.debug = options.debug
}
if (options.auth) {
this.auth = {...this.auth, ...options.auth}
}
if (options.api) {
this.api = {
...this.api,
...options.api,
endpoints: {...this.api.endpoints, ...(options.api.endpoints ?? {})},
businessCallback: {...this.api.businessCallback, ...(options.api.businessCallback ?? {})},
responseCallback: {...this.api.responseCallback, ...(options.api.responseCallback ?? {})},
}
}
2026-04-08 01:07:10 +08:00
}
get isDebug() {
return this.debug
}
setDebug(debug: boolean) {
this.debug = debug;
}
2026-05-03 23:11:55 +08:00
setEndpoints(endpoints: { [name: string]: string }): void {
this.api.endpoints = {...this.api.endpoints, ...endpoints}
2026-04-08 01:07:10 +08:00
}
2026-05-03 23:11:55 +08:00
getEndpoint(name?: string): string {
return this.api.endpoints[name ?? this.api.defaultEndpoint] ?? ''
2026-04-08 01:07:10 +08:00
}
2026-05-03 23:11:55 +08:00
setBusinessCallback(code: number | string, callback: (data: any) => void): void {
this.api.businessCallback[code] = callback
2026-04-08 01:07:10 +08:00
}
2026-05-03 23:11:55 +08:00
getToken(): string {
2026-05-04 16:57:08 +08:00
const name = this.auth.token_name
const match = document.cookie.match(new RegExp(`(?:^|;)\\s?${name}=([^;]+)`))
return match ? match[1] : ''
2026-05-03 23:11:55 +08:00
}
setToken(token: string): void {
2026-05-04 16:57:08 +08:00
const name = this.auth.token_name
2026-05-05 07:16:29 +08:00
document.cookie = `${name}=${token}; path=/; max-age=315360000; secure; samesite=none`
2026-05-03 23:11:55 +08:00
}
removeToken(): void {
2026-05-04 16:57:08 +08:00
const name = this.auth.token_name
2026-05-05 07:16:29 +08:00
document.cookie = `${name}=; path=/; max-age=0; secure; samesite=none`
2026-05-03 23:11:55 +08:00
}
2026-05-04 16:57:08 +08:00
2026-05-03 23:11:55 +08:00
genS4(): string {
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
.toString(16)
.substring(1)
}
/**
* 日志工具(仅在 debug 模式下输出)
*
* @example
* api.logDebug('用户登录', { id: 1, name: 'test' })
* api.logDebug('请求开始', uri, params)
*/
logDebug(...args: any[]): void {
if (!this.debug) return
console.log('[DEBUG]', ...args)
}
2026-08-10 20:46:40 +08:00
async getConfig(reload: boolean = false, endpointName: string = 'default'): Promise<Record<string, object>> {
2026-08-10 20:37:23 +08:00
if (!this.webConfig.has(endpointName) || reload) {
const versions: SimApiVersions = {
uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion,
uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
apiApp: '0.0.0',
apiSimApi: '0.0.0',
apiAppFull: '0.0.0',
apiSimApiFull: '0.0.0',
};
const resp = await this.query<any>('/config', {}, endpointName)
2026-05-03 23:11:55 +08:00
if (resp?.data) {
2026-08-10 20:37:23 +08:00
const d = resp.data.versions;
2026-05-03 23:11:55 +08:00
versions.apiApp = d.App?.split('+')[0] ?? '0.0.0';
versions.apiSimApi = d.SimApi?.split('+')[0] ?? '0.0.0';
versions.apiAppFull = d.App ?? '0.0.0';
versions.apiSimApiFull = d.SimApi ?? '0.0.0';
if (this.debug) {
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
}
2026-08-10 20:37:23 +08:00
resp.data.versions = versions;
this.webConfig.set(endpointName, resp.data);
2026-05-03 23:11:55 +08:00
}
}
2026-08-10 20:37:23 +08:00
return this.webConfig.get(endpointName)!;
2026-05-03 23:11:55 +08:00
}
async query<T = any>(
uri: string,
params: any = {},
endpointKey?: string,
2026-05-04 02:27:43 +08:00
extraHeaders?: Record<string, string>,
selfHandleError: boolean = false
2026-05-03 23:11:55 +08:00
): Promise<SimApiBaseResponse<T>> {
const headers: Record<string, string> = {...extraHeaders, ...{}}
const queryId = this.genS4()
if (!(params instanceof FormData)) {
headers['Content-Type'] = 'application/json'
}
const token = this.getToken()
if (token) {
headers['Token'] = token
}
if (this.debug) {
headers['Query-Id'] = queryId
2026-05-04 16:57:08 +08:00
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', this.getToken())
2026-05-03 23:11:55 +08:00
}
const url = this.getEndpoint(endpointKey) + uri
try {
const respData = await fetchPost<T>(
url,
params,
headers,
this.api.timeout ?? 10000
)
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', respData)
}
const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse<T>
// 业务回调处理
2026-05-04 02:27:43 +08:00
if (!selfHandleError) {
if (this.api.businessCallback.hasOwnProperty(processedData.code)) {
this.api.businessCallback[processedData.code](processedData)
} else if (this.api.businessCallback['common'] && processedData.code !== 200) {
this.api.businessCallback['common'](processedData)
}
2026-05-03 23:11:55 +08:00
}
// code != 200 时抛出业务错误
if (processedData.code !== 200) {
throw processedData
}
return processedData
} catch (error: any) {
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', error)
}
// 网络/HTTP 错误:包装成标准响应格式抛出
if (!error?.code) {
this.api.responseCallback.error(error)
throw {
code: -1,
message: error?.message || '网络错误',
data: error,
} as SimApiBaseResponse<T>
}
// 业务错误直接抛出
throw error
}
}
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
const result = await this.query<string>(this.auth.login_url, request)
if (result?.data) {
this.setToken(result.data)
}
return result
}
async logout(url?: string | null): Promise<any> {
if (url !== null) {
this.query(url ?? this.auth.logout_url).catch(() => true)
2026-05-03 23:11:55 +08:00
}
this.removeToken()
return true
}
2026-05-04 23:28:35 +08:00
async checkLogin(url?: string | null): Promise<any> {
return this.query(url ?? this.auth.check_url);
2026-04-08 01:07:10 +08:00
}
}