Match-id-8268380b87ec03e0a0ee0015af2700f6a8c2b498

This commit is contained in:
* 2022-05-12 10:07:26 +08:00 committed by *
commit a76c48daad
27 changed files with 636 additions and 200 deletions

View File

@ -15,21 +15,21 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/core": "7.12.3", "@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-env": "7.12.1",
"@babel/preset-react": "7.12.1", "@babel/preset-react": "7.12.1",
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "7.16.7",
"@types/jest": "^27.4.1", "@types/jest": "27.4.1",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"css-loader": "^6.7.1", "css-loader": "6.7.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "5.5.0",
"jest": "^27.5.1", "jest": "27.5.1",
"less": "^4.1.2", "less": "4.1.2",
"less-loader": "^10.2.0", "less-loader": "10.2.0",
"style-loader": "^3.3.1", "style-loader": "3.3.1",
"ts-jest": "^27.1.4", "ts-jest": "27.1.4",
"webpack": "^5.70.0", "webpack": "5.70.0",
"webpack-cli": "^4.9.2", "webpack-cli": "4.9.2",
"webpack-dev-server": "^4.7.4" "webpack-dev-server": "^4.7.4"
} }
} }

View File

@ -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主页面 devtools_page: devtool主页面
default_popup: 拓展图标点击时弹窗页面 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注入全局变量 通过content_scripts在document初始化时给页面添加script脚本在新添加的脚本中给window注入全局变量
## horizon页面判断
在页面完成渲染后往全局变量中添加信息,并传递 tabId 给 background 告知这是 horizon 页面
## 通信方式: ## 通信方式:
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@ -47,22 +101,37 @@ sequenceDiagram
``` ```
## 传输数据结构 ## 传输数据结构
**<font color=#8B0000>限制chrome.runtime.sendMessage只能传递 JSON-serializable 数据</font>**
```ts ```ts
type passData = { type passData = {
type: 'HORIZON_DEV_TOOLS', type: 'HORIZON_DEV_TOOLS',
payload: { payload: {
type: string, type: string,
data: any, data: any,
} },
from: string,
} }
``` ```
## horizon和devTools的主要交互 ## horizon和devTools的主要交互
- 页面初始渲染 - App初始渲染
- 页面更新 - App更新
- 页面销毁 - App销毁
- 整个页面刷新
- devTools触发组件属性更新 - devTools触发组件属性更新
## 对组件的操作
我们希望插件和Horizon能够尽量解耦所以Horizon提供了Helper注入给插件提供相关方法操作组件。
## 触发组件更新方式
- 类组件的state调用实例的 setState 函数触发更新
- 类组件的props浅复制props后更新props值并调用 forceUpdate 触发更新
- 函数组件的props新增了devProps属性在特定时刻重新给props赋值触发更新
- 函数组件的state调用 useState 函数触发更新
## VNode的清理 ## VNode的清理
全局 hook 中保存了root VNode在解析 VNode 树的时候也会保存 VNode 的引用在清理VNode的时候这些 VNode 的引用也需要删除。 全局 hook 中保存了root VNode在解析 VNode 树的时候也会保存 VNode 的引用在清理VNode的时候这些 VNode 的引用也需要删除。
@ -73,7 +142,7 @@ type passData = {
- 通过解析 path 值可以分析出组件树的结构 - 通过解析 path 值可以分析出组件树的结构
## 组件props/state/hook等数据的传输和解析 ## 组件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 ## 滚动动态渲染 Tree

View File

@ -1,6 +1,6 @@
import { checkMessage, packagePayload, changeSource } from '../utils/transferTool'; import { checkMessage, packagePayload, changeSource } from '../utils/transferTool';
import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants'; import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants';
import { DevToolPanel, DevToolContentScript } from './../utils/constants'; import { DevToolPanel, DevToolContentScript } from '../utils/constants';
// 多个页面、tab页共享一个 background需要建立连接池给每个tab建立连接 // 多个页面、tab页共享一个 background需要建立连接池给每个tab建立连接
const connections = {}; const connections = {};
@ -11,29 +11,18 @@ chrome.runtime.onConnect.addListener(function (port) {
const isHorizonMessage = checkMessage(message, DevToolPanel); const isHorizonMessage = checkMessage(message, DevToolPanel);
if (isHorizonMessage) { if (isHorizonMessage) {
const { payload } = message; const { payload } = message;
const { type, data } = payload; // tabId 值指当前浏览器分配给 web_page 的 id 值。是panel页面查询得到指定向该页面发送消息
const { type, data, tabId } = payload;
let passMessage; let passMessage;
if (type === InitDevToolPageConnection) { if (type === InitDevToolPageConnection) {
if (!connections[data]) { // 记录 panel 所在 tab 页的tabId如果已经记录了覆盖原有port因为原有port可能关闭了
// 获取 panel 所在 tab 页的tabId // 可能这次是 panel 发起的重新建立请求
connections[data] = port; connections[tabId] = port;
}
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground); passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
} else { } else {
passMessage = message; passMessage = packagePayload({type, data}, DevToolBackground);
changeSource(passMessage, DevToolBackground);
} }
// 查询参数有 active 和 currentWindow 如果开发者工具与页面分离会导致currentWindow为false才能找到 chrome.tabs.sendMessage(tabId, passMessage);
// 所以只用 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');
}
});
} }
} }
// Listen to messages sent from the DevTools page // 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 // Messages from content scripts should have sender.tab set
if (sender.tab) { if (sender.tab) {
const tabId = sender.tab.id; const tabId = sender.tab.id;
// 和 InitDevToolPageConnection 时得到的 tabId 值一致时,向指定的 panel 页面 port 发送消息
if (tabId in connections && checkMessage(message, DevToolContentScript)) { if (tabId in connections && checkMessage(message, DevToolContentScript)) {
changeSource(message, DevToolBackground); changeSource(message, DevToolBackground);
connections[tabId].postMessage(message); connections[tabId].postMessage(message);

View File

@ -1,21 +1,22 @@
import styles from './ComponentsInfo.less'; import styles from './ComponentsInfo.less';
import Eye from '../svgs/Eye'; import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug'; import Debug from '../svgs/Debug';
import Copy from '../svgs/Copy';
import Triangle from '../svgs/Triangle'; import Triangle from '../svgs/Triangle';
import { useState, useEffect } from 'horizon'; import { useState, useEffect } from 'horizon';
import { IData } from './VTree'; 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 = { type IComponentInfo = {
name: string; name: string;
attrs: { attrs: {
props?: IAttr[]; parsedProps?: IAttr[],
context?: IAttr[]; parsedState?: IAttr[],
state?: IAttr[]; parsedHooks?: IAttr[],
hooks?: IAttr[];
}; };
parents: IData[]; parents: IData[];
id: number;
onClickParent: (item: IData) => void; 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 [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
const [editableAttrs, setEditableAttrs] = useState(attrs);
useEffect(() => { useEffect(() => {
setCollapsedNode(collapseAllNodes(attrs)); setCollapsedNode(collapseAllNodes(attrs));
setEditableAttrs(attrs);
}, [attrs]); }, [attrs]);
const handleCollapse = (item: IAttr) => { const handleCollapse = (item: IAttr) => {
const nodes = [...collapsedNode]; const nodes = [...collapsedNode];
const i = nodes.indexOf(item); const i = nodes.indexOf(item);
@ -44,7 +52,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
const showAttr = []; const showAttr = [];
let currentIndentation = null; let currentIndentation = null;
attrs.forEach((item, index) => { editableAttrs.forEach((item, index) => {
const indentation = item.indentation; const indentation = item.indentation;
if (currentIndentation !== null) { if (currentIndentation !== null) {
if (indentation > currentIndentation) { if (indentation > currentIndentation) {
@ -53,17 +61,55 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
currentIndentation = null; currentIndentation = null;
} }
} }
const nextItem = attrs[index + 1]; const nextItem = editableAttrs[index + 1];
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false; const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
const isCollapsed = collapsedNode.includes(item); const isCollapsed = collapsedNode.includes(item);
showAttr.push( showAttr.push(
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(item))}> <div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => handleCollapse(item)}>
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span> <span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
<span className={styles.attrName}>{`${item.name}`}</span> <span className={styles.attrName}>{`${item.name}`}</span>
{' :'} {' :'}
{item.type === 'string' || item.type === 'number' {item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null'
? <input value={item.value} className={styles.attrValue}>{item.value}</input> ? <input
: <span className={styles.attrValue}>{item.value}</span>} value={String(item.value)}
className={styles.attrValue}
onChange={(event) => {
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'
? <input
type={'checkbox'}
checked={item.value}
className={styles.attrValue}
onChange={(event) => {
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);
}
}}/>
: <span className={styles.attrValue}>{item.value}</span>
)}
</div> </div>
); );
if (isCollapsed) { if (isCollapsed) {
@ -74,10 +120,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
return ( return (
<div className={styles.attrContainer}> <div className={styles.attrContainer}>
<div className={styles.attrHead}> <div className={styles.attrHead}>
<span className={styles.attrType}>{name}</span> <span className={styles.attrType}>{attrsName}</span>
<span className={styles.attrCopy}>
<Copy />
</span>
</div> </div>
<div className={styles.attrDetail}> <div className={styles.attrDetail}>
{showAttr} {showAttr}
@ -86,8 +129,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
); );
} }
export default function ComponentInfo({ name, attrs, parents, onClickParent }: IComponentInfo) { export default function ComponentInfo({ name, attrs, parents, id, onClickParent }: IComponentInfo) {
const { state, props, context, hooks } = attrs;
return ( return (
<div className={styles.infoContainer} > <div className={styles.infoContainer} >
<div className={styles.componentInfoHead}> <div className={styles.componentInfoHead}>
@ -95,19 +137,27 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
<span className={styles.name}> <span className={styles.name}>
{name} {name}
</span> </span>
<span className={styles.eye} > <span className={styles.eye} title={'Inspect dom element'} onClick={() => {
postMessageToBackground(InspectDom, id);
}}>
<Eye /> <Eye />
</span> </span>
<span className={styles.debug}> <span className={styles.debug} title={'Log this component data'} onClick={() => {
postMessageToBackground(LogComponentData, id);
}}>
<Debug /> <Debug />
</span> </span>
</>} </>}
</div> </div>
<div className={styles.componentInfoMain}> <div className={styles.componentInfoMain}>
{context && <ComponentAttr name={'context'} attrs={context} />} {Object.keys(attrs).map(attrsType => {
{props && props.length !== 0 && <ComponentAttr name={'props'} attrs={props} />} const parsedAttrs = attrs[attrsType];
{state && state.length !== 0 && <ComponentAttr name={'state'} attrs={state} />} if (parsedAttrs && parsedAttrs.length !== 0) {
{hooks && hooks.length !== 0 && <ComponentAttr name={'hook'} attrs={hooks} />} const attrsName = attrsType.slice(6); // parsedState => State
return <ComponentAttr attrsName={attrsName} attrs={parsedAttrs} id={id} attrsType={attrsType}/>;
}
return null;
})}
<div className={styles.parentsInfo}> <div className={styles.parentsInfo}>
{name && <div> {name && <div>
parents: { parents: {

View File

@ -20,11 +20,13 @@
.eye { .eye {
flex: 0 0 1rem; flex: 0 0 1rem;
padding-right: 1rem; padding-right: 1rem;
cursor: pointer;
} }
.debug { .debug {
flex: 0 0 1rem; flex: 0 0 1rem;
padding-right: 1rem; padding-right: 1rem;
cursor: pointer;
} }
} }
@ -71,7 +73,7 @@
} }
.attrValue { .attrValue {
margin-left: 4px; margin: 0 0 0 4px;
} }
} }
} }

View File

@ -2,7 +2,7 @@
// data 数组更新后不修改滚动位置, // data 数组更新后不修改滚动位置,
// 只有修改scrollToItem才会修改滚动位置 // 只有修改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 styles from './VList.less';
import ItemMap from './ItemMap'; import ItemMap from './ItemMap';

View File

@ -1 +1,2 @@
export { VList, renderInfoType } from './VList'; export { VList } from './VList';
export type { renderInfoType } from './VList';

View File

@ -5,8 +5,9 @@
import { parseAttr } from '../parser/parseAttr'; import { parseAttr } from '../parser/parseAttr';
import parseTreeRoot from '../parser/parseVNode'; import parseTreeRoot from '../parser/parseVNode';
import { VNode } from './../../../horizon/src/renderer/vnode/VNode'; import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { FunctionComponent, ClassComponent } from './../../../horizon/src/renderer/vnode/VNodeTags'; 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']; const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi', 'Coconut'];
@ -78,7 +79,7 @@ function getMockVNodeTree(node: IMockTree, vNode: VNode) {
const rootVNode = MockVNode(tree.tag); const rootVNode = MockVNode(tree.tag);
getMockVNodeTree(tree, rootVNode); getMockVNodeTree(tree, rootVNode);
export const mockParsedVNodeData = parseTreeRoot(rootVNode); export const mockParsedVNodeData = parseTreeRoot(helper.travelVNodeTree, rootVNode);
const mockState = { const mockState = {
str: 'jenny', str: 'jenny',

View File

@ -17,6 +17,7 @@ function reducer(state, action) {
export default function MockFunctionComponent(props) { export default function MockFunctionComponent(props) {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const [age, setAge] = useState(0); const [age, setAge] = useState(0);
const [name, setName] = useState({test: 1});
const domRef = useRef<HTMLDivElement>(); const domRef = useRef<HTMLDivElement>();
const objRef = useRef({ str: 'string' }); const objRef = useRef({ str: 'string' });
const context = useContext(MockContext); const context = useContext(MockContext);
@ -26,6 +27,7 @@ export default function MockFunctionComponent(props) {
return ( return (
<div> <div>
age: {age} age: {age}
name: {name.test}
<button onClick={() => setAge(age + 1)} >update age</button> <button onClick={() => setAge(age + 1)} >update age</button>
count: {props.count} count: {props.count}
<div ref={domRef} /> <div ref={domRef} />

View File

@ -1,4 +1,4 @@
import { render } from 'horizon'; import { render, useState } from 'horizon';
import MockClassComponent from './MockClassComponent'; import MockClassComponent from './MockClassComponent';
import MockFunctionComponent from './MockFunctionComponent'; import MockFunctionComponent from './MockFunctionComponent';
import { MockContext } from './MockContext'; import { MockContext } from './MockContext';
@ -6,11 +6,13 @@ import { MockContext } from './MockContext';
const root = document.createElement('div'); const root = document.createElement('div');
document.body.append(root); document.body.append(root);
function App() { function App() {
const [count, setCount] = useState(12);
return ( return (
<div> <div>
<button onClick={() => (setCount(count + 1))} >add count</button>
<MockContext.Provider value={{ ctx: 'I am ctx' }}> <MockContext.Provider value={{ ctx: 'I am ctx' }}>
<MockClassComponent fruit={'apple'} /> <MockClassComponent fruit={'apple'} />
<MockFunctionComponent /> <MockFunctionComponent count={count}/>
</MockContext.Provider> </MockContext.Provider>
abc abc
</div> </div>

View File

@ -1,17 +1,21 @@
import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode'; import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
import { packagePayload, checkMessage } from './../utils/transferTool'; import { packagePayload, checkMessage } from '../utils/transferTool';
import { import {
RequestAllVNodeTreeInfos, RequestAllVNodeTreeInfos,
AllVNodeTreesInfos, AllVNodeTreesInfos,
RequestComponentAttrs, RequestComponentAttrs,
ComponentAttrs, ComponentAttrs,
DevToolHook, DevToolHook,
DevToolContentScript DevToolContentScript,
} from './../utils/constants'; ModifyAttrs,
import { VNode } from './../../../horizon/src/renderer/vnode/VNode'; ModifyHooks,
import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags'; ModifyState,
import { parseAttr, parseHooks } from '../parser/parseAttr'; ModifyProps,
import { FunctionComponent } from './../../../horizon/src/renderer/vnode/VNodeTags'; InspectDom,
LogComponentData
} from '../utils/constants';
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { parseVNodeAttrs } from '../parser/parseAttr';
const roots = []; const roots = [];
@ -23,7 +27,7 @@ function addIfNotInclude(treeRoot: VNode) {
function send() { function send() {
const result = roots.reduce((pre, current) => { const result = roots.reduce((pre, current) => {
const info = parseTreeRoot(current); const info = parseTreeRoot(helper.travelVNodeTree ,current);
pre.push(info); pre.push(info);
return pre; return pre;
}, []); }, []);
@ -47,25 +51,89 @@ function postMessage(type: string, data) {
} }
function parseCompAttrs(id: number) { function parseCompAttrs(id: number) {
const vNode: VNode = queryVNode(id); const vNode = queryVNode(id);
const tag = vNode.tag; if (!vNode) {
if (tag === ClassComponent) { console.error('Do not find match vNode, this is a bug, please report us');
const { props, state } = vNode; return;
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 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() { function injectHook() {
@ -75,6 +143,7 @@ function injectHook() {
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
enumerable: false, enumerable: false,
value: { value: {
init,
addIfNotInclude, addIfNotInclude,
send, send,
deleteVNode, deleteVNode,
@ -93,6 +162,14 @@ function injectHook() {
send(); send();
} else if (type === RequestComponentAttrs) { } else if (type === RequestComponentAttrs) {
parseCompAttrs(data); 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);
} }
} }
}); });

View File

@ -9,15 +9,21 @@ import { FilterTree } from '../hooks/FilterTree';
import Close from '../svgs/Close'; import Close from '../svgs/Close';
import Arrow from './../svgs/Arrow'; import Arrow from './../svgs/Arrow';
import { import {
InitDevToolPageConnection,
AllVNodeTreesInfos, AllVNodeTreesInfos,
RequestComponentAttrs, RequestComponentAttrs,
ComponentAttrs, ComponentAttrs,
DevToolPanel, } from '../utils/constants';
} from './../utils/constants'; import {
import { packagePayload } from './../utils/transferTool'; 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: { const idIndentationMap: {
[id: string]: number; [id: string]: number;
} = {}; } = {};
@ -34,11 +40,24 @@ const parseVNodeData = (rawData) => {
i++; i++;
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1; const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
idIndentationMap[id] = indentation; idIndentationMap[id] = indentation;
const lastItem = idToTreeNodeMap[id];
if (lastItem) {
// 由于 diff 算法限制,一个 vNode 的 nameuserKeyindentation 属性不会发生变化
// 但是在跳转到新页面时id 值重置,此时原有 id 对应的节点都发生了变化,需要更新
// 为了让架构尽可能简单,我们不区分是否是页面跳转,所以每次都需要重新赋值
nextIdToTreeNodeMap[id] = lastItem;
lastItem.name = name;
lastItem.indentation = indentation;
lastItem.userKey = userKey;
data.push(lastItem);
} else {
const item = { const item = {
id, name, indentation, userKey id, name, indentation, userKey
}; };
nextIdToTreeNodeMap[id] = item;
data.push(item); data.push(item);
} }
}
return data; return data;
}; };
@ -59,47 +78,19 @@ const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
return parents; return parents;
}; };
let connection; interface IIdToNodeMap {
if (!isDev) { [id: number]: IData;
// 与 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);
}
} }
function App() { function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]); const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentAttrs, setComponentAttrs] = useState({}); const [componentAttrs, setComponentAttrs] = useState<{
parsedProps?: IAttr[],
parsedState?: IAttr[],
parsedHooks?: IAttr[],
}>({});
const [selectComp, setSelectComp] = useState(null); const [selectComp, setSelectComp] = useState(null);
const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度, const idToTreeNodeMapRef = useRef<IIdToNodeMap>({});
const { const {
filterValue, filterValue,
@ -116,42 +107,47 @@ function App() {
useEffect(() => { useEffect(() => {
if (isDev) { if (isDev) {
const parsedData = parseVNodeData(mockParsedVNodeData); const nextIdToTreeNodeMap: IIdToNodeMap = {};
const parsedData = parseVNodeData(mockParsedVNodeData, idToTreeNodeMapRef.current, nextIdToTreeNodeMap);
idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(parsedData); setParsedVNodeData(parsedData);
setComponentAttrs({ setComponentAttrs({
state: parsedMockState, parsedProps: parsedMockState,
props: parsedMockState, parsedState: parsedMockState,
}); });
} else { } else {
// 页面打开后发送初始化请求 const handleBackgroundMessage = (message) => {
postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
// 监听 background消息
connection.onMessage.addListener(function (message) {
const { payload } = message; const { payload } = message;
// 对象数据只是记录了引用,内容可能在后续被修改,打印字符串可以获取当前真正内容,不被后续修改影响
logger.info(JSON.stringify(payload));
if (payload) { if (payload) {
const { type, data } = payload; const { type, data } = payload;
if (type === AllVNodeTreesInfos) { if (type === AllVNodeTreesInfos) {
const idToTreeNodeMap = idToTreeNodeMapRef.current;
const nextIdToTreeNodeMap: IIdToNodeMap = {};
const allTreeData = data.reduce((pre, current) => { const allTreeData = data.reduce((pre, current) => {
const parsedTreeData = parseVNodeData(current); const parsedTreeData = parseVNodeData(current, idToTreeNodeMap, nextIdToTreeNodeMap);
const length = parsedTreeData.length;
treeRootInfos.current.length = 0;
if (length) {
const treeRoot = parsedTreeData[0];
treeRootInfos.current.push({id: treeRoot.id, length: length});
}
return pre.concat(parsedTreeData); return pre.concat(parsedTreeData);
}, []); }, []);
idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(allTreeData); setParsedVNodeData(allTreeData);
} else if (type === ComponentAttrs) { } else if (type === ComponentAttrs) {
const {parsedProps, parsedState, parsedHooks} = data; const {parsedProps, parsedState, parsedHooks} = data;
setComponentAttrs({ setComponentAttrs({
props: parsedProps, parsedProps,
state: parsedState, parsedState,
hooks: parsedHooks, parsedHooks,
}); });
} }
} }
}); };
// 在页面渲染后初始化连接
initBackgroundConnection();
// 监听 background消息
addBackgroundMessageListener(handleBackgroundMessage);
return () => {
removeBackgroundMessageListener(handleBackgroundMessage);
};
} }
}, []); }, []);
@ -162,11 +158,11 @@ function App() {
const handleSelectComp = (item: IData) => { const handleSelectComp = (item: IData) => {
if (isDev) { if (isDev) {
setComponentAttrs({ setComponentAttrs({
state: parsedMockState, parsedProps: parsedMockState,
props: parsedMockState, parsedState: parsedMockState,
}); });
} else { } else {
postMessage(RequestComponentAttrs, item.id); postMessageToBackground(RequestComponentAttrs, item.id);
} }
setSelectComp(item); setSelectComp(item);
}; };
@ -216,6 +212,7 @@ function App() {
name={selectComp ? selectComp.name : null} name={selectComp ? selectComp.name : null}
attrs={selectComp ? componentAttrs : {}} attrs={selectComp ? componentAttrs : {}}
parents={parents} parents={parents}
id={selectComp ? selectComp.id : null}
onClickParent={handleClickParent} /> onClickParent={handleClickParent} />
</div> </div>
</div> </div>

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
export * from './PanelConnection';

View File

@ -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 的可编辑类型 // 展示值为 string 的可编辑类型
type editableStringType = 'string' | 'number' | 'undefined' | 'null'; type editableStringType = 'string' | 'number' | 'undefined' | 'null';
@ -12,7 +15,7 @@ type showAsStringType = editableStringType | unEditableStringType;
export type IAttr = { export type IAttr = {
name: string; name: string | number;
indentation: number; indentation: number;
hIndex?: number; // 用于记录 hook 的 hIndex 值 hIndex?: number; // 用于记录 hook 的 hIndex 值
} & ({ } & ({
@ -96,7 +99,7 @@ const parseSubAttr = (
value, value,
indentation: parentIndentation + 1, indentation: parentIndentation + 1,
}; };
if (hIndex) { if (hIndex !== undefined) {
item.hIndex = hIndex; item.hIndex = hIndex;
} }
result.push(item); result.push(item);
@ -116,18 +119,81 @@ export function parseAttr(rootAttr: any) {
return result; return result;
} }
export function parseHooks(hooks: Hook<any, any>[]) { export function parseHooks(hooks: Hook<any, any>[], getHookInfo) {
const result: IAttr[] = []; const result: IAttr[] = [];
const indentation = 0; const indentation = 0;
hooks.forEach(hook => { hooks.forEach(hook => {
const { hIndex, state ,type } = hook; const hookInfo = getHookInfo(hook);
if (type === 'useState') { if (hookInfo) {
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'state', result, hIndex); const {name, hIndex, value} = hookInfo;
} else if (type === 'useRef') { parseSubAttr(value, indentation, name, result, hIndex);
parseSubAttr((state as Ref<any>).current, indentation, 'ref', result, hIndex);
} else if (type === 'useReducer') {
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'reducer', result, hIndex);
} }
}); });
return result; 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,
};
}

View File

@ -1,13 +1,16 @@
import { travelVNodeTree } from '../../../../libs/horizon/src/renderer/vnode/VNodeUtils'; import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { VNode } from '../../../../libs/horizon/src/renderer/Types'; import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
import { ClassComponent, FunctionComponent } from '../../../../libs/horizon/src/renderer/vnode/VNodeTags';
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode // 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
const VNodeToIdMap = new Map<VNode, number>(); const VNodeToIdMap = new Map<VNode, number>();
const IdToVNodeMap = new Map<number, VNode>(); const IdToVNodeMap = new Map<number, VNode>();
let uid = 0; let uid = 0;
function generateUid () { function generateUid (vNode: VNode) {
const id = VNodeToIdMap.get(vNode);
if (id !== undefined) {
return id;
}
uid++; uid++;
return uid; return uid;
} }
@ -28,12 +31,12 @@ function getParentUserComponent(node: VNode) {
return parent; return parent;
} }
function parseTreeRoot(treeRoot: VNode) { function parseTreeRoot(travelVNodeTree, treeRoot: VNode) {
const result: any[] = []; const result: any[] = [];
travelVNodeTree(treeRoot, (node: VNode) => { travelVNodeTree(treeRoot, (node: VNode) => {
const tag = node.tag; const tag = node.tag;
if (isUserComponent(tag)) { if (isUserComponent(tag)) {
const id = generateUid(); const id = generateUid(node);
result.push(id); result.push(id);
const name = node.type.name; const name = node.type.name;
result.push(name); result.push(name);
@ -53,11 +56,11 @@ function parseTreeRoot(treeRoot: VNode) {
VNodeToIdMap.set(node, id); VNodeToIdMap.set(node, id);
IdToVNodeMap.set(id, node); IdToVNodeMap.set(id, node);
} }
}, null, treeRoot, null); });
return result; return result;
} }
export function queryVNode(id: number) { export function queryVNode(id: number): VNode|undefined {
return IdToVNodeMap.get(id); return IdToVNodeMap.get(id);
} }

View File

@ -1,8 +0,0 @@
export default function Copy() {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd' />
</svg>
);
}

View File

@ -11,6 +11,19 @@ export const RequestComponentAttrs = 'get component attrs';
// 返回组件属性 // 返回组件属性
export const ComponentAttrs = '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'; export const DevToolPanel = 'dev tool panel';

View File

@ -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);
}

View File

@ -13,6 +13,7 @@ import { createContext } from './src/renderer/components/context/CreateContext';
import { lazy } from './src/renderer/components/Lazy'; import { lazy } from './src/renderer/components/Lazy';
import { forwardRef } from './src/renderer/components/ForwardRef'; import { forwardRef } from './src/renderer/components/ForwardRef';
import { memo } from './src/renderer/components/Memo'; import { memo } from './src/renderer/components/Memo';
import './src/external/devtools';
import { import {
useCallback, useCallback,

84
libs/horizon/src/external/devtools.ts vendored Normal file
View File

@ -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<any, any>) => {
const { hIndex, state } = hook;
if ((state as Reducer<any, any>).trigger) {
if ((state as Reducer<any, any>).isUseState) {
return {name: 'state', hIndex, value: (state as Reducer<any, any>).stateValue};
}
} else if ((state as Ref<any>).current) {
return {name: 'ref', hIndex, value: (state as Ref<any>).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<any, any>;
// 暂时只支持更新 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<any, any>;
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();

View File

@ -236,7 +236,11 @@ function buildVNodeTree(treeRoot: VNode) {
// 重置环境变量,为重新进行深度遍历做准备 // 重置环境变量,为重新进行深度遍历做准备
resetProcessingVariables(startVNode); resetProcessingVariables(startVNode);
// devProps 用于插件手动更新props值
if (startVNode.devProps !== undefined) {
startVNode.props = startVNode.devProps;
startVNode.devProps = undefined;
}
while (processing !== null) { while (processing !== null) {
try { try {
while (processing !== null) { while (processing !== null) {

View File

@ -3,7 +3,6 @@ import {EffectConstant} from './EffectConstant';
export interface Hook<S, A> { export interface Hook<S, A> {
state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>; state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>;
hIndex: number; hIndex: number;
type?: 'useState' | 'useRef' | 'useReducer';
} }
export interface Reducer<S, A> { export interface Reducer<S, A> {

View File

@ -87,7 +87,6 @@ export function useReducerForInit<S, A>(reducer, initArg, init, isUseState?: boo
} }
const hook = createHook(); const hook = createHook();
hook.type = isUseState ? 'useState' : 'useReducer';
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState} // 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
hook.state = { hook.state = {
stateValue: stateValue, stateValue: stateValue,

View File

@ -12,7 +12,6 @@ export function useRefImpl<V>(value: V): Ref<V> {
if (stage === HookStage.Init) { if (stage === HookStage.Init) {
hook = createHook(); hook = createHook();
hook.state = {current: value}; hook.state = {current: value};
hook.type = 'useRef';
} else if (stage === HookStage.Update) { } else if (stage === HookStage.Update) {
hook = getCurrentHook(); hook = getCurrentHook();
} }

View File

@ -70,7 +70,7 @@ export class VNode {
oldRef: RefType | ((handle: any) => void) | null = null; oldRef: RefType | ((handle: any) => void) | null = null;
oldChild: VNode | null = null; oldChild: VNode | null = null;
promiseResolve: boolean; // suspense的promise是否resolve promiseResolve: boolean; // suspense的promise是否resolve
devProps: any; // 用于dev插件临时保存更新props值
suspenseState: SuspenseState; suspenseState: SuspenseState;
path = ''; // 保存从根到本节点的路径 path = ''; // 保存从根到本节点的路径

View File

@ -97,6 +97,10 @@ export function clearVNode(vNode: VNode) {
vNode.toUpdateNodes = null; vNode.toUpdateNodes = null;
vNode.belongClassVNode = null; vNode.belongClassVNode = null;
if (window.__HORIZON_DEV_HOOK__) {
const hook = window.__HORIZON_DEV_HOOK__;
hook.delete(vNode);
}
} }
// 是dom类型的vNode // 是dom类型的vNode