UNPKG

@truenewx/tnxcore

Version:

互联网技术解决方案:Web核心扩展支持

792 lines (785 loc) 32.2 kB
// 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); }, };