import { ShiosCacheStorage } from './cache'; const shiosCaches = new ShiosCacheStorage(); //import { ShiosResponse, ShiosRequestInit, URLParams } from './type'; const httpStatus: Record = { 200: 'OK', 201: 'Created', 204: 'No Content', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 408: 'Request Time-out', 409: 'Conflict', 500: 'Internal Server Error', }; enum MethodType { OPTIONS = 'OPTIONS', GET = 'GET', HEAD = 'HEAD', POST = 'POST', PUT = 'PUT', PATCH = 'PATCH', DELETE = 'DELETE', TRACE = 'TRACE', CONNECT = 'CONNECT', } //content-disposition: attachment; filename=42%___________-__12208071526.txt; filename*=UTF-8''42%25%E4%B8%89%E6%B0%AF%E5%BC%82%E6%B0%B0%E5%B0%BF%E9%85%B8%E5%8F%AF%E6%B9%BF%E6%80%A7%E7%B2%89%E5%89%82-%E6%B5%8B%E8%AF%9512208071526.txt function getFileName(disposition: string) { let dispositions = disposition.split('; '); if (dispositions[0] != 'attachment') { return ''; } if (dispositions[2].startsWith(`filename*=UTF-8''`)) { return decodeURIComponent(dispositions[2].substring(17)); } else if (dispositions[1].startsWith('filename=')) { return decodeURIComponent(dispositions[1].substring(9)); } } type Next = (response: Response, next?: Next) => unknown; interface ChainFunc { (response: Response, next?: ChainFunc): unknown; } async function getText(response: Response, next?: ChainFunc) { let contentType = response.headers.get('content-type'); if (contentType?.includes('text')) { return response.text(); } else { return next?.(response); } } async function getJson(response: Response, next?: ChainFunc) { let contentType = response.headers.get('content-type'); if (contentType?.includes('json')) { return response.json(); } else { return next?.(response); } } async function getBlob(response: Response, next?: ChainFunc) { let disposition = response.headers.get('content-disposition'); if (disposition) { let fileName = getFileName(disposition); if (fileName) { let blob = await response.blob(); const tmpLink = document.createElement('a'); const objectUrl = URL.createObjectURL(blob); tmpLink.href = objectUrl; tmpLink.download = fileName; // document.body.appendChild(tmpLink); // 如果不需要显示下载链接可以不需要这行代码 tmpLink.click(); URL.revokeObjectURL(objectUrl); } } else { return next?.(response); } } /** * 网络http请求 */ export class Shios { private _chain: Array = []; private _token?: string; baseUrl?: string; headers: Record = { 'Content-Type': 'application/json' }; /** 超时时间,单位 ms */ timeout?: number; isCache = false; public get token(): string | undefined { return this._token; } public set token(value: string | undefined) { if (value) { this.headers.authorization = `Bearer ${value}`; } else { delete this.headers.authorization; } this._token = value; } private _isDebug = false; debug: (...data: any[]) => void = () => {}; requestAfter?: (response: ShiosResponse, ...args: any[]) => void; exceptionHandler?: (response: ShiosResponse, ...args: any[]) => void; failureHandler?: ( response: ShiosResponse, pathname: string, init: ShiosRequestInit ) => void; public get isDebug() { return this._isDebug; } public set isDebug(value: boolean) { if (value) { this.debug = console.log.bind(console); } this._isDebug = value; } /** * 私有构造函数 */ private constructor() { //this.use(getBlob); this.use(getJson); this.use(getText); // this.use(this.getError); } static instance: Shios; static getInstance() { if (!Shios.instance) { Shios.instance = new Shios(); } return Shios.instance; } /** * * @param pathname apipath api路径 * @param { ShiosRequestInit} init { path, search } * @return {Promise>} Promise */ get( pathname: string, init: ShiosRequestInit = {} ): Promise> { init.method = MethodType.GET; return this.requst(pathname, init); } /** * * @param {string}pathname apipath api路径 * @param { ShiosRequestInit}init */ post(pathname: string, init: ShiosRequestInit = {}) { init.method = MethodType.POST; return this.requst(pathname, init); } put(pathname: string, init: ShiosRequestInit = {}) { init.method = MethodType.PUT; return this.requst(pathname, init); } patch(pathname: string, init: ShiosRequestInit = {}) { init.method = MethodType.PATCH; return this.requst(pathname, init); } delete(pathname: string, init: ShiosRequestInit = {}) { init.method = MethodType.DELETE; return this.requst(pathname, init); } async download(pathname: string, init: ShiosRequestInit = {}) { this._chain.unshift(getBlob); var result = await this.get(pathname, init); this._chain.shift(); return result; } async requst( pathname: string, init: ShiosRequestInit ): Promise> { if (init.data) { init.body = JSON.stringify(init.data); } init.headers = this.setHeaders(init.headers); this.debug('init', init); let res: ShiosResponse = { data: undefined as T, url: '', ok: false, headers: {}, }; if (typeof uni != 'undefined' && typeof uni.request == 'function') { res = await this.uniRequest(pathname, init); } else { try { let response = await this.fetchAsync(pathname, init); this.debug(response); res.ok = response.ok ?? response.status < 400; res.url = response.url; res.status = response.status ?? 0; res.statusText = response.statusText || httpStatus[response.status]; let headers: Record = {}; response.headers.forEach((v, k) => { res!.headers![k] = v; }); this.index = 0; if (res.ok) { res.data = (await this.handleResponse(response)) as T; } else { res.error = (await this.handleResponse(response)) as any; res.requestData = init.data; } } catch (e) { if (e instanceof TypeError && e.message == 'Failed to fetch') { console.error('response', e.message); res.error = e.message; // throw new URIError(`${res.method} ${res.url} Failed`); } else { throw e; } } } if (this.requestAfter) { this.requestAfter(res); } this.debug('res', res); if (!res.ok) { res.requestData = init.data; console.error('res:', res); if (this.failureHandler) { this.failureHandler(res, pathname, init); //res = await this.requst(pathname, init); } } return res; } private async fetchAsync(pathname: string, init: ShiosRequestInit) { let response; const url = this.CombinedURL(pathname, init); const controllerName = pathname.split(/[/?]/, 1)[0]; if (this.isCache && init?.method == 'GET') { response = await caches.match(url); if (response) { console.log('cached'); return response; } } if (this.timeout) { response = await Promise.race([ fetch(url, init), new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('超时')); }, this.timeout); }), ]); } else { response = await fetch(url, init); } if (this.isCache) { switch (init?.method?.toUpperCase()) { case 'GET': case 'HEAD': (await caches.open(controllerName)).put(url, response.clone()); break; case 'POST': await ( await caches.open(controllerName) ).delete(url, { ignoreSearch: true, }); break; case 'PUT': case 'DELETE': caches.delete(controllerName); break; default: break; } } return response; } clearCache() { caches; } private async uniRequest( pathname: string, init: ShiosRequestInit ): Promise> { let response: ShiosResponse | undefined; const url = this.CombinedURL(pathname, init); if (init.method == 'GET') { response = await shiosCaches.match(url); if (response) { return response; } } this.debug(init.headers); const options: UniNamespace.RequestOptions = { url, method: init.method, data: init.data, header: init.headers, timeout: this.timeout, }; this.debug('UniNamespace.RequestOptions', options); const res = await new Promise( (resolve, reject) => { uni?.request({ ...options, success: (res) => { resolve(res); }, fail: (err) => { reject(err); }, }); } ); response = { url, method: init.method, status: res.statusCode, ok: res.statusCode < 400, headers: res.header, data: res.data as T, }; const controllerName = pathname.split(/[/?]/, 1)[0]; switch (init?.method?.toUpperCase()) { case 'GET': case 'HEAD': (await shiosCaches.open(controllerName)).put(url, response); break; case 'POST': await ( await shiosCaches.open(controllerName) ).delete(url, { ignoreSearch: true, }); break; case 'PUT': case 'DELETE': shiosCaches.delete(controllerName); break; default: break; } return response; } /** * 组合URL * @param {string}pathname URL 部分路径 * @param {URLParams} urlParams path 与 search * @returns {string} URL */ CombinedURL(pathname: string, urlParams?: URLParams): string { if (pathname.charAt(0) == '/') { pathname = pathname.substring(1); } // 请求的controller let url = this.baseUrl + pathname; if (urlParams?.path) { let path = urlParams.path.toString(); const joiner = url.charAt(url.length - 1) == '/' ? '' : '/'; if (path.charAt(0) == '/') { path = path.substring(1); } url += joiner + path; } let searchString = [ ...this.getSearchStrings(urlParams?.search), ...this.getSearchStrings(urlParams?.query), ].join('&'); if (!!searchString) { url += '?' + searchString; } return url; } getSearchStrings(searchParams?: SearchParams) { let searchStrings: string[] = []; if (!searchParams) { return searchStrings; } for (const key in searchParams) { // if (Object.prototype.hasOwnProperty.call(searchParams, key)) { // } let element = searchParams[key] ?? ''; //let URIComponent: string; if (element instanceof Array) { element = element.join(','); } searchStrings.push(`${key}=${encodeURIComponent(element)}`); } return searchStrings; } /** * 设置请求标头 * @param {HeadersInit} headersInit - headersInit * @return {Headers} header - headers */ setHeaders(headersInit?: Record): Record { // let headers = new Headers(headersInit); // if (!headers.has("Content-Type")) { // headers.append("Content-Type", "application/json"); // } // if (this.token && !headers.has("Authorization")) { // headers.append("Authorization", `Bearer ${this.token}`); // } return Object.assign(this.headers, headersInit); //{ ...this.headers, ...headersInit }; } private index: number = 0; private handleResponse(response: Response): unknown { if (this.index > this._chain.length - 1) { return; } let middleware = this._chain[this.index]; this.index++; return middleware(response, this.handleResponse.bind(this)); } public use(handle: ChainFunc) { this._chain.push(handle); } // getError(response: Response, next?: ChainFunc) { // if (!response.ok) { // console.error(response); // return response; // } // return next?.(response); // } } /** * 初始化 shios * @param {string? } baseUrl - 例:https://example.com * - 当baseUrl包含pathname 时,请以 / 结尾 例: https://example.com/api/test/ * - 当baseUrl为空或为pathname 时,请以 / 结尾, baseUrl 为 window.location.orign 例: 'api/', 结果为 window.location.orign + baseUrl * @return { Shios } shios - 实例化Shios */ export function useShios(baseUrl?: string): Shios { let shios = Shios.getInstance(); if (baseUrl) { // if (!/http[s]?:\/\//.test(baseUrl)) { // throw new SyntaxError('URL 请以 http[s]:// 开头'); // } else if ( // !/^http[s]?:\/\/(\w+(\.\w+){1,3}|localhost)(:\d+)?/.test(baseUrl) // ) { // throw new SyntaxError(`${baseUrl} 不是一个URL.`); // } shios.baseUrl = baseUrl; if (!shios.baseUrl.endsWith('/')) { shios.baseUrl += '/'; } } // if (baseUrl && baseUrl != '/') { // if (baseUrl.startsWith('http')) { // shios.baseUrl = new URL(baseUrl); // } else { // shios.baseUrl.pathname = baseUrl; // } // } return shios; }