@truenewx/tnxcore
Version:
互联网技术解决方案:JavaScript核心扩展支持
299 lines (275 loc) • 10.7 kB
text/typescript
import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios';
import SafePromise from '../foundation/safe-promise.ts';
import util from '../util/index.ts';
axios.defaults.baseURL = '';
axios.defaults.withCredentials = true; // 允许携带Cookie
axios.defaults.timeout = util.build.isProduction() ? 3000 : 0; // 默认超时时间,生产模式:3秒,开发模式不限时,以便于服务端调试
Object.assign(axios.defaults.headers.common, {'X-Requested-With': 'XMLHttpRequest'}); // 标记为AJAX请求
type ApiRequestOptions = {
url: string;
method: 'GET' | 'POST';
params?: Record<string, any>;
body?: Record<string, any> | FormData;
timeout?: number;
onUploadProgress?: (ratio: number) => void;
}
type BusinessError = {
code: string;
message: string;
type: string;
field?: string;
detail?: string;
}
type ReminderMessage = {
type: 'ERROR' | 'WARNING' | 'INFO' | 'SUCCESS';
content: string;
seconds?: number;
}
type ApiResponse<R> = {
successful: boolean;
result?: R;
errors?: BusinessError[];
reminders?: ReminderMessage[];
}
export type ApiRequestErrorLevel = 'ALERT' | 'TOAST' | 'CONSOLE';
export type ApiRequestError = {
url: string;
status: number;
level: ApiRequestErrorLevel | 'FORWARD';
payload: any;
}
export function setBaseUrl(baseUrl: string) {
axios.defaults.baseURL = baseUrl;
}
export function getBaseUrl(app?: string) {
let baseUrl = axios.defaults.baseURL;
if (app) {
baseUrl += ':' + app + '/';
}
return baseUrl;
}
const APP_CONTEXT_URLS: Record<string, string> = {};
/**
* 设置应用及其上下文根的对应关系
* @param appContextUrls 应用及其上下文根的对应关系,key为应用名,value为应用对应的上下文根,以/开头,末尾没有/
*/
export function setAppContextUrls(appContextUrls: Record<string, string>) {
Object.keys(appContextUrls).forEach((key: string) => {
let contextUrl = appContextUrls[key];
if (contextUrl.startsWith('/')) { // 只接受/开头的上下文根
if (contextUrl.endsWith('/')) {
contextUrl = contextUrl.substring(0, contextUrl.length - 1);
}
APP_CONTEXT_URLS[key] = contextUrl;
}
});
}
export function get<T>(url: string, params?: Record<string, any>, timeout?: number): Promise<T> {
return request<T>({url, method: 'GET', params, timeout});
}
export function post<T>(url: string, body?: Record<string, any>, timeout?: number): Promise<T> {
return request<T>({url, method: 'POST', body, timeout});
}
export function upload<T>(url: string, body: FormData, timeout?: number, onUploadProgress?: (ratio: number) => void): Promise<T> {
return request<T>({url, method: 'POST', body, timeout, onUploadProgress});
}
export function toRequestUrl(url: string) {
if (url.startsWith(':')) {
let index = url.indexOf('/');
// 含/则:到首个/之间为应用名称,否则:之后均为应用名称
let appName = index > 0 ? url.substring(1, index) : url.substring(1);
// 含/则/之后均为相对路径,否则相对路径为空
let relativeUrl = index > 0 ? url.substring(index) : '';
url = (APP_CONTEXT_URLS[appName] || ('/' + appName)) + relativeUrl;
}
return url;
}
export function request<T>(options: ApiRequestOptions): Promise<T> {
const config: AxiosRequestConfig<Record<string, any> | FormData> = {
url: toRequestUrl(options.url),
method: options.method,
params: options.params,
data: options.body,
timeout: options.timeout,
};
if (config.params) {
config.paramsSerializer = params => {
return util.net.toParameterString(params);
};
}
if (config.data && !(config.data instanceof FormData)) {
let keys = Object.keys(config.data);
for (let key of keys) {
let value = config.data[key];
if (value instanceof Date) {
config.data[key] = value.formatDateTime();
}
}
}
if (options.onUploadProgress) {
config.onUploadProgress = event => {
const ratio = event.total ? (event.loaded / event.total) : 0;
options.onUploadProgress(ratio);
}
}
return doRequest<T>(config);
}
function doRequest<T>(config: AxiosRequestConfig<Record<string, any>>): Promise<T> {
return new SafePromise<T>((resolve, reject) => {
axios.request<ApiResponse<T>>(config).then((response: AxiosResponse<ApiResponse<T>>) => {
const data = response.data;
if (data.successful) {
resolve(data.result);
} else {
reject({
url: config.url,
status: 200,
level: 'ALERT',
payload: data.errors,
} as ApiRequestError);
}
}).catch((error: AxiosError<any>) => {
const response = error.response;
if (response) {
switch (response.status) {
case 401: {
reject({
url: config.url,
status: response.status,
level: 'FORWARD',
} as ApiRequestError);
break;
}
case 400: {
let errors = response.data.errors;
if (errors && errors.length) { // 字段格式异常
reject({
url: config.url,
status: response.status,
level: 'ALERT',
payload: errors,
} as ApiRequestError);
} else if (response.data.message) {
reject({
url: config.url,
status: response.status,
level: 'TOAST',
payload: response.data.message,
} as ApiRequestError);
}
break;
}
case 404: {
console.error('在服务器未启动时也会报该错误');
reject({
url: config.url,
status: response.status,
level: 'ALERT',
payload: '服务器正在更新,请稍候再试',
} as ApiRequestError);
break;
}
case 500: {
let message: string;
if (response.data && response.data.message) {
message = response.data.message;
} else {
message = response.data;
}
if (message && message.includes('time out')) {
console.error(message);
reject({
url: config.url,
status: response.status,
level: 'TOAST',
payload: '请求超时'
} as ApiRequestError);
} else {
reject({
url: config.url,
status: response.status,
level: 'CONSOLE',
payload: message,
} as ApiRequestError);
}
break;
}
default: {
reject({
url: config.url,
status: response.status,
level: 'CONSOLE',
payload: error,
} as ApiRequestError);
}
}
} else {
reject({
url: config.url,
status: 0,
level: 'CONSOLE',
payload: error,
} as ApiRequestError);
}
});
}, handleRequestError);
}
const errorHandlers: Record<string, (message: string) => void> = {};
export function setErrorHandler(level: ApiRequestErrorLevel, handler: (message: string) => void): void {
errorHandlers[level] = handler;
}
/**
* 跳转到登录页的函数,由业务工程覆写
* @param url 出错时的请求地址
*/
let fnToLoginPage: (url: string) => void;
export function ifToLoginPage(fn: (url: string) => void): void {
fnToLoginPage = fn;
}
export function toLogin(url: string): void {
if (fnToLoginPage) {
fnToLoginPage(url);
} else {
console.error('未实现跳转到首页的方法,请初始化时调用:', ifToLoginPage);
}
}
export function handleRequestError(error: ApiRequestError): void {
const payload = error.payload;
let message = '';
if (Array.isArray(payload)) {
for (let e of payload) {
if (e.field) {
message += e.field + ' ';
}
message += e.message + '\n';
}
message = message.trim();
} else if (typeof payload === 'string') {
message = payload;
} else if (payload) {
message = payload.message || payload.stack || payload.toString();
}
let handler = errorHandlers[error.level];
if (handler) {
handler(message);
} else {
switch (error.level) {
case 'ALERT': {
window.tnx.error(message);
break;
}
case 'TOAST': {
window.tnx.toast(message);
break;
}
case 'CONSOLE': {
console.error(message);
break;
}
case 'FORWARD': {
toLogin(error.url);
break;
}
}
}
}