@truenewx/tnxcore
Version:
互联网技术解决方案:Web核心扩展支持
792 lines (785 loc) • 32.2 kB
JavaScript
// tnxcore-app-rpc.js
import {util} from './tnxcore-util';
import Axios from 'axios';
Axios.defaults.baseURL = '';
Axios.defaults.withCredentials = true; // 允许携带Cookie
Axios.defaults.timeout = util.build.isProduction() ? 3000 : 1000; // 默认超时时间,生产模式:3秒,开发模式1秒
const ajaxHeader = {'X-Requested-With': 'XMLHttpRequest'};
Object.assign(Axios.defaults.headers.common, ajaxHeader); // 标记为AJAX请求
function createClient(app) {
return {
baseUrl: app.baseUrl,
request: Axios.create({
baseURL: app.baseUrl,
withCredentials: true,
headers: ajaxHeader,
}),
};
}
const LOAD_CONFIG_URL = '/api/meta/context';
const LOAD_ENUM_TYPE_URL = '/api/meta/enum/type';
export default {
API_META_URL_PREFIX: '', // api元数据获取url的前缀部分,正常情况下都无需改动
MOCK_REQUEST_RESULT_LOCAL_STORAGE_PREFIX: 'tnx-app-rpc.mock-request-result.',
defaultClient: {
baseUrl: Axios.defaults.baseURL,
request: Axios,
},
appClients: {},
loginSuccessRedirectParameter: '_next',
logoutProcessUrl: '/logout',
mocks: null,
initMocks() {
if (!util.build.isProduction()) {
if (this.mocks === null) {
this.mocks = [];
let modules = this.getMockModules();
if (modules) {
for (let path in modules) {
let module = modules[path];
for (let mock of module.default) {
if (mock.url) {
this.mocks.push(mock);
} else if (mock.name) { // 枚举类型模拟
this.mocks.push({
url: LOAD_ENUM_TYPE_URL,
data({name, subname}) {
if (name === mock.name && subname === mock.subname) {
let enumType = {name, subname};
if (Array.isArray(mock.items)) {
enumType.items = mock.items;
} else if (typeof mock.items === 'object') {
enumType.items = [];
for (let key in mock.items) {
enumType.items.push({
key: key,
caption: mock.items[key],
});
}
}
return enumType;
}
return undefined;
},
});
}
}
}
}
// 如果应用工程没有模拟加载配置,则构建默认的加载配置模拟,以免初始化加载配置时报错
if (!this.mocks.some(m => m.url === LOAD_CONFIG_URL)) {
this.mocks.push({
url: LOAD_CONFIG_URL,
data: {},
});
}
console.info(`Initialized ${this.mocks.length} mocks.`);
}
return true;
}
return false;
},
getMockModules() {
return undefined;
},
getDefaultBaseUrl() {
return this.defaultClient.baseUrl;
},
setDefaultBaseUrl(baseUrl) {
if (baseUrl !== this.getDefaultBaseUrl()) {
Axios.defaults.baseURL = baseUrl;
this.defaultClient = createClient({baseUrl});
}
},
getBaseUrl(appName) {
let appClient = this.appClients[appName];
return appClient ? appClient.baseUrl : undefined;
},
/**
* 从后端服务器加载配置
* @param baseUrl 获取配置的后端服务器基础路径
* @param callback 配置初始化后的回调函数
*/
loadConfig(baseUrl, callback) {
if (typeof baseUrl === 'function') {
callback = baseUrl;
baseUrl = undefined;
}
this.setDefaultBaseUrl(baseUrl);
this.initMocks();
let _this = this;
this.get(this.API_META_URL_PREFIX + LOAD_CONFIG_URL, function (context) {
_this.setConfig(context);
if (typeof callback === 'function') {
callback(context);
}
});
},
setConfig(config) {
config = config || {};
Object.assign(Axios.defaults.headers.common, config.headers);
if (config.loginSuccessRedirectParameter) {
this.loginSuccessRedirectParameter = config.loginSuccessRedirectParameter;
}
if (config.apps) {
let appNames = Object.keys(config.apps);
for (let appName of appNames) {
let app = config.apps[appName];
if (appName === config.baseApp) {
if (app.baseUrl !== this.getDefaultBaseUrl()) {
this.defaultClient = createClient(app);
}
this.appClients[appName] = this.defaultClient;
} else {
this.appClients[appName] = createClient(app);
}
if (app.subs) {
let refClient = this.appClients[appName];
let subAppNames = Object.keys(app.subs);
for (let subAppName of subAppNames) {
this.appClients[subAppName] = {
ref: refClient,
contextUrl: app.subs[subAppName],
}
}
}
}
}
},
get(url, params, callback, options) {
if (typeof params === 'function' || (callback && typeof callback === 'object')) {
options = callback;
callback = params;
params = {};
}
if (typeof options === 'function') {
options = {
error: options
};
}
this.request(url, Object.assign({}, options, {
method: 'get',
params: params,
success: callback,
}));
},
post(url, body, callback, options) {
if (typeof body === 'function' || (callback && typeof callback === 'object')) {
options = callback;
callback = body;
body = {};
}
if (typeof options === 'function') {
options = {
error: options
};
}
options = Object.assign({}, options, {
method: 'post',
body: body,
success: callback,
});
this.request(url, options);
},
request(url, options) {
const config = {
headers: {
'Original-Page': window.location.href, // 用于登录失效时,跳转到登录页重新登录后,返回当前所在页
},
referer: url,
method: options.method,
params: options.params,
data: options.body,
timeout: options.timeout,
};
if (config.params) {
config.paramsSerializer = function (params) {
return util.net.toParameterString(params);
};
}
if (config.data) {
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 (typeof options.onUploadProgress === 'function') {
config.onUploadProgress = function (event) {
const ratio = (event.loaded / event.total) || 0;
options.onUploadProgress.call(event, ratio);
}
}
this._request(url, config, options);
},
_request(url, config, options) {
let client = options.app ? this.appClients[options.app] : null;
if (!url.startsWith('/')) { // 绝对地址需找到对应的应用客户端,并转换为相对地址
let appNames = Object.keys(this.appClients);
for (let appName of appNames) {
let appClient = this.appClients[appName];
if (url.startsWith(appClient.baseUrl)) {
url = url.substr(appClient.baseUrl.length);
client = appClient;
break;
}
}
if (!url.startsWith('/')) { // 无法转换为相对地址的一律使用全局客户端
client = {request: Axios};
}
}
client = client || this.defaultClient;
if (client.ref) {
// 登录凭证验证请求直接向引用的所属应用发送,其它请求才需要添加上下文路径前缀
if (!url.startsWith(this.authenticationContextUrl + '/')) {
url = client.contextUrl + url;
}
client = client.ref;
}
let _this = this;
client.request(url, config).then(function (response) {
if (_this._redirectRequest(response, config, options)) { // 执行了重定向跳转,则不作后续处理
return;
}
if (typeof options.success === 'function') {
options.success(response.data);
}
// 开发模式下,未模拟请求,则将结果缓存至本地,以便于后续脱离后端运行时进行模拟
if (response.data !== undefined && !util.build.isProduction() && !_this.isMockedRequest(url, config)) {
let localStorageKey = _this._getMockRequestResultLocalStorageKey(url, config);
localStorage.setItem(localStorageKey, util.string.toJson(response.data));
}
}).catch(function (error) {
const response = error.response;
if (response) {
if (_this._isIgnored(options, response.status)) {
return;
}
if (_this._redirectRequest(response, config, options)) { // 执行了重定向跳转,则不作后续处理
return;
}
switch (response.status) {
case 401: {
const originalRequest = util.net.getHeader(response.headers, 'Original-Request');
let originalMethod;
let originalUrl;
if (originalRequest) {
const array = originalRequest.split(' ');
originalMethod = array[0];
originalUrl = array[1];
}
let loginUrl = util.net.getHeader(response.headers, 'Login-Url');
if (loginUrl) {
// 默认登录后跳转回当前页面,如果已指定跳转目标地址,则忽略
if (!loginUrl.contains(
'?' + _this.loginSuccessRedirectParameter + '=') && !loginUrl.contains(
'&' + _this.loginSuccessRedirectParameter + '=')) {
let loginSuccessRedirectUrl = encodeURIComponent(window.location.href);
if (loginUrl.contains('?')) {
loginUrl += '&';
} else {
loginUrl += '?';
}
loginUrl += _this.loginSuccessRedirectParameter + '=' + loginSuccessRedirectUrl;
}
// 原始地址是授权验证地址或登出地址,视为框架特有请求,无需应用做个性化处理
if (originalUrl) {
if (originalUrl.startsWith(
_this.authenticationContextUrl + '/') || originalUrl === _this.logoutProcessUrl || loginUrl.endsWith(
url)) {
originalUrl = undefined;
originalMethod = undefined;
}
}
let toLogin = options.toLogin || _this.toLogin;
if (toLogin(loginUrl, originalUrl, originalMethod) !== false) {
return;
}
}
_this.handleOtherError(url + ':\n' + error, options);
break;
}
case 400: {
let errors = response.data.errors;
if (errors && errors.length) { // 字段格式异常
errors.forEach(error => {
if (!error.message && error.defaultMessage) {
error.message = error.field + ' ' + error.defaultMessage;
}
});
// 转换错误消息之后,与403错误做相同处理
if (_this.handleErrors(errors, options)) {
return;
}
} else if (response.data.message) {
console.error(response.data.message);
return;
}
break;
}
case 403: {
if (response.data === '') { // 服务端已修正无操作权限时不正常报错的问题,此处暂留以待观察
response.data = {
errors: [{
code: 'error.web.security.no_operation_authority',
message: '没有权限访问 ' + url,
}]
};
}
if (response.data.errors) {
if (_this.handleErrors(response.data.errors, options)) {
return;
}
} else {
_this.handleOtherError(response.data, options);
}
break;
}
case 500: {
let message;
if (response.data && response.data.message) {
message = response.data.message;
} else {
message = response.data;
}
if (message && message.includes('time out')) {
console.error(message);
_this.handleErrors([{
message: '请求超时'
}], options);
} else {
_this.handle500Error(message, options);
}
break;
}
case 0: {
let errors = [{
code: error.code,
url: url,
}]; // 不含message,以避免默认弹框提示,仅用于业务代码判断code后处理
if (_this.handleErrors(errors, options)) {
return;
}
break;
}
default: {
_this.handleOtherError(url + ':\n' + error.stack || error.message, options);
}
}
} else if (error.code === 'ERR_NETWORK' || error.code === 'ERR_CONNECTION_REFUSED' || error.code === 'ECONNABORTED') {
_this.handleConnectError(error, url, config, options);
} else {
_this.handleOtherError(url + ':\n' + error, options);
}
});
},
_redirectRequest(response, config, options) {
let redirectUrl = util.net.getHeader(response.headers, 'Redirect-To');
if (redirectUrl) { // 指定了重定向地址,则执行重定向操作
if (this._isIgnored(options, 'Redirect-To')) {
return true;
}
config.headers = config.headers || {};
config.headers['Original-Request'] = options.method + ' ' + config.referer;
config.method = 'GET'; // 重定向一定是GET请求
this._request(redirectUrl, config, options);
return true;
}
return false;
},
_isIgnored(options, type) {
if (options && options.ignored) {
if (options.ignored instanceof Array) {
return options.ignored.contains(type);
} else {
return options.ignored === type;
}
}
return false;
},
_getMockRequestResultLocalStorageKey(url, config) {
url = util.net.appendParams(url, config.params);
let method = (config.method || 'get').toUpperCase();
return this.MOCK_REQUEST_RESULT_LOCAL_STORAGE_PREFIX + `[${method}]${url}`;
},
isMockedRequest(url, config) {
return this.forMock(url, config, () => {
return true;
}) || false;
},
forMock(url, config, callback) {
if (this.mocks?.length) {
for (let mock of this.mocks) {
if (mock.url) {
let configMethod = (config.method || 'get').toLowerCase();
let mockMethod = (mock.method || 'get').toLowerCase();
if (mockMethod === configMethod) {
let pathVariables = util.net.getPathVariables(mock.url, url);
if (pathVariables) {
let result = callback(mock, pathVariables, configMethod);
if (result !== undefined) {
return result;
}
}
}
}
}
}
return undefined;
},
/**
* 获取模拟请求结果
* @param url 请求地址
* @param config 请求配置
* @return 模拟请求结果
*/
getMockRequestResult(url, config) {
config = config || {};
let result = this.forMock(url, config, (mock, pathVariables, method) => {
let data;
if (typeof mock.data === 'function') {
if (method === 'get') {
if (Object.keys(pathVariables).length) {
data = mock.data(pathVariables, config.params);
} else {
data = mock.data(config.params);
}
} else {
if (Object.keys(pathVariables).length) {
data = mock.data(pathVariables, config.body, config.params);
} else {
data = mock.data(config.body, config.params);
}
}
} else {
data = mock.data;
}
if (data !== undefined) {
return data;
}
});
// 如果自定义模拟未生效,则尝试从本地存储中获取缓存的上次请求结果作为模拟数据
if (result === undefined) {
let localStorageKey = this._getMockRequestResultLocalStorageKey(url, config);
let resultString = localStorage.getItem(localStorageKey);
if (resultString) {
result = util.string.parseJson(resultString);
}
}
return result;
},
/**
* 打开登录表单的函数,由业务应用覆盖提供,以决定用何种方式打开登录表单页面。
* 默认不做任何处理,直接返回false
* @param loginFormUrl 登录表单URL
* @param originalUrl 原始请求地址
* @param originalMethod 原始请求方法
* @returns {boolean} 是否已经正常打开登录表单
*/
toLogin(loginFormUrl, originalUrl, originalMethod) {
return false;
},
handleConnectError(error, url, config, options) {
if (typeof options.success === 'function') {
if (this.initMocks()) {
let result = this.getMockRequestResult(url, config);
if (result !== undefined) {
options.success(result);
return;
}
}
}
this.handleOtherError(util.net.appendParams(url, config.params) + ': ' + error, options);
},
handle500Error(message, options) {
console.error(message);
this.handleErrors([{
message: '非常抱歉,系统出了点小小的错误,这并不影响你的其它操作,我们会尽快修正这个问题。'
}], options);
},
handleOtherError(message, options) {
if (options && typeof options.error === 'function') {
options.error(message);
} else {
console.error(message);
}
},
handleErrors(errors, options) {
if (errors) {
if (options && typeof options.error === 'function') {
options.error(errors);
} else {
this.error(errors);
}
return true;
}
return false;
},
error(errors) {
let message = this.getErrorMessage(errors);
if (message) {
window.tnx.error(message);
} else {
console.error(errors);
}
},
getErrorMessage(errors) {
let message = '';
if (!Array.isArray(errors)) {
errors = [errors];
}
for (let error of errors) {
if (error.message) {
message += error.message + '\n';
}
}
return message.trim();
},
/**
* 登录凭证验证地址上下文前缀
*/
authenticationContextUrl: '/authentication',
isLogined(callback, options) {
this.get(this.authenticationContextUrl + '/authorized', callback, options);
},
/**
* 确保已登录
* @param callback 校验通过的回调
* @param options 请求选项集
*/
ensureLogined(callback, options) {
this.get(this.authenticationContextUrl + '/validate', callback, options);
},
/**
* 确保已具有指定授权
* @param authority 授权:{type,rank,permission}
* @param callback 校验通过时的回调
* @param options 请求选项集
*/
ensureGranted(authority, callback, options) {
this.get(this.authenticationContextUrl + '/validate', authority, callback, options);
},
getLoginUrl(callback, options) {
this.get(this.authenticationContextUrl + '/login-url', callback, options);
},
_metas: {},
getMeta(urlOrType, callback, app) {
const metaKey = app ? (app + ':/' + urlOrType) : urlOrType;
const metas = this._metas;
if (metas[metaKey]) {
if (typeof callback === 'function') {
callback(metas[metaKey]);
}
} else {
let url = this.API_META_URL_PREFIX + '/api/meta/' + (urlOrType.contains('/') ? 'method' : 'model');
let params = urlOrType.contains('/') ? {url: urlOrType} : {type: urlOrType};
this.get(url, params, function (meta) {
if (meta) {
metas[metaKey] = meta;
if (typeof callback === 'function') {
callback(meta);
}
}
}, {app});
}
},
_enumTypeMapping: {},
_getEnumTypeMappingKey(type, subType) {
return subType ? (type + '-' + subType) : type;
},
addEnumType(enumType) {
const mappingKey = this._getEnumTypeMappingKey(enumType.name, enumType.subname);
if (!this._enumTypeMapping[mappingKey]) {
this._enumTypeMapping[mappingKey] = enumType;
}
},
loadEnumType(name, subname, callback, options) {
this.resolveEnumType(name, subname, callback, options);
},
resolveEnumType(name, subname, callback, options) {
if (typeof subname === 'function') {
options = callback;
callback = subname;
subname = undefined;
}
const mapping = this._enumTypeMapping;
const mappingKey = this._getEnumTypeMappingKey(name, subname);
if (mapping[mappingKey]) {
if (typeof callback === 'function') {
callback(mapping[mappingKey]);
}
} else {
this.get(this.API_META_URL_PREFIX + LOAD_ENUM_TYPE_URL, {name, subname},
enumType => {
mapping[mappingKey] = enumType;
if (typeof callback === 'function') {
callback(enumType);
}
}, options);
}
},
loadEnumItems(type, subtype, callback, options) {
this.resolveEnumItems(type, subtype, callback, options);
},
resolveEnumItems(type, subtype, callback, options) {
if (typeof subtype === 'function') {
options = callback;
callback = subtype;
subtype = undefined;
}
this.resolveEnumType(type, subtype, enumType => {
if (typeof callback === 'function') {
// 返回深度克隆的选项集,以免调用者改动选项影响缓存数据
callback((enumType && enumType.items) ? enumType.items.clone(true) : undefined);
}
}, options);
},
resolveEnumItem(type, subtype, key, callback, options) {
if (typeof key === 'function') {
options = callback;
callback = key;
key = subtype;
subtype = undefined;
}
if (key === undefined || key === null) {
callback(null);
} else {
let items = this._getCachedEnumItems(type, subtype);
if (items) {
let item = this._getEnumItem(items, key);
if (item) {
callback(item);
}
} else {
let _this = this;
this.resolveEnumItems(type, subtype, function (items) {
let item = _this._getEnumItem(items, key);
callback(item);
}, options);
}
}
},
_getCachedEnumItems(type, subtype) {
const mappingKey = this._getEnumTypeMappingKey(type, subtype);
let enumType = this._enumTypeMapping[mappingKey];
return enumType ? enumType.items : null;
},
_getEnumItem(items, key) {
if (Array.isArray(items)) {
for (let item of items) {
if (item.key === key) {
return item;
}
}
}
return null;
},
resolveEnumCaption(type, subtype, key, callback, options) {
if (typeof key === 'function') {
options = callback;
callback = key;
key = subtype;
subtype = undefined;
}
this.resolveEnumItem(type, subtype, key, function (item) {
let caption = item ? item.caption : '';
callback(caption);
}, options);
},
/**
* 当可确定已缓存指定枚举类型时,获取指定枚举项的显示名称
* @param type 枚举类型名
* @param subtype 子类型名
* @param key 枚举项的键
* @return {string|null} 指定枚举项的显示名称,没找到时返回null
*/
getEnumCaption(type, subtype, key) {
if (key === undefined) {
key = subtype;
subtype = undefined;
}
let items = this._getCachedEnumItems(type, subtype);
if (items) {
for (let item of items) {
if (item.key === key) {
return item.caption;
}
}
}
return null;
},
/**
* 当可确定已缓存指定枚举类型时,获取指定枚举项的键
* @param type 枚举类型名
* @param subtype 子类型名
* @param caption 枚举项的显示名称
* @return {*|null} 指定枚举项的键,没找到时返回null
*/
getEnumKey(type, subtype, caption) {
if (caption === undefined) {
caption = subtype;
subtype = undefined;
}
let items = this._getCachedEnumItems(type, subtype);
if (items) {
for (let item of items) {
if (item.caption === caption) {
return item.key;
}
}
}
return null;
},
clearEnumTypeCache(name, subname) {
const mappingKey = this._getEnumTypeMappingKey(name, subname);
delete this._enumTypeMapping[mappingKey];
},
getRandomEnumItemKey(type, onlyDev = true) {
if (onlyDev && util.build.isProduction()) {
return null;
}
let items = this._getCachedEnumItems(type);
if (items && items.length) {
let index = util.math.randomInt(0, items.length - 1);
return items[index].key;
}
return null;
},
_regionMapping: {},
loadRegion(regionCode, level, callback, options) {
if (typeof level === 'function') {
options = callback;
callback = level;
level = undefined;
}
level = Math.min(level || 3, 3);
let cacheKey = regionCode + '.' + level;
let region = this._regionMapping[cacheKey];
if (region) {
callback(region);
} else {
let _this = this;
this.get('/api/region/' + regionCode, undefined, function (region) {
_this._filterRegionSubs(region, level);
_this._regionMapping[cacheKey] = region;
callback(region);
}, options);
}
},
_filterRegionSubs(region, level) {
if (region.subs) {
if (region.level >= level) {
delete region.subs;
} else {
for (let sub of region.subs) {
this._filterRegionSubs(sub, level);
}
}
}
},
logout() {
this.post(this.logoutProcessUrl);
},
};