Match-id-474b061fef13390c1c27879d396452c02602ca12

This commit is contained in:
* 2023-02-17 18:12:46 +08:00
commit 27a94beecc
21 changed files with 272 additions and 63 deletions

View File

@ -20,7 +20,7 @@ if [ -n "${releaseVersion}" ] ; then
cd umd
# umd生产包多暴露全局名HorizonDOM
# 以解决webpack的externals react-dom和react都指向Horizon时,webpack随机使用key名造成源码交付问题
sed -i '$a window.HorizonDOM = window.Horizon;' horizon.production.js
sed -i '$a window.HorizonDOM = window.Horizon;' horizon.production.min.js
cd -
# 写入新版本号

View File

@ -22,8 +22,8 @@ steps:
- checkout:
path: horizon-core
- gitlab:
url: https://szv-open.codehub.huawei.com/innersource/shanhai/wutong/react/horizon-test.git
branch: one_tree_dev
url: https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-test.git
branch: master
path: horizon-test
BUILD:
- build_execute:

View File

@ -1,3 +1,25 @@
## 0.0.38 (2023-02-01)
- **core**: 增加flushSync接口
## 0.0.37 (2023-01-31)
- **core**: 增加jsxs方法
## 0.0.36 (2023-01-30)
- **core**: #100 horizon从上层页面透传到iframe页面里使用创建的dom元素instanceof HTMLElement为false
## 0.0.35 (2023-01-28)
- **core**: 在 cloneDeep JSXElement 的时候会出现死循环
## 0.0.34 (2023-01-19)
- **core**: #95 新增jsx接口
- **core**: #96 #97 fix testing-library 的UT错误
## 0.0.33 (2023-01-11)
- **horizonX-devtool**: 修复IE中报错
## 0.0.32 (2023-01-04)
- **CI**: 生成态输出文件改为horiozn.producion.min.js
## 0.0.26 (2022-11-09)
- **CI**: 包信息同步CMC

View File

@ -19,3 +19,4 @@
declare var isDev: boolean;
declare var isTest: boolean;
declare const __VERSION__: string;
declare var setImmediate: Function;

View File

@ -42,9 +42,6 @@ import {
useState,
useDebugValue,
} from './src/renderer/hooks/HookExternal';
import { asyncUpdates } from './src/renderer/TreeBuilder';
import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue';
import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler';
import {
isContextProvider,
isContextConsumer,
@ -59,13 +56,7 @@ import {
import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler';
import * as reduxAdapter from './src/horizonx/adapters/redux';
import { watch } from './src/horizonx/proxy/watch';
// act用于测试作用是如果fun触发了刷新包含了异步刷新可以保证在act后面的代码是在刷新完成后才执行。
const act = fun => {
asyncUpdates(fun);
callRenderQueueImmediate();
runAsyncEffects();
};
import { act } from './src/external/TestUtil';
import {
render,
@ -75,6 +66,8 @@ import {
unmountComponentAtNode,
} from './src/dom/DOMExternal';
import { syncUpdates as flushSync } from './src/renderer/TreeBuilder';
const Horizon = {
Children,
createRef,
@ -107,6 +100,7 @@ const Horizon = {
findDOMNode,
unmountComponentAtNode,
act,
flushSync,
createStore,
useStore,
clearStore,
@ -156,6 +150,7 @@ export {
findDOMNode,
unmountComponentAtNode,
act,
flushSync,
// 状态管理器HorizonX接口
createStore,
useStore,

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
*
* openGauss is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import {
TYPE_FRAGMENT as Fragment,
} from './src/external/JSXElementType';
import { jsx, jsx as jsxs } from './src/external/JSXElement';
export {
jsx,
jsxs,
Fragment
};

View File

@ -16,7 +16,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/horizon.production.js');
module.exports = require('./cjs/horizon.production.min.js');
} else {
module.exports = require('./cjs/horizon.development.js');
}

View File

@ -4,7 +4,7 @@
"keywords": [
"horizon"
],
"version": "0.0.26",
"version": "0.0.38",
"homepage": "",
"bugs": "",
"main": "index.js",

View File

@ -16,7 +16,7 @@
import { saveVNode, updateVNodeProps } from './DOMInternalKeys';
import { createDom } from './utils/DomCreator';
import { getSelectionInfo, resetSelectionRange, SelectionData } from './SelectionRangeHandler';
import { shouldAutoFocus } from './utils/Common';
import { isDocument, shouldAutoFocus } from './utils/Common';
import { NSS } from './utils/DomCreator';
import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler';
import type { VNode } from '../renderer/Types';
@ -26,6 +26,7 @@ import { isNativeElement, validateProps } from './validators/ValidateProps';
import { watchValueChange } from './valueHandler/ValueChangeHandler';
import { DomComponent, DomText } from '../renderer/vnode/VNodeTags';
import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp';
import {getCurrentRoot} from '../renderer/RootStack';
export type Props = Record<string, any> & {
autoFocus?: boolean;
@ -70,7 +71,12 @@ export function resetAfterSubmit(): void {
// 创建 DOM 对象
export function newDom(tagName: string, props: Props, parentNamespace: string, vNode: VNode): Element {
const dom: Element = createDom(tagName, parentNamespace);
// document取值于treeRoot对应的DOM的ownerDocument。
// 解决在iframe中使用top的horizon时horizon在创建DOM时用到的document并不是iframe的document而是top中的document的问题。
const rootDom = getCurrentRoot().realNode;
const doc = isDocument(rootDom) ? rootDom : rootDom.ownerDocument;
const dom: Element = createDom(tagName, parentNamespace, doc);
// 将 vNode 节点挂到 DOM 对象上
saveVNode(vNode, dom);
// 将属性挂到 DOM 对象上

View File

@ -20,15 +20,15 @@ export const NSS = {
};
// 创建DOM元素
export function createDom(tagName: string, parentNamespace: string): Element {
export function createDom(tagName: string, parentNamespace: string, doc: Document): Element {
let dom: Element;
const selfNamespace = NSS[tagName] || NSS.html;
const ns = parentNamespace !== NSS.html ? parentNamespace : selfNamespace;
if (ns !== NSS.html) {
dom = document.createElementNS(ns, tagName);
dom = doc.createElementNS(ns, tagName);
} else {
dom = document.createElement(tagName);
dom = doc.createElement(tagName);
}
return dom;
}

View File

@ -25,10 +25,10 @@ import { Source } from '../renderer/Types';
* props
*/
export function JSXElement(type, key, ref, vNode, props, source: Source | null) {
return {
const ele = {
// 元素标识符
vtype: TYPE_COMMON_ELEMENT,
src: isDev ? source : null,
src: null,
// 属于元素的内置属性
type: type,
@ -37,8 +37,27 @@ export function JSXElement(type, key, ref, vNode, props, source: Source | null)
props: props,
// 所属的class组件
belongClassVNode: vNode,
belongClassVNode: null,
};
// 在 cloneDeep JSXElement 的时候会出现死循环需要设置belongClassVNode的enumerable为false
Object.defineProperty(ele, 'belongClassVNode', {
configurable: false,
enumerable: false,
value: vNode,
});
if (isDev) {
// 为了test判断两个 JSXElement 对象是否相等时忽略src属性需要设置src的enumerable为false
Object.defineProperty(ele, 'src', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
}
return ele;
}
function isValidKey(key) {
@ -107,3 +126,12 @@ export function cloneElement(element, setting, ...children) {
export function isValidElement(element) {
return !!(element && element.vtype === TYPE_COMMON_ELEMENT);
}
// 兼容高版本的babel编译方式
export function jsx(type, setting, key) {
if (setting.key === undefined && key !== undefined) {
setting.key = key;
}
return buildElement(false, type, setting, []);
}

74
libs/horizon/src/external/TestUtil.ts vendored Normal file
View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
*
* openGauss is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import {asyncUpdates} from '../renderer/TreeBuilder';
import {callRenderQueueImmediate} from '../renderer/taskExecutor/RenderQueue';
import {runAsyncEffects} from '../renderer/submit/HookEffectHandler';
import {isPromise} from '../renderer/ErrorHandler';
interface Thenable {
then(resolve: (val?: any) => void, reject: (err: any) => void): void;
}
// act用于测试作用是如果fun触发了刷新包含了异步刷新可以保证在act后面的代码是在刷新完成后才执行。
function act(fun: () => void | Thenable): Thenable {
const funRet = asyncUpdates(fun);
callRenderQueue();
// 如果fun返回的是Promise
if (isPromise(funRet)) {
// testing-library会返回Promise
return {
then(resolve, reject) {
funRet.then(
() => {
if (typeof setImmediate === 'function') {
// 通过setImmediate回调用于等待业务的setTimeout完成
setImmediate(() => {
callRenderQueue();
resolve();
});
} else {
callRenderQueue();
resolve();
}
},
err => {
reject(err);
},
);
},
};
} else {
return {
then(resolve) {
resolve();
},
};
}
}
function callRenderQueue() {
callRenderQueueImmediate();
runAsyncEffects();
// effects可能产生刷新任务这里再执行一次
callRenderQueueImmediate();
}
export {
act
};

View File

@ -3,10 +3,12 @@ import { OBSERVED_COMPONENTS } from './constants';
const sessionId = Date.now();
// this function is used to detect devtool connection
export function isPanelActive() {
return window['__HORIZON_DEV_HOOK__'];
}
// serializes store and creates expanded object with baked-in containing current computed values
function makeStoreSnapshot({ type, data }) {
const expanded = {};
Object.keys(data.store.$c).forEach(key => {
@ -21,6 +23,7 @@ function makeStoreSnapshot({ type, data }) {
return snapshot;
}
// safely serializes variables containing values wrapped in Proxy object
function makeProxySnapshot(obj) {
let clone;
try {
@ -47,6 +50,7 @@ function makeProxySnapshot(obj) {
}
export const devtools = {
// returns vNode id from horizon devtools
getVNodeId: vNode => {
if (!isPanelActive()) return;
getVNodeId(vNode);
@ -61,6 +65,7 @@ export const devtools = {
},
};
// collects components that are dependant on horizonx store and their ids
function getAffectedComponents() {
const allStores = getAllStores();
const keys = Object.keys(allStores);
@ -87,8 +92,9 @@ function getAffectedComponents() {
return res;
}
// listens to messages from background
window.addEventListener('message', messageEvent => {
if (messageEvent.data.payload.type === 'horizonx request observed components') {
if (messageEvent?.data?.payload?.type === 'horizonx request observed components') {
// get observed components
setTimeout(() => {
window.postMessage({
@ -99,15 +105,15 @@ window.addEventListener('message', messageEvent => {
}, 100);
}
if (messageEvent.data.payload.type === 'horizonx executue action') {
// executes store action
if (messageEvent.data?.payload?.type === 'horizonx executue action') {
const data = messageEvent.data.payload.data;
const store = getStore(data.storeId);
if (!store?.[data.action]) {
}
if (!store?.[data.action]) return;
const action = store[data.action];
const params = data.params;
action(...params).bind(store);
action(...params);
}
});

View File

@ -79,7 +79,9 @@ export class Observer implements IObserver {
// 对象的属性被赋值时调用
setProp(key: string | symbol, mutation: any): void {
const vNodes = this.keyVNodes.get(key);
vNodes?.forEach((vNode: VNode) => {
//NOTE: using Set directly can lead to deadlock
const vNodeArray = Array.from(vNodes || []);
vNodeArray?.forEach((vNode: VNode) => {
if (vNode.isStoreChange) {
// VNode已经被触发过不再重复触发
return;

View File

@ -72,7 +72,7 @@ function createClassErrorUpdate(vNode: VNode, error: any): Update {
}
return update;
}
function isPromise(error: any): error is PromiseType<any> {
export function isPromise(error: any): error is PromiseType<any> {
return error !== null && typeof error === 'object' && typeof error.then === 'function';
}
// 处理capture和bubble阶段抛出的错误

View File

@ -14,6 +14,7 @@
*/
import { VNode } from './vnode/VNode';
const currentRootStack: VNode[] = [];
export function getCurrentRoot() {
return currentRootStack[currentRootStack.length - 1];

View File

@ -77,3 +77,5 @@ export type Source = {
fileName: string;
lineNumber: number;
};
export type Callback = () => void;

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details.
*/
import type { VNode } from './Types';
import type { VNode, Callback } from './Types';
import { FlagUtils, ShouldCapture } from './vnode/VNodeFlags';
export type Update = {
@ -22,8 +22,6 @@ export type Update = {
callback: Callback | null;
};
export type Callback = () => any;
export type Updates = Array<Update> | null;
export enum UpdateState {

View File

@ -13,6 +13,8 @@
* See the Mulan PSL v2 for more details.
*/
import {Callback} from '../Types';
/**
* Component的api setState和forceUpdate在实例生成阶段实现
*/
@ -29,7 +31,7 @@ class Component<P, S, C> {
this.context = context;
}
setState(state: S) {
setState(state: S, callback?: Callback) {
if (isDev) {
console.error('Cant not call `this.setState` in the constructor of class component, it will do nothing');
}

View File

@ -19,7 +19,9 @@
let isMessageLoopRunning = false;
let browserCallback = null;
const { port1, port2 } = new MessageChannel();
let port1 = null;
let port2 = null;
let isTestRuntime = false;
export function isOverTime() {
return false;
@ -41,21 +43,38 @@ const callRenderTasks = () => {
browserCallback = null;
} else {
// 还有task继续调用
port2.postMessage(null);
asyncCall();
}
} catch (error) {
port2.postMessage(null);
asyncCall();
throw error;
}
};
port1.onmessage = callRenderTasks;
if (typeof MessageChannel === 'function') {
const mc = new MessageChannel();
port1 = mc.port1;
port1.onmessage = callRenderTasks;
port2 = mc.port2;
} else {
// 测试环境没有 MessageChannel
isTestRuntime = true;
}
function asyncCall() {
if (isTestRuntime) {
setTimeout(callRenderTasks, 0);
} else {
port2.postMessage(null);
}
}
export function requestBrowserCallback(callback) {
browserCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port2.postMessage(null);
asyncCall();
}
}

View File

@ -38,44 +38,58 @@ if (!fs.existsSync(outDir)) {
const outputResolve = (...p) => path.resolve(outDir, ...p);
const isDev = (mode) => {
return mode === 'development';
}
const getBasicPlugins = (mode) => {
return [
nodeResolve({
extensions,
modulesOnly: true,
}),
babel({
exclude: 'node_modules/**',
configFile: path.join(__dirname, '../../babel.config.js'),
babelHelpers: 'runtime',
extensions,
}),
replace({
values: {
'process.env.NODE_ENV': `"${mode}"`,
isDev: isDev(mode).toString(),
isTest: false,
__VERSION__: `"${horizonVersion}"`,
},
preventAssignment: true,
}),
];
}
function getOutputName(mode) {
return mode === 'production' ? `horizon.${mode}.min.js` : `horizon.${mode}.js`;
}
function genConfig(mode) {
const isDev = mode === 'development';
const sourcemap = isDev ? 'inline' : false;
const sourcemap = isDev(mode) ? 'inline' : false;
return {
input: path.resolve(libDir, 'index.ts'),
output: [
{
file: outputResolve('cjs', `horizon.${mode}.js`),
file: outputResolve('cjs', getOutputName(mode)),
sourcemap,
format: 'cjs',
},
{
file: outputResolve('umd', `horizon.${mode}.js`),
file: outputResolve('umd', getOutputName(mode)),
sourcemap,
name: 'Horizon',
format: 'umd',
},
],
plugins: [
nodeResolve({
extensions,
modulesOnly: true,
}),
babel({
exclude: 'node_modules/**',
configFile: path.join(__dirname, '../../babel.config.js'),
babelHelpers: 'runtime',
extensions,
}),
replace({
values: {
'process.env.NODE_ENV': `"${mode}"`,
isDev: isDev.toString(),
isTest: false,
__VERSION__: `"${horizonVersion}"`,
},
preventAssignment: true,
}),
...getBasicPlugins(mode),
execute('npm run build-types'),
mode === 'production' && terser(),
copy([
@ -92,4 +106,18 @@ function genConfig(mode) {
};
}
export default [genConfig('development'), genConfig('production')];
function genJSXRuntimeConfig(mode) {
return {
input: path.resolve(libDir, 'jsx-runtime.ts'),
output: [
{
file: outputResolve('jsx-runtime.js'),
format: 'cjs',
}
],
plugins: [
...getBasicPlugins(mode)
]
};
}
export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig('')];