Match-id-ebcb6eea592b64d78460f9977b1ad0236ad8285d
This commit is contained in:
commit
28dc82a7c4
|
@ -0,0 +1,57 @@
|
|||
## 文件清单说明:
|
||||
devtools_page: devtool主页面
|
||||
default_popup: 拓展图标点击时弹窗页面
|
||||
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
|
||||
web_accessible_resources: 注入全局变量代码
|
||||
|
||||
## 打开 panel 页面调试面板的方式
|
||||
|
||||
1. Open the developer tools.
|
||||
1. Undock the developer tools if not already done (via the button in the bottom-left corner).
|
||||
1. Press Ctrl + Shift + J to open the developer tools of the developer tools.
|
||||
Optional: Feel free to dock the developer tools again if you had undocked it at step 2.
|
||||
1. Switch from "<top frame>" to devtoolsBackground.html (or whatever name you have chosen for your devtools). (example)
|
||||
1. Now you can use the Console tab to play with the chrome.devtools API.
|
||||
|
||||
## 全局变量注入
|
||||
通过content_scripts在document初始化时给页面添加script脚本,在新添加的脚本中给window注入全局变量
|
||||
|
||||
## horizon页面判断
|
||||
在页面完成渲染后往全局变量中添加信息,并传递 tabId 给 background 告知这是 horizon 页面
|
||||
|
||||
## 通信方式:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant web_page
|
||||
participant script_content
|
||||
participant background
|
||||
participant panel
|
||||
|
||||
Note over web_page: window.postMessage
|
||||
web_page ->> script_content : {}
|
||||
Note over script_content: window.addEventListener
|
||||
Note over script_content: chrome.runtime.sendMessage
|
||||
script_content ->> background : {}
|
||||
Note over background: chrome.runtime.onMessage
|
||||
Note over background: port.postMessage
|
||||
background ->> panel : {}
|
||||
Note over panel: connection.onMessage.addListener
|
||||
Note over panel: connection.postMessage
|
||||
panel ->> background : {}
|
||||
Note over background: port.onMessage.addListener
|
||||
Note over background: chrome.tabs.sendMessage
|
||||
background ->> script_content : {}
|
||||
Note over script_content: chrome.runtime.onMessage
|
||||
Note over script_content: window.postMessage
|
||||
script_content ->> web_page : {}
|
||||
Note over web_page: window.addEventListener
|
||||
```
|
||||
|
||||
## 数据压缩
|
||||
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
|
||||
- 相同的组件名可以进行压缩
|
||||
- 每个vNode有唯一的 path 属性,可以作为标识使用
|
||||
- 通过解析 path 值可以分析出组件树的结构
|
||||
|
||||
## 滚动动态渲染 Tree
|
||||
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
|
|
@ -0,0 +1,108 @@
|
|||
import styles from './ComponentsInfo.less';
|
||||
import Eye from '../svgs/Eye';
|
||||
import Debug from '../svgs/Debug';
|
||||
import Copy from '../svgs/Copy';
|
||||
import Arrow from '../svgs/Arrow';
|
||||
import { useState } from 'horizon';
|
||||
|
||||
type IComponentInfo = {
|
||||
name: string;
|
||||
attrs: {
|
||||
props?: IAttr[];
|
||||
context?: IAttr[];
|
||||
state?: IAttr[];
|
||||
hooks?: IAttr[];
|
||||
}
|
||||
};
|
||||
|
||||
type IAttr = {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string | boolean;
|
||||
indentation: number;
|
||||
}
|
||||
|
||||
function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
|
||||
const [collapsedNode, setCollapsedNode] = useState(new Set());
|
||||
const handleCollapse = (index: number) => {
|
||||
const newSet = new Set<number>();
|
||||
collapsedNode.forEach(value => {
|
||||
newSet.add(value);
|
||||
});
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index);
|
||||
} else {
|
||||
newSet.add(index);
|
||||
}
|
||||
setCollapsedNode(newSet);
|
||||
};
|
||||
|
||||
const showAttr = [];
|
||||
let currentIndentation = null;
|
||||
attr.forEach((item, index) => {
|
||||
const indentation = item.indentation;
|
||||
if (currentIndentation !== null) {
|
||||
if (indentation > currentIndentation) {
|
||||
return;
|
||||
} else {
|
||||
currentIndentation = null;
|
||||
}
|
||||
}
|
||||
const nextItem = attr[index + 1];
|
||||
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||
const isCollapsed = collapsedNode.has(index);
|
||||
showAttr.push(
|
||||
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(index))}>
|
||||
<span className={styles.attrArrow}>{hasChild && <Arrow director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||
{' :'}
|
||||
<span className={styles.attrValue}>{item.value}</span>
|
||||
</div>
|
||||
);
|
||||
if (isCollapsed) {
|
||||
currentIndentation = indentation;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.attrContainer}>
|
||||
<div className={styles.attrHead}>
|
||||
<span className={styles.attrType}>{name}</span>
|
||||
<span className={styles.attrCopy}>
|
||||
<Copy />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.attrDetail}>
|
||||
{showAttr}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComponentInfo({ name, attrs }: IComponentInfo) {
|
||||
const { state, props, context, hooks } = attrs;
|
||||
return (
|
||||
<div className={styles.infoContainer} >
|
||||
<div className={styles.componentInfoHead}>
|
||||
<span className={styles.name}>
|
||||
{name}
|
||||
</span>
|
||||
<span className={styles.eye} >
|
||||
<Eye />
|
||||
</span>
|
||||
<span className={styles.debug}>
|
||||
<Debug />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.componentInfoMain}>
|
||||
{context && <ComponentAttr name={'context'} attr={context} />}
|
||||
{props && <ComponentAttr name={'props'} attr={props} />}
|
||||
{state && <ComponentAttr name={'state'} attr={state} />}
|
||||
{hooks && <ComponentAttr name={'hook'} attr={hooks} />}
|
||||
<div className={styles.renderInfo}>
|
||||
rendered by
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
@import 'assets.less';
|
||||
|
||||
.infoContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
|
||||
.componentInfoHead {
|
||||
flex: 0 0 @top-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: @divider-style;
|
||||
|
||||
.name {
|
||||
flex: 1 1 0;
|
||||
padding: 0 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.eye {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.debug {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.componentInfoMain {
|
||||
overflow-y: auto;
|
||||
|
||||
:last-child {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
padding: unset;
|
||||
}
|
||||
|
||||
>div {
|
||||
border-bottom: @divider-style;
|
||||
padding: 0.5rem
|
||||
}
|
||||
|
||||
.attrContainer {
|
||||
flex: 0 0;
|
||||
|
||||
.attrHead {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.5rem 0 0.5rem;
|
||||
|
||||
.attrType {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
|
||||
.attrCopy {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.attrDetail {
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
.attrArrow {
|
||||
color: @arrow-color;
|
||||
width: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.attrName {
|
||||
color: @attr-name-color;
|
||||
}
|
||||
|
||||
.attrValue {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.renderInfo {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.search {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import styles from './Search.less';
|
||||
|
||||
interface SearchProps {
|
||||
onChange: (event: any) => void,
|
||||
}
|
||||
|
||||
export default function Search(props: SearchProps) {
|
||||
const { onChange } = props;
|
||||
const handleChange = (event) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
return (
|
||||
<input
|
||||
onChange={handleChange}
|
||||
className={styles.search}
|
||||
placeholder={'Search (text or /regex/)'}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
@import 'assets.less';
|
||||
|
||||
.treeContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.treeItem {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
line-height: 18px;
|
||||
|
||||
&:hover {
|
||||
background-color: @select-color;
|
||||
}
|
||||
|
||||
.treeIcon {
|
||||
color: @arrow-color;
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.componentName {
|
||||
color: @component-name-color;
|
||||
}
|
||||
|
||||
.componentKeyName {
|
||||
color: @component-key-color;
|
||||
}
|
||||
|
||||
.componentKeyValue {
|
||||
color: @componentKeyValue-color;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: rgb(141 199 248 / 60%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
import { useState } from 'horizon';
|
||||
import styles from './VTree.less';
|
||||
import Arrow from '../svgs/Arrow';
|
||||
import { createRegExp } from './../utils';
|
||||
|
||||
export interface IData {
|
||||
id: string;
|
||||
name: string;
|
||||
indentation: number;
|
||||
userKey: string;
|
||||
}
|
||||
|
||||
type IItem = {
|
||||
style: any,
|
||||
hasChild: boolean,
|
||||
onCollapse: (id: string) => void,
|
||||
onClick: (id: string) => void,
|
||||
isCollapsed: boolean,
|
||||
isSelect: boolean,
|
||||
highlightValue: string,
|
||||
} & IData
|
||||
|
||||
// TODO: 计算可以展示的最多数量,并且监听显示器高度变化修改数值
|
||||
const showNum = 70;
|
||||
const lineHeight = 18;
|
||||
const indentationLength = 20;
|
||||
|
||||
function Item(props: IItem) {
|
||||
const {
|
||||
name,
|
||||
style,
|
||||
userKey,
|
||||
hasChild,
|
||||
onCollapse,
|
||||
isCollapsed,
|
||||
id,
|
||||
indentation,
|
||||
onClick,
|
||||
isSelect,
|
||||
highlightValue,
|
||||
} = props;
|
||||
const isShowKey = userKey !== '';
|
||||
const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||
const handleClickCollapse = () => {
|
||||
onCollapse(id);
|
||||
};
|
||||
const handleClick = () => {
|
||||
onClick(id);
|
||||
};
|
||||
const itemAttr: any = { style, className: styles.treeItem, onClick: handleClick };
|
||||
if (isSelect) {
|
||||
itemAttr.tabIndex = 0;
|
||||
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
||||
}
|
||||
const reg = createRegExp(highlightValue);
|
||||
const heightCharacters = name.match(reg);
|
||||
let showName;
|
||||
if (heightCharacters) {
|
||||
let cutName = name;
|
||||
showName = [];
|
||||
// 高亮第一次匹配即可
|
||||
const char = heightCharacters[0];
|
||||
const index = name.search(char);
|
||||
const notHighlightStr = cutName.slice(0, index);
|
||||
showName.push(notHighlightStr);
|
||||
showName.push(<mark>{char}</mark>);
|
||||
cutName = cutName.slice(index + char.length);
|
||||
showName.push(cutName);
|
||||
} else {
|
||||
showName = name;
|
||||
}
|
||||
return (
|
||||
<div {...itemAttr}>
|
||||
<div style={{ marginLeft: indentation * indentationLength }} className={styles.treeIcon} onClick={handleClickCollapse} >
|
||||
{showIcon}
|
||||
</div>
|
||||
<span className={styles.componentName} >
|
||||
{showName}
|
||||
</span>
|
||||
{isShowKey && (
|
||||
<>
|
||||
<span className={styles.componentKeyName}>
|
||||
{' '}key
|
||||
</span>
|
||||
{'="'}
|
||||
<span className={styles.componentKeyValue}>
|
||||
{userKey}
|
||||
</span>
|
||||
{'"'}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const [collapseNode, setCollapseNode] = useState(new Set<string>());
|
||||
const [selectItem, setSelectItem] = useState();
|
||||
const changeCollapseNode = (id: string) => {
|
||||
const nodes = new Set<string>();
|
||||
collapseNode.forEach(value => {
|
||||
nodes.add(value);
|
||||
});
|
||||
if (nodes.has(id)) {
|
||||
nodes.delete(id);
|
||||
} else {
|
||||
nodes.add(id);
|
||||
}
|
||||
setCollapseNode(nodes);
|
||||
};
|
||||
const handleClickItem = (id: string) => {
|
||||
setSelectItem(id);
|
||||
};
|
||||
const showList: any[] = [];
|
||||
|
||||
let totalHeight = 0;
|
||||
let currentCollapseIndentation: null | number = null;
|
||||
data.forEach((item, index) => {
|
||||
// 存在未处理完的收起节点
|
||||
if (currentCollapseIndentation !== null) {
|
||||
const indentation = item.indentation;
|
||||
// 缩进更大,不显示
|
||||
if (indentation > currentCollapseIndentation) {
|
||||
return;
|
||||
} else {
|
||||
// 缩进小,说明完成了该收起节点的子节点处理。
|
||||
currentCollapseIndentation = null;
|
||||
}
|
||||
}
|
||||
const id = item.id;
|
||||
const isCollapsed = collapseNode.has(id);
|
||||
if (totalHeight >= scrollTop && showList.length <= showNum) {
|
||||
const nextItem = data[index + 1];
|
||||
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
|
||||
const hasChild = nextItem ? nextItem.indentation > item.indentation : false;
|
||||
showList.push(
|
||||
<Item
|
||||
key={id}
|
||||
hasChild={hasChild}
|
||||
style={{
|
||||
transform: `translateY(${totalHeight}px)`,
|
||||
}}
|
||||
onCollapse={changeCollapseNode}
|
||||
onClick={handleClickItem}
|
||||
isCollapsed={isCollapsed}
|
||||
isSelect={id === selectItem}
|
||||
highlightValue={highlightValue}
|
||||
{...item} />
|
||||
);
|
||||
}
|
||||
totalHeight = totalHeight + lineHeight;
|
||||
if (isCollapsed) {
|
||||
// 该节点需要收起子节点
|
||||
currentCollapseIndentation = item.indentation;
|
||||
}
|
||||
});
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
const scrollTop = event.target.scrollTop;
|
||||
// 顶部留 100px 冗余高度
|
||||
setScrollTop(Math.max(scrollTop - 100, 0));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.treeContainer} onScroll={handleScroll}>
|
||||
{showList}
|
||||
{/* 确保有足够的高度 */}
|
||||
<div style={{ marginTop: totalHeight }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VTree;
|
|
@ -0,0 +1,14 @@
|
|||
@arrow-color: rgb(95, 99, 104);
|
||||
@divider-color: rgb(202, 205, 209);
|
||||
@attr-name-color: rgb(200, 0, 0);
|
||||
@component-name-color: rgb(136, 18, 128);
|
||||
@component-key-color: rgb(153, 69, 0);
|
||||
@componentKeyValue-color: rgb(26, 26, 166);
|
||||
@component-attr-color: rgb(200, 0, 0);
|
||||
@select-color: rgb(141 199 248 / 60%);
|
||||
|
||||
@top-height: 2.625rem;
|
||||
@divider-width: 0.2px;
|
||||
@common-font-size: 12px;
|
||||
|
||||
@divider-style: @divider-color solid @divider-width;
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 用一个纯数据类型的对象 tree 去表示树的结构是非常清晰的,但是它不能准确的模拟 VNode 中存在的引用
|
||||
* 关系,需要进行转换 getMockVNodeTree
|
||||
*/
|
||||
|
||||
import { parseAttr } from '../parser/parseAttr';
|
||||
import parseTreeRoot from '../parser/parseVNode';
|
||||
import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
|
||||
import { FunctionComponent, ClassComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
|
||||
|
||||
const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi', 'Coconut'];
|
||||
|
||||
function MockVNode(tag: string, props = {}, key = null, realNode = {}) {
|
||||
const vNode = new VNode(tag, props, key, realNode);
|
||||
const name = mockComponentNames.shift() || 'MockComponent';
|
||||
vNode.type = { name };
|
||||
return vNode;
|
||||
}
|
||||
|
||||
interface IMockTree {
|
||||
tag: string,
|
||||
children?: IMockTree[],
|
||||
}
|
||||
|
||||
// 模拟树
|
||||
const tree: IMockTree = {
|
||||
tag: ClassComponent,
|
||||
children: [
|
||||
{ tag: FunctionComponent },
|
||||
{ tag: ClassComponent },
|
||||
{ tag: FunctionComponent },
|
||||
{
|
||||
tag: FunctionComponent,
|
||||
children: [
|
||||
{ tag: ClassComponent }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function addOneThousandNode(node: IMockTree) {
|
||||
const nodes = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
nodes.push({ tag: FunctionComponent });
|
||||
}
|
||||
node?.children.push({ tag: ClassComponent, children: nodes });
|
||||
}
|
||||
|
||||
addOneThousandNode(tree);
|
||||
|
||||
/**
|
||||
* 将mock数据转变为 VNode 树
|
||||
*
|
||||
* @param node 树节点
|
||||
* @param vNode VNode节点
|
||||
*/
|
||||
function getMockVNodeTree(node: IMockTree, vNode: VNode) {
|
||||
const children = node.children;
|
||||
if (children && children.length !== 0) {
|
||||
const childNode = children[0];
|
||||
let childVNode = MockVNode(childNode.tag);
|
||||
childVNode.key = '0';
|
||||
getMockVNodeTree(childNode, childVNode);
|
||||
// 需要建立双链
|
||||
vNode.child = childVNode;
|
||||
childVNode.parent = vNode;
|
||||
for (let i = 1; i < children.length; i++) {
|
||||
const nextNode = children[i];
|
||||
const nextVNode = MockVNode(nextNode.tag);
|
||||
nextVNode.key = String(i);
|
||||
nextVNode.parent = vNode;
|
||||
getMockVNodeTree(nextNode, nextVNode);
|
||||
childVNode.next = nextVNode;
|
||||
childVNode = nextVNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootVNode = MockVNode(tree.tag);
|
||||
getMockVNodeTree(tree, rootVNode);
|
||||
|
||||
export const mockParsedVNodeData = parseTreeRoot(rootVNode);
|
||||
|
||||
const mockState = {
|
||||
str: 'jenny',
|
||||
num: 3,
|
||||
boolean: true,
|
||||
und: undefined,
|
||||
fun: () => ({}),
|
||||
symbol: Symbol('sym'),
|
||||
map: new Map([['a', 'a']]),
|
||||
set: new Set(['a', 1, 2, Symbol('bambi')]),
|
||||
arr: [1, 2, 3, 4],
|
||||
obj: {
|
||||
niko: { jenny: 'jenny' }
|
||||
}
|
||||
};
|
||||
|
||||
export const parsedMockState = parseAttr(mockState);
|
|
@ -0,0 +1,53 @@
|
|||
@import '../components/assets.less';
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
font-size: @common-font-size;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.left_top {
|
||||
border-bottom: @divider-style;
|
||||
flex: 0 0 @top-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.select {
|
||||
padding: 0 0.25rem 0 0.25rem;
|
||||
flex: 0 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
flex: 0 0 1px;
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
border-left: @divider-style;
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left_bottom {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 3;
|
||||
border-left: @divider-style;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { useState, useEffect } from 'horizon';
|
||||
import VTree, { IData } from '../components/VTree';
|
||||
import Search from '../components/Search';
|
||||
import ComponentInfo from '../components/ComponentInfo';
|
||||
import styles from './App.less';
|
||||
import Select from '../svgs/Select';
|
||||
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
||||
|
||||
function App() {
|
||||
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||
const [componentInfo, setComponentInfo] = useState({ name: null, attrs: {} });
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
useEffect(() => {
|
||||
if (isDev) {
|
||||
setParsedVNodeData(mockParsedVNodeData);
|
||||
setComponentInfo({
|
||||
name: 'Demo',
|
||||
attrs: {
|
||||
state: parsedMockState,
|
||||
props: parsedMockState,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const idIndentationMap: {
|
||||
[id: string]: number;
|
||||
} = {};
|
||||
const data: IData[] = [];
|
||||
let i = 0;
|
||||
while (i < parsedVNodeData.length) {
|
||||
const id = parsedVNodeData[i] as string;
|
||||
i++;
|
||||
const name = parsedVNodeData[i] as string;
|
||||
i++;
|
||||
const parentId = parsedVNodeData[i] as string;
|
||||
i++;
|
||||
const userKey = parsedVNodeData[i] as string;
|
||||
i++;
|
||||
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
|
||||
idIndentationMap[id] = indentation;
|
||||
const item = {
|
||||
id, name, indentation, userKey
|
||||
};
|
||||
data.push(item);
|
||||
}
|
||||
|
||||
const handleSearchChange = (str: string) => {
|
||||
setFilterValue(str);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.left_top} >
|
||||
<div className={styles.select} >
|
||||
<Select />
|
||||
</div>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.search}>
|
||||
<Search onChange={handleSearchChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.left_bottom}>
|
||||
<VTree data={data} highlightValue={filterValue} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<ComponentInfo name={componentInfo.name} attrs={componentInfo.attrs} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,7 @@
|
|||
import { render } from 'horizon';
|
||||
import App from './App';
|
||||
|
||||
render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Horizon</title>
|
||||
<script src='horizon.production.js'></script>
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,83 @@
|
|||
|
||||
// 将状态的值解析成固定格式
|
||||
export function parseAttr(rootAttr: any) {
|
||||
const result: {
|
||||
name: string,
|
||||
type: string,
|
||||
value: string,
|
||||
indentation: number
|
||||
}[] = [];
|
||||
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();
|
||||
}
|
||||
};
|
||||
Object.keys(rootAttr).forEach(key => {
|
||||
parseSubAttr(rootAttr[key], indentation, key);
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { travelVNodeTree } from '../../../../libs/horizon/src/renderer/vnode/VNodeUtils';
|
||||
import { VNode } from '../../../../libs/horizon/src/renderer/Types';
|
||||
import { ClassComponent, FunctionComponent } from '../../../../libs/horizon/src/renderer/vnode/VNodeTags';
|
||||
|
||||
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
|
||||
const VNodeToIdMap = new Map<VNode, number>();
|
||||
const IdToVNodeMap = new Map<number, VNode>();
|
||||
|
||||
let uid = 0;
|
||||
function generateUid () {
|
||||
uid++;
|
||||
return uid;
|
||||
}
|
||||
|
||||
function isUserComponent(tag: string) {
|
||||
// TODO: 添加其他组件
|
||||
return tag === ClassComponent || tag === FunctionComponent;
|
||||
}
|
||||
|
||||
function getParentUserComponent(node: VNode) {
|
||||
let parent = node.parent;
|
||||
while(parent) {
|
||||
if (isUserComponent(parent.tag)) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function parseTreeRoot(treeRoot: VNode) {
|
||||
const result: any[] = [];
|
||||
travelVNodeTree(treeRoot, (node: VNode) => {
|
||||
const tag = node.tag;
|
||||
if (isUserComponent(tag)) {
|
||||
const id = generateUid();
|
||||
result.push(id);
|
||||
const name = node.type.name;
|
||||
result.push(name);
|
||||
const parent = getParentUserComponent(node);
|
||||
if (parent) {
|
||||
const parentId = VNodeToIdMap.get(parent);
|
||||
result.push(parentId);
|
||||
} else {
|
||||
result.push('');
|
||||
}
|
||||
const key = node.key;
|
||||
if (key !== null) {
|
||||
result.push(key);
|
||||
} else {
|
||||
result.push('');
|
||||
}
|
||||
VNodeToIdMap.set(node, id);
|
||||
IdToVNodeMap.set(id, node);
|
||||
}
|
||||
}, null, treeRoot, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
export default parseTreeRoot;
|
|
@ -0,0 +1,17 @@
|
|||
interface IArrow {
|
||||
director: 'right' | 'down'
|
||||
}
|
||||
|
||||
export default function Arrow({ director }: IArrow) {
|
||||
let d: string;
|
||||
if (director === 'right') {
|
||||
d = 'm2 0l12 8l-12 8 z';
|
||||
} else if (director === 'down') {
|
||||
d = 'm0 2h16 l-8 12 z';
|
||||
}
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
|
||||
<path d={d} fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Copy() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Debug() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='m2 0l12 8l-12 8 z' fill='#000'/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
export default function Eye() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<ellipse cx="8" cy="8" rx="8" ry="6" />
|
||||
<circle cx="8" cy="8" r="4" fill="rgb(255, 255, 255)" />
|
||||
<circle cx="8" cy="8" r="2" fill="#000000" />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Select() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M14 6 V3 C14 2.5 13.5 2 13 2 H3 C2.5 2 2 2.5 2 3 V13 C2 13.5 2.5 14 3 14H6 V13 H3 V3 H13 V6z M7 7 L9 15 L11 12 L14 15 L15 14 L12 11 L15 9z' fill='#000' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
export function createRegExp(expression: string) {
|
||||
let str = expression;
|
||||
if (str[0] === '/') {
|
||||
str = str.slice(1);
|
||||
}
|
||||
if (str[str.length - 1] === '/') {
|
||||
str = str.slice(0, str.length - 1);
|
||||
}
|
||||
try {
|
||||
return new RegExp(str, 'i');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// 用于 panel 页面开发
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
panel: path.join(__dirname, './src/panel/index.tsx'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env',
|
||||
'@babel/preset-typescript',
|
||||
['@babel/preset-react', {
|
||||
runtime: 'classic',
|
||||
'pragma': 'Horizon.createElement',
|
||||
'pragmaFrag': 'Horizon.Fragment',
|
||||
}]],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.less/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
|
||||
}
|
||||
},
|
||||
'less-loader'],
|
||||
}]
|
||||
},
|
||||
externals: {
|
||||
'horizon': 'Horizon',
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'dist'),
|
||||
},
|
||||
open: 'panel.html',
|
||||
port: 9000,
|
||||
magicHtml: true,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'panel.html',
|
||||
template: './src/panel/panel.html'
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
isDev: 'true',
|
||||
}),
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue