From ed8d86f538b10817c3b2b1fa861a94cc8e147b6e Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 17:50:12 +0800 Subject: [PATCH 1/9] Match-id-64ca0140120735cae09f6abda4732cb3c0165d8c --- libs/extension/src/manifest.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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": [""] + } + ] } From be5b864736a0613fcdb54ec038cc35239d719d5c Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 17:50:38 +0800 Subject: [PATCH 2/9] Match-id-0a5df1caf20a3855a1bc4052117f5835e113fd74 --- libs/extension/webpack.config.js | 7 +++++++ 1 file changed, 7 insertions(+) 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; From 48342612539c104e9367ed9152f62bb001648931 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 17:51:07 +0800 Subject: [PATCH 3/9] Match-id-dcc1f7995d057864df9b055c29397e3166c23599 --- libs/extension/readme.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) 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 的行为从而展开测试工作。 + From e9b9f69269099bd74a2da63e9b63de26056de5d7 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 19:45:55 +0800 Subject: [PATCH 4/9] Match-id-645d6786f413f96296f5fbf6cc123242c12adf82 --- libs/extension/src/background/index.ts | 53 ++++++++++++-------- libs/extension/src/contentScript/index.ts | 21 ++++---- libs/extension/src/injector/index.ts | 59 +++++++++++++++++------ libs/extension/src/panel/App.tsx | 52 +++++++++++++++++--- libs/extension/src/panel/index.tsx | 2 +- libs/extension/src/parser/parseVNode.ts | 8 +++ libs/extension/src/utils/constants.ts | 12 +++++ libs/extension/src/utils/transferTool.ts | 21 ++++++++ libs/horizon/src/renderer/TreeBuilder.ts | 5 ++ 9 files changed, 176 insertions(+), 57 deletions(-) create mode 100644 libs/extension/src/utils/constants.ts create mode 100644 libs/extension/src/utils/transferTool.ts diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts index 2745c61a..cc724452 100644 --- a/libs/extension/src/background/index.ts +++ b/libs/extension/src/background/index.ts @@ -1,30 +1,41 @@ +import { checkData, packagePayload } from '../utils/transferTool'; +import { requestAllVNodeTreeInfos, initDevToolPageConnection } 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 = checkData(message); + if (isHorizonMessage) { + console.log('received message', message); + 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 }); + } else { + passMessage = message; + } + console.log('post message:', passMessage); + // 查询参数有 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,11 +53,11 @@ chrome.runtime.onConnect.addListener(function (port) { }); // 监听来自 content script 的消息,并将消息发送给对应的 devTools page,也就是 panel -chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { // Messages from content scripts should have sender.tab set if (sender.tab) { const tabId = sender.tab.id; - if (tabId in connections) { + if (tabId in connections && checkData(request)) { connections[tabId].postMessage(request); } else { console.log('Tab not found in connection list.'); diff --git a/libs/extension/src/contentScript/index.ts b/libs/extension/src/contentScript/index.ts index 2236496a..9357f69f 100644 --- a/libs/extension/src/contentScript/index.ts +++ b/libs/extension/src/contentScript/index.ts @@ -1,4 +1,5 @@ import { injectCode } from '../utils/injectUtils'; +import { checkData } from '../utils/transferTool'; // 页面的window对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入hook injectCode(chrome.runtime.getURL('/injector.js')); @@ -10,12 +11,10 @@ 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 (checkData(data)) { // 传递给background - chrome.runtime.sendMessage(event.data.vNode, function (response) { - console.log(response); - }); + chrome.runtime.sendMessage(data); } }, false); @@ -23,14 +22,12 @@ 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 (request, sender) { + // 该方法可以监听页面 contentScript 和插件的消息 + // 没有 tab 信息说明消息来自插件 + if (!sender.tab && checkData(request)) { // 传递消息给页面 - console.log('start pass info to webpage'); - window.postMessage({type: 'HORIZON_DEV_TOOLS', id: 1}, '*'); + window.postMessage(request, '*'); } } ); diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts index 178cf608..d1920678 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -1,4 +1,8 @@ -import parseTreeRoot from "../parser/parseVNode"; +import parseTreeRoot, { deleteVNode } from '../parser/parseVNode'; +import { packagePayload, checkData } from './../utils/transferTool'; +import { oneVNodeTreeInfos, allVNodeTreesInfos, requestAllVNodeTreeInfos } from './../utils/constants'; + +const roots = []; function injectHook() { if (window.__HORIZON_DEV_HOOK__) { @@ -7,26 +11,49 @@ function injectHook() { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { enumerable: false, value: { - roots: [], + addIfNotInclude: function( treeRoot: any) { + if (!roots.includes(treeRoot)) { + roots.push(treeRoot); + } + }, send: function (vNode: any) { const result = parseTreeRoot(vNode); - window.postMessage({ - type: 'HORIZON_DEV_TOOLS', vNode: result - }, '*'); + window.postMessage(packagePayload({ + data: result, + type: oneVNodeTreeInfos + }), '*'); }, - 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'); - } - }); + delete: function (vNode: any) { + // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除 + deleteVNode(vNode); + const index = roots.indexOf(vNode); + if (index !== -1) { + roots.splice(index, 1); + } } }, }); + window.addEventListener('message', function (event) { + // We only accept messages from ourselves + if (event.source !== window) { + return; + } + const request = event.data; + if (checkData(request)) { + const { payload } = request; + const { type, data } = payload; + if (type === requestAllVNodeTreeInfos) { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(current); + pre.push(info); + return pre; + }, []); + window.postMessage(packagePayload({ + data: result, + type: allVNodeTreesInfos + }), '*'); + } + } + }); } injectHook(); diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index 0c62fb19..7ba1151f 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,8 @@ 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 } from './../utils/constants'; +import { packagePayload } from './../utils/transferTool'; const parseVNodeData = (rawData) => { const idIndentationMap: { @@ -55,6 +57,7 @@ function App() { const [parsedVNodeData, setParsedVNodeData] = useState([]); const [componentAttrs, setComponentAttrs] = useState({}); const [selectComp, setSelectComp] = useState(null); + const treeRootInfos = useRef<{[id: string]: number}>({}); // 记录保存的根节点 id 和长度 const { filterValue, @@ -77,6 +80,34 @@ function App() { state: parsedMockState, props: parsedMockState, }); + } else { + const connection = chrome.runtime.connect({ + name: 'panel' + }); + // 页面打开后发送初始化请求 + connection.postMessage(packagePayload({ + type: initDevToolPageConnection, + data: 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; + if (length) { + const treeRoot = parsedTreeData[0]; + treeRootInfos.current[treeRoot.id] = length; + } + return pre.concat(parsedTreeData); + }, []); + setParsedVNodeData(allTreeData); + } + } + }); } }, []); @@ -85,10 +116,17 @@ function App() { }; const handleSelectComp = (item: IData) => { - setComponentAttrs({ - state: parsedMockState, - props: parsedMockState, - }); + if (isDev) { + setComponentAttrs({ + state: parsedMockState, + props: parsedMockState, + }); + } else { + connection.postMessage({ + name: requestComponentAttrs, + data: item.id + }); + } setSelectComp(item); }; @@ -134,8 +172,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/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts index 91104058..813f9a73 100644 --- a/libs/extension/src/parser/parseVNode.ts +++ b/libs/extension/src/parser/parseVNode.ts @@ -57,4 +57,12 @@ function parseTreeRoot(treeRoot: VNode) { return result; } +export function deleteVNode(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..dd6ce7cc --- /dev/null +++ b/libs/extension/src/utils/constants.ts @@ -0,0 +1,12 @@ +// 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'; \ 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..655be4e7 --- /dev/null +++ b/libs/extension/src/utils/transferTool.ts @@ -0,0 +1,21 @@ +const devTools = 'HORIZON_DEV_TOOLS'; + +interface payLoadType { + type: string, + data?: any, +} + +export function packagePayload(payload: payLoadType) { + return { + type: devTools, + payload, + }; +} + +export function checkData(data: any) { + if (data?.type === devTools) { + return true; + } + return false; +} + 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; } From 18a5f366049ec985d4ac93c1af354eb91659e1fd Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 19 Apr 2022 15:28:54 +0800 Subject: [PATCH 5/9] Match-id-9696388f05085d0c1eb211cd745a75e2c2a6ab46 --- libs/extension/src/background/index.ts | 21 ++++---- libs/extension/src/components/VList.tsx | 4 +- libs/extension/src/components/VTree.tsx | 2 +- libs/extension/src/contentScript/index.ts | 14 ++++-- libs/extension/src/injector/index.ts | 61 ++++++++++++++++------- libs/extension/src/panel/App.tsx | 51 +++++++++++++------ libs/extension/src/parser/parseVNode.ts | 4 ++ libs/extension/src/utils/constants.ts | 22 +++++--- libs/extension/src/utils/transferTool.ts | 17 +++++-- 9 files changed, 136 insertions(+), 60 deletions(-) diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts index cc724452..a750c1bb 100644 --- a/libs/extension/src/background/index.ts +++ b/libs/extension/src/background/index.ts @@ -1,5 +1,6 @@ -import { checkData, packagePayload } from '../utils/transferTool'; -import { requestAllVNodeTreeInfos, initDevToolPageConnection } from '../utils/constants'; +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 = {}; @@ -7,22 +8,21 @@ const connections = {}; // panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数 chrome.runtime.onConnect.addListener(function (port) { function extensionListener(message) { - const isHorizonMessage = checkData(message); + const isHorizonMessage = checkMessage(message, DevToolPanel); if (isHorizonMessage) { - console.log('received message', message); const { payload } = message; const { type, data } = payload; let passMessage; - if (type === initDevToolPageConnection) { + if (type === InitDevToolPageConnection) { if (!connections[data]) { // 获取 panel 所在 tab 页的tabId connections[data] = port; } - passMessage = packagePayload({ type: requestAllVNodeTreeInfos }); + passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground); } else { passMessage = message; + changeSource(passMessage, DevToolBackground); } - console.log('post message:', passMessage); // 查询参数有 active 和 currentWindow, 如果开发者工具与页面分离,会导致currentWindow为false才能找到 // 所以只用 active 参数查找,但不确定这么写是否会引发查询错误的情况 // 或许需要用不同的查询参数查找两次 @@ -53,12 +53,13 @@ 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 && checkData(request)) { - 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.'); } 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.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 9357f69f..92c7150f 100644 --- a/libs/extension/src/contentScript/index.ts +++ b/libs/extension/src/contentScript/index.ts @@ -1,5 +1,7 @@ import { injectCode } from '../utils/injectUtils'; -import { checkData } from '../utils/transferTool'; +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')); @@ -12,7 +14,8 @@ window.addEventListener('message', event => { } const data = event.data; - if (checkData(data)) { + if (checkMessage(data, DevToolHook)) { + changeSource(data, DevToolContentScript); // 传递给background chrome.runtime.sendMessage(data); } @@ -22,12 +25,13 @@ window.addEventListener('message', event => { // 监听来自background的消息 chrome.runtime.onMessage.addListener( - function (request, sender) { + function (message, sender) { // 该方法可以监听页面 contentScript 和插件的消息 // 没有 tab 信息说明消息来自插件 - if (!sender.tab && checkData(request)) { + if (!sender.tab && checkMessage(message, DevToolBackground)) { + changeSource(message, DevToolContentScript); // 传递消息给页面 - window.postMessage(request, '*'); + window.postMessage(message, '*'); } } ); diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts index d1920678..58432849 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -1,6 +1,23 @@ -import parseTreeRoot, { deleteVNode } from '../parser/parseVNode'; -import { packagePayload, checkData } from './../utils/transferTool'; -import { oneVNodeTreeInfos, allVNodeTreesInfos, requestAllVNodeTreeInfos } from './../utils/constants'; +import parseTreeRoot, { deleteVNode, 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 } from '../parser/parseAttr'; + +function postMessage(type: string, data) { + window.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolHook), '*'); +} const roots = []; @@ -11,19 +28,20 @@ function injectHook() { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { enumerable: false, value: { - addIfNotInclude: function( treeRoot: any) { + addIfNotInclude: function (treeRoot: VNode) { if (!roots.includes(treeRoot)) { roots.push(treeRoot); } }, - send: function (vNode: any) { - const result = parseTreeRoot(vNode); - window.postMessage(packagePayload({ - data: result, - type: oneVNodeTreeInfos - }), '*'); + send: function () { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(current); + pre.push(info); + return pre; + }, []); + postMessage(AllVNodeTreesInfos, result); }, - delete: function (vNode: any) { + delete: function (vNode: VNode) { // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除 deleteVNode(vNode); const index = roots.indexOf(vNode); @@ -39,19 +57,28 @@ function injectHook() { return; } const request = event.data; - if (checkData(request)) { + if (checkMessage(request, DevToolContentScript)) { const { payload } = request; const { type, data } = payload; - if (type === requestAllVNodeTreeInfos) { + if (type === RequestAllVNodeTreeInfos) { const result = roots.reduce((pre, current) => { const info = parseTreeRoot(current); pre.push(info); return pre; }, []); - window.postMessage(packagePayload({ - data: result, - type: allVNodeTreesInfos - }), '*'); + postMessage(AllVNodeTreesInfos, result); + } else if (type === RequestComponentAttrs) { + const vNode: VNode = queryVNode(data); + const tag = vNode.tag; + if (tag === ClassComponent) { + const { props, state } = vNode; + const parsedProps = parseAttr(props); + const parsedState = parseAttr(state); + postMessage(ComponentAttrs, { + parsedProps, + parsedState, + }); + } } } }); diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index 7ba1151f..d5283843 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -8,7 +8,13 @@ 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 } from './../utils/constants'; +import { + InitDevToolPageConnection, + AllVNodeTreesInfos, + RequestComponentAttrs, + ComponentAttrs, + DevToolPanel, +} from './../utils/constants'; import { packagePayload } from './../utils/transferTool'; const parseVNodeData = (rawData) => { @@ -18,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++; @@ -53,11 +59,26 @@ const getParents = (item: IData | null, parsedVNodeData: IData[]) => { return parents; }; +let connection; +if (!isDev) { + // 与 background 的唯一连接 + connection = chrome.runtime.connect({ + name: 'panel' + }); +} + +function postMessage(type: string, data: any) { + connection.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolPanel)); +} + function App() { const [parsedVNodeData, setParsedVNodeData] = useState([]); const [componentAttrs, setComponentAttrs] = useState({}); const [selectComp, setSelectComp] = useState(null); - const treeRootInfos = useRef<{[id: string]: number}>({}); // 记录保存的根节点 id 和长度 + const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度, const { filterValue, @@ -81,30 +102,31 @@ function App() { props: parsedMockState, }); } else { - const connection = chrome.runtime.connect({ - name: 'panel' - }); // 页面打开后发送初始化请求 - connection.postMessage(packagePayload({ - type: initDevToolPageConnection, - data: chrome.devtools.inspectedWindow.tabId - })); + postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId); // 监听 background消息 connection.onMessage.addListener(function (message) { const { payload } = message; if (payload) { const { type, data } = payload; - if (type === allVNodeTreesInfos) { + 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[treeRoot.id] = length; + treeRootInfos.current.push({id: treeRoot.id, length: length}); } return pre.concat(parsedTreeData); }, []); setParsedVNodeData(allTreeData); + } else if (type === ComponentAttrs) { + const {parsedProps, parsedState} = data; + setComponentAttrs({ + state: parsedProps, + props: parsedState, + }); } } }); @@ -122,10 +144,7 @@ function App() { props: parsedMockState, }); } else { - connection.postMessage({ - name: requestComponentAttrs, - data: item.id - }); + postMessage(RequestComponentAttrs, item.id); } setSelectComp(item); }; diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts index 813f9a73..e4ecc276 100644 --- a/libs/extension/src/parser/parseVNode.ts +++ b/libs/extension/src/parser/parseVNode.ts @@ -57,6 +57,10 @@ function parseTreeRoot(treeRoot: VNode) { return result; } +export function queryVNode(id: number) { + return IdToVNodeMap.get(id); +} + export function deleteVNode(vNode: VNode) { if (VNodeToIdMap.has(vNode)) { const id = VNodeToIdMap.get(vNode); diff --git a/libs/extension/src/utils/constants.ts b/libs/extension/src/utils/constants.ts index dd6ce7cc..e779a143 100644 --- a/libs/extension/src/utils/constants.ts +++ b/libs/extension/src/utils/constants.ts @@ -1,12 +1,22 @@ // panel 页面打开后初始化连接标志 -export const initDevToolPageConnection = 'init dev tool page connection'; +export const InitDevToolPageConnection = 'init dev tool page connection'; // background 解析全部 root VNodes 标志 -export const requestAllVNodeTreeInfos = 'request all vNodes tree infos'; +export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos'; // vNodes 全部树解析结果标志 -export const allVNodeTreesInfos = 'vNode trees Infos'; +export const AllVNodeTreesInfos = 'vNode trees Infos'; // 一棵树的解析 -export const oneVNodeTreeInfos = 'one vNode tree'; +export const OneVNodeTreeInfos = 'one vNode tree'; // 获取组件属性 -export const requestComponentAttrs = 'get component attrs'; +export const RequestComponentAttrs = 'get component attrs'; // 返回组件属性 -export const componentAttrs = 'component attrs'; \ No newline at end of file +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 index 655be4e7..c25fc4d7 100644 --- a/libs/extension/src/utils/transferTool.ts +++ b/libs/extension/src/utils/transferTool.ts @@ -5,17 +5,28 @@ interface payLoadType { data?: any, } -export function packagePayload(payload: payLoadType) { +interface message { + type: typeof devTools, + payload: payLoadType, + from: string, +} + +export function packagePayload(payload: payLoadType, from: string): message { return { type: devTools, payload, + from, }; } -export function checkData(data: any) { - if (data?.type === devTools) { +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; +} + From d31bbc937d4a6ae4221637482183e8f253057de3 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 19 Apr 2022 16:15:02 +0800 Subject: [PATCH 6/9] Match-id-094551ccf405a03b88a0a038c149dac31ae48e12 --- libs/extension/src/injector/index.ts | 86 +++++++++++++------------ libs/extension/src/parser/parseVNode.ts | 2 +- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts index 58432849..0d1ad023 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -1,4 +1,4 @@ -import parseTreeRoot, { deleteVNode, queryVNode } from '../parser/parseVNode'; +import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode'; import { packagePayload, checkMessage } from './../utils/transferTool'; import { RequestAllVNodeTreeInfos, @@ -12,6 +12,32 @@ import { VNode } from './../../../horizon/src/renderer/vnode/VNode'; import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags'; import { parseAttr } from '../parser/parseAttr'; +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, @@ -19,7 +45,19 @@ function postMessage(type: string, data) { }, DevToolHook), '*'); } -const roots = []; +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, + }); + } +} function injectHook() { if (window.__HORIZON_DEV_HOOK__) { @@ -28,27 +66,9 @@ function injectHook() { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { enumerable: false, value: { - addIfNotInclude: function (treeRoot: VNode) { - if (!roots.includes(treeRoot)) { - roots.push(treeRoot); - } - }, - send: function () { - const result = roots.reduce((pre, current) => { - const info = parseTreeRoot(current); - pre.push(info); - return pre; - }, []); - postMessage(AllVNodeTreesInfos, result); - }, - delete: function (vNode: VNode) { - // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除 - deleteVNode(vNode); - const index = roots.indexOf(vNode); - if (index !== -1) { - roots.splice(index, 1); - } - } + addIfNotInclude, + send, + deleteVNode, }, }); window.addEventListener('message', function (event) { @@ -61,26 +81,12 @@ function injectHook() { const { payload } = request; const { type, data } = payload; if (type === RequestAllVNodeTreeInfos) { - const result = roots.reduce((pre, current) => { - const info = parseTreeRoot(current); - pre.push(info); - return pre; - }, []); - postMessage(AllVNodeTreesInfos, result); + send(); } else if (type === RequestComponentAttrs) { - const vNode: VNode = queryVNode(data); - const tag = vNode.tag; - if (tag === ClassComponent) { - const { props, state } = vNode; - const parsedProps = parseAttr(props); - const parsedState = parseAttr(state); - postMessage(ComponentAttrs, { - parsedProps, - parsedState, - }); - } + parseCompAttrs(data); } } }); } + injectHook(); diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts index e4ecc276..fbf4b0bd 100644 --- a/libs/extension/src/parser/parseVNode.ts +++ b/libs/extension/src/parser/parseVNode.ts @@ -61,7 +61,7 @@ export function queryVNode(id: number) { return IdToVNodeMap.get(id); } -export function deleteVNode(vNode: VNode) { +export function clearVNode(vNode: VNode) { if (VNodeToIdMap.has(vNode)) { const id = VNodeToIdMap.get(vNode); VNodeToIdMap.delete(vNode); From a5af9c4c9e218889eea71a43453ed22fd02b0c6e Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 19 Apr 2022 19:29:56 +0800 Subject: [PATCH 7/9] Match-id-daf7043d5b9449b93aa1cee20aaf82cd485b8c9f --- libs/extension/src/background/index.ts | 3 ++- libs/extension/src/contentScript/index.ts | 3 ++- libs/extension/src/panel/App.tsx | 20 ++++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts index a750c1bb..ed9698fb 100644 --- a/libs/extension/src/background/index.ts +++ b/libs/extension/src/background/index.ts @@ -66,5 +66,6 @@ chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { } 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/contentScript/index.ts b/libs/extension/src/contentScript/index.ts index 92c7150f..9a7fe2e8 100644 --- a/libs/extension/src/contentScript/index.ts +++ b/libs/extension/src/contentScript/index.ts @@ -25,7 +25,7 @@ window.addEventListener('message', event => { // 监听来自background的消息 chrome.runtime.onMessage.addListener( - function (message, sender) { + function (message, sender, sendResponse) { // 该方法可以监听页面 contentScript 和插件的消息 // 没有 tab 信息说明消息来自插件 if (!sender.tab && checkMessage(message, DevToolBackground)) { @@ -33,5 +33,6 @@ chrome.runtime.onMessage.addListener( // 传递消息给页面 window.postMessage(message, '*'); } + sendResponse({status: 'ok'}); } ); diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index d5283843..a0455831 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -68,10 +68,22 @@ if (!isDev) { } function postMessage(type: string, data: any) { - connection.postMessage(packagePayload({ - type: type, - data: data, - }, DevToolPanel)); + try { + connection.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolPanel)); + } catch(err) { + // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性 + console.error(err); + connection = chrome.runtime.connect({ + name: 'panel' + }); + connection.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolPanel)); + } } function App() { From ec0f0dd2c4451eeb6de2486da9e98d9e1617a0d2 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 21 Apr 2022 14:54:34 +0800 Subject: [PATCH 8/9] Match-id-aad38a74387ab110540aeb594a643b1be7c42a17 --- .../src/components/ComponentInfo.tsx | 23 +- libs/extension/src/components/VTree.less | 2 +- libs/extension/src/injector/index.ts | 11 +- libs/extension/src/panel/App.tsx | 24 ++- libs/extension/src/parser/parseAttr.ts | 196 +++++++++++------- libs/horizon/src/renderer/hooks/HookType.ts | 1 + .../src/renderer/hooks/UseReducerHook.ts | 1 + libs/horizon/src/renderer/hooks/UseRefHook.ts | 1 + 8 files changed, 167 insertions(+), 92 deletions(-) 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/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/injector/index.ts b/libs/extension/src/injector/index.ts index 0d1ad023..e0372d10 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -10,7 +10,8 @@ import { } from './../utils/constants'; import { VNode } from './../../../horizon/src/renderer/vnode/VNode'; import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags'; -import { parseAttr } from '../parser/parseAttr'; +import { parseAttr, parseHooks } from '../parser/parseAttr'; +import { FunctionComponent } from './../../../horizon/src/renderer/vnode/VNodeTags'; const roots = []; @@ -56,6 +57,14 @@ function parseCompAttrs(id: number) { parsedProps, parsedState, }); + } else if (tag === FunctionComponent) { + const { props, hooks } = vNode; + const parsedProps = parseAttr(props); + const parsedHooks = parseHooks(hooks); + postMessage(ComponentAttrs, { + parsedProps, + parsedHooks, + }); } } diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index a0455831..af6e5d9a 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -67,6 +67,8 @@ if (!isDev) { }); } +let reconnectTimes = 0; + function postMessage(type: string, data: any) { try { connection.postMessage(packagePayload({ @@ -75,14 +77,21 @@ function postMessage(type: string, data: any) { }, DevToolPanel)); } catch(err) { // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性 + if (reconnectTimes === 20) { + reconnectTimes = 0; + console.error('reconnect failed'); + return; + } console.error(err); + reconnectTimes++; + // 重建连接 connection = chrome.runtime.connect({ name: 'panel' }); - connection.postMessage(packagePayload({ - type: type, - data: data, - }, DevToolPanel)); + // 重新发送初始化消息 + postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId); + // 初始化成功后才会重新发送消息 + postMessage(type, data); } } @@ -134,10 +143,11 @@ function App() { }, []); setParsedVNodeData(allTreeData); } else if (type === ComponentAttrs) { - const {parsedProps, parsedState} = data; + const {parsedProps, parsedState, parsedHooks} = data; setComponentAttrs({ - state: parsedProps, - props: parsedState, + props: parsedProps, + state: parsedState, + hooks: parsedHooks, }); } } 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/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(); } From 54ad44525e96ec6d3ee516f85233110609655297 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 21 Apr 2022 14:56:02 +0800 Subject: [PATCH 9/9] Match-id-63ab7802bc94b2c6034d615683e1c3dc915e8db9 --- .../src/devtools/mockPage/MockContext.ts | 3 +++ .../mockPage/MockFunctionComponent.tsx | 22 +++++++++++++++---- .../extension/src/devtools/mockPage/index.tsx | 10 +++++---- 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 libs/extension/src/devtools/mockPage/MockContext.ts 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);