diff --git a/libs/extension/readme.md b/libs/extension/readme.md
index ee9ec4cd..52e952cc 100644
--- a/libs/extension/readme.md
+++ b/libs/extension/readme.md
@@ -27,25 +27,45 @@ sequenceDiagram
participant panel
Note over web_page: window.postMessage
- web_page ->> script_content : {}
+ web_page ->> script_content : data
Note over script_content: window.addEventListener
Note over script_content: chrome.runtime.sendMessage
- script_content ->> background : {}
+ script_content ->> background : data
Note over background: chrome.runtime.onMessage
Note over background: port.postMessage
- background ->> panel : {}
+ background ->> panel : data
Note over panel: connection.onMessage.addListener
Note over panel: connection.postMessage
- panel ->> background : {}
+ panel ->> background : data
Note over background: port.onMessage.addListener
Note over background: chrome.tabs.sendMessage
- background ->> script_content : {}
+ background ->> script_content : data
Note over script_content: chrome.runtime.onMessage
Note over script_content: window.postMessage
- script_content ->> web_page : {}
+ script_content ->> web_page : data
Note over web_page: window.addEventListener
```
+## 传输数据结构
+```ts
+type passData = {
+ type: 'HORIZON_DEV_TOOLS',
+ payload: {
+ type: string,
+ data: any,
+ }
+}
+```
+
+## horizon和devTools的主要交互
+- 页面初始渲染
+- 页面更新
+- 页面销毁
+- devTools触发组件属性更新
+
+## VNode的清理
+全局 hook 中保存了root VNode,在解析 VNode 树的时候也会保存 VNode 的引用,在清理VNode的时候这些 VNode 的引用也需要删除。
+
## 数据压缩
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
- 相同的组件名可以进行压缩
@@ -59,5 +79,13 @@ sequenceDiagram
## 滚动动态渲染 Tree
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
+## 开发者页面打开场景
+- 先有页面,然后打开开发者工具:工具建立连接,发送通知,页面hook收到后发送VNode树信息给工具页面
+- 已经打开开发者工具,然后打开页面:业务页面渲染完毕,发送VNode树信息给工具页面
+
+## 开发者工具页面响应组件树变更
+组件树变更会带来新旧两个组件树信息数组,新旧数组存在数据一致而引用不一致的情况,而VTree和VList组件中相关信息的计算依赖引用而非数据本身,在收到新的组件树信息后需要对数据本身进行判断,将新数组中的相同数据使用旧对象代替。
+
## 测试框架
jest测试框架不提供浏览器插件的相关 api,我们在封装好相关 api 后需要模拟这些 api 的行为从而展开测试工作。
+
diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts
index 2745c61a..ed9698fb 100644
--- a/libs/extension/src/background/index.ts
+++ b/libs/extension/src/background/index.ts
@@ -1,30 +1,41 @@
+import { checkMessage, packagePayload, changeSource } from '../utils/transferTool';
+import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants';
+import { DevToolPanel, DevToolContentScript } from './../utils/constants';
+
// 多个页面、tab页共享一个 background,需要建立连接池,给每个tab建立连接
const connections = {};
// panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数
chrome.runtime.onConnect.addListener(function (port) {
-
- // The original connection event doesn't include the tab ID of the
- // DevTools page, so we need to send it explicitly.
- function extensionListener(message, sender, sendResponse) {
- // 在backgroundPageConnection创建后会发送初始化请求,这样就可以获取tabId,给连接编号
- if (message.name === 'init') {
- // 获取 panel 所在 tab 页的tabId
- connections[message.tabId] = port;
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
- chrome.tabs.sendMessage(tabs[0].id, {tag: 'init horizon info'}, function(response) {
- console.log(response.farewell);
- });
+ function extensionListener(message) {
+ const isHorizonMessage = checkMessage(message, DevToolPanel);
+ if (isHorizonMessage) {
+ const { payload } = message;
+ const { type, data } = payload;
+ let passMessage;
+ if (type === InitDevToolPageConnection) {
+ if (!connections[data]) {
+ // 获取 panel 所在 tab 页的tabId
+ connections[data] = port;
+ }
+ passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
+ } else {
+ passMessage = message;
+ changeSource(passMessage, 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');
+ }
});
- return;
}
-
- if (message.name === 'update') {
- return;
- }
- // other message handling
}
-
// Listen to messages sent from the DevTools page
port.onMessage.addListener(extensionListener);
@@ -42,17 +53,19 @@ chrome.runtime.onConnect.addListener(function (port) {
});
// 监听来自 content script 的消息,并将消息发送给对应的 devTools page,也就是 panel
-chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
+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;
- if (tabId in connections) {
- connections[tabId].postMessage(request);
+ if (tabId in connections && checkMessage(message, DevToolContentScript)) {
+ changeSource(message, DevToolBackground);
+ connections[tabId].postMessage(message);
} else {
console.log('Tab not found in connection list.');
}
} else {
console.log('sender.tab not defined.');
}
- return true;
+ // 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received
+ sendResponse({status: 'ok'});
});
diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx
index 7d78c814..f5ff0051 100644
--- a/libs/extension/src/components/ComponentInfo.tsx
+++ b/libs/extension/src/components/ComponentInfo.tsx
@@ -3,8 +3,9 @@ import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
import Copy from '../svgs/Copy';
import Triangle from '../svgs/Triangle';
-import { useState } from 'horizon';
+import { useState, useEffect } from 'horizon';
import { IData } from './VTree';
+import { IAttr } from '../parser/parseAttr';
type IComponentInfo = {
name: string;
@@ -18,13 +19,6 @@ type IComponentInfo = {
onClickParent: (item: IData) => void;
};
-export type IAttr = {
- name: string;
- type: string;
- value: string | boolean;
- indentation: number;
-}
-
function collapseAllNodes(attrs: IAttr[]) {
return attrs.filter((item, index) => {
const nextItem = attrs[index + 1];
@@ -34,6 +28,9 @@ function collapseAllNodes(attrs: IAttr[]) {
function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
+ useEffect(() => {
+ setCollapsedNode(collapseAllNodes(attrs));
+ }, [attrs]);
const handleCollapse = (item: IAttr) => {
const nodes = [...collapsedNode];
const i = nodes.indexOf(item);
@@ -64,7 +61,9 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
{hasChild && }
{`${item.name}`}
{' :'}
- {item.value}
+ {item.type === 'string' || item.type === 'number'
+ ? {item.value}
+ : {item.value}}
);
if (isCollapsed) {
@@ -106,9 +105,9 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
{context &&
}
- {props &&
}
- {state &&
}
- {hooks &&
}
+ {props && props.length !== 0 &&
}
+ {state && state.length !== 0 &&
}
+ {hooks && hooks.length !== 0 &&
}
{name &&
parents: {
diff --git a/libs/extension/src/components/VList.tsx b/libs/extension/src/components/VList.tsx
index aeee688a..73036fae 100644
--- a/libs/extension/src/components/VList.tsx
+++ b/libs/extension/src/components/VList.tsx
@@ -4,7 +4,7 @@
import { useState, useRef, useEffect } from 'horizon';
import styles from './VList.less';
-interface IProps
{
+interface IProps {
data: T[],
width: number, // 暂时未用到,当需要支持横向滚动时使用
height: number, // VList 的高度
@@ -20,7 +20,7 @@ export type renderInfoType = {
skipItemCountBeforeScrollItem: number,
};
-export function VList(props: IProps) {
+export function VList(props: IProps) {
const {
data,
height,
diff --git a/libs/extension/src/components/VTree.less b/libs/extension/src/components/VTree.less
index 0f34f9cd..a95f6986 100644
--- a/libs/extension/src/components/VTree.less
+++ b/libs/extension/src/components/VTree.less
@@ -6,7 +6,7 @@
.treeItem {
width: 100%;
position: absolute;
- line-height: 18px;
+ line-height: 1.125rem;
&:hover {
background-color: @select-color;
diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx
index 6b990dc1..05c9d4e3 100644
--- a/libs/extension/src/components/VTree.tsx
+++ b/libs/extension/src/components/VTree.tsx
@@ -6,7 +6,7 @@ import { SizeObserver } from './SizeObserver';
import { renderInfoType, VList } from './VList';
export interface IData {
- id: string;
+ id: number;
name: string;
indentation: number;
userKey: string;
diff --git a/libs/extension/src/contentScript/index.ts b/libs/extension/src/contentScript/index.ts
index 2236496a..9a7fe2e8 100644
--- a/libs/extension/src/contentScript/index.ts
+++ b/libs/extension/src/contentScript/index.ts
@@ -1,4 +1,7 @@
import { injectCode } from '../utils/injectUtils';
+import { checkMessage } from '../utils/transferTool';
+import { DevToolContentScript, DevToolHook, DevToolBackground } from './../utils/constants';
+import { changeSource } from './../utils/transferTool';
// 页面的window对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入hook
injectCode(chrome.runtime.getURL('/injector.js'));
@@ -10,12 +13,11 @@ window.addEventListener('message', event => {
return;
}
- if (event.data.type && (event.data.type === 'HORIZON_DEV_TOOLS')) {
- console.log('Content script received: ' + JSON.stringify(event.data.vNode));
+ const data = event.data;
+ if (checkMessage(data, DevToolHook)) {
+ changeSource(data, DevToolContentScript);
// 传递给background
- chrome.runtime.sendMessage(event.data.vNode, function (response) {
- console.log(response);
- });
+ chrome.runtime.sendMessage(data);
}
}, false);
@@ -23,14 +25,14 @@ window.addEventListener('message', event => {
// 监听来自background的消息
chrome.runtime.onMessage.addListener(
- function (request, sender, sendResponse) {
- console.log(sender.tab ?
- 'from a content script:' + sender.tab.url :
- 'from the extension');
- if (request.tag === 'init horizon info') {
+ function (message, sender, sendResponse) {
+ // 该方法可以监听页面 contentScript 和插件的消息
+ // 没有 tab 信息说明消息来自插件
+ if (!sender.tab && checkMessage(message, DevToolBackground)) {
+ changeSource(message, DevToolContentScript);
// 传递消息给页面
- console.log('start pass info to webpage');
- window.postMessage({type: 'HORIZON_DEV_TOOLS', id: 1}, '*');
+ window.postMessage(message, '*');
}
+ sendResponse({status: 'ok'});
}
);
diff --git a/libs/extension/src/devtools/mockPage/MockContext.ts b/libs/extension/src/devtools/mockPage/MockContext.ts
new file mode 100644
index 00000000..68bd8d1e
--- /dev/null
+++ b/libs/extension/src/devtools/mockPage/MockContext.ts
@@ -0,0 +1,3 @@
+import { createContext } from 'horizon';
+
+export const MockContext = createContext({value: 'default context value'});
diff --git a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
index 48a76e93..41437e38 100644
--- a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
+++ b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
@@ -1,12 +1,26 @@
-import { useState, useEffect, useRef, createContext } from 'horizon';
+import { useState, useEffect, useRef, useContext, useReducer } from 'horizon';
+import { MockContext } from './MockContext';
-const Ctx = createContext();
+const initialState = {count: 0};
+
+function reducer(state, action) {
+ switch (action.type) {
+ case 'increment':
+ return {count: state.count + 1};
+ case 'decrement':
+ return {count: state.count - 1};
+ default:
+ throw new Error();
+ }
+}
export default function MockFunctionComponent(props) {
+ const [state, dispatch] = useReducer(reducer, initialState);
const [age, setAge] = useState(0);
const domRef = useRef();
const objRef = useRef({ str: 'string' });
-
+ const context = useContext(MockContext);
+
useEffect(() => { }, []);
return (
@@ -16,7 +30,7 @@ export default function MockFunctionComponent(props) {
count: {props.count}
{objRef.current.str}
-
+ {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 9b5332e3..57c96b44 100644
--- a/libs/extension/src/devtools/mockPage/index.tsx
+++ b/libs/extension/src/devtools/mockPage/index.tsx
@@ -1,18 +1,20 @@
import { render } from 'horizon';
import MockClassComponent from './MockClassComponent';
import MockFunctionComponent from './MockFunctionComponent';
+import { MockContext } from './MockContext';
const root = document.createElement('div');
document.body.append(root);
-
function App() {
return (
+
+
+
+
abc
-
-
);
}
-render(
, root);
+render(
, root);
diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts
index 178cf608..e0372d10 100644
--- a/libs/extension/src/injector/index.ts
+++ b/libs/extension/src/injector/index.ts
@@ -1,4 +1,72 @@
-import parseTreeRoot from "../parser/parseVNode";
+import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
+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';
+
+const roots = [];
+
+function addIfNotInclude(treeRoot: VNode) {
+ if (!roots.includes(treeRoot)) {
+ roots.push(treeRoot);
+ }
+}
+
+function send() {
+ const result = roots.reduce((pre, current) => {
+ const info = parseTreeRoot(current);
+ pre.push(info);
+ return pre;
+ }, []);
+ postMessage(AllVNodeTreesInfos, result);
+}
+
+function deleteVNode(vNode: VNode) {
+ // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除
+ clearVNode(vNode);
+ const index = roots.indexOf(vNode);
+ if (index !== -1) {
+ roots.splice(index, 1);
+ }
+}
+
+function postMessage(type: string, data) {
+ window.postMessage(packagePayload({
+ type: type,
+ data: data,
+ }, DevToolHook), '*');
+}
+
+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,
+ });
+ }
+}
function injectHook() {
if (window.__HORIZON_DEV_HOOK__) {
@@ -7,26 +75,27 @@ function injectHook() {
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
enumerable: false,
value: {
- roots: [],
- send: function (vNode: any) {
- const result = parseTreeRoot(vNode);
- window.postMessage({
- type: 'HORIZON_DEV_TOOLS', vNode: result
- }, '*');
- },
- listen: function (id: number) {
- window.addEventListener('message', function(event) {
- // We only accept messages from ourselves
- if (event.source !== window) {
- return;
- }
-
- if (event.data.type && (event.data.type === 'HORIZON_DEV_TOOLS') && event.data.id === id) {
- console.log('todo');
- }
- });
- }
+ addIfNotInclude,
+ send,
+ deleteVNode,
},
});
+ window.addEventListener('message', function (event) {
+ // We only accept messages from ourselves
+ if (event.source !== window) {
+ return;
+ }
+ const request = event.data;
+ if (checkMessage(request, DevToolContentScript)) {
+ const { payload } = request;
+ const { type, data } = payload;
+ if (type === RequestAllVNodeTreeInfos) {
+ send();
+ } else if (type === RequestComponentAttrs) {
+ parseCompAttrs(data);
+ }
+ }
+ });
}
+
injectHook();
diff --git a/libs/extension/src/manifest.json b/libs/extension/src/manifest.json
index 3db7b374..0679215a 100644
--- a/libs/extension/src/manifest.json
+++ b/libs/extension/src/manifest.json
@@ -18,5 +18,10 @@
"run_at": "document_start"
}
],
- "web_accessible_resources": []
+ "web_accessible_resources": [
+ {
+ "resources": [ "injector.js", "background.js" ],
+ "matches": ["
"]
+ }
+ ]
}
diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx
index 0c62fb19..af6e5d9a 100644
--- a/libs/extension/src/panel/App.tsx
+++ b/libs/extension/src/panel/App.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'horizon';
+import { useState, useEffect, useRef } from 'horizon';
import VTree, { IData } from '../components/VTree';
import Search from '../components/Search';
import ComponentInfo from '../components/ComponentInfo';
@@ -8,6 +8,14 @@ import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
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';
const parseVNodeData = (rawData) => {
const idIndentationMap: {
@@ -16,7 +24,7 @@ const parseVNodeData = (rawData) => {
const data: IData[] = [];
let i = 0;
while (i < rawData.length) {
- const id = rawData[i] as string;
+ const id = rawData[i] as number;
i++;
const name = rawData[i] as string;
i++;
@@ -51,10 +59,47 @@ 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);
+ }
+}
+
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentAttrs, setComponentAttrs] = useState({});
const [selectComp, setSelectComp] = useState(null);
+ const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度,
const {
filterValue,
@@ -77,6 +122,36 @@ function App() {
state: parsedMockState,
props: parsedMockState,
});
+ } else {
+ // 页面打开后发送初始化请求
+ postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
+ // 监听 background消息
+ connection.onMessage.addListener(function (message) {
+ const { payload } = message;
+ if (payload) {
+ const { type, data } = payload;
+ if (type === AllVNodeTreesInfos) {
+ 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});
+ }
+ return pre.concat(parsedTreeData);
+ }, []);
+ setParsedVNodeData(allTreeData);
+ } else if (type === ComponentAttrs) {
+ const {parsedProps, parsedState, parsedHooks} = data;
+ setComponentAttrs({
+ props: parsedProps,
+ state: parsedState,
+ hooks: parsedHooks,
+ });
+ }
+ }
+ });
}
}, []);
@@ -85,10 +160,14 @@ function App() {
};
const handleSelectComp = (item: IData) => {
- setComponentAttrs({
- state: parsedMockState,
- props: parsedMockState,
- });
+ if (isDev) {
+ setComponentAttrs({
+ state: parsedMockState,
+ props: parsedMockState,
+ });
+ } else {
+ postMessage(RequestComponentAttrs, item.id);
+ }
setSelectComp(item);
};
@@ -134,8 +213,8 @@ function App() {
diff --git a/libs/extension/src/panel/index.tsx b/libs/extension/src/panel/index.tsx
index a6174e37..2f97d3ce 100644
--- a/libs/extension/src/panel/index.tsx
+++ b/libs/extension/src/panel/index.tsx
@@ -4,4 +4,4 @@ import App from './App';
render(
,
document.getElementById('root')
-);
\ No newline at end of file
+);
diff --git a/libs/extension/src/parser/parseAttr.ts b/libs/extension/src/parser/parseAttr.ts
index f710141e..e18f9bad 100644
--- a/libs/extension/src/parser/parseAttr.ts
+++ b/libs/extension/src/parser/parseAttr.ts
@@ -1,79 +1,133 @@
-import { IAttr } from "../components/ComponentInfo";
-// 将状态的值解析成固定格式
+import { Hook, Reducer, Ref } from './../../../horizon/src/renderer/hooks/HookType';
+
+// 展示值为 string 的可编辑类型
+type editableStringType = 'string' | 'number' | 'undefined' | 'null';
+// 展示值为 string 的不可编辑类型
+type unEditableStringType = 'function' | 'symbol' | 'object' | 'map' | 'set' | 'array'
+ | 'dom' // 值为 dom 元素的 ref 类型
+ | 'ref'; // 值为其他数据的 ref 类型
+
+type showAsStringType = editableStringType | unEditableStringType;
+
+
+export type IAttr = {
+ name: string;
+ indentation: number;
+ hIndex?: number; // 用于记录 hook 的 hIndex 值
+} & ({
+ type: showAsStringType;
+ value: string;
+} | {
+ type: 'boolean';
+ value: boolean;
+})
+
+type showType = showAsStringType | 'boolean';
+
+const parseSubAttr = (
+ attr: any,
+ parentIndentation: number,
+ attrName: string,
+ result: IAttr[],
+ hIndex?: number) => {
+ const attrType = typeof attr;
+ let value: any;
+ let showType: showType;
+ let addSubState;
+ if (attrType === 'boolean' ||
+ attrType === 'number' ||
+ attrType === 'string' ||
+ attrType === 'undefined') {
+ value = attr;
+ showType = attrType;
+ } else if (attrType === 'function') {
+ const funName = attr.name;
+ value = `f() ${funName}{}`;
+ } else if (attrType === 'symbol') {
+ value = attr.description;
+ } else if (attrType === 'object') {
+ if (attr === null) {
+ showType = 'null';
+ } else if (attr instanceof Map) {
+ showType = 'map';
+ const size = attr.size;
+ value = `Map(${size})`;
+ addSubState = () => {
+ attr.forEach((value, key) => {
+ parseSubAttr(value, parentIndentation + 2, key, result);
+ });
+ };
+ } else if (attr instanceof Set) {
+ showType = 'set';
+ const size = attr.size;
+ value = `Set(${size})`;
+ addSubState = () => {
+ let i = 0;
+ attr.forEach((value) => {
+ parseSubAttr(value, parentIndentation + 2, String(i), result);
+ });
+ i++;
+ };
+ } else if (Array.isArray(attr)) {
+ showType = 'array';
+ value = `Array(${attr.length})`;
+ addSubState = () => {
+ attr.forEach((value, index) => {
+ parseSubAttr(value, parentIndentation + 2, String(index), result);
+ });
+ };
+ } else if (attr instanceof Element) {
+ showType = 'dom';
+ value = attr.tagName;
+ } else {
+ showType = attrType;
+ value = '{...}';
+ addSubState = () => {
+ Object.keys(attr).forEach((key) => {
+ parseSubAttr(attr[key], parentIndentation + 2, key, result);
+ });
+ };
+ }
+ }
+ const item: IAttr = {
+ name: attrName,
+ type: showType,
+ value,
+ indentation: parentIndentation + 1,
+ };
+ if (hIndex) {
+ item.hIndex = hIndex;
+ }
+ result.push(item);
+ if (addSubState) {
+ addSubState();
+ }
+};
+
+// 将属性的值解析成固定格式,props 和 类组件的 state 必须是一个对象
export function parseAttr(rootAttr: any) {
const result: IAttr[] = [];
const indentation = 0;
- const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => {
- const stateType = typeof attr;
- let value: any;
- let showType;
- let addSubState;
- if (stateType === 'boolean' ||
- stateType === 'number' ||
- stateType === 'string' ||
- stateType === 'undefined') {
- value = attr;
- showType = stateType;
- } else if (stateType === 'function') {
- const funName = attr.name;
- value = `f() ${funName}{}`;
- } else if (stateType === 'symbol') {
- value = attr.description;
- } else if (stateType === 'object') {
- if (attr === null) {
- showType = 'null';
- } else if (attr instanceof Map) {
- showType = 'map';
- const size = attr.size;
- value = `Map(${size})`;
- addSubState = () => {
- attr.forEach((value, key) => {
- parseSubAttr(value, parentIndentation + 2, key);
- });
- };
- } else if (attr instanceof Set) {
- showType = 'set';
- const size = attr.size;
- value = `Set(${size})`;
- addSubState = () => {
- let i = 0;
- attr.forEach((value) => {
- parseSubAttr(value, parentIndentation + 2, String(i));
- });
- i++;
- };
- } else if (Array.isArray(attr)) {
- showType = 'array';
- value = `Array(${attr.length})`;
- addSubState = () => {
- attr.forEach((value, index) => {
- parseSubAttr(value, parentIndentation + 2, String(index));
- });
- };
- } else {
- showType = stateType;
- value = '{...}';
- addSubState = () => {
- Object.keys(attr).forEach((key) => {
- parseSubAttr(attr[key], parentIndentation + 2, key);
- });
- };
- }
- }
-
- result.push({
- name: attrName,
- type: showType,
- value,
- indentation: parentIndentation + 1,
- });
- if (addSubState) {
- addSubState();
- }
- };
+ if (typeof rootAttr === 'object' && rootAttr !== null)
Object.keys(rootAttr).forEach(key => {
- parseSubAttr(rootAttr[key], indentation, key);
+ parseSubAttr(rootAttr[key], indentation, key, result);
+ });
+ return result;
+}
+
+export function parseHooks(hooks: Hook
[]) {
+ 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);
+ }
});
return result;
}
diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts
index 91104058..fbf4b0bd 100644
--- a/libs/extension/src/parser/parseVNode.ts
+++ b/libs/extension/src/parser/parseVNode.ts
@@ -57,4 +57,16 @@ function parseTreeRoot(treeRoot: VNode) {
return result;
}
+export function queryVNode(id: number) {
+ return IdToVNodeMap.get(id);
+}
+
+export function clearVNode(vNode: VNode) {
+ if (VNodeToIdMap.has(vNode)) {
+ const id = VNodeToIdMap.get(vNode);
+ VNodeToIdMap.delete(vNode);
+ IdToVNodeMap.delete(id);
+ }
+}
+
export default parseTreeRoot;
diff --git a/libs/extension/src/utils/constants.ts b/libs/extension/src/utils/constants.ts
new file mode 100644
index 00000000..e779a143
--- /dev/null
+++ b/libs/extension/src/utils/constants.ts
@@ -0,0 +1,22 @@
+// panel 页面打开后初始化连接标志
+export const InitDevToolPageConnection = 'init dev tool page connection';
+// background 解析全部 root VNodes 标志
+export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos';
+// vNodes 全部树解析结果标志
+export const AllVNodeTreesInfos = 'vNode trees Infos';
+// 一棵树的解析
+export const OneVNodeTreeInfos = 'one vNode tree';
+// 获取组件属性
+export const RequestComponentAttrs = 'get component attrs';
+// 返回组件属性
+export const ComponentAttrs = 'component attrs';
+
+
+// 传递消息来源标志
+export const DevToolPanel = 'dev tool panel';
+
+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
diff --git a/libs/extension/src/utils/transferTool.ts b/libs/extension/src/utils/transferTool.ts
new file mode 100644
index 00000000..c25fc4d7
--- /dev/null
+++ b/libs/extension/src/utils/transferTool.ts
@@ -0,0 +1,32 @@
+const devTools = 'HORIZON_DEV_TOOLS';
+
+interface payLoadType {
+ type: string,
+ data?: any,
+}
+
+interface message {
+ type: typeof devTools,
+ payload: payLoadType,
+ from: string,
+}
+
+export function packagePayload(payload: payLoadType, from: string): message {
+ return {
+ type: devTools,
+ payload,
+ from,
+ };
+}
+
+export function checkMessage(data: any, from: string) {
+ if (data?.type === devTools && data?.from === from) {
+ return true;
+ }
+ return false;
+}
+
+export function changeSource(message: message, from: string) {
+ message.from = from;
+}
+
diff --git a/libs/extension/webpack.config.js b/libs/extension/webpack.config.js
index f712808b..25dab94b 100644
--- a/libs/extension/webpack.config.js
+++ b/libs/extension/webpack.config.js
@@ -1,4 +1,5 @@
const path = require('path');
+const webpack = require('webpack');
const config = {
entry: {
@@ -45,6 +46,12 @@ const config = {
externals: {
'horizon': 'Horizon',
},
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': '"development"',
+ isDev: 'false',
+ }),
+ ],
};
module.exports = config;
diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts
index 1680c949..394319e0 100644
--- a/libs/horizon/src/renderer/TreeBuilder.ts
+++ b/libs/horizon/src/renderer/TreeBuilder.ts
@@ -275,6 +275,11 @@ function renderFromRoot(treeRoot) {
// 2. 提交变更
submitToRender(treeRoot);
+ if (window.__HORIZON_DEV_HOOK__) {
+ const hook = window.__HORIZON_DEV_HOOK__;
+ hook.addIfNotInclude(treeRoot);
+ hook.send(treeRoot);
+ }
return null;
}
diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts
index e965fdf1..cb8be892 100644
--- a/libs/horizon/src/renderer/hooks/HookType.ts
+++ b/libs/horizon/src/renderer/hooks/HookType.ts
@@ -3,6 +3,7 @@ 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 52399713..480f43bb 100644
--- a/libs/horizon/src/renderer/hooks/UseReducerHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
@@ -87,6 +87,7 @@ 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 754a16d2..381ef61e 100644
--- a/libs/horizon/src/renderer/hooks/UseRefHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseRefHook.ts
@@ -12,6 +12,7 @@ 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();
}