diff --git a/packages/rax-render/package.json b/packages/rax-render/package.json index 90d0070ba..ade1a3756 100644 --- a/packages/rax-render/package.json +++ b/packages/rax-render/package.json @@ -33,13 +33,24 @@ "prepublish": "npm run build" }, "dependencies": { - "@ali/iceluna-sdk": "^1.0.7-beta.12", + "@ali/b3-one": "^0.0.17", + "@ali/bzb-request": "2.6.1", + "@ali/lib-mtop": "^2.5.1", "classnames": "^2.2.6", "debug": "^4.1.1", - "lodash.isempty": "^4.4.0", + "events": "^3.0.0", + "fetch-jsonp": "^1.1.3", + "intl-messageformat": "^7.7.2", + "jsonuri": "^2.1.2", + "keymaster": "^1.6.2", + "lodash": "^4.17.11", + "moment": "^2.24.0", "rax-find-dom-node": "^1.0.1", "rax-text": "^1.1.6", - "rax-view": "^1.0.0" + "rax-view": "^1.0.0", + "react-is": "^16.10.1", + "serialize-javascript": "^1.7.0", + "whatwg-fetch": "^3.0.0" }, "devDependencies": { "@alib/build-scripts": "^0.1.0", diff --git a/packages/rax-render/src/engine/base.jsx b/packages/rax-render/src/engine/base.jsx index eedb51d87..95d1dcaff 100644 --- a/packages/rax-render/src/engine/base.jsx +++ b/packages/rax-render/src/engine/base.jsx @@ -2,7 +2,7 @@ import { Component, createElement } from 'rax'; import PropTypes from 'prop-types'; import Debug from 'debug'; import View from 'rax-view'; -import DataHelper from '@ali/iceluna-sdk/lib/utils/dataHelper'; +import DataHelper from '../utils/dataHelper'; import { forEach, getValue, @@ -18,7 +18,7 @@ import { checkPropTypes, generateI18n, acceptsRef, -} from '@ali/iceluna-sdk/lib/utils'; +} from '../utils'; import VisualDom from '../comp/visualDom'; import AppContext from '../context/appContext'; import CompWrapper from '../hoc/compWrapper'; diff --git a/packages/rax-render/src/engine/blockEngine.jsx b/packages/rax-render/src/engine/blockEngine.jsx index 2067d0d67..ab986cc1a 100644 --- a/packages/rax-render/src/engine/blockEngine.jsx +++ b/packages/rax-render/src/engine/blockEngine.jsx @@ -2,7 +2,7 @@ import { createElement } from 'rax'; import PropTypes from 'prop-types'; import Debug from 'debug'; import classnames from 'classnames'; -import { isSchema, getFileCssName } from '@ali/iceluna-sdk/lib/utils'; +import { isSchema, getFileCssName } from '../utils'; import BaseEngine from './base'; const debug = Debug('engine:block'); diff --git a/packages/rax-render/src/engine/compEngine.jsx b/packages/rax-render/src/engine/compEngine.jsx index 4a0cf54b0..235ad4996 100644 --- a/packages/rax-render/src/engine/compEngine.jsx +++ b/packages/rax-render/src/engine/compEngine.jsx @@ -2,7 +2,7 @@ import { createElement } from 'rax'; import PropTypes from 'prop-types'; import Debug from 'debug'; import classnames from 'classnames'; -import { isSchema, getFileCssName } from '@ali/iceluna-sdk/lib/utils'; +import { isSchema, getFileCssName } from '../utils'; import BaseEngine from './base'; const debug = Debug('engine:comp'); diff --git a/packages/rax-render/src/engine/index.jsx b/packages/rax-render/src/engine/index.jsx index e969f1a91..7bbe0fad4 100644 --- a/packages/rax-render/src/engine/index.jsx +++ b/packages/rax-render/src/engine/index.jsx @@ -1,9 +1,9 @@ import { Component, createElement } from 'rax'; import PropTypes from 'prop-types'; import Debug from 'debug'; -import * as isEmpty from 'lodash.isempty'; +import * as isEmpty from 'lodash/isEmpty'; import findDOMNode from 'rax-find-dom-node'; -import { isFileSchema, goldlog } from '@ali/iceluna-sdk/lib/utils'; +import { isFileSchema, goldlog } from '../utils'; import AppContext from '../context/appContext'; import Page from './pageEngine'; import CustomComp from './compEngine'; diff --git a/packages/rax-render/src/engine/pageEngine.jsx b/packages/rax-render/src/engine/pageEngine.jsx index 25eed613c..c09eaa265 100644 --- a/packages/rax-render/src/engine/pageEngine.jsx +++ b/packages/rax-render/src/engine/pageEngine.jsx @@ -2,7 +2,7 @@ import { createElement } from 'rax'; import PropTypes from 'prop-types'; import Debug from 'debug'; import classnames from 'classnames'; -import { isSchema, getFileCssName } from '@ali/iceluna-sdk/lib/utils'; +import { isSchema, getFileCssName } from '../utils'; import BaseEngine from './base'; const debug = Debug('engine:page'); diff --git a/packages/rax-render/src/engine/tempEngine.jsx b/packages/rax-render/src/engine/tempEngine.jsx index d76e01c69..b55436320 100644 --- a/packages/rax-render/src/engine/tempEngine.jsx +++ b/packages/rax-render/src/engine/tempEngine.jsx @@ -1,7 +1,7 @@ import { createElement } from 'rax'; import PropTypes from 'prop-types'; import Debug from 'debug'; -import { isSchema } from '@ali/iceluna-sdk/lib/utils'; +import { isSchema } from '../utils'; import AppContext from '../context/appContext'; import BaseEngine from './base'; diff --git a/packages/rax-render/src/hoc/compFactory.js b/packages/rax-render/src/hoc/compFactory.js index a28ca36a5..d56a8b5f3 100644 --- a/packages/rax-render/src/hoc/compFactory.js +++ b/packages/rax-render/src/hoc/compFactory.js @@ -1,7 +1,7 @@ import { Component, createElement } from 'rax'; import PropTypes from 'prop-types'; -import AppHelper from '@ali/iceluna-sdk/lib/utils/appHelper'; -import { forEach, isFileSchema } from '@ali/iceluna-sdk/lib/utils'; +import AppHelper from '../utils/appHelper'; +import { forEach, isFileSchema } from '../utils'; import CompEngine from '../engine/compEngine'; import BlockEngine from '../engine/blockEngine'; import AppContext from '../context/appContext'; diff --git a/packages/rax-render/src/utils/appHelper.js b/packages/rax-render/src/utils/appHelper.js new file mode 100644 index 000000000..e39741ddc --- /dev/null +++ b/packages/rax-render/src/utils/appHelper.js @@ -0,0 +1,49 @@ +import EventEmitter from 'events'; + +let instance = null; + +EventEmitter.defaultMaxListeners = 100; + +export default class AppHelper extends EventEmitter { + static getInstance = () => { + if (!instance) { + instance = new AppHelper(); + } + return instance; + }; + + constructor(config) { + super(); + instance = this; + Object.assign(this, config); + } + + get(key) { + return this[key]; + } + + set(key, val) { + if (typeof key === 'string') { + this[key] = val; + } else if (typeof key === 'object') { + Object.keys(key).forEach((item) => { + this[item] = key[item]; + }); + } + } + + batchOn(events, lisenter) { + if (!Array.isArray(events)) return; + events.forEach((event) => this.on(event, lisenter)); + } + + batchOnce(events, lisenter) { + if (!Array.isArray(events)) return; + events.forEach((event) => this.once(event, lisenter)); + } + + batchOff(events, lisenter) { + if (!Array.isArray(events)) return; + events.forEach((event) => this.off(event, lisenter)); + } +} diff --git a/packages/rax-render/src/utils/dataHelper.js b/packages/rax-render/src/utils/dataHelper.js new file mode 100644 index 000000000..6c838a739 --- /dev/null +++ b/packages/rax-render/src/utils/dataHelper.js @@ -0,0 +1,299 @@ +import { transformArrayToMap, isJSFunction, transformStringToFunction, clone } from './index'; +import { jsonp, mtop, request, get, post, bzb } from './request'; + +const DS_STATUS = { + INIT: 'init', + LOADING: 'loading', + LOADED: 'loaded', + ERROR: 'error' +}; + +export default class DataHelper { + constructor(comp, config = {}, appHelper, parser) { + this.host = comp; + this.config = config; + this.parser = parser; + this.ajaxList = (config && config.list) || []; + this.ajaxMap = transformArrayToMap(this.ajaxList, 'id'); + this.dataSourceMap = this.generateDataSourceMap(); + this.appHelper = appHelper; + } + + // 重置config,dataSourceMap状态会被重置; + resetConfig(config = {}) { + this.config = config; + this.ajaxList = (config && config.list) || []; + this.ajaxMap = transformArrayToMap(this.ajaxList, 'id'); + this.dataSourceMap = this.generateDataSourceMap(); + return this.dataSourceMap; + } + + // 更新config,只会更新配置,状态保存; + updateConfig(config = {}) { + this.config = config; + this.ajaxList = (config && config.list) || []; + const ajaxMap = transformArrayToMap(this.ajaxList, 'id'); + // 删除已经移除的接口 + Object.keys(this.ajaxMap).forEach(key => { + if (!ajaxMap[key]) { + delete this.dataSourceMap[key]; + } + }); + this.ajaxMap = ajaxMap; + // 添加未加入到dataSourceMap中的接口 + this.ajaxList.forEach(item => { + if (!this.dataSourceMap[item.id]) { + this.dataSourceMap[item.id] = { + status: DS_STATUS.INIT, + load: (...args) => { + return this.getDataSource(item.id, ...args); + } + }; + } + }); + return this.dataSourceMap; + } + + generateDataSourceMap() { + const res = {}; + this.ajaxList.forEach(item => { + res[item.id] = { + status: DS_STATUS.INIT, + load: (...args) => { + return this.getDataSource(item.id, ...args); + } + }; + }); + return res; + } + + updateDataSourceMap(id, data, error) { + this.dataSourceMap[id].error = error ? error : undefined; + this.dataSourceMap[id].data = data; + this.dataSourceMap[id].status = error ? DS_STATUS.ERROR : DS_STATUS.LOADED; + } + + getInitData() { + const initSyncData = this.parser(this.ajaxList).filter(item => { + if (item.isInit) { + this.dataSourceMap[item.id].status = DS_STATUS.LOADING; + return true; + } + return false; + }); + return this.asyncDataHandler(initSyncData).then(res => { + let dataHandler = this.config.dataHandler; + if (isJSFunction(dataHandler)) { + dataHandler = transformStringToFunction(dataHandler.value); + } + if (!dataHandler || typeof dataHandler !== 'function') return res; + try { + return dataHandler.call(this.host, res); + } catch (e) { + console.error('请求数据处理函数运行出错', e); + return; + } + }); + } + + getDataSource(id, params, otherOptions, callback) { + const req = this.parser(this.ajaxMap[id]); + const options = req.options || {}; + if (typeof otherOptions === 'function') { + callback = otherOptions; + otherOptions = {}; + } + const { headers, ...otherProps } = otherOptions || {}; + if (!req) { + console.warn(`getDataSource API named ${id} not exist`); + return; + } + return this.asyncDataHandler([ + { + ...req, + options: { + ...options, + // 支持参数为array的情况,当参数为array时,不做参数合并 + params: + Array.isArray(options.params) || Array.isArray(params) + ? params || options.params + : { + ...options.params, + ...params + }, + headers: { + ...options.headers, + ...headers + }, + ...otherProps + } + } + ]) + .then(res => { + try { + callback && callback(res && res[id]); + } catch (e) { + console.error('load请求回调函数报错', e); + } + + return res && res[id]; + }) + .catch(err => { + try { + callback && callback(null, err); + } catch (e) { + console.error('load请求回调函数报错', e); + } + + return err; + }); + } + + asyncDataHandler(asyncDataList) { + return new Promise((resolve, reject) => { + const allReq = []; + const doserReq = []; + const doserList = []; + const beforeRequest = this.appHelper && this.appHelper.utils && this.appHelper.utils.beforeRequest; + const afterRequest = this.appHelper && this.appHelper.utils && this.appHelper.utils.afterRequest; + const csrfInput = document.getElementById('_csrf_token'); + const _tb_token_ = csrfInput && csrfInput.value; + asyncDataList.map(req => { + const { id, type, options } = req; + if (!id || !type) return; + if (type === 'doServer') { + const { uri, params } = options || {}; + if (!uri) return; + doserList.push(id); + doserReq.push({ name: uri, package: 'cms', params }); + } else { + allReq.push(req); + } + }); + + if (doserReq.length > 0) { + allReq.push({ + type: 'doServer', + options: { + uri: '/nrsService.do', + cors: true, + method: 'POST', + params: { + data: JSON.stringify(doserReq), + _tb_token_ + } + } + }); + } + if (allReq.length === 0) resolve({}); + const res = {}; + Promise.all( + allReq.map(item => { + return new Promise(resolve => { + const { type, id, dataHandler, options } = item; + const doFetch = (type, options) => { + this.fetchOne(type, options) + .then(async data => { + if (afterRequest) { + this.appHelper.utils.afterRequest(item, data, undefined, async (data, error) => { + await fetchHandler(data, error); + }); + } else { + await fetchHandler(data, undefined); + } + }) + .catch(async err => { + if (afterRequest) { + // 必须要这么调用,否则beforeRequest中的this会丢失 + this.appHelper.utils.afterRequest(item, undefined, err, async (data, error) => { + await fetchHandler(data, error); + }); + } else { + await fetchHandler(undefined, err); + } + }); + }; + const fetchHandler = async (data, error) => { + if (type === 'doServer') { + if (!Array.isArray(data)) { + data = [data]; + } + doserList.forEach(async (id, idx) => { + const req = this.ajaxMap[id]; + if (req) { + res[id] = await this.dataHandler(id, req.dataHandler, data && data[idx], error); + this.updateDataSourceMap(id, res[id], error); + } + }); + } else { + res[id] = await this.dataHandler(id, dataHandler, data, error); + this.updateDataSourceMap(id, res[id], error); + } + resolve(); + }; + + if (type === 'doServer') { + doserList.forEach(item => { + this.dataSourceMap[item].status = DS_STATUS.LOADING; + }); + } else { + this.dataSourceMap[id].status = DS_STATUS.LOADING; + } + // 请求切片 + if (beforeRequest) { + // 必须要这么调用,否则beforeRequest中的this会丢失 + this.appHelper.utils.beforeRequest(item, clone(options), options => doFetch(type, options)); + } else { + doFetch(type, options); + } + }); + }) + ) + .then(() => { + resolve(res); + }) + .catch(e => { + reject(e); + }); + }); + } + + async dataHandler(id, dataHandler, data, error) { + if (isJSFunction(dataHandler)) { + dataHandler = transformStringToFunction(dataHandler.value); + } + if (!dataHandler || typeof dataHandler !== 'function') return data; + try { + return await dataHandler.call(this.host, data, error); + } catch (e) { + console.error('[' + id + ']单个请求数据处理函数运行出错', e); + return; + } + } + + fetchOne(type, options) { + let { uri, method = 'GET', headers, params, ...otherProps } = options; + otherProps = otherProps || {}; + switch (type) { + case 'mtop': + method && (otherProps.method = method); + return mtop(uri, params, otherProps); + case 'jsonp': + return jsonp(uri, params, otherProps); + case 'bzb': + return bzb(uri, params, { + method, + headers, + ...otherProps + }); + default: + method = method.toUpperCase(); + if (method === 'GET') { + return get(uri, params, headers, otherProps); + } else if (method === 'POST') { + return post(uri, params, headers, otherProps); + } + return request(uri, method, params, headers, otherProps); + } + } +} diff --git a/packages/rax-render/src/utils/index.js b/packages/rax-render/src/utils/index.js new file mode 100644 index 000000000..814f1edc9 --- /dev/null +++ b/packages/rax-render/src/utils/index.js @@ -0,0 +1,718 @@ +import Debug from 'debug'; +import _keymaster from 'keymaster'; +import { forEach as _forEach, shallowEqual as _shallowEqual } from '@ali/b3-one/lib/obj'; +import { serialize as serializeParams } from '@ali/b3-one/lib/url'; +import _moment from 'moment'; +import 'moment/locale/zh-cn'; +import _pick from 'lodash/pick'; +import _deepEqual from 'lodash/isEqualWith'; +import _clone from 'lodash/cloneDeep'; +import _isEmpty from 'lodash/isEmpty'; +import _throttle from 'lodash/throttle'; +import _debounce from 'lodash/debounce'; +import _serialize from 'serialize-javascript'; +import * as _jsonuri from 'jsonuri'; +import IntlMessageFormat from 'intl-messageformat'; +import pkg from '../../package.json'; + +window.sdkVersion = pkg.version; + +export const moment = _moment; +moment.locale('zh-cn'); +export const forEach = _forEach; +export const shallowEqual = _shallowEqual; +export const keymaster = _keymaster; +export const pick = _pick; +export const deepEqual = _deepEqual; +export const clone = _clone; +export const isEmpty = _isEmpty; +export const throttle = _throttle; +export const debounce = _debounce; +export const serialize = _serialize; +export const jsonuri = _jsonuri; +export { get, post, jsonp, mtop, request } from './request'; + +const ReactIs = require('react-is'); + +const ReactPropTypesSecret = require('prop-types/lib/ReactPropTypesSecret'); + +const factoryWithTypeCheckers = require('prop-types/factoryWithTypeCheckers'); + +const PropTypes2 = factoryWithTypeCheckers(ReactIs.isElement, true); + +const EXPRESSION_TYPE = { + JSEXPRESSION: 'JSExpression', + JSFUNCTION: 'JSFunction', + JSSLOT: 'JSSlot' +}; +const EXPRESSION_REG = /^\{\{(\{.*\}|.*?)\}\}$/; +const hasSymbol = typeof Symbol === 'function' && Symbol['for']; +const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol['for']('react.forward_ref') : 0xead0; +const debug = Debug('utils:index'); + +const ENV = { + TBE: 'TBE', + WEBIDE: 'WEB-IDE', + VSCODE: 'VSCODE', + WEB: 'WEB' +}; + +/** + * @name isSchema + * @description 判断是否是模型结构 + */ +export function isSchema(schema, ignoreArr) { + if (isEmpty(schema)) return false; + if (!ignoreArr && Array.isArray(schema)) return schema.every(item => isSchema(item)); + return !!(schema.componentName && schema.props && (typeof schema.props === 'object' || isJSExpression(schema.props))); +} + +export function isFileSchema(schema) { + if (isEmpty(schema)) return false; + return ['Page', 'Block', 'Component', 'Addon', 'Temp'].includes(schema.componentName); +} + +// 判断当前页面是否被嵌入到同域的页面中 +export function inSameDomain() { + try { + return window.parent !== window && window.parent.location.host === window.location.host; + } catch (e) { + return false; + } +} + +export function getFileCssName(fileName) { + if (!fileName) return; + let name = fileName.replace(/([A-Z])/g, '-$1').toLowerCase(); + return ('luna-' + name) + .split('-') + .filter(p => !!p) + .join('-'); +} + +export function isJSSlot(obj) { + return obj && typeof obj === 'object' && EXPRESSION_TYPE.JSSLOT === obj.type; +} +export function isJSFunction(obj) { + return obj && typeof obj === 'object' && EXPRESSION_TYPE.JSFUNCTION === obj.type; +} +export function isJSExpression(obj) { + //兼容两种写法,有js构造表达式的情况 + const isJSExpressionObj = + obj && typeof obj === 'object' && EXPRESSION_TYPE.JSEXPRESSION === obj.type && typeof obj.value === 'string'; + const isJSExpressionStr = typeof obj === 'string' && EXPRESSION_REG.test(obj.trim()); + return isJSExpressionObj || isJSExpressionStr; +} + +/** + * @name wait + * @description 等待函数 + */ +export function wait(ms) { + return new Promise(resolve => setTimeout(() => resolve(true), ms)); +} + +export function curry(Comp, hocs = []) { + return hocs.reverse().reduce((pre, cur) => { + return cur(pre); + }, Comp); +} + +/** + * 获取parent window上的值,需要做同域判断 + * @param {string} key + */ +export function getParentWinValue(key) { + if (inSameDomain()) { + return window.parent && window.parent[key]; + } +} + +export function getValue(obj, path, defaultValue) { + if (isEmpty(obj) || typeof obj !== 'object') return defaultValue; + const res = path.split('.').reduce((pre, cur) => { + return pre && pre[cur]; + }, obj); + if (res === undefined) return defaultValue; + return res; +} + +export function parseObj(schemaStr) { + if (typeof schemaStr !== 'string') return schemaStr; + //默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象 + try { + if (inSameDomain() && window.parent.__newFunc) { + return window.parent.__newFunc(`"use strict"; return ${schemaStr}`)(); + } + return new Function(`"use strict"; return ${schemaStr}`)(); + } catch (err) { + return undefined; + } +} + +export function parseSearch(search) { + if (!search || typeof search !== 'string') { + return {}; + } + const str = search.replace(/^\?/, ''); + const paramStr = str.split('&'); + const res = {}; + paramStr.forEach(item => { + const regRes = item.split('='); + if (regRes[0] && regRes[1]) { + res[regRes[0]] = decodeURIComponent(regRes[1]); + } + }); + return res; +} + +export function fastClone(obj) { + return parseObj(serialize(obj, { unsafe: true })); +} + +// 更新obj的内容但不改变obj的指针 +export function fillObj(receiver = {}, ...suppliers) { + Object.keys(receiver).forEach(item => { + delete receiver[item]; + }); + Object.assign(receiver, ...suppliers); + return receiver; +} + +// 中划线转驼峰 +export function toHump(name) { + return name.replace(/\-(\w)/g, function(all, letter) { + return letter.toUpperCase(); + }); +} +// 驼峰转中划线 +export function toLine(name) { + return name.replace(/([A-Z])/g, '-$1').toLowerCase(); +} + +// 获取当前环境 +export function getEnv() { + const userAgent = navigator.userAgent; + const isVscode = /Electron\//.test(userAgent); + if (isVscode) return ENV.VSCODE; + const isTheia = window.is_theia === true; + if (isTheia) return ENV.WEBIDE; + return ENV.WEB; +} + +/** + * 合并skeleton配置 + * @param {*} 骨架默认配置 + * @param {*} 用户自定义配置 + */ +export function comboSkeletonConfig(defaultConfig = {}, customConfig) { + const { skeleton, theme, addons, hooks, shortCuts, extensions, constants, utils, i18n } = customConfig || {}; + + if (skeleton && skeleton.handler && typeof skeleton.handler === 'function') { + return skeleton.handler({ + skeleton, + ...defaultConfig + }); + } + + const defaultShortCuts = transformArrayToMap(defaultConfig.shortCuts || [], 'keyboard'); + const customShortCuts = transformArrayToMap(shortCuts || [], 'keyboard'); + const localeList = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP']; + const i18nConfig = {}; + localeList.forEach(key => { + i18nConfig[key] = { + ...(defaultConfig.i18n && defaultConfig.i18n[key]), + ...(i18n && i18n[key]) + }; + }); + return { + skeleton, + theme: { + ...defaultConfig.theme, + ...theme + }, + addons: { + ...defaultConfig.addons, + ...addons + }, + hooks: [...(defaultConfig.hooks || []), ...(hooks || [])], + shortCuts: Object.values({ + ...defaultShortCuts, + ...customShortCuts + }), + extensions: { + ...defaultConfig.extensions, + ...extensions + }, + constants: { + ...defaultConfig.constants, + ...constants + }, + utils: [...(defaultConfig.utils || []), ...(utils || [])], + i18n: i18nConfig + }; +} + +/** + * 用于构造国际化字符串处理函数 + * @param {*} locale 国际化标识,例如 zh-CN、en-US + * @param {*} messages 国际化语言包 + */ +export function generateI18n(locale = 'zh-CN', messages = {}) { + return (key, values = {}) => { + if (!messages || !messages[key]) return ''; + const formater = new IntlMessageFormat(messages[key], locale); + return formater.format(values); + }; +} + +/** + * 判断当前组件是否能够设置ref + * @param {*} Comp 需要判断的组件 + */ +export function acceptsRef(Comp) { + return ( + (Comp.$$typeof && Comp.$$typeof === REACT_FORWARD_REF_TYPE) || (Comp.prototype && Comp.prototype.isReactComponent) + ); +} + +/** + * 黄金令箭埋点 + * @param {String} gmKey 为黄金令箭业务类型 + * @param {Object} params 参数 + * @param {String} logKey 属性串 + */ +export function goldlog(gmKey, params = {}, logKey = 'other') { + // vscode 黄金令箭API + const sendIDEMessage = window.sendIDEMessage || getParentWinValue('sendIDEMessage'); + const goKey = serializeParams({ + sdkVersion: pkg.version, + env: getEnv(), + ...params + }); + if (sendIDEMessage) { + sendIDEMessage({ + action: 'goldlog', + data: { + logKey: `/iceluna.core.${logKey}`, + gmKey, + goKey + } + }); + } + window.goldlog && window.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST'); +} + +// utils为编辑器打包生成的utils文件内容,utilsConfig为数据库存放的utils配置 +export function generateUtils(utils, utilsConfig) { + if (!Array.isArray(utilsConfig)) return { ...utils }; + const res = {}; + utilsConfig.forEach(item => { + if (!item.name || !item.type || !item.content) return; + if (item.type === 'function' && typeof item.content === 'function') { + res[item.name] = item.content; + } else if (item.type === 'npm' && utils[item.name]) { + res[item.name] = utils[item.name]; + } + }); + return res; +} +// 复制到粘贴板 +export function setClipboardData(str) { + return new Promise((resolve, reject) => { + if (typeof str !== 'string') reject('不支持拷贝'); + if (navigator.clipboard) { + navigator.clipboard + .writeText(str) + .then(() => { + resolve(); + }) + .catch(err => { + reject('复制失败,请重试!', err); + }); + } else { + const textArea = document.createElement('textarea'); + textArea.value = str; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + let successful = document.execCommand('copy'); + if (successful) { + document.body.removeChild(textArea); + resolve(); + } + } catch (err) { + document.body.removeChild(textArea); + reject('复制失败,请重试!', err); + } + } + }); +} +// 获取粘贴板数据 +export function getClipboardData() { + return new Promise((resolve, reject) => { + if (window.clipboardData) { + resolve(window.clipboardData.getData('text')); + } else if (navigator.clipboard) { + return navigator.clipboard + .readText() + .then(res => { + resolve(res); + }) + .catch(err => { + reject('粘贴板获取失败', err); + }); + } else { + reject('粘贴板获取失败'); + } + }); +} +// 将函数返回结果转成promise形式,如果函数有返回值则根据返回值的bool类型判断是reject还是resolve,若函数无返回值默认执行resolve +export function transformToPromise(input) { + if (input instanceof Promise) return input; + return new Promise((resolve, reject) => { + if (input || input === undefined) { + resolve(); + } else { + reject(); + } + }); +} + +export function moveArrayItem(arr, sourceIdx, distIdx, direction) { + if ( + !Array.isArray(arr) || + sourceIdx === distIdx || + sourceIdx < 0 || + sourceIdx >= arr.length || + distIdx < 0 || + distIdx >= arr.length + ) + return arr; + const item = arr[sourceIdx]; + if (direction === 'after') { + arr.splice(distIdx + 1, 0, item); + } else { + arr.splice(distIdx, 0, item); + } + if (sourceIdx < distIdx) { + arr.splice(sourceIdx, 1); + } else { + arr.splice(sourceIdx + 1, 1); + } + return arr; +} + +export function transformArrayToMap(arr, key, overwrite = true) { + if (isEmpty(arr) || !Array.isArray(arr)) return {}; + const res = {}; + arr.forEach(item => { + const curKey = item[key]; + if (item[key] === undefined) return; + if (res[curKey] && !overwrite) return; + res[curKey] = item; + }); + return res; +} + +export function checkPropTypes(value, name, rule, componentName) { + if (typeof rule === 'string') { + rule = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2); + } + if (!rule || typeof rule !== 'function') { + console.warn('checkPropTypes should have a function type rule argument'); + return true; + } + const err = rule( + { + [name]: value + }, + name, + componentName, + 'prop', + null, + ReactPropTypesSecret + ); + if (err) { + console.warn(err); + } + return !err; +} + +export function transformSchemaToPure(obj) { + const pureObj = obj => { + if (Array.isArray(obj)) { + return obj.map(item => pureObj(item)); + } else if (typeof obj === 'object') { + // 对于undefined及null直接返回 + if (!obj) return obj; + const res = {}; + forEach(obj, (val, key) => { + if (key.startsWith('__') && key !== '__ignoreParse') return; + res[key] = pureObj(val); + }); + return res; + } + return obj; + }; + return pureObj(obj); +} + +export function transformSchemaToStandard(obj) { + const standardObj = obj => { + if (Array.isArray(obj)) { + return obj.map(item => standardObj(item)); + } else if (typeof obj === 'object') { + // 对于undefined及null直接返回 + if (!obj) return obj; + const res = {}; + forEach(obj, (val, key) => { + if (key.startsWith('__') && key !== '__ignoreParse') return; + if (isSchema(val) && key !== 'children' && obj.type !== 'JSSlot') { + res[key] = { + type: 'JSSlot', + value: standardObj(val) + }; + // table特殊处理 + if (key === 'cell') { + res[key].params = ['value', 'index', 'record']; + } + } else { + res[key] = standardObj(val); + } + }); + return res; + } else if (typeof obj === 'function') { + return { + type: 'JSFunction', + value: obj.toString() + }; + } else if (typeof obj === 'string' && EXPRESSION_REG.test(obj.trim())) { + const regRes = obj.trim().match(EXPRESSION_REG); + return { + type: 'JSExpression', + value: (regRes && regRes[1]) || '' + }; + } + return obj; + }; + return standardObj(obj, false); +} + +export function transformStringToFunction(str) { + if (typeof str !== 'string') return str; + if (inSameDomain() && window.parent.__newFunc) { + return window.parent.__newFunc(`"use strict"; return ${str}`)(); + } else { + return new Function(`"use strict"; return ${str}`)(); + } +} + +export function addCssTag(id, content) { + let styleTag = document.getElementById(id); + if (styleTag) { + styleTag.innerHTML = content; + return; + } + styleTag = document.createElement('style'); + styleTag.id = id; + styleTag.class = 'luna-style'; + styleTag.innerHTML = content; + document.head.appendChild(styleTag); +} + +// 注册快捷 +export function registShortCuts(config, appHelper) { + const keyboardFilter = (keymaster.filter = event => { + const eTarget = event.target || event.srcElement; + const tagName = eTarget.tagName; + const isInput = !!(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); + const isContenteditable = target => { + while (target) { + if (target.contentEditable === 'true') return true; + target = target.parentNode; + } + return false; + }; + if (isInput || isContenteditable(eTarget)) { + if (event.metaKey === true && [70, 83].includes(event.keyCode)) event.preventDefault(); //禁止触发chrome原生的页面保存或查找 + return false; + } else { + return true; + } + }); + + const ideMessage = appHelper.utils && appHelper.utils.ideMessage; + + //复制 + if (!document.copyListener) { + document.copyListener = e => { + if (!keyboardFilter(e) || appHelper.isCopying) return; + const schema = appHelper.schemaHelper && appHelper.schemaHelper.schemaMap[appHelper.activeKey]; + if (!schema || !isSchema(schema)) return; + appHelper.isCopying = true; + const schemaStr = serialize(transformSchemaToPure(schema), { + unsafe: true + }); + setClipboardData(schemaStr) + .then(() => { + ideMessage && ideMessage('success', '当前内容已复制到剪贴板,请使用快捷键Command+v进行粘贴'); + appHelper.emit('schema.copy', schemaStr, schema); + appHelper.isCopying = false; + }) + .catch(errMsg => { + ideMessage && ideMessage('error', errMsg); + appHelper.isCopying = false; + }); + }; + document.addEventListener('copy', document.copyListener); + if (getParentWinValue('vscode')) { + keymaster('command+c', document.copyListener); + } + } + + //粘贴 + if (!document.pasteListener) { + const doPaste = (e, text) => { + if (!keyboardFilter(e) || appHelper.isPasting) return; + const schemaHelper = appHelper.schemaHelper; + let targetKey = appHelper.activeKey; + let direction = 'after'; + const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey; + if (!targetKey || topKey === targetKey) { + const schemaHelper = appHelper.schemaHelper; + const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey; + if (!topKey) return; + targetKey = topKey; + direction = 'in'; + } + appHelper.isPasting = true; + const schema = parseObj(text); + if (!isSchema(schema)) { + appHelper.emit('illegalSchema.paste', text); + // ideMessage && ideMessage('error', '当前内容不是模型结构,不能粘贴进来!'); + console.warn('paste schema illegal'); + appHelper.isPasting = false; + return; + } + appHelper.emit('material.add', { + schema, + targetKey, + direction + }); + appHelper.isPasting = false; + appHelper.emit('schema.paste', schema); + }; + document.pasteListener = e => { + const clipboardData = e.clipboardData || window.clipboardData; + const text = clipboardData && clipboardData.getData('text'); + doPaste(e, text); + }; + document.addEventListener('paste', document.pasteListener); + if (getParentWinValue('vscode')) { + keymaster('command+v', e => { + const sendIDEMessage = getParentWinValue('sendIDEMessage'); + sendIDEMessage && + sendIDEMessage({ + action: 'readClipboard' + }) + .then(text => { + doPaste(e, text); + }) + .catch(err => { + console.warn(err); + }); + }); + } + } + + (config || []).forEach(item => { + keymaster(item.keyboard, ev => { + ev.preventDefault(); + item.handler(ev, appHelper, keymaster); + }); + }); +} + +// 取消注册快捷 +export function unRegistShortCuts(config) { + (config || []).forEach(item => { + keymaster.unbind(item.keyboard); + }); + if (getParentWinValue('vscode')) { + keymaster.unbind('command+c'); + keymaster.unbind('command+v'); + } + if (document.copyListener) { + document.removeEventListener('copy', document.copyListener); + delete document.copyListener; + } + if (document.pasteListener) { + document.removeEventListener('paste', document.pasteListener); + delete document.pasteListener; + } +} + +export function parseData(schema, self) { + if (isJSExpression(schema)) { + return parseExpression(schema, self); + } else if (typeof schema === 'string') { + return schema.trim(); + } else if (Array.isArray(schema)) { + return schema.map(item => parseData(item, self)); + } else if (typeof schema === 'function') { + return schema.bind(self); + } else if (typeof schema === 'object') { + // 对于undefined及null直接返回 + if (!schema) return schema; + const res = {}; + forEach(schema, (val, key) => { + if (key.startsWith('__')) return; + res[key] = parseData(val, self); + }); + return res; + } + return schema; +} + +/*全匹配{{开头,}}结尾的变量表达式,或者对象类型JSExpression,且均不支持省略this */ +export function parseExpression(str, self) { + try { + const contextArr = ['"use strict";', 'var __self = arguments[0];']; + contextArr.push('return '); + let tarStr; + //向前兼容,支持标准协议新格式 + if (typeof str === 'string') { + const regRes = str.trim().match(EXPRESSION_REG); + tarStr = regRes[1]; + } else { + tarStr = (str.value || '').trim(); + } + tarStr = tarStr.replace(/this(\W|$)/g, (a, b) => `__self${b}`); + tarStr = contextArr.join('\n') + tarStr; + //默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象 + if (inSameDomain() && window.parent.__newFunc) { + return window.parent.__newFunc(tarStr)(self); + } + return new Function(tarStr)(self); + } catch (err) { + debug('parseExpression.error', err, str, self); + return undefined; + } +} + +/** + * 判断组件配置中是否有reactNode且type为function的props + * @param {*} componentInfo + */ +export function hasReactNodeFuncProps(componentInfo) { + const isReactNodeFuncProps = config => { + if (config.type === 'ReactNode') { + return config.props && config.props.type === 'function'; + } else if (config.type === 'Mixin') { + return config.props && config.props.reactNodeProps && config.props.reactNodeProps.type === 'function'; + } + }; + return componentInfo && (componentInfo.props || []).some(item => isReactNodeFuncProps(item)); +} diff --git a/packages/rax-render/src/utils/request.js b/packages/rax-render/src/utils/request.js new file mode 100644 index 000000000..f03bd4203 --- /dev/null +++ b/packages/rax-render/src/utils/request.js @@ -0,0 +1,171 @@ +import 'whatwg-fetch'; +import fetchMtop from '@ali/lib-mtop'; +import fetchJsonp from 'fetch-jsonp'; +import bzbRequest from '@ali/bzb-request'; +import { serialize, buildUrl, parseUrl } from '@ali/b3-one/lib/url'; + +export function get(dataAPI, params = {}, headers = {}, otherProps = {}) { + headers = { + Accept: 'application/json', + ...headers + }; + dataAPI = buildUrl(dataAPI, params); + return request(dataAPI, 'GET', null, headers, otherProps); +} + +export function post(dataAPI, params = {}, headers = {}, otherProps = {}) { + headers = { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + ...headers + }; + return request( + dataAPI, + 'POST', + headers['Content-Type'].indexOf('application/json') > -1 || Array.isArray(params) + ? JSON.stringify(params) + : serialize(params), + headers, + otherProps + ); +} + +export function request(dataAPI, method = 'GET', data, headers = {}, otherProps = {}) { + switch (method) { + case 'PUT': + case 'DELETE': + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers + }; + data = JSON.stringify(data || {}); + break; + } + return new Promise((resolve, reject) => { + if (otherProps.timeout) { + setTimeout(() => { + reject(new Error('timeout')); + }, otherProps.timeout); + } + fetch(dataAPI, { + method, + credentials: 'include', + headers, + body: data, + ...otherProps + }) + .then(response => { + switch (response.status) { + case 200: + case 201: + case 202: + return response.json(); + case 204: + if (method === 'DELETE') { + return { + success: true + }; + } else { + return { + __success: false, + code: response.status + }; + } + case 400: + case 401: + case 403: + case 404: + case 406: + case 410: + case 422: + case 500: + return response + .json() + .then(res => { + return { + __success: false, + code: response.status, + data: res + }; + }) + .catch(() => { + return { + __success: false, + code: response.status + }; + }); + } + return null; + }) + .then(json => { + if (json && json.__success !== false) { + resolve(json); + } else { + delete json.__success; + reject(json); + } + }) + .catch(err => { + reject(err); + }); + }); +} + +export function jsonp(dataAPI, params = {}, otherProps = {}) { + return new Promise((resolve, reject) => { + otherProps = { + timeout: 5000, + ...otherProps + }; + fetchJsonp(buildUrl(dataAPI, params), otherProps) + .then(response => response.json()) + .then(json => { + if (json) { + resolve(json); + } else { + reject(); + } + }) + .catch(err => { + reject(err); + }); + }); +} + +export function mtop(dataAPI, params, otherProps = {}) { + fetchMtop.config.subDomain = otherProps.subDomain || 'm'; + return fetchMtop.request({ + api: dataAPI, + v: '1.0', + data: params, + ecode: otherProps.ecode || 0, + type: otherProps.method || 'GET', + dataType: otherProps.dataType || 'jsonp', + AntiFlood: true, // 防刷 + timeout: otherProps.timeout || 20000 + }); +} + +export function bzb(apiCode, params, otherProps = {}) { + // 通过url参数设置小二工作台请求环境 + const getUrlEnv = () => { + try { + if (window.parent && window.parent.location.host === window.location.host) { + const urlInfo = parseUrl(window.parent && window.parent.location.href); + return urlInfo && urlInfo.params && urlInfo.params._env; + } + const urlInfo = parseUrl(window.location.href); + return urlInfo && urlInfo.params && urlInfo.params._env; + } catch (e) { + return null; + } + }; + + otherProps.method = otherProps.method || 'GET'; + otherProps.env = getUrlEnv() || otherProps.env || 'prod'; + return bzbRequest(apiCode, { + data: params, + ...otherProps + }); +}