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