@morjs/runtime-web
Version:
mor runtime for web
529 lines • 23.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KBComponent = void 0;
const tslib_1 = require("tslib");
const lodash_clonedeep_1 = tslib_1.__importDefault(require("lodash.clonedeep"));
const lodash_get_1 = tslib_1.__importDefault(require("lodash.get"));
const lodash_isequal_1 = tslib_1.__importDefault(require("lodash.isequal"));
const lodash_set_1 = tslib_1.__importDefault(require("lodash.set"));
const lodash_topath_1 = tslib_1.__importDefault(require("lodash.topath"));
const react_1 = tslib_1.__importDefault(require("react"));
const react_dom_1 = tslib_1.__importDefault(require("react-dom"));
const event_convert_1 = tslib_1.__importDefault(require("./event-convert"));
const common_1 = require("./utils/common");
const errorHandler_1 = require("./utils/errorHandler");
const instanceApi_1 = require("./utils/instanceApi");
const cacheMap_1 = tslib_1.__importDefault(require("./utils/cacheMap"));
const OWNER_PROPS_NAME = '__oid__';
let componetLastId = 0;
function isEqual(v1, v2) {
// 不能直接调用 lodash.isequal。 因为如果开发者在调用setData 之前直接对原来的数据做处理,那么两次的比较肯定是一样的。这样就不会触发render
return v1 === v2;
}
class KBComponent extends react_1.default.PureComponent {
/**
*
* @param {*} props
* @param {*} componentConfig
* @param {isComplexComponents,isComponent} options
*/
constructor(props, componentConfig, options) {
super(props);
this.cachePreDataAction = {};
this.updateDataQueue = [];
this.options = options || {};
this.state = {
// 将deriveDataFromProps 放在state中,实在是无奈之举
// 因为 getDerivedStateFromProps 是静态方法,只能使用这样的方式来实现在 getDerivedStateFromProps 调用组件中的方法。
deriveDataFromProps: this.deriveDataFromProps.bind(this),
prevProps: (0, lodash_clonedeep_1.default)(componentConfig.props || {}),
cachePrePropsAction: {}
};
// 避免自定义组件props为undefined
componentConfig.props = componentConfig.props || {};
componetLastId++;
// 得益于JS是单线程,因此这里也不用考虑串的问题。
this.$id = componetLastId;
// 用于判断组件当前是否已经mounted,用于解决在组件未挂载就执行 forceData 的问题
this._isMounted = false;
this.forceResetConfig(componentConfig);
this.onInit();
this.eventsInfo = {};
this._raiseEvent = this._raiseEvent.bind(this);
if (!(window === null || window === void 0 ? void 0 : window.$__USING_RAX__)) {
/*
描述: 解决 React 中一直报 warning 的问题
具体错误: uses getDerivedStateFromProps() but also contains the following legacy lifecycles: componentWillReceiveProps
*/
this.componentWillReceiveProps = undefined;
}
// 如果有值,说明当前元素有子元素,将当前组件实例推入缓存,方面后续子组件通过 id 获取
if (options.ownerComponentId)
cacheMap_1.default.set(options.ownerComponentId, this.componentConfig);
}
forceResetConfig(componentConfig) {
if (this.options.isComponent) {
componentConfig.$id = this.$id;
}
// 默认属性
this.$defaultProps = (0, lodash_clonedeep_1.default)(componentConfig.props);
// 是否使用Proxy代理methods函数以便提供onError捕获
const shouldProxyComponentMethods = this.options.isComponent && typeof componentConfig.onError === 'function';
// 组件配置
this.componentConfig = shouldProxyComponentMethods
? (0, errorHandler_1.catchComponentMethodsError)(componentConfig, componentConfig.onError)
: componentConfig;
// 将methods中的方法移动到config中。方便在用户使用的时候以this来访问
if (componentConfig.methods) {
Object.keys(componentConfig.methods).forEach((key) => {
if (typeof componentConfig.methods[key] === 'function') {
componentConfig[key] = componentConfig.methods[key];
}
});
delete componentConfig.methods;
}
this.mergePropsToComponent(this.mergeProps(this.props));
// 绑定componentConfig中方法的this指针
for (const key in this.componentConfig) {
const v = this.componentConfig[key];
if (typeof v === 'function') {
this.componentConfig[key] = this._bindMethod(v);
}
}
// 绑定componentConfig.data中方法的this指针
this.componentConfig.data = this.componentConfig.data || {};
for (const key in this.componentConfig.data) {
const v = this.componentConfig.data[key];
if (typeof v === 'function') {
this.componentConfig.data[key] = this._bindMethod(v);
}
}
// 添加refs 引用变量
this.componentConfig.refs = {};
// 绑定 setData 方法
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
this.componentConfig.setData = function (data, callback) {
const updateData = (obj, callback) => {
let hasUpdateData = false; // 是否有更新data中的数据
Object.keys(obj).forEach((key) => {
// 这里做了简单的 相等 判断。 防止某些生命周期的死循环
const newValue = (0, lodash_get_1.default)(obj, key);
if (newValue !== undefined &&
!isEqual(newValue, (0, lodash_get_1.default)(this.data, key))) {
hasUpdateData = true;
Object.prototype.hasOwnProperty.call(this.data, key) &&
(_this.cachePreDataAction[key] = (0, lodash_get_1.default)(this.data, key));
(0, lodash_set_1.default)(this.data, key, newValue);
}
});
if (hasUpdateData && _this._isMounted) {
// false 的情况只有发生在,用户只是更新 props中的value
_this.forceUpdate(callback);
}
else {
callback && callback();
}
};
if (!_this._isMounted)
return updateData(data, callback);
_this.updateDataQueue.push({ data, callback });
// 合并批量更新
setTimeout(() => {
const obj = (0, common_1.combineValue)(_this.updateDataQueue);
const callback = (0, common_1.mergeExecution)(_this.updateDataQueue);
_this.updateDataQueue = [];
updateData(obj, callback);
});
}.bind(this.componentConfig);
// 绑定 $spliceData 方法
this.componentConfig.$spliceData = function (obj, callback) {
const updateDataArray = {};
Object.keys(obj).forEach((key) => {
const propValue = obj[key];
const dataValue = (0, lodash_get_1.default)(this.data, key);
// 对非数组值进行过滤
if (!Array.isArray(dataValue) || !Array.isArray(propValue)) {
console.warn(`${key}: 用法错误,请检查参数或者当前 this.data 的值`);
return;
}
// 进行浅拷贝
const resultValue = [...dataValue];
Array.prototype.splice.apply(resultValue, propValue);
updateDataArray[key] = resultValue;
});
this.setData(updateDataArray, callback);
}.bind(this.componentConfig);
this.componentConfig._updateProp = function (propName, value) {
// 更新 prop 必须由 外部调用 setData 来实现。组件内部不会调用
// 更新prop的value必须由外部来更新
if (this.props._onUpdateProp) {
//使用propName来找到 实际更新的 valuePath,
const valuePath = this.props._twoWayBindingPropPair[propName];
this.props._onUpdateProp(valuePath, value);
}
};
this.componentConfig.route = window.$getRoute && window.$getRoute();
this.componentConfig.pageId = window.$getPageId && window.$getPageId();
this.componentConfig.createIntersectionObserver = (options) => {
return (0, instanceApi_1.mountIntersectionObserver)(options, this.getRoot());
};
if (this.options.isComponent) {
this.componentConfig.selectOwnerComponent = () => {
const { props } = this.componentConfig;
if (props && props[OWNER_PROPS_NAME]) {
const instance = cacheMap_1.default.get(props[OWNER_PROPS_NAME]);
if (instance)
return instance;
}
console.warn(`selectOwnerComponent: 未获取到当前元素的父元素实例,请确认当前是否开启 web.appConfig.apis.enableSelectOwnerComponent`);
};
}
this.updatePageConfig();
}
updatePageConfig() {
if (!window.getCurrentPages)
return;
const pageStack = getCurrentPages() || [];
const currentPage = pageStack[pageStack.length - 1];
// TODO: 这里的 router 判断和 pageId 判断可以考虑合并, 两者的作用类似
if (!currentPage || currentPage.route !== this.componentConfig.route)
return;
if (this.options.isComponent) {
this.componentConfig.$page = new Proxy(currentPage, {
get(object, prop) {
return Reflect.get(object, prop);
},
set(object, prop, value) {
if (currentPage &&
currentPage.__tigaPage &&
currentPage.__tigaPage.componentConfig) {
currentPage.__tigaPage.componentConfig[prop] = value;
}
return Reflect.set(object, prop, value);
}
});
}
else {
// 页面实例相同才需要更新
if (currentPage.pageId === this.componentConfig.pageId) {
Object.assign(currentPage, this.componentConfig);
}
}
}
onInit() {
// 组件生命周期函数,组件创建时触发
if (this.componentConfig.onInit) {
this.componentConfig.onInit();
}
}
componentWillReceiveProps(props) {
// 非 rax 环境不执行后续逻辑
if (!(window === null || window === void 0 ? void 0 : window.$__USING_RAX__))
return;
const state = this.state;
let hasPropData = false; // 是否有更新data中的数据
Object.keys(props).forEach((key) => {
// 这里做了简单的 相等 判断。 防止某些生命周期的死循环
const newValue = (0, lodash_get_1.default)(props, key);
const preValue = (0, lodash_get_1.default)(state.prevProps, key);
if (newValue !== undefined && !(0, lodash_isequal_1.default)(newValue, preValue)) {
hasPropData = true;
typeof preValue !== 'undefined' &&
(state.cachePrePropsAction[key] = preValue);
(0, lodash_set_1.default)(state.prevProps, key, newValue);
}
});
state.deriveDataFromProps(props);
if (hasPropData) {
return Object.assign({}, state.prevProps);
}
return null;
}
static getDerivedStateFromProps(props, state) {
let hasPropData = false; // 是否有更新data中的数据
Object.keys(props).forEach((key) => {
// 这里做了简单的 相等 判断。 防止某些生命周期的死循环
const newValue = (0, lodash_get_1.default)(props, key);
const preValue = (0, lodash_get_1.default)(state.prevProps, key);
if (newValue !== undefined && !(0, lodash_isequal_1.default)(newValue, preValue)) {
hasPropData = true;
typeof preValue !== 'undefined' &&
(state.cachePrePropsAction[key] = preValue);
(0, lodash_set_1.default)(state.prevProps, key, newValue);
}
});
state.deriveDataFromProps(props);
if (hasPropData) {
return Object.assign({}, state.prevProps);
}
return null;
}
// NOTE:组件创建时和更新前触发
deriveDataFromProps(props) {
/**
* 合并props,如果传过来的props value 是undifined 那么就使用defualtProp 替代
*/
const newProps = this.mergeProps(props);
this.state.prevProps = (() => {
const keys = Object.keys(this.state.cachePrePropsAction);
const result = Object.assign(Object.assign({}, newProps), this.state.prevProps);
keys.forEach((key) => {
const value = newProps[key];
if (typeof value === 'object')
result[key] = (0, lodash_clonedeep_1.default)(value);
});
return result;
})();
// mixin deriveDataFromProps 相关
if (this.componentConfig.deriveDataFromProps) {
this.componentConfig.deriveDataFromProps(newProps);
}
this.mergePropsToComponent(newProps);
requestAnimationFrame(() => this.updatePageConfig());
}
mergeProps(props) {
const newProps = this.hasPropsChanged(props)
? Object.assign({}, this.componentConfig.props) : this.componentConfig.props || props;
Object.keys(props).forEach((key) => {
if (key !== 'children') {
const value = props[key];
if (value !== undefined) {
newProps[key] = value;
}
}
});
// 删除children,当前runtime 不暴露 children
delete newProps.children;
return newProps;
}
hasPropsChanged(props) {
let hasPropsChanged = false;
Object.keys(props).forEach((key) => {
if (key !== 'children') {
const newValue = (0, lodash_get_1.default)(props, key);
if (newValue !== undefined &&
!(0, lodash_isequal_1.default)(newValue, (0, lodash_get_1.default)(this.componentConfig.props, key))) {
hasPropsChanged = true;
}
}
});
return hasPropsChanged;
}
// 合并props
mergePropsToComponent(props) {
// 将props 合并到自定义组件中
if (!this.componentConfig.props ||
typeof this.componentConfig.props !== 'object') {
this.componentConfig.props = props;
}
else {
Object.assign(this.componentConfig.props, props);
}
}
componentDidMount() {
this._isMounted = true;
this._bindEvents();
setTimeout(() => {
this.didMount();
});
}
didMount() {
// 组件生命周期函数,组件创建完毕时触发
if (this.componentConfig.didMount) {
this.componentConfig.didMount();
}
}
componentDidUpdate() {
this.didUpdate();
// 执行完didUpdate可能还有元素未显示出来导致dom无法获取绑定事件失败
setTimeout(() => this._bindEvents());
}
getPreSnapshot(source, cacheActions) {
const keys = Object.keys(cacheActions);
const result = Object.assign({}, source);
keys.forEach((key) => {
const path = (0, lodash_topath_1.default)(key);
if (path.length > 1) {
// 对象值改动需要深克隆
result[path[0]] = (0, lodash_clonedeep_1.default)(result[path[0]]);
(0, lodash_set_1.default)(result, path, cacheActions[key]);
}
else {
(0, lodash_set_1.default)(result, key, cacheActions[key]);
}
});
return result;
}
didUpdate() {
// 组件生命周期函数,组件更新完毕时触发
if (this.componentConfig.didUpdate && this.options.isComponent) {
this.componentConfig.didUpdate(this.getPreSnapshot(this.componentConfig.props || {}, this.state.cachePrePropsAction), this.getPreSnapshot(this.componentConfig.data || {}, this.cachePreDataAction));
this.state.cachePrePropsAction = {};
this.cachePreDataAction = {};
}
}
componentWillUnmount() {
this._isMounted = false;
this.state.cachePrePropsAction = {};
this.cachePreDataAction = {};
this.updateDataQueue = [];
// 如果之前将当前实例推到了缓存中,在组件销毁的时候,应该将组件移除
if (this.options.ownerComponentId)
cacheMap_1.default.delete(this.options.ownerComponentId);
this.didUnmount();
}
didUnmount() {
// 组件生命周期函数,组件删除时触发
if (this.componentConfig.didUnmount) {
this.componentConfig.didUnmount();
}
}
getRenderData() {
// data 的数据会覆盖 prop 的数据。 这个是区别于微信小程序的,在微信小程序中,props会覆盖data中数据
const $slots = {};
if (this.props.children) {
if (this.props.children instanceof Array) {
this.props.children.forEach((i) => {
if (i.props && i.props._slot) {
$slots[i.props._slot] = i;
}
});
}
else {
const i = this.props.children;
if (i.props && i.props._slot) {
$slots[i.props._slot] = i;
}
}
}
return Object.assign({
$children: this.props.children,
$reactComp: this,
$slots,
$id: this.$id
}, this.componentConfig.props, this.componentConfig.data);
}
// 当触发ref的时候
onRef(ref, value) {
if (ref && typeof this.componentConfig[value] === 'function') {
// 从这里的代码可以看出,ref 有可能有三种不同的形态, 1、dom 原生元素 2、非小程序的react 组件 3、小程序自定义组件
if (ref instanceof KBComponent) {
let _ref = ref.componentConfig;
if (typeof _ref.ref === 'function') {
_ref = _ref.ref();
}
this.componentConfig[value].call(this.componentConfig, _ref);
}
else {
this.componentConfig[value].call(this.componentConfig, ref);
}
return true;
}
else {
// 进入自动绑定流程
if (ref instanceof KBComponent) {
this.componentConfig.refs[value] = ref.componentConfig;
}
else {
this.componentConfig.refs[value] = ref;
}
}
return false;
}
// 根据方法名获取组件实例的方法
getCompMethod(methodName) {
let method;
if (typeof methodName === 'string') {
const func = this.componentConfig[methodName];
if (func && typeof func === 'function') {
method = func;
}
}
else if (typeof methodName === 'function') {
method = methodName;
}
else {
throw new Error('绑定的事件名必须是 string 或者 function');
}
return this._bindMethod(method);
}
_bindMethod(method) {
if (method && !method.__isBind__) {
method = method.bind(this.componentConfig);
method.__isBind__ = true;
if (method.name in this.componentConfig) {
this.componentConfig[method.name] = method; //绑定到组件
}
}
return method;
}
/**
* 动态绑定事件
*/
_bindEvents() {
// 避免卸载之后继续执行 findDOMNode 操作
if (!this._isMounted)
return;
const root = this.getRoot();
if (!root)
return;
for (const nodeId in this.eventsInfo) {
const events = this.eventsInfo[nodeId];
// NOTE: 这块后续可以优化成一次性找出所有的元素。
// 先通过 tiga_node_id 来找到对应的节点元素。
let elments;
if (this.options.isComplexComponents) {
elments = Array.from((root.parentElement || root).getElementsByClassName(`${nodeId} comp-id-${this.$id}`));
}
else {
if ('classList' in root && root.classList.contains(nodeId)) {
elments = [root];
}
else {
if ('getElementsByClassName' in root) {
elments = Array.from(root.getElementsByClassName(nodeId));
}
}
}
// 然后动态添加绑定。
for (const el of elments) {
// 动态给元素设置 一个属性 tigaNodeId,方便事件触发的时候找到 事件回调函数
el.tigaNodeId = nodeId;
for (const eventInfo of events) {
el.addEventListener(eventInfo.name, this._raiseEvent);
}
}
}
}
_raiseEvent(e) {
const currentTarget = e.currentTarget;
// 根据nodeId找到对应的事件配置
const nodeId = currentTarget.tigaNodeId;
if (nodeId) {
const events = this.eventsInfo[nodeId];
if (events) {
const eventInfo = events.filter((i) => i.name === e.type).pop();
// NOTE:catch 会阻止事件冒泡 https://opendocs.alipay.com/mini/framework/events#%E4%BA%8B%E4%BB%B6%E7%B1%BB%E5%9E%8B
if (eventInfo.catch) {
e.stopPropagation();
}
const func = this.componentConfig[eventInfo.event];
func && func.call(this.componentConfig, (0, event_convert_1.default)(e));
}
}
}
registEvents(events, nodeId) {
this.eventsInfo[nodeId] = events;
}
getRoot() {
try {
return react_dom_1.default.findDOMNode(this);
}
catch (e) {
return null;
}
}
}
exports.KBComponent = KBComponent;
//# sourceMappingURL=component.js.map