diff --git a/libs/extension/package.json b/libs/extension/package.json
index ade577c7..45f5cb8a 100644
--- a/libs/extension/package.json
+++ b/libs/extension/package.json
@@ -15,21 +15,21 @@
"license": "ISC",
"devDependencies": {
"@babel/core": "7.12.3",
- "@babel/plugin-proposal-class-properties": "^7.16.7",
+ "@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/preset-env": "7.12.1",
"@babel/preset-react": "7.12.1",
- "@babel/preset-typescript": "^7.16.7",
- "@types/jest": "^27.4.1",
+ "@babel/preset-typescript": "7.16.7",
+ "@types/jest": "27.4.1",
"babel-loader": "8.1.0",
- "css-loader": "^6.7.1",
- "html-webpack-plugin": "^5.5.0",
- "jest": "^27.5.1",
- "less": "^4.1.2",
- "less-loader": "^10.2.0",
- "style-loader": "^3.3.1",
- "ts-jest": "^27.1.4",
- "webpack": "^5.70.0",
- "webpack-cli": "^4.9.2",
+ "css-loader": "6.7.1",
+ "html-webpack-plugin": "5.5.0",
+ "jest": "27.5.1",
+ "less": "4.1.2",
+ "less-loader": "10.2.0",
+ "style-loader": "3.3.1",
+ "ts-jest": "27.1.4",
+ "webpack": "5.70.0",
+ "webpack-cli": "4.9.2",
"webpack-dev-server": "^4.7.4"
}
}
diff --git a/libs/extension/readme.md b/libs/extension/readme.md
index b54fecf2..fecc8f4f 100644
--- a/libs/extension/readme.md
+++ b/libs/extension/readme.md
@@ -1,3 +1,60 @@
+## 为什么要做 devTool 插件
+让Horizon开发者获得更好的开发体验,获取准确的组件树结构、状态信息和真实dom对应关系。
+
+## 上下文关系
+devTool功能的实现依赖浏览器 extension 开放的能力,用于绘制展示组件信息和获取真实 dom 元素。同时也需要 Horizon 提供相关接口获取组件树信息和提供调试能力。
+
+## 目标
+1. 查看组件树结构并支持过滤
+2. 查看组件与真实dom的关系
+3. 查看组件props, state, hooks 等信息
+4. 调试单个组件及其子组件
+5. 支持状态管理解决方案调试
+
+## 和 react devTool 能力对比
+||react | Horizon|
+|-|-|-|
+|查看组件树|Y |Y |
+|查看真实DOM|Y|Y|
+|查看组件信息|Y|Y|
+|调试能力|Y| Y |
+|性能调试|Y|N|
+|解析Hook名|Y|N|
+|状态管理解决方案调试|N|Y|
+
+## 架构草图
+```plantuml
+@startuml
+package "Horizon" {
+ [U I]
+ [Helper]
+}
+
+package "Script Content" {
+ [GlobalHook]
+ [MessageHandler]
+ [Parser]
+
+}
+package "Browser" {
+ [Background]
+ [Panel]
+}
+
+[GlobalHook] <-- [U I]
+[GlobalHook] --> [MessageHandler]
+[Helper] <-- [MessageHandler]
+[Helper] --> [U I]
+[MessageHandler] <--> [Background]
+[Background] <--> [Panel]
+[Parser] --> [MessageHandler]
+@enduml
+```
+
+#### 说明
+Helper: 提供接口给插件操控组件以及提供工具方法。
+Parser: 负责将组件树结构和组件信息解析成特定的数据结构,供Panel展示。
+
## 文件清单说明:
devtools_page: devtool主页面
default_popup: 拓展图标点击时弹窗页面
@@ -15,9 +72,6 @@ Optional: Feel free to dock the developer tools again if you had undocked it at
## 全局变量注入
通过content_scripts在document初始化时给页面添加script脚本,在新添加的脚本中给window注入全局变量
-## horizon页面判断
-在页面完成渲染后往全局变量中添加信息,并传递 tabId 给 background 告知这是 horizon 页面
-
## 通信方式:
```mermaid
sequenceDiagram
@@ -47,22 +101,37 @@ sequenceDiagram
```
## 传输数据结构
+**限制:chrome.runtime.sendMessage只能传递 JSON-serializable 数据**
+
+
```ts
type passData = {
type: 'HORIZON_DEV_TOOLS',
payload: {
type: string,
data: any,
- }
+ },
+ from: string,
}
```
## horizon和devTools的主要交互
-- 页面初始渲染
-- 页面更新
-- 页面销毁
+- App初始渲染
+- App更新
+- App销毁
+- 整个页面刷新
- devTools触发组件属性更新
+
+## 对组件的操作
+我们希望插件和Horizon能够尽量解耦,所以Horizon提供了Helper注入给插件,提供相关方法操作组件。
+
+## 触发组件更新方式
+- 类组件的state:调用实例的 setState 函数触发更新
+- 类组件的props:浅复制props后更新props值并调用 forceUpdate 触发更新
+- 函数组件的props:新增了devProps属性,在特定时刻重新给props赋值,触发更新
+- 函数组件的state:调用 useState 函数触发更新
+
## VNode的清理
全局 hook 中保存了root VNode,在解析 VNode 树的时候也会保存 VNode 的引用,在清理VNode的时候这些 VNode 的引用也需要删除。
@@ -73,7 +142,7 @@ type passData = {
- 通过解析 path 值可以分析出组件树的结构
## 组件props/state/hook等数据的传输和解析
-将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串,他们的解析和 props/state 的解析同样存在差异。
+将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串或者 dom 元素,他们的解析和 props/state 的解析同样存在差异,需要单独处理。
## 滚动动态渲染 Tree
diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts
index ed9698fb..6a8c4591 100644
--- a/libs/extension/src/background/index.ts
+++ b/libs/extension/src/background/index.ts
@@ -1,6 +1,6 @@
import { checkMessage, packagePayload, changeSource } from '../utils/transferTool';
import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants';
-import { DevToolPanel, DevToolContentScript } from './../utils/constants';
+import { DevToolPanel, DevToolContentScript } from '../utils/constants';
// 多个页面、tab页共享一个 background,需要建立连接池,给每个tab建立连接
const connections = {};
@@ -11,29 +11,18 @@ chrome.runtime.onConnect.addListener(function (port) {
const isHorizonMessage = checkMessage(message, DevToolPanel);
if (isHorizonMessage) {
const { payload } = message;
- const { type, data } = payload;
+ // tabId 值指当前浏览器分配给 web_page 的 id 值。是panel页面查询得到,指定向该页面发送消息
+ const { type, data, tabId } = payload;
let passMessage;
if (type === InitDevToolPageConnection) {
- if (!connections[data]) {
- // 获取 panel 所在 tab 页的tabId
- connections[data] = port;
- }
+ // 记录 panel 所在 tab 页的tabId,如果已经记录了,覆盖原有port,因为原有port可能关闭了
+ // 可能这次是 panel 发起的重新建立请求
+ connections[tabId] = port;
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
} else {
- passMessage = message;
- changeSource(passMessage, DevToolBackground);
+ passMessage = packagePayload({type, data}, DevToolBackground);
}
- // 查询参数有 active 和 currentWindow, 如果开发者工具与页面分离,会导致currentWindow为false才能找到
- // 所以只用 active 参数查找,但不确定这么写是否会引发查询错误的情况
- // 或许需要用不同的查询参数查找两次
- chrome.tabs.query({ active: true }, function (tabs) {
- if (tabs.length) {
- chrome.tabs.sendMessage(tabs[0].id, passMessage);
- console.log('post message end');
- } else {
- console.log('do not find message');
- }
- });
+ chrome.tabs.sendMessage(tabId, passMessage);
}
}
// Listen to messages sent from the DevTools page
@@ -57,6 +46,7 @@ chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
// Messages from content scripts should have sender.tab set
if (sender.tab) {
const tabId = sender.tab.id;
+ // 和 InitDevToolPageConnection 时得到的 tabId 值一致时,向指定的 panel 页面 port 发送消息
if (tabId in connections && checkMessage(message, DevToolContentScript)) {
changeSource(message, DevToolBackground);
connections[tabId].postMessage(message);
diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx
index f5ff0051..844da1ba 100644
--- a/libs/extension/src/components/ComponentInfo.tsx
+++ b/libs/extension/src/components/ComponentInfo.tsx
@@ -1,21 +1,22 @@
import styles from './ComponentsInfo.less';
import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
-import Copy from '../svgs/Copy';
import Triangle from '../svgs/Triangle';
import { useState, useEffect } from 'horizon';
import { IData } from './VTree';
-import { IAttr } from '../parser/parseAttr';
+import { buildAttrModifyData, IAttr } from '../parser/parseAttr';
+import { postMessageToBackground } from '../panelConnection';
+import { InspectDom, LogComponentData, ModifyAttrs } from '../utils/constants';
type IComponentInfo = {
name: string;
attrs: {
- props?: IAttr[];
- context?: IAttr[];
- state?: IAttr[];
- hooks?: IAttr[];
+ parsedProps?: IAttr[],
+ parsedState?: IAttr[],
+ parsedHooks?: IAttr[],
};
parents: IData[];
+ id: number;
onClickParent: (item: IData) => void;
};
@@ -26,11 +27,18 @@ function collapseAllNodes(attrs: IAttr[]) {
});
}
-function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
+function ComponentAttr({ attrsName, attrsType, attrs, id }: {
+ attrsName: string,
+ attrsType: string,
+ attrs: IAttr[],
+ id: number}) {
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
+ const [editableAttrs, setEditableAttrs] = useState(attrs);
useEffect(() => {
setCollapsedNode(collapseAllNodes(attrs));
+ setEditableAttrs(attrs);
}, [attrs]);
+
const handleCollapse = (item: IAttr) => {
const nodes = [...collapsedNode];
const i = nodes.indexOf(item);
@@ -44,7 +52,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
const showAttr = [];
let currentIndentation = null;
- attrs.forEach((item, index) => {
+ editableAttrs.forEach((item, index) => {
const indentation = item.indentation;
if (currentIndentation !== null) {
if (indentation > currentIndentation) {
@@ -53,17 +61,55 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
currentIndentation = null;
}
}
- const nextItem = attrs[index + 1];
+ const nextItem = editableAttrs[index + 1];
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
const isCollapsed = collapsedNode.includes(item);
showAttr.push(
-
(handleCollapse(item))}>
+
handleCollapse(item)}>
{hasChild && }
{`${item.name}`}
{' :'}
- {item.type === 'string' || item.type === 'number'
- ? {item.value}
- : {item.value}}
+ {item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null'
+ ? {
+ const nextAttrs = [...editableAttrs];
+ const nextItem = {...item};
+ nextItem.value = event.target.value;
+ nextAttrs[index] = nextItem;
+ setEditableAttrs(nextAttrs);
+ }}
+ onKeyUp={(event) => {
+ const value = (event.target as HTMLInputElement).value;
+ if (event.key === 'Enter') {
+ if(isDev) {
+ console.log('post attr change', value);
+ } else {
+ const data = buildAttrModifyData(attrsType,attrs, value,item, index, id);
+ postMessageToBackground(ModifyAttrs, data);
+ }
+ }
+ }}
+ />
+ : (item.type === 'boolean'
+ ? {
+ const nextAttrs = [...editableAttrs];
+ const nextItem = {...item};
+ nextItem.value = event.target.checked;
+ nextAttrs[index] = nextItem;
+ setEditableAttrs(nextAttrs);
+ if (!isDev) {
+ const data = buildAttrModifyData(attrsType,attrs, nextItem.value,item, index, id);
+ postMessageToBackground(ModifyAttrs, data);
+ }
+ }}/>
+ : {item.value}
+ )}
);
if (isCollapsed) {
@@ -74,10 +120,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
return (
- {name}
-
-
-
+ {attrsName}
{showAttr}
@@ -86,8 +129,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
);
}
-export default function ComponentInfo({ name, attrs, parents, onClickParent }: IComponentInfo) {
- const { state, props, context, hooks } = attrs;
+export default function ComponentInfo({ name, attrs, parents, id, onClickParent }: IComponentInfo) {
return (
@@ -95,19 +137,27 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
{name}
-
+ {
+ postMessageToBackground(InspectDom, id);
+ }}>
-
+ {
+ postMessageToBackground(LogComponentData, id);
+ }}>
>}
- {context &&
}
- {props && props.length !== 0 &&
}
- {state && state.length !== 0 &&
}
- {hooks && hooks.length !== 0 &&
}
+ {Object.keys(attrs).map(attrsType => {
+ const parsedAttrs = attrs[attrsType];
+ if (parsedAttrs && parsedAttrs.length !== 0) {
+ const attrsName = attrsType.slice(6); // parsedState => State
+ return
;
+ }
+ return null;
+ })}
{name &&
parents: {
@@ -122,4 +172,4 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
);
-}
\ No newline at end of file
+}
diff --git a/libs/extension/src/components/ComponentsInfo.less b/libs/extension/src/components/ComponentsInfo.less
index 853d4209..d1fd0de1 100644
--- a/libs/extension/src/components/ComponentsInfo.less
+++ b/libs/extension/src/components/ComponentsInfo.less
@@ -20,11 +20,13 @@
.eye {
flex: 0 0 1rem;
padding-right: 1rem;
+ cursor: pointer;
}
.debug {
flex: 0 0 1rem;
padding-right: 1rem;
+ cursor: pointer;
}
}
@@ -71,7 +73,7 @@
}
.attrValue {
- margin-left: 4px;
+ margin: 0 0 0 4px;
}
}
}
diff --git a/libs/extension/src/components/VList/VList.tsx b/libs/extension/src/components/VList/VList.tsx
index 0879fbf4..edf4b16b 100644
--- a/libs/extension/src/components/VList/VList.tsx
+++ b/libs/extension/src/components/VList/VList.tsx
@@ -2,7 +2,7 @@
// data 数组更新后不修改滚动位置,
// 只有修改scrollToItem才会修改滚动位置
-import { useState, useRef, useEffect, useMemo } from 'libs/extension/src/components/VList/node_modules/horizon';
+import { useState, useRef, useEffect, useMemo } from 'horizon';
import styles from './VList.less';
import ItemMap from './ItemMap';
diff --git a/libs/extension/src/components/VList/index.ts b/libs/extension/src/components/VList/index.ts
index d0154206..f30db06a 100644
--- a/libs/extension/src/components/VList/index.ts
+++ b/libs/extension/src/components/VList/index.ts
@@ -1 +1,2 @@
-export { VList, renderInfoType } from './VList';
+export { VList } from './VList';
+export type { renderInfoType } from './VList';
diff --git a/libs/extension/src/devtools/mock.ts b/libs/extension/src/devtools/mock.ts
index 3a3eef88..1c7447dc 100644
--- a/libs/extension/src/devtools/mock.ts
+++ b/libs/extension/src/devtools/mock.ts
@@ -5,8 +5,9 @@
import { parseAttr } from '../parser/parseAttr';
import parseTreeRoot from '../parser/parseVNode';
-import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
-import { FunctionComponent, ClassComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
+import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
+import { FunctionComponent, ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
+import { helper } from '../../../horizon/src/external/devtools';
const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi', 'Coconut'];
@@ -50,7 +51,7 @@ addOneThousandNode(tree);
/**
* 将mock数据转变为 VNode 树
- *
+ *
* @param node 树节点
* @param vNode VNode节点
*/
@@ -78,7 +79,7 @@ function getMockVNodeTree(node: IMockTree, vNode: VNode) {
const rootVNode = MockVNode(tree.tag);
getMockVNodeTree(tree, rootVNode);
-export const mockParsedVNodeData = parseTreeRoot(rootVNode);
+export const mockParsedVNodeData = parseTreeRoot(helper.travelVNodeTree, rootVNode);
const mockState = {
str: 'jenny',
diff --git a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
index 41437e38..9495cd60 100644
--- a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
+++ b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
@@ -17,6 +17,7 @@ function reducer(state, action) {
export default function MockFunctionComponent(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const [age, setAge] = useState(0);
+ const [name, setName] = useState({test: 1});
const domRef = useRef();
const objRef = useRef({ str: 'string' });
const context = useContext(MockContext);
@@ -26,6 +27,7 @@ export default function MockFunctionComponent(props) {
return (
age: {age}
+ name: {name.test}
count: {props.count}
@@ -33,4 +35,4 @@ export default function MockFunctionComponent(props) {
{context.ctx}
);
-}
\ No newline at end of file
+}
diff --git a/libs/extension/src/devtools/mockPage/index.tsx b/libs/extension/src/devtools/mockPage/index.tsx
index 57c96b44..f59908ff 100644
--- a/libs/extension/src/devtools/mockPage/index.tsx
+++ b/libs/extension/src/devtools/mockPage/index.tsx
@@ -1,4 +1,4 @@
-import { render } from 'horizon';
+import { render, useState } from 'horizon';
import MockClassComponent from './MockClassComponent';
import MockFunctionComponent from './MockFunctionComponent';
import { MockContext } from './MockContext';
@@ -6,11 +6,13 @@ import { MockContext } from './MockContext';
const root = document.createElement('div');
document.body.append(root);
function App() {
+ const [count, setCount] = useState(12);
return (
+
-
+
abc
diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts
index e0372d10..940f0ab9 100644
--- a/libs/extension/src/injector/index.ts
+++ b/libs/extension/src/injector/index.ts
@@ -1,17 +1,21 @@
import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
-import { packagePayload, checkMessage } from './../utils/transferTool';
+import { packagePayload, checkMessage } from '../utils/transferTool';
import {
RequestAllVNodeTreeInfos,
AllVNodeTreesInfos,
RequestComponentAttrs,
ComponentAttrs,
DevToolHook,
- DevToolContentScript
-} from './../utils/constants';
-import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
-import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
-import { parseAttr, parseHooks } from '../parser/parseAttr';
-import { FunctionComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
+ DevToolContentScript,
+ ModifyAttrs,
+ ModifyHooks,
+ ModifyState,
+ ModifyProps,
+ InspectDom,
+ LogComponentData
+} from '../utils/constants';
+import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
+import { parseVNodeAttrs } from '../parser/parseAttr';
const roots = [];
@@ -23,7 +27,7 @@ function addIfNotInclude(treeRoot: VNode) {
function send() {
const result = roots.reduce((pre, current) => {
- const info = parseTreeRoot(current);
+ const info = parseTreeRoot(helper.travelVNodeTree ,current);
pre.push(info);
return pre;
}, []);
@@ -47,25 +51,89 @@ function postMessage(type: string, data) {
}
function parseCompAttrs(id: number) {
- const vNode: VNode = queryVNode(id);
- const tag = vNode.tag;
- if (tag === ClassComponent) {
- const { props, state } = vNode;
- const parsedProps = parseAttr(props);
- const parsedState = parseAttr(state);
- postMessage(ComponentAttrs, {
- parsedProps,
- parsedState,
- });
- } else if (tag === FunctionComponent) {
- const { props, hooks } = vNode;
- const parsedProps = parseAttr(props);
- const parsedHooks = parseHooks(hooks);
- postMessage(ComponentAttrs, {
- parsedProps,
- parsedHooks,
- });
+ const vNode = queryVNode(id);
+ if (!vNode) {
+ console.error('Do not find match vNode, this is a bug, please report us');
+ return;
}
+ const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo);
+ postMessage(ComponentAttrs, parsedAttrs);
+}
+
+function calculateNextValue(editValue, value, attrPath) {
+ let nextState;
+ const editValueType = typeof editValue;
+ if (editValueType === 'string' || editValueType === 'undefined' || editValueType === 'boolean') {
+ nextState = value;
+ } else if (editValueType === 'number') {
+ const numValue = Number(value);
+ nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字,用原值
+ } else if(editValueType === 'object') {
+ if (editValue === null) {
+ nextState = value;
+ } else {
+ const newValue = Array.isArray(editValue) ? [...editValue] : {...editValue};
+ // 遍历读取到直接指向需要修改值的对象
+ let attr = newValue;
+ for(let i = 0; i < attrPath.length - 1; i++) {
+ attr = attr[attrPath[i]];
+ }
+ // 修改对象上的值
+ attr[attrPath[attrPath.length - 1]] = value;
+ nextState = newValue;
+ }
+ } else {
+ console.error('The devTool tried to edit a non-editable value, this is a bug, please report', editValue);
+ }
+ return nextState;
+}
+
+function modifyVNodeAttrs(data) {
+ const {type, id, value, path} = data;
+ const vNode = queryVNode(id);
+ if (!vNode) {
+ console.error('Do not find match vNode, this is a bug, please report us');
+ return;
+ }
+ if (type === ModifyProps) {
+ const nextProps = calculateNextValue(vNode.props, value, path);
+ helper.updateProps(vNode, nextProps);
+ } else if (type === ModifyHooks) {
+ const hooks = vNode.hooks;
+ const editHook = hooks[path[0]];
+ const hookInfo = helper.getHookInfo(editHook);
+ if (hookInfo) {
+ const editValue = hookInfo.value;
+ // path 的第一个值指向 hIndex,从第二个值才开始指向具体属性访问路径
+ const nextState = calculateNextValue(editValue, value, path.slice(1));
+ helper.updateHooks(vNode, path[0], nextState);
+ } else {
+ console.error('The devTool tried to edit a non-editable hook, this is a bug, please report', hooks);
+ }
+ } else if (type === ModifyState) {
+ const oldState = vNode.state || {};
+ const nextState = {...oldState};
+ let accessRef = nextState;
+ for(let i = 0; i < path.length - 1; i++) {
+ accessRef = accessRef[path[i]];
+ }
+ accessRef[path[path.length - 1]] = value;
+ helper.updateState(vNode, nextState);
+ }
+}
+
+function logComponentData(id: number) {
+ const vNode = queryVNode(id);
+ if (vNode) {
+ const info = helper.getComponentInfo(vNode);
+ console.log('Component Info: ', info);
+ }
+}
+
+let helper;
+
+function init(horizonHelper) {
+ helper = horizonHelper;
}
function injectHook() {
@@ -75,6 +143,7 @@ function injectHook() {
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
enumerable: false,
value: {
+ init,
addIfNotInclude,
send,
deleteVNode,
@@ -93,6 +162,14 @@ function injectHook() {
send();
} else if (type === RequestComponentAttrs) {
parseCompAttrs(data);
+ } else if (type === ModifyAttrs) {
+ modifyVNodeAttrs(data);
+ } else if (type === InspectDom) {
+ console.log(data);
+ } else if (type === LogComponentData) {
+ logComponentData(data);
+ } else {
+ console.warn('unknown command', type);
}
}
});
diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx
index af6e5d9a..22994dcc 100644
--- a/libs/extension/src/panel/App.tsx
+++ b/libs/extension/src/panel/App.tsx
@@ -9,15 +9,21 @@ import { FilterTree } from '../hooks/FilterTree';
import Close from '../svgs/Close';
import Arrow from './../svgs/Arrow';
import {
- InitDevToolPageConnection,
AllVNodeTreesInfos,
RequestComponentAttrs,
ComponentAttrs,
- DevToolPanel,
-} from './../utils/constants';
-import { packagePayload } from './../utils/transferTool';
+} from '../utils/constants';
+import {
+ addBackgroundMessageListener,
+ initBackgroundConnection,
+ postMessageToBackground, removeBackgroundMessageListener,
+} from '../panelConnection';
+import { IAttr } from '../parser/parseAttr';
+import { createLogger } from '../utils/logUtil';
-const parseVNodeData = (rawData) => {
+const logger = createLogger('panelApp');
+
+const parseVNodeData = (rawData, idToTreeNodeMap , nextIdToTreeNodeMap) => {
const idIndentationMap: {
[id: string]: number;
} = {};
@@ -34,10 +40,23 @@ const parseVNodeData = (rawData) => {
i++;
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
idIndentationMap[id] = indentation;
- const item = {
- id, name, indentation, userKey
- };
- data.push(item);
+ const lastItem = idToTreeNodeMap[id];
+ if (lastItem) {
+ // 由于 diff 算法限制,一个 vNode 的 name,userKey,indentation 属性不会发生变化
+ // 但是在跳转到新页面时,id 值重置,此时原有 id 对应的节点都发生了变化,需要更新
+ // 为了让架构尽可能简单,我们不区分是否是页面跳转,所以每次都需要重新赋值
+ nextIdToTreeNodeMap[id] = lastItem;
+ lastItem.name = name;
+ lastItem.indentation = indentation;
+ lastItem.userKey = userKey;
+ data.push(lastItem);
+ } else {
+ const item = {
+ id, name, indentation, userKey
+ };
+ nextIdToTreeNodeMap[id] = item;
+ data.push(item);
+ }
}
return data;
};
@@ -59,47 +78,19 @@ const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
return parents;
};
-let connection;
-if (!isDev) {
- // 与 background 的唯一连接
- connection = chrome.runtime.connect({
- name: 'panel'
- });
-}
-
-let reconnectTimes = 0;
-
-function postMessage(type: string, data: any) {
- try {
- connection.postMessage(packagePayload({
- type: type,
- data: data,
- }, DevToolPanel));
- } catch(err) {
- // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
- if (reconnectTimes === 20) {
- reconnectTimes = 0;
- console.error('reconnect failed');
- return;
- }
- console.error(err);
- reconnectTimes++;
- // 重建连接
- connection = chrome.runtime.connect({
- name: 'panel'
- });
- // 重新发送初始化消息
- postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
- // 初始化成功后才会重新发送消息
- postMessage(type, data);
- }
+interface IIdToNodeMap {
+ [id: number]: IData;
}
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
- const [componentAttrs, setComponentAttrs] = useState({});
+ const [componentAttrs, setComponentAttrs] = useState<{
+ parsedProps?: IAttr[],
+ parsedState?: IAttr[],
+ parsedHooks?: IAttr[],
+ }>({});
const [selectComp, setSelectComp] = useState(null);
- const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度,
+ const idToTreeNodeMapRef = useRef({});
const {
filterValue,
@@ -116,42 +107,47 @@ function App() {
useEffect(() => {
if (isDev) {
- const parsedData = parseVNodeData(mockParsedVNodeData);
+ const nextIdToTreeNodeMap: IIdToNodeMap = {};
+ const parsedData = parseVNodeData(mockParsedVNodeData, idToTreeNodeMapRef.current, nextIdToTreeNodeMap);
+ idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(parsedData);
setComponentAttrs({
- state: parsedMockState,
- props: parsedMockState,
+ parsedProps: parsedMockState,
+ parsedState: parsedMockState,
});
} else {
- // 页面打开后发送初始化请求
- postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
- // 监听 background消息
- connection.onMessage.addListener(function (message) {
+ const handleBackgroundMessage = (message) => {
const { payload } = message;
+ // 对象数据只是记录了引用,内容可能在后续被修改,打印字符串可以获取当前真正内容,不被后续修改影响
+ logger.info(JSON.stringify(payload));
if (payload) {
const { type, data } = payload;
if (type === AllVNodeTreesInfos) {
+ const idToTreeNodeMap = idToTreeNodeMapRef.current;
+ const nextIdToTreeNodeMap: IIdToNodeMap = {};
const allTreeData = data.reduce((pre, current) => {
- const parsedTreeData = parseVNodeData(current);
- const length = parsedTreeData.length;
- treeRootInfos.current.length = 0;
- if (length) {
- const treeRoot = parsedTreeData[0];
- treeRootInfos.current.push({id: treeRoot.id, length: length});
- }
+ const parsedTreeData = parseVNodeData(current, idToTreeNodeMap, nextIdToTreeNodeMap);
return pre.concat(parsedTreeData);
}, []);
+ idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(allTreeData);
} else if (type === ComponentAttrs) {
const {parsedProps, parsedState, parsedHooks} = data;
setComponentAttrs({
- props: parsedProps,
- state: parsedState,
- hooks: parsedHooks,
+ parsedProps,
+ parsedState,
+ parsedHooks,
});
}
}
- });
+ };
+ // 在页面渲染后初始化连接
+ initBackgroundConnection();
+ // 监听 background消息
+ addBackgroundMessageListener(handleBackgroundMessage);
+ return () => {
+ removeBackgroundMessageListener(handleBackgroundMessage);
+ };
}
}, []);
@@ -162,11 +158,11 @@ function App() {
const handleSelectComp = (item: IData) => {
if (isDev) {
setComponentAttrs({
- state: parsedMockState,
- props: parsedMockState,
+ parsedProps: parsedMockState,
+ parsedState: parsedMockState,
});
} else {
- postMessage(RequestComponentAttrs, item.id);
+ postMessageToBackground(RequestComponentAttrs, item.id);
}
setSelectComp(item);
};
@@ -216,6 +212,7 @@ function App() {
name={selectComp ? selectComp.name : null}
attrs={selectComp ? componentAttrs : {}}
parents={parents}
+ id={selectComp ? selectComp.id : null}
onClickParent={handleClickParent} />
diff --git a/libs/extension/src/panelConnection/PanelConnection.ts b/libs/extension/src/panelConnection/PanelConnection.ts
new file mode 100644
index 00000000..27bcc4ac
--- /dev/null
+++ b/libs/extension/src/panelConnection/PanelConnection.ts
@@ -0,0 +1,61 @@
+import { packagePayload } from '../utils/transferTool';
+import { DevToolPanel, InitDevToolPageConnection } from '../utils/constants';
+
+let connection;
+const callbacks = [];
+
+export function addBackgroundMessageListener(fun: (message) => void) {
+ callbacks.push(fun);
+}
+
+export function removeBackgroundMessageListener(fun: (message) => void) {
+ const index = callbacks.indexOf(fun);
+ if (index !== -1) {
+ callbacks.splice(index, 1);
+ }
+}
+
+export function initBackgroundConnection() {
+ console.log(!isDev);
+ if (!isDev) {
+ try {
+ connection = chrome.runtime.connect({ name: 'panel' });
+ const notice = message => {
+ callbacks.forEach(fun => {
+ fun(message);
+ });
+ };
+ // TODO: 我们需要删除 notice 吗?如果需要,在什么时候删除
+ // 监听 background 消息
+ connection.onMessage.addListener(notice);
+ // 页面打开后发送初始化请求
+ postMessageToBackground(InitDevToolPageConnection);
+ } catch (e) {
+ console.error('create connection failed');
+ console.error(e);
+ }
+ }
+}
+
+let reconnectTimes = 0;
+export function postMessageToBackground(type: string, data?: any) {
+ try{
+ const payLoad = data
+ ? { type, tabId: chrome.devtools.inspectedWindow.tabId, data }
+ : { type, tabId: chrome.devtools.inspectedWindow.tabId };
+ connection.postMessage(packagePayload(payLoad, DevToolPanel));
+ } catch(err) {
+ // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
+ if (reconnectTimes === 20) {
+ reconnectTimes = 0;
+ console.error('reconnect failed');
+ return;
+ }
+ console.error(err);
+ reconnectTimes++;
+ // 重建连接
+ initBackgroundConnection();
+ // 初始化成功后才会重新发送消息
+ postMessageToBackground(type, data);
+ }
+}
diff --git a/libs/extension/src/panelConnection/index.ts b/libs/extension/src/panelConnection/index.ts
new file mode 100644
index 00000000..b2d3e64d
--- /dev/null
+++ b/libs/extension/src/panelConnection/index.ts
@@ -0,0 +1 @@
+export * from './PanelConnection';
diff --git a/libs/extension/src/parser/parseAttr.ts b/libs/extension/src/parser/parseAttr.ts
index e18f9bad..0719771d 100644
--- a/libs/extension/src/parser/parseAttr.ts
+++ b/libs/extension/src/parser/parseAttr.ts
@@ -1,5 +1,8 @@
-import { Hook, Reducer, Ref } from './../../../horizon/src/renderer/hooks/HookType';
+import { Hook } from '../../../horizon/src/renderer/hooks/HookType';
+import { ModifyHooks, ModifyProps, ModifyState } from '../utils/constants';
+import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
+import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
// 展示值为 string 的可编辑类型
type editableStringType = 'string' | 'number' | 'undefined' | 'null';
@@ -12,7 +15,7 @@ type showAsStringType = editableStringType | unEditableStringType;
export type IAttr = {
- name: string;
+ name: string | number;
indentation: number;
hIndex?: number; // 用于记录 hook 的 hIndex 值
} & ({
@@ -96,7 +99,7 @@ const parseSubAttr = (
value,
indentation: parentIndentation + 1,
};
- if (hIndex) {
+ if (hIndex !== undefined) {
item.hIndex = hIndex;
}
result.push(item);
@@ -116,18 +119,81 @@ export function parseAttr(rootAttr: any) {
return result;
}
-export function parseHooks(hooks: Hook
[]) {
+export function parseHooks(hooks: Hook[], getHookInfo) {
const result: IAttr[] = [];
const indentation = 0;
hooks.forEach(hook => {
- const { hIndex, state ,type } = hook;
- if (type === 'useState') {
- parseSubAttr((state as Reducer).stateValue, indentation, 'state', result, hIndex);
- } else if (type === 'useRef') {
- parseSubAttr((state as Ref).current, indentation, 'ref', result, hIndex);
- } else if (type === 'useReducer') {
- parseSubAttr((state as Reducer).stateValue, indentation, 'reducer', result, hIndex);
+ const hookInfo = getHookInfo(hook);
+ if (hookInfo) {
+ const {name, hIndex, value} = hookInfo;
+ parseSubAttr(value, indentation, name, result, hIndex);
}
});
return result;
}
+
+export function parseVNodeAttrs(vNode: VNode, getHookInfo) {
+ const tag = vNode.tag;
+ if (tag === ClassComponent) {
+ const { props, state } = vNode;
+ const parsedProps = parseAttr(props);
+ const parsedState = parseAttr(state);
+ return {
+ parsedProps,
+ parsedState,
+ };
+ } else if (tag === FunctionComponent) {
+ const { props, hooks } = vNode;
+ const parsedProps = parseAttr(props);
+ const parsedHooks = parseHooks(hooks, getHookInfo);
+ return {
+ parsedProps,
+ parsedHooks,
+ };
+ }
+}
+
+// 计算属性的访问顺序
+function calculateAttrAccessPath(item: IAttr, index: number, attrs: IAttr[], isHook: boolean) {
+ let currentIndentation = item.indentation;
+ const path = [item.name];
+ let hookRootItem: IAttr = item;
+ for(let i = index - 1; i >= 0; i--) {
+ const lastItem = attrs[i];
+ const lastIndentation = lastItem.indentation;
+ if (lastIndentation < currentIndentation) {
+ hookRootItem = lastItem;
+ path.push(lastItem.name);
+ currentIndentation = lastIndentation;
+ }
+ }
+ path.reverse();
+ if (isHook) {
+ if (hookRootItem) {
+ path[0] = hookRootItem.hIndex;
+ } else {
+ console.error('There is a bug, please report');
+ }
+ }
+ return path;
+}
+
+export function buildAttrModifyData(parsedAttrsType: string, attrs: IAttr[], value, item: IAttr, index: number, id: number) {
+ let type;
+ if (parsedAttrsType === 'parsedProps') {
+ type = ModifyProps;
+ } else if (parsedAttrsType === 'parsedState') {
+ type = ModifyState;
+ } else if (parsedAttrsType === 'parsedHooks') {
+ type = ModifyHooks;
+ } else {
+ return null;
+ }
+ const path = calculateAttrAccessPath(item, index, attrs, parsedAttrsType === 'parsedHooks');
+ return {
+ id: id,
+ type: type,
+ value: value,
+ path: path,
+ };
+}
diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts
index fbf4b0bd..a70e7189 100644
--- a/libs/extension/src/parser/parseVNode.ts
+++ b/libs/extension/src/parser/parseVNode.ts
@@ -1,13 +1,16 @@
-import { travelVNodeTree } from '../../../../libs/horizon/src/renderer/vnode/VNodeUtils';
-import { VNode } from '../../../../libs/horizon/src/renderer/Types';
-import { ClassComponent, FunctionComponent } from '../../../../libs/horizon/src/renderer/vnode/VNodeTags';
+import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
+import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
const VNodeToIdMap = new Map();
const IdToVNodeMap = new Map();
let uid = 0;
-function generateUid () {
+function generateUid (vNode: VNode) {
+ const id = VNodeToIdMap.get(vNode);
+ if (id !== undefined) {
+ return id;
+ }
uid++;
return uid;
}
@@ -28,12 +31,12 @@ function getParentUserComponent(node: VNode) {
return parent;
}
-function parseTreeRoot(treeRoot: VNode) {
+function parseTreeRoot(travelVNodeTree, treeRoot: VNode) {
const result: any[] = [];
travelVNodeTree(treeRoot, (node: VNode) => {
const tag = node.tag;
if (isUserComponent(tag)) {
- const id = generateUid();
+ const id = generateUid(node);
result.push(id);
const name = node.type.name;
result.push(name);
@@ -53,11 +56,11 @@ function parseTreeRoot(treeRoot: VNode) {
VNodeToIdMap.set(node, id);
IdToVNodeMap.set(id, node);
}
- }, null, treeRoot, null);
+ });
return result;
}
-export function queryVNode(id: number) {
+export function queryVNode(id: number): VNode|undefined {
return IdToVNodeMap.get(id);
}
diff --git a/libs/extension/src/svgs/Copy.tsx b/libs/extension/src/svgs/Copy.tsx
deleted file mode 100644
index 483d9ece..00000000
--- a/libs/extension/src/svgs/Copy.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-
-export default function Copy() {
- return (
-
- );
-}
diff --git a/libs/extension/src/utils/constants.ts b/libs/extension/src/utils/constants.ts
index e779a143..e448bc2b 100644
--- a/libs/extension/src/utils/constants.ts
+++ b/libs/extension/src/utils/constants.ts
@@ -11,6 +11,19 @@ export const RequestComponentAttrs = 'get component attrs';
// 返回组件属性
export const ComponentAttrs = 'component attrs';
+export const ModifyAttrs = 'modify attrs';
+
+export const ModifyProps = 'modify props';
+
+export const ModifyState = 'modify state';
+
+export const ModifyHooks = 'modify hooks';
+
+export const InspectDom = 'inspect component dom';
+
+export const LogComponentData = 'log component data';
+
+export const CopyComponentAttr = 'copy component attr';
// 传递消息来源标志
export const DevToolPanel = 'dev tool panel';
@@ -19,4 +32,4 @@ export const DevToolBackground = 'dev tool background';
export const DevToolContentScript = 'dev tool content script';
-export const DevToolHook = 'dev tool hook';
\ No newline at end of file
+export const DevToolHook = 'dev tool hook';
diff --git a/libs/extension/src/utils/logUtil.ts b/libs/extension/src/utils/logUtil.ts
new file mode 100644
index 00000000..d57e8bfe
--- /dev/null
+++ b/libs/extension/src/utils/logUtil.ts
@@ -0,0 +1,19 @@
+// chrome 通过 iframe 的方式将 panel 页面嵌入到开发者工具中,如果有报错是无法感知到的
+// 同时也无法在运行时打断点,需要适当的日志辅助开发和问题定位
+
+interface loggerType {
+ error: typeof console.error,
+ info: typeof console.info,
+ log: typeof console.log,
+ warn: typeof console.warn,
+}
+
+export function createLogger(id: string): loggerType {
+ return ['error', 'info', 'log', 'warn'].reduce((pre, current) => {
+ const prefix = `[horizon_dev_tool][${id}] `;
+ pre[current] = (...data) => {
+ console[current](prefix, ...data);
+ };
+ return pre;
+ }, {} as loggerType);
+}
diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts
index cd194973..7004f914 100644
--- a/libs/horizon/index.ts
+++ b/libs/horizon/index.ts
@@ -13,6 +13,7 @@ import { createContext } from './src/renderer/components/context/CreateContext';
import { lazy } from './src/renderer/components/Lazy';
import { forwardRef } from './src/renderer/components/ForwardRef';
import { memo } from './src/renderer/components/Memo';
+import './src/external/devtools';
import {
useCallback,
diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts
new file mode 100644
index 00000000..38167db5
--- /dev/null
+++ b/libs/horizon/src/external/devtools.ts
@@ -0,0 +1,84 @@
+import { travelVNodeTree } from '../renderer/vnode/VNodeUtils';
+import { Hook, Reducer, Ref } from '../renderer/hooks/HookType';
+import { VNode } from '../renderer/vnode/VNode';
+import { launchUpdateFromVNode } from '../renderer/TreeBuilder';
+import { DomComponent } from '../renderer/vnode/VNodeTags';
+
+export const helper = {
+ travelVNodeTree: (rootVNode, fun) => {
+ travelVNodeTree(rootVNode, fun, null, rootVNode, null);
+ },
+ // 获取 hook 名,hIndex值和存储的值
+ // 目前只处理 useState和useRef
+ getHookInfo:(hook: Hook) => {
+ const { hIndex, state } = hook;
+ if ((state as Reducer).trigger) {
+ if ((state as Reducer).isUseState) {
+ return {name: 'state', hIndex, value: (state as Reducer).stateValue};
+ }
+ } else if ((state as Ref).current) {
+ return {name: 'ref', hIndex, value: (state as Ref).current};
+ }
+ return null;
+ },
+ updateProps: (vNode: VNode, props: any) =>{
+ vNode.devProps = props;
+ launchUpdateFromVNode(vNode);
+ },
+ updateState: (vNode: VNode, nextState) => {
+ const instance = vNode.realNode;
+ instance.setState(nextState);
+ },
+ updateHooks: (vNode: VNode, hIndex, nextState) => {
+ const hooks = vNode.hooks;
+ if (hooks) {
+ const editHook = hooks[hIndex];
+ const editState = editHook.state as Reducer;
+ // 暂时只支持更新 useState 的值
+ if (editState.trigger && editState.isUseState) {
+ editState.trigger(nextState);
+ }
+ } else {
+ console.error('Target vNode is not a hook vNode: ', vNode);
+ }
+ },
+ getComponentInfo: (vNode: VNode) => {
+ const { props, state, hooks } = vNode;
+ const info:any = {};
+ if (props && Object.keys(props).length !== 0) {
+ info['Props'] = props;
+ }
+ if (state && Object.keys(state).length !== 0) {
+ info['State'] = state;
+ }
+ if (hooks && hooks.length !== 0) {
+ const logHookInfo: any[] = [];
+ hooks.forEach((hook) =>{
+ const state = hook.state as Reducer;
+ if (state.trigger && state.isUseState) {
+ logHookInfo.push(state.stateValue);
+ }
+ });
+ info['Hooks'] = logHookInfo;
+ }
+ travelVNodeTree(vNode, (node: VNode) => {
+ if (node.tag === DomComponent) {
+ // 找到组件的第一个dom元素,返回它所在父节点的全部子节点
+ const dom = node.realNode;
+ info['Nodes'] = dom?.parentNode?.childNodes;
+ return true;
+ }
+ return false;
+ }, null, vNode, null);
+ return info;
+ },
+};
+
+function injectUpdater() {
+ const hook = window.__HORIZON_DEV_HOOK__;
+ if (hook) {
+ hook.init(helper);
+ }
+}
+
+injectUpdater();
diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts
index 394319e0..8fff0435 100644
--- a/libs/horizon/src/renderer/TreeBuilder.ts
+++ b/libs/horizon/src/renderer/TreeBuilder.ts
@@ -236,7 +236,11 @@ function buildVNodeTree(treeRoot: VNode) {
// 重置环境变量,为重新进行深度遍历做准备
resetProcessingVariables(startVNode);
-
+ // devProps 用于插件手动更新props值
+ if (startVNode.devProps !== undefined) {
+ startVNode.props = startVNode.devProps;
+ startVNode.devProps = undefined;
+ }
while (processing !== null) {
try {
while (processing !== null) {
diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts
index cb8be892..e965fdf1 100644
--- a/libs/horizon/src/renderer/hooks/HookType.ts
+++ b/libs/horizon/src/renderer/hooks/HookType.ts
@@ -3,7 +3,6 @@ import {EffectConstant} from './EffectConstant';
export interface Hook {
state: Reducer | Effect | Memo | CallBack | Ref;
hIndex: number;
- type?: 'useState' | 'useRef' | 'useReducer';
}
export interface Reducer {
diff --git a/libs/horizon/src/renderer/hooks/UseReducerHook.ts b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
index 480f43bb..52399713 100644
--- a/libs/horizon/src/renderer/hooks/UseReducerHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
@@ -87,7 +87,6 @@ export function useReducerForInit(reducer, initArg, init, isUseState?: boo
}
const hook = createHook();
- hook.type = isUseState ? 'useState' : 'useReducer';
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
hook.state = {
stateValue: stateValue,
diff --git a/libs/horizon/src/renderer/hooks/UseRefHook.ts b/libs/horizon/src/renderer/hooks/UseRefHook.ts
index 381ef61e..754a16d2 100644
--- a/libs/horizon/src/renderer/hooks/UseRefHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseRefHook.ts
@@ -12,7 +12,6 @@ export function useRefImpl(value: V): Ref {
if (stage === HookStage.Init) {
hook = createHook();
hook.state = {current: value};
- hook.type = 'useRef';
} else if (stage === HookStage.Update) {
hook = getCurrentHook();
}
diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts
index 29fb008b..812d2b8e 100644
--- a/libs/horizon/src/renderer/vnode/VNode.ts
+++ b/libs/horizon/src/renderer/vnode/VNode.ts
@@ -70,7 +70,7 @@ export class VNode {
oldRef: RefType | ((handle: any) => void) | null = null;
oldChild: VNode | null = null;
promiseResolve: boolean; // suspense的promise是否resolve
-
+ devProps: any; // 用于dev插件临时保存更新props值
suspenseState: SuspenseState;
path = ''; // 保存从根到本节点的路径
diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts
index 2875a125..ef60b566 100644
--- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts
+++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts
@@ -97,6 +97,10 @@ export function clearVNode(vNode: VNode) {
vNode.toUpdateNodes = null;
vNode.belongClassVNode = null;
+ if (window.__HORIZON_DEV_HOOK__) {
+ const hook = window.__HORIZON_DEV_HOOK__;
+ hook.delete(vNode);
+ }
}
// 是dom类型的vNode