Match-id-a001034c114f96898ee9724d85fd7af88484cd4c
This commit is contained in:
commit
c8ccf1ef60
|
@ -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 的行为从而展开测试工作。
|
||||
|
||||
|
|
|
@ -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') {
|
||||
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[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);
|
||||
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'});
|
||||
});
|
||||
|
|
|
@ -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[] }) {
|
|||
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||
{' :'}
|
||||
<span className={styles.attrValue}>{item.value}</span>
|
||||
{item.type === 'string' || item.type === 'number'
|
||||
? <input value={item.value} className={styles.attrValue}>{item.value}</input>
|
||||
: <span className={styles.attrValue}>{item.value}</span>}
|
||||
</div>
|
||||
);
|
||||
if (isCollapsed) {
|
||||
|
@ -106,9 +105,9 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
|
|||
</div>
|
||||
<div className={styles.componentInfoMain}>
|
||||
{context && <ComponentAttr name={'context'} attrs={context} />}
|
||||
{props && <ComponentAttr name={'props'} attrs={props} />}
|
||||
{state && <ComponentAttr name={'state'} attrs={state} />}
|
||||
{hooks && <ComponentAttr name={'hook'} attrs={hooks} />}
|
||||
{props && props.length !== 0 && <ComponentAttr name={'props'} attrs={props} />}
|
||||
{state && state.length !== 0 && <ComponentAttr name={'state'} attrs={state} />}
|
||||
{hooks && hooks.length !== 0 && <ComponentAttr name={'hook'} attrs={hooks} />}
|
||||
<div className={styles.parentsInfo}>
|
||||
{name && <div>
|
||||
parents: {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { useState, useRef, useEffect } from 'horizon';
|
||||
import styles from './VList.less';
|
||||
|
||||
interface IProps<T extends { id: string }> {
|
||||
interface IProps<T extends { id: number | string }> {
|
||||
data: T[],
|
||||
width: number, // 暂时未用到,当需要支持横向滚动时使用
|
||||
height: number, // VList 的高度
|
||||
|
@ -20,7 +20,7 @@ export type renderInfoType<T> = {
|
|||
skipItemCountBeforeScrollItem: number,
|
||||
};
|
||||
|
||||
export function VList<T extends { id: string }>(props: IProps<T>) {
|
||||
export function VList<T extends { id: number | string }>(props: IProps<T>) {
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
.treeItem {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
line-height: 18px;
|
||||
line-height: 1.125rem;
|
||||
|
||||
&:hover {
|
||||
background-color: @select-color;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { createContext } from 'horizon';
|
||||
|
||||
export const MockContext = createContext({value: 'default context value'});
|
|
@ -1,11 +1,25 @@
|
|||
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<HTMLDivElement>();
|
||||
const objRef = useRef({ str: 'string' });
|
||||
const context = useContext(MockContext);
|
||||
|
||||
useEffect(() => { }, []);
|
||||
|
||||
|
@ -16,7 +30,7 @@ export default function MockFunctionComponent(props) {
|
|||
count: {props.count}
|
||||
<div ref={domRef} />
|
||||
<div>{objRef.current.str}</div>
|
||||
<Ctx.Provider value={{ctx: 'I am ctx'}}></Ctx.Provider>
|
||||
<div>{context.ctx}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
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 (
|
||||
<div>
|
||||
abc
|
||||
<MockContext.Provider value={{ ctx: 'I am ctx' }}>
|
||||
<MockClassComponent fruit={'apple'} />
|
||||
<MockFunctionComponent />
|
||||
</MockContext.Provider>
|
||||
abc
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}, '*');
|
||||
addIfNotInclude,
|
||||
send,
|
||||
deleteVNode,
|
||||
},
|
||||
listen: function (id: number) {
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type && (event.data.type === 'HORIZON_DEV_TOOLS') && event.data.id === id) {
|
||||
console.log('todo');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
injectHook();
|
||||
|
|
|
@ -18,5 +18,10 @@
|
|||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": []
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [ "injector.js", "background.js" ],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
if (isDev) {
|
||||
setComponentAttrs({
|
||||
state: parsedMockState,
|
||||
props: parsedMockState,
|
||||
});
|
||||
} else {
|
||||
postMessage(RequestComponentAttrs, item.id);
|
||||
}
|
||||
setSelectComp(item);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,26 +1,52 @@
|
|||
import { IAttr } from "../components/ComponentInfo";
|
||||
|
||||
// 将状态的值解析成固定格式
|
||||
export function parseAttr(rootAttr: any) {
|
||||
const result: IAttr[] = [];
|
||||
const indentation = 0;
|
||||
const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => {
|
||||
const stateType = typeof attr;
|
||||
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;
|
||||
let showType: showType;
|
||||
let addSubState;
|
||||
if (stateType === 'boolean' ||
|
||||
stateType === 'number' ||
|
||||
stateType === 'string' ||
|
||||
stateType === 'undefined') {
|
||||
if (attrType === 'boolean' ||
|
||||
attrType === 'number' ||
|
||||
attrType === 'string' ||
|
||||
attrType === 'undefined') {
|
||||
value = attr;
|
||||
showType = stateType;
|
||||
} else if (stateType === 'function') {
|
||||
showType = attrType;
|
||||
} else if (attrType === 'function') {
|
||||
const funName = attr.name;
|
||||
value = `f() ${funName}{}`;
|
||||
} else if (stateType === 'symbol') {
|
||||
} else if (attrType === 'symbol') {
|
||||
value = attr.description;
|
||||
} else if (stateType === 'object') {
|
||||
} else if (attrType === 'object') {
|
||||
if (attr === null) {
|
||||
showType = 'null';
|
||||
} else if (attr instanceof Map) {
|
||||
|
@ -29,7 +55,7 @@ export function parseAttr(rootAttr: any) {
|
|||
value = `Map(${size})`;
|
||||
addSubState = () => {
|
||||
attr.forEach((value, key) => {
|
||||
parseSubAttr(value, parentIndentation + 2, key);
|
||||
parseSubAttr(value, parentIndentation + 2, key, result);
|
||||
});
|
||||
};
|
||||
} else if (attr instanceof Set) {
|
||||
|
@ -39,7 +65,7 @@ export function parseAttr(rootAttr: any) {
|
|||
addSubState = () => {
|
||||
let i = 0;
|
||||
attr.forEach((value) => {
|
||||
parseSubAttr(value, parentIndentation + 2, String(i));
|
||||
parseSubAttr(value, parentIndentation + 2, String(i), result);
|
||||
});
|
||||
i++;
|
||||
};
|
||||
|
@ -48,32 +74,60 @@ export function parseAttr(rootAttr: any) {
|
|||
value = `Array(${attr.length})`;
|
||||
addSubState = () => {
|
||||
attr.forEach((value, index) => {
|
||||
parseSubAttr(value, parentIndentation + 2, String(index));
|
||||
parseSubAttr(value, parentIndentation + 2, String(index), result);
|
||||
});
|
||||
};
|
||||
} else if (attr instanceof Element) {
|
||||
showType = 'dom';
|
||||
value = attr.tagName;
|
||||
} else {
|
||||
showType = stateType;
|
||||
showType = attrType;
|
||||
value = '{...}';
|
||||
addSubState = () => {
|
||||
Object.keys(attr).forEach((key) => {
|
||||
parseSubAttr(attr[key], parentIndentation + 2, key);
|
||||
parseSubAttr(attr[key], parentIndentation + 2, key, result);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
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;
|
||||
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<any, any>[]) {
|
||||
const result: IAttr[] = [];
|
||||
const indentation = 0;
|
||||
hooks.forEach(hook => {
|
||||
const { hIndex, state ,type } = hook;
|
||||
if (type === 'useState') {
|
||||
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'state', result, hIndex);
|
||||
} else if (type === 'useRef') {
|
||||
parseSubAttr((state as Ref<any>).current, indentation, 'ref', result, hIndex);
|
||||
} else if (type === 'useReducer') {
|
||||
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'reducer', result, hIndex);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {EffectConstant} from './EffectConstant';
|
|||
export interface Hook<S, A> {
|
||||
state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>;
|
||||
hIndex: number;
|
||||
type?: 'useState' | 'useRef' | 'useReducer';
|
||||
}
|
||||
|
||||
export interface Reducer<S, A> {
|
||||
|
|
|
@ -87,6 +87,7 @@ export function useReducerForInit<S, A>(reducer, initArg, init, isUseState?: boo
|
|||
}
|
||||
|
||||
const hook = createHook();
|
||||
hook.type = isUseState ? 'useState' : 'useReducer';
|
||||
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
|
||||
hook.state = {
|
||||
stateValue: stateValue,
|
||||
|
|
|
@ -12,6 +12,7 @@ export function useRefImpl<V>(value: V): Ref<V> {
|
|||
if (stage === HookStage.Init) {
|
||||
hook = createHook();
|
||||
hook.state = {current: value};
|
||||
hook.type = 'useRef';
|
||||
} else if (stage === HookStage.Update) {
|
||||
hook = getCurrentHook();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue