Match-id-d6cdcae074e494e26e431e1f3439c1e8daa08ddf
This commit is contained in:
commit
c6af88190c
|
@ -1,15 +0,0 @@
|
|||
module.exports = api => {
|
||||
const isTest = api.env('test');
|
||||
console.log('isTest', isTest);
|
||||
return {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-typescript',
|
||||
['@babel/preset-react', {
|
||||
runtime: 'classic',
|
||||
'pragma': 'Horizon.createElement',
|
||||
'pragmaFrag': 'Horizon.Fragment',
|
||||
}]],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
};
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"name": "extension",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "",
|
||||
"scripts": {
|
||||
"build": "webpack --config ./webpack.config.js",
|
||||
"watch": "webpack --config ./webpack.config.js --watch",
|
||||
"build-dev": "webpack --config ./webpack.dev.js",
|
||||
"start": "npm run build && webpack serve --config ./webpack.dev.js ",
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.12.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/preset-env": "7.12.1",
|
||||
"@babel/preset-react": "7.12.1",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@types/jest": "27.4.1",
|
||||
"babel-loader": "8.1.0",
|
||||
"css-loader": "6.7.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"jest": "27.5.1",
|
||||
"less": "4.1.2",
|
||||
"less-loader": "10.2.0",
|
||||
"style-loader": "3.3.1",
|
||||
"ts-jest": "27.1.4",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
}
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
## 为什么要做 devTool 插件
|
||||
让Horizon开发者获得更好的开发体验,获取准确的组件树结构、状态信息和真实dom对应关系。
|
||||
|
||||
## 上下文关系
|
||||
devTool功能的实现依赖浏览器 extension 开放的能力,用于绘制展示组件信息和获取真实 dom 元素。同时也需要 Horizon 提供相关接口获取组件树信息和提供调试能力。
|
||||
|
||||
## 目标
|
||||
1. 查看组件树结构并支持过滤
|
||||
2. 查看组件与真实dom的关系
|
||||
3. 查看组件props, state, hooks 等信息
|
||||
4. 调试单个组件及其子组件
|
||||
5. 支持状态管理解决方案调试
|
||||
|
||||
## 和 react devTool 能力对比
|
||||
||react | Horizon|
|
||||
|-|-|-|
|
||||
|查看组件树|Y |Y |
|
||||
|查看真实DOM|Y|Y|
|
||||
|查看组件信息|Y|Y|
|
||||
|调试能力|Y| Y |
|
||||
|性能调试|Y|N|
|
||||
|解析Hook名|Y|N|
|
||||
|状态管理解决方案调试|N|Y|
|
||||
|
||||
## 架构草图
|
||||
```plantuml
|
||||
@startuml
|
||||
package "Horizon" {
|
||||
[U I]
|
||||
[Helper]
|
||||
}
|
||||
|
||||
package "Script Content" {
|
||||
[GlobalHook]
|
||||
[MessageHandler]
|
||||
[Parser]
|
||||
|
||||
}
|
||||
package "Browser" {
|
||||
[Background]
|
||||
[Panel]
|
||||
}
|
||||
|
||||
[GlobalHook] <-- [U I]
|
||||
[GlobalHook] --> [MessageHandler]
|
||||
[Helper] <-- [MessageHandler]
|
||||
[Helper] --> [U I]
|
||||
[MessageHandler] <--> [Background]
|
||||
[Background] <--> [Panel]
|
||||
[Parser] --> [MessageHandler]
|
||||
@enduml
|
||||
```
|
||||
|
||||
#### 说明
|
||||
Helper: 提供接口给插件操控组件以及提供工具方法。
|
||||
Parser: 负责将组件树结构和组件信息解析成特定的数据结构,供Panel展示。
|
||||
|
||||
## 文件清单说明:
|
||||
devtools_page: devtool主页面
|
||||
default_popup: 拓展图标点击时弹窗页面
|
||||
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
|
||||
|
||||
## 打开 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注入全局变量
|
||||
|
||||
## 通信方式:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant web_page
|
||||
participant script_content
|
||||
participant background
|
||||
participant panel
|
||||
|
||||
Note over web_page: window.postMessage
|
||||
web_page ->> script_content : data
|
||||
Note over script_content: window.addEventListener
|
||||
Note over script_content: chrome.runtime.sendMessage
|
||||
script_content ->> background : data
|
||||
Note over background: chrome.runtime.onMessage
|
||||
Note over background: port.postMessage
|
||||
background ->> panel : data
|
||||
Note over panel: connection.onMessage.addListener
|
||||
Note over panel: connection.postMessage
|
||||
panel ->> background : data
|
||||
Note over background: port.onMessage.addListener
|
||||
Note over background: chrome.tabs.sendMessage
|
||||
background ->> script_content : data
|
||||
Note over script_content: chrome.runtime.onMessage
|
||||
Note over script_content: window.postMessage
|
||||
script_content ->> web_page : data
|
||||
Note over web_page: window.addEventListener
|
||||
```
|
||||
|
||||
## 传输数据结构
|
||||
**<font color=#8B0000>限制:chrome.runtime.sendMessage只能传递 JSON-serializable 数据</font>**
|
||||
|
||||
|
||||
```ts
|
||||
type passData = {
|
||||
type: 'HORIZON_DEV_TOOLS',
|
||||
payload: {
|
||||
type: string,
|
||||
data: any,
|
||||
},
|
||||
from: string,
|
||||
}
|
||||
```
|
||||
|
||||
## horizon和devTools的主要交互
|
||||
- App初始渲染
|
||||
- App更新
|
||||
- App销毁
|
||||
- 整个页面刷新
|
||||
- devTools触发组件属性更新
|
||||
|
||||
|
||||
## 对组件的操作
|
||||
我们希望插件和Horizon能够尽量解耦,所以Horizon提供了Helper注入给插件,提供相关方法操作组件。
|
||||
|
||||
## 触发组件更新方式
|
||||
- 类组件的state:调用实例的 setState 函数触发更新
|
||||
- 类组件的props:浅复制props后更新props值并调用 forceUpdate 触发更新
|
||||
- 函数组件的props:新增了devProps属性,在特定时刻重新给props赋值,触发更新
|
||||
- 函数组件的state:调用 useState 函数触发更新
|
||||
|
||||
## VNode的清理
|
||||
全局 hook 中保存了root VNode,在解析 VNode 树的时候也会保存 VNode 的引用,在清理VNode的时候这些 VNode 的引用也需要删除。
|
||||
|
||||
## 数据压缩
|
||||
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
|
||||
- 相同的组件名可以进行压缩
|
||||
- 每个vNode有唯一的 path 属性,可以作为标识使用
|
||||
- 通过解析 path 值可以分析出组件树的结构
|
||||
|
||||
## 组件props/state/hook等数据的传输和解析
|
||||
将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串或者 dom 元素,他们的解析和 props/state 的解析同样存在差异,需要单独处理。
|
||||
|
||||
|
||||
## 滚动动态渲染 Tree
|
||||
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
|
||||
|
||||
## 虚拟列表针对 UI 框架的优化
|
||||
列表中增减不同 key 项意味着 dom 增删,我们需要让框架尽可能减少 dom 操作。
|
||||
- 不管渲染列表项怎么变化,应该始终以 index 作为 key,这样只会更新 dom 的属性,不会有 dom 增删操作。
|
||||
- 如果在滚动过程中,一个 item 没有被移出渲染列表,它在列表中的 key 值不应该发生变化,由于 item 本身的数据没有变化,所以渲染的 children 也不会发生变化。结合上条的结论,它的属性值也不会变化,所以该 item 对应的 dom 都不会更新。
|
||||
|
||||
## 开发者页面打开场景
|
||||
- 先有页面,然后打开开发者工具:工具建立连接,发送通知,页面hook收到后发送VNode树信息给工具页面
|
||||
- 已经打开开发者工具,然后打开页面:业务页面渲染完毕,发送VNode树信息给工具页面
|
||||
|
||||
## 开发者工具页面响应组件树变更
|
||||
组件树变更会带来新旧两个组件树信息数组,新旧数组存在数据一致而引用不一致的情况,而VTree和VList组件中相关信息的计算依赖引用而非数据本身,在收到新的组件树信息后需要对数据本身进行判断,将新数组中的相同数据使用旧对象代替。
|
||||
|
||||
## 测试框架
|
||||
jest测试框架不提供浏览器插件的相关 api,我们在封装好相关 api 后需要模拟这些 api 的行为从而展开测试工作。
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
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) {
|
||||
function extensionListener(message) {
|
||||
const isHorizonMessage = checkMessage(message, DevToolPanel);
|
||||
if (isHorizonMessage) {
|
||||
const { payload } = message;
|
||||
// tabId 值指当前浏览器分配给 web_page 的 id 值。是panel页面查询得到,指定向该页面发送消息
|
||||
const { type, data, tabId } = payload;
|
||||
let passMessage;
|
||||
if (type === InitDevToolPageConnection) {
|
||||
// 记录 panel 所在 tab 页的tabId,如果已经记录了,覆盖原有port,因为原有port可能关闭了
|
||||
// 可能这次是 panel 发起的重新建立请求
|
||||
connections[tabId] = port;
|
||||
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
|
||||
} else {
|
||||
passMessage = packagePayload({type, data}, DevToolBackground);
|
||||
}
|
||||
chrome.tabs.sendMessage(tabId, passMessage);
|
||||
}
|
||||
}
|
||||
// Listen to messages sent from the DevTools page
|
||||
port.onMessage.addListener(extensionListener);
|
||||
|
||||
port.onDisconnect.addListener(function (port) {
|
||||
port.onMessage.removeListener(extensionListener);
|
||||
|
||||
const tabs = Object.keys(connections);
|
||||
for (let i = 0, len = tabs.length; i < len; i++) {
|
||||
if (connections[tabs[i]] == port) {
|
||||
delete connections[tabs[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听来自 content script 的消息,并将消息发送给对应的 devTools page,也就是 panel
|
||||
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;
|
||||
// 和 InitDevToolPageConnection 时得到的 tabId 值一致时,向指定的 panel 页面 port 发送消息
|
||||
if (tabId in connections && checkMessage(message, DevToolContentScript)) {
|
||||
changeSource(message, DevToolBackground);
|
||||
connections[tabId].postMessage(message);
|
||||
} else {
|
||||
// TODO: 如果查询失败,发送 chrome message,请求 panel 主动建立连接
|
||||
console.log('Tab not found in connection list.');
|
||||
}
|
||||
} else {
|
||||
console.log('sender.tab not defined.');
|
||||
}
|
||||
// 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received
|
||||
sendResponse({status: 'ok'});
|
||||
});
|
|
@ -1,175 +0,0 @@
|
|||
import styles from './ComponentsInfo.less';
|
||||
import Eye from '../svgs/Eye';
|
||||
import Debug from '../svgs/Debug';
|
||||
import Triangle from '../svgs/Triangle';
|
||||
import { useState, useEffect } from 'horizon';
|
||||
import { IData } from './VTree';
|
||||
import { buildAttrModifyData, IAttr } from '../parser/parseAttr';
|
||||
import { postMessageToBackground } from '../panelConnection';
|
||||
import { InspectDom, LogComponentData, ModifyAttrs } from '../utils/constants';
|
||||
|
||||
type IComponentInfo = {
|
||||
name: string;
|
||||
attrs: {
|
||||
parsedProps?: IAttr[],
|
||||
parsedState?: IAttr[],
|
||||
parsedHooks?: IAttr[],
|
||||
};
|
||||
parents: IData[];
|
||||
id: number;
|
||||
onClickParent: (item: IData) => void;
|
||||
};
|
||||
|
||||
function collapseAllNodes(attrs: IAttr[]) {
|
||||
return attrs.filter((item, index) => {
|
||||
const nextItem = attrs[index + 1];
|
||||
return nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||
});
|
||||
}
|
||||
|
||||
function ComponentAttr({ attrsName, attrsType, attrs, id }: {
|
||||
attrsName: string,
|
||||
attrsType: string,
|
||||
attrs: IAttr[],
|
||||
id: number}) {
|
||||
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
|
||||
const [editableAttrs, setEditableAttrs] = useState(attrs);
|
||||
useEffect(() => {
|
||||
setCollapsedNode(collapseAllNodes(attrs));
|
||||
setEditableAttrs(attrs);
|
||||
}, [attrs]);
|
||||
|
||||
const handleCollapse = (item: IAttr) => {
|
||||
const nodes = [...collapsedNode];
|
||||
const i = nodes.indexOf(item);
|
||||
if (i === -1) {
|
||||
nodes.push(item);
|
||||
} else {
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
setCollapsedNode(nodes);
|
||||
};
|
||||
|
||||
const showAttr = [];
|
||||
let currentIndentation = null;
|
||||
editableAttrs.forEach((item, index) => {
|
||||
const indentation = item.indentation;
|
||||
if (currentIndentation !== null) {
|
||||
if (indentation > currentIndentation) {
|
||||
return;
|
||||
} else {
|
||||
currentIndentation = null;
|
||||
}
|
||||
}
|
||||
const nextItem = editableAttrs[index + 1];
|
||||
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||
const isCollapsed = collapsedNode.includes(item);
|
||||
showAttr.push(
|
||||
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => handleCollapse(item)}>
|
||||
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||
{' :'}
|
||||
{item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null'
|
||||
? <input
|
||||
value={String(item.value)}
|
||||
className={styles.attrValue}
|
||||
onChange={(event) => {
|
||||
const nextAttrs = [...editableAttrs];
|
||||
const nextItem = {...item};
|
||||
nextItem.value = event.target.value;
|
||||
nextAttrs[index] = nextItem;
|
||||
setEditableAttrs(nextAttrs);
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
if (event.key === 'Enter') {
|
||||
if(isDev) {
|
||||
console.log('post attr change', value);
|
||||
} else {
|
||||
const data = buildAttrModifyData(attrsType,attrs, value,item, index, id);
|
||||
postMessageToBackground(ModifyAttrs, data);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
: (item.type === 'boolean'
|
||||
? <input
|
||||
type={'checkbox'}
|
||||
checked={item.value}
|
||||
className={styles.attrValue}
|
||||
onChange={(event) => {
|
||||
const nextAttrs = [...editableAttrs];
|
||||
const nextItem = {...item};
|
||||
nextItem.value = event.target.checked;
|
||||
nextAttrs[index] = nextItem;
|
||||
setEditableAttrs(nextAttrs);
|
||||
if (!isDev) {
|
||||
const data = buildAttrModifyData(attrsType,attrs, nextItem.value,item, index, id);
|
||||
postMessageToBackground(ModifyAttrs, data);
|
||||
}
|
||||
}}/>
|
||||
: <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}>{attrsName}</span>
|
||||
</div>
|
||||
<div className={styles.attrDetail}>
|
||||
{showAttr}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComponentInfo({ name, attrs, parents, id, onClickParent }: IComponentInfo) {
|
||||
return (
|
||||
<div className={styles.infoContainer} >
|
||||
<div className={styles.componentInfoHead}>
|
||||
{name && <>
|
||||
<span className={styles.name}>
|
||||
{name}
|
||||
</span>
|
||||
<span className={styles.eye} title={'Inspect dom element'} onClick={() => {
|
||||
postMessageToBackground(InspectDom, id);
|
||||
}}>
|
||||
<Eye />
|
||||
</span>
|
||||
<span className={styles.debug} title={'Log this component data'} onClick={() => {
|
||||
postMessageToBackground(LogComponentData, id);
|
||||
}}>
|
||||
<Debug />
|
||||
</span>
|
||||
</>}
|
||||
</div>
|
||||
<div className={styles.componentInfoMain}>
|
||||
{Object.keys(attrs).map(attrsType => {
|
||||
const parsedAttrs = attrs[attrsType];
|
||||
if (parsedAttrs && parsedAttrs.length !== 0) {
|
||||
const attrsName = attrsType.slice(6); // parsedState => State
|
||||
return <ComponentAttr attrsName={attrsName} attrs={parsedAttrs} id={id} attrsType={attrsType}/>;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<div className={styles.parentsInfo}>
|
||||
{name && <div>
|
||||
parents: {
|
||||
parents.map(item => (<button
|
||||
className={styles.parent}
|
||||
onClick={() => (onClickParent(item))}>
|
||||
{item.name}
|
||||
</button>))
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
@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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.componentInfoMain {
|
||||
overflow-y: auto;
|
||||
|
||||
>:last-child {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
>div {
|
||||
border-bottom: @divider-style;
|
||||
}
|
||||
|
||||
.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: 0 0 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parentsInfo {
|
||||
flex: 1 1 0;
|
||||
.parent {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: @component-name-color;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background-color: @select-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
*
|
||||
* 由于 ResizeObserver 对 IE 和低版本主流浏览器不兼容,需要我们自己解决这个问题。
|
||||
* 这是一个不依赖任何框架的监听 dom 元素尺寸变化的解决方案。
|
||||
* 浏览器出于性能的考虑,只有 window 的 resize 事件会触发。我们通过 object 标签可以得到
|
||||
* 一个 window 对象,让 object dom 元素成为待观测 dom 的子元素,并且和待观测 dom 大小一致。
|
||||
* 这样一旦待观测 dom 的大小发生变化, window 的大小也会发生变化,我们就可以通过监听 window
|
||||
* 大小变化的方式监听待观测 dom 的大小变化。
|
||||
*
|
||||
* <div id='test'>
|
||||
* <object> --> 和父 div 保持大小一致
|
||||
* <html></html> --> 添加 resize 事件监听
|
||||
* </object>
|
||||
* </div>
|
||||
*
|
||||
*/
|
||||
|
||||
function timeout(fn) {
|
||||
return setTimeout(fn, 20);
|
||||
}
|
||||
|
||||
function requestFrame(fn) {
|
||||
const raf = requestAnimationFrame || timeout;
|
||||
return raf(fn);
|
||||
}
|
||||
|
||||
function cancelFrame(id) {
|
||||
const cancel = cancelAnimationFrame || clearTimeout;
|
||||
cancel(id);
|
||||
}
|
||||
|
||||
// 在闲置帧触发回调事件,如果在本次触发前存在未处理回调事件,
|
||||
// 需要取消未处理的回调事件
|
||||
function resizeListener(event) {
|
||||
const win = event.target;
|
||||
if (win.__resizeRAF__) {
|
||||
cancelFrame(win.__resizeRAF__);
|
||||
}
|
||||
win.__resizeRAF__ = requestFrame(function () {
|
||||
const observeElement = win.__observeElement__;
|
||||
observeElement.__resizeCallbacks__.forEach(function (fn) {
|
||||
fn.call(observeElement, observeElement, event);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadObserver() {
|
||||
// 将待观测元素传递给 object 标签的 window 对象,这样在触发 resize 事件时可以拿到待观测元素
|
||||
this.contentDocument.defaultView.__observeElement__ = this.__observeElement__;
|
||||
// 给 html 的 window 对象添加 resize 事件
|
||||
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
|
||||
}
|
||||
|
||||
export function addResizeListener(element: any, fn: any) {
|
||||
if (!element.__resizeCallbacks__) {
|
||||
element.__resizeCallbacks__ = [fn];
|
||||
element.style.position = 'relative';
|
||||
const observer = document.createElement('object');
|
||||
observer.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
|
||||
observer.data = 'about:blank';
|
||||
observer.onload = loadObserver;
|
||||
observer.type = 'text/html';
|
||||
observer['__observeElement__'] = element;
|
||||
element.__observer__ = observer;
|
||||
element.appendChild(observer);
|
||||
} else {
|
||||
element.__resizeCallbacks__.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeResizeListener(element, fn) {
|
||||
element.__resizeCallbacks__.splice(element.__resizeCallbacks__.indexOf(fn), 1);
|
||||
if (!element.__resizeCallbacks__.length) {
|
||||
element.__observer__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
|
||||
element.removeChild(element.__observer__);
|
||||
element.__observer__ = null;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.search {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import styles from './Search.less';
|
||||
|
||||
interface SearchProps {
|
||||
onChange: (event: any) => void,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export default function Search(props: SearchProps) {
|
||||
const { onChange, value } = props;
|
||||
const handleChange = (event) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
return (
|
||||
<input
|
||||
onChange={handleChange}
|
||||
className={styles.search}
|
||||
value={value}
|
||||
placeholder={'Search (text or /regex/)'}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { useEffect, useState, useRef } from 'horizon';
|
||||
import { addResizeListener, removeResizeListener } from './ResizeEvent';
|
||||
|
||||
|
||||
export function SizeObserver(props) {
|
||||
const { children, ...rest } = props;
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [size, setSize] = useState<{width: number, height: number}>();
|
||||
const notifyChild = (element) => {
|
||||
setSize({
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
const element = containerRef.current;
|
||||
setSize({
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
addResizeListener(element, notifyChild);
|
||||
return () => {
|
||||
removeResizeListener(element, notifyChild);
|
||||
};
|
||||
}, []);
|
||||
const myChild = size ? children(size.width, size.height) : null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} {...rest}>
|
||||
{myChild}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// 用于在滚动的过程中,对比上一次渲染的结果和本次需要渲染项
|
||||
// 确保继续渲染项在新渲染数组中的位置和旧渲染数组中的位置不发生改变
|
||||
|
||||
export default class ItemMap<T>{
|
||||
|
||||
// 不要用 indexOf 进行位置计算,它会遍历数组
|
||||
private lastRenderItemToIndexMap: Map<T, number>;
|
||||
|
||||
constructor(){
|
||||
this.lastRenderItemToIndexMap = new Map();
|
||||
}
|
||||
|
||||
public calculateReSortedItems(nextItems: T[]): (T|undefined)[] {
|
||||
if (this.lastRenderItemToIndexMap.size === 0) {
|
||||
nextItems.forEach((item, index) => {
|
||||
this.lastRenderItemToIndexMap.set(item, index);
|
||||
});
|
||||
return nextItems;
|
||||
}
|
||||
const nextRenderItems: T[] = [];
|
||||
const length = nextItems.length;
|
||||
const nextRenderItemToIndexMap = new Map<T,number>();
|
||||
const addItems = [];
|
||||
// 遍历 nextItems 找到复用 item 和 新增 item
|
||||
nextItems.forEach(item => {
|
||||
const lastIndex = this.lastRenderItemToIndexMap.get(item);
|
||||
// 处理旧 item
|
||||
if (lastIndex !== undefined) {
|
||||
// 使用上一次的位置
|
||||
nextRenderItems[lastIndex] = item;
|
||||
// 记录位置
|
||||
nextRenderItemToIndexMap.set(item, lastIndex);
|
||||
} else {
|
||||
// 记录新增的 item
|
||||
addItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理新增 item
|
||||
// 翻转数组,后面在调用pop时拿到的是最后一个,以确保顺序
|
||||
addItems.reverse();
|
||||
for(let i = 0; i < length; i++) {
|
||||
// 优先将新增 item 放置在空位置上
|
||||
if (!nextRenderItems[i]) {
|
||||
const item = addItems.pop();
|
||||
nextRenderItems[i] = item;
|
||||
nextRenderItemToIndexMap.set(item, i);
|
||||
}
|
||||
}
|
||||
// 剩余新 item 补在数组后面
|
||||
for(let i = addItems.length - 1; i >= 0; i--) {
|
||||
const item = addItems[i];
|
||||
nextRenderItemToIndexMap.set(item, nextRenderItems.length);
|
||||
nextRenderItems.push(item);
|
||||
}
|
||||
// 如果 nextRenderItems 中存在空 index, nextItems 已经耗尽,不用处理
|
||||
// 确保新旧数组中 item 的 index 值不会发生变化
|
||||
this.lastRenderItemToIndexMap = nextRenderItemToIndexMap;
|
||||
return nextRenderItems;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
.container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
// 内部只记录滚动位置状态值
|
||||
// data 数组更新后不修改滚动位置,
|
||||
// 只有修改scrollToItem才会修改滚动位置
|
||||
|
||||
import { useState, useRef, useEffect, useMemo } from 'horizon';
|
||||
import styles from './VList.less';
|
||||
import ItemMap from './ItemMap';
|
||||
|
||||
interface IProps<T extends { id: number | string }> {
|
||||
data: T[],
|
||||
width: number, // 暂时未用到,当需要支持横向滚动时使用
|
||||
height: number, // VList 的高度
|
||||
children: any, // horizon 组件,组件类型是 T
|
||||
itemHeight: number,
|
||||
scrollToItem?: T, // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果不在,则滚动到中间位置
|
||||
onRendered: (renderInfo: renderInfoType<T>) => void;
|
||||
filter?(data: T): boolean, // false 表示该行不显示
|
||||
}
|
||||
|
||||
export type renderInfoType<T> = {
|
||||
visibleItems: T[];
|
||||
};
|
||||
|
||||
function parseTranslate<T>(data: T[], itemHeight: number) {
|
||||
const map = new Map<T, number>();
|
||||
data.forEach((item, index) => {
|
||||
map.set(item, index * itemHeight);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
export function VList<T extends { id: number | string }>(props: IProps<T>) {
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
children,
|
||||
itemHeight,
|
||||
scrollToItem,
|
||||
onRendered,
|
||||
} = props;
|
||||
const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem), 0) * itemHeight);
|
||||
const renderInfoRef: { current: renderInfoType<T> } = useRef({
|
||||
visibleItems: [],
|
||||
});
|
||||
// 每个 item 的 translateY 值固定不变
|
||||
const itemToTranslateYMap = useMemo(() => parseTranslate(data, itemHeight), [data]);
|
||||
const itemIndexMap = useMemo(() => new ItemMap<T>(), []);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
useEffect(() => {
|
||||
onRendered(renderInfoRef.current);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToItem) {
|
||||
const renderInfo = renderInfoRef.current;
|
||||
// 在显示区域,不滚动
|
||||
if (!renderInfo.visibleItems.includes(scrollToItem)) {
|
||||
const index = data.indexOf(scrollToItem);
|
||||
// 显示在页面中间
|
||||
const top = Math.max(index * itemHeight - height / 2, 0);
|
||||
containerRef.current.scrollTo({ top: top });
|
||||
}
|
||||
}
|
||||
}, [scrollToItem]);
|
||||
|
||||
// 滚动事件会频繁触发,通过框架提供的代理会有大量计算寻找 dom 元素。
|
||||
// 直接绑定到原生事件上减少计算量
|
||||
useEffect(() => {
|
||||
const handleScroll = (event: any) => {
|
||||
const scrollTop = event.target.scrollTop;
|
||||
setScrollTop(scrollTop);
|
||||
};
|
||||
const container = containerRef.current;
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const totalHeight = itemHeight * data.length;
|
||||
const maxIndex = data.length; // slice 截取渲染 item 数组时最大位置不能超过自身长度
|
||||
// 第一个可见 item index
|
||||
const firstInViewItemIndex = Math.floor(scrollTop / itemHeight);
|
||||
// 可见区域前最多冗余 4 个 item
|
||||
const startRenderIndex = Math.max(firstInViewItemIndex - 4, 0); // index 不能小于0
|
||||
// 最多可见数量
|
||||
const maxInViewCount = Math.floor(height / itemHeight);
|
||||
// 最后可见item index
|
||||
const lastInViewIndex = Math.min(firstInViewItemIndex + maxInViewCount, maxIndex);
|
||||
// 记录可见 items
|
||||
renderInfoRef.current.visibleItems = data.slice(firstInViewItemIndex, lastInViewIndex);
|
||||
// 可见区域后冗余 4 个 item
|
||||
const lastRenderIndex = Math.min(lastInViewIndex + 4, maxIndex);
|
||||
// 需要渲染的 items
|
||||
const renderItems = data.slice(startRenderIndex, lastRenderIndex);
|
||||
// 给 items 重新排序,确保未移出渲染数组的 item 在新的渲染数组中位置不变
|
||||
// 这样在diff算法比较后,这部分的 dom 不会发生更新
|
||||
const nextRenderList = itemIndexMap.calculateReSortedItems(renderItems);
|
||||
const list = nextRenderList.map((item, i) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={String(i)} // 固定 key 值,这样就只会更新 translateY 的值
|
||||
className={styles.item}
|
||||
style={{ transform: `translateY(${itemToTranslateYMap.get(item)}px)` }} >
|
||||
{children(item)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.container}>
|
||||
{list}
|
||||
<div style={{ marginTop: totalHeight }} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { VList } from './VList';
|
||||
export type { renderInfoType } from './VList';
|
|
@ -1,38 +0,0 @@
|
|||
@import 'assets.less';
|
||||
|
||||
.treeContainer {
|
||||
height: 100%;
|
||||
|
||||
.treeItem {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
line-height: 1.125rem;
|
||||
|
||||
&: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%);
|
||||
}
|
||||
}
|
|
@ -1,201 +0,0 @@
|
|||
import { useState, useEffect } from 'horizon';
|
||||
import styles from './VTree.less';
|
||||
import Triangle from '../svgs/Triangle';
|
||||
import { createRegExp } from '../utils/regExpUtils';
|
||||
import { SizeObserver } from './SizeObserver';
|
||||
import { renderInfoType, VList } from './VList';
|
||||
|
||||
export interface IData {
|
||||
id: number;
|
||||
name: string;
|
||||
indentation: number;
|
||||
userKey: string;
|
||||
}
|
||||
|
||||
interface IItem {
|
||||
hasChild: boolean,
|
||||
onCollapse: (data: IData) => void,
|
||||
onClick: (id: IData) => void,
|
||||
isCollapsed: boolean,
|
||||
isSelect: boolean,
|
||||
highlightValue: string,
|
||||
data: IData,
|
||||
}
|
||||
|
||||
const indentationLength = 20;
|
||||
|
||||
function Item(props: IItem) {
|
||||
const {
|
||||
hasChild,
|
||||
onCollapse,
|
||||
isCollapsed,
|
||||
data,
|
||||
onClick,
|
||||
isSelect,
|
||||
highlightValue = '',
|
||||
} = props;
|
||||
|
||||
const {
|
||||
name,
|
||||
userKey,
|
||||
indentation,
|
||||
} = data;
|
||||
|
||||
const isShowKey = userKey !== '';
|
||||
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||
const handleClickCollapse = () => {
|
||||
onCollapse(data);
|
||||
};
|
||||
const handleClick = () => {
|
||||
onClick(data);
|
||||
};
|
||||
const itemAttr: any = { 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(props: {
|
||||
data: IData[],
|
||||
highlightValue: string,
|
||||
scrollToItem: IData,
|
||||
onRendered: (renderInfo: renderInfoType<IData>) => void,
|
||||
collapsedNodes?: IData[],
|
||||
onCollapseNode?: (item: IData[]) => void,
|
||||
selectItem: IData,
|
||||
onSelectItem: (item: IData) => void,
|
||||
}) {
|
||||
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
|
||||
const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []);
|
||||
const [selectItem, setSelectItem] = useState(props.selectItem);
|
||||
useEffect(() => {
|
||||
setSelectItem(scrollToItem);
|
||||
}, [scrollToItem]);
|
||||
useEffect(() => {
|
||||
if (props.selectItem !== selectItem) {
|
||||
setSelectItem(props.selectItem);
|
||||
}
|
||||
}, [props.selectItem]);
|
||||
useEffect(() => {
|
||||
setCollapseNode(props.collapsedNodes || []);
|
||||
}, [props.collapsedNodes]);
|
||||
|
||||
const changeCollapseNode = (item: IData) => {
|
||||
const nodes: IData[] = [...collapseNode];
|
||||
const index = nodes.indexOf(item);
|
||||
if (index === -1) {
|
||||
nodes.push(item);
|
||||
} else {
|
||||
nodes.splice(index, 1);
|
||||
}
|
||||
setCollapseNode(nodes);
|
||||
if (onCollapseNode) {
|
||||
onCollapseNode(nodes);
|
||||
}
|
||||
};
|
||||
const handleClickItem = (item: IData) => {
|
||||
setSelectItem(item);
|
||||
if (onSelectItem) {
|
||||
onSelectItem(item);
|
||||
}
|
||||
};
|
||||
|
||||
let currentCollapseIndentation: null | number = null;
|
||||
// 过滤掉折叠的 item,不展示在 VList 中
|
||||
const filter = (item: IData) => {
|
||||
if (currentCollapseIndentation !== null) {
|
||||
// 缩进更大,不显示
|
||||
if (item.indentation > currentCollapseIndentation) {
|
||||
return false;
|
||||
} else {
|
||||
// 缩进小,说明完成了该收起节点的子节点处理。
|
||||
currentCollapseIndentation = null;
|
||||
}
|
||||
}
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
if (isCollapsed) {
|
||||
// 该节点需要收起子节点
|
||||
currentCollapseIndentation = item.indentation;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const showList = data.filter(filter);
|
||||
|
||||
return (
|
||||
<SizeObserver className={styles.treeContainer}>
|
||||
{(width: number, height: number) => {
|
||||
return (
|
||||
<VList
|
||||
data={showList}
|
||||
width={width}
|
||||
height={height}
|
||||
itemHeight={18}
|
||||
scrollToItem={selectItem}
|
||||
onRendered={onRendered}
|
||||
>
|
||||
{(item: IData) => {
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
const index = showList.indexOf(item);
|
||||
// 如果收起,一定有 child
|
||||
// 不收起场景,如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
|
||||
const hasChild = isCollapsed || (showList[index + 1]?.indentation > item.indentation);
|
||||
return (
|
||||
<Item
|
||||
hasChild={hasChild}
|
||||
isCollapsed={collapseNode.includes(item)}
|
||||
isSelect={selectItem === item}
|
||||
onCollapse={changeCollapseNode}
|
||||
onClick={handleClickItem}
|
||||
highlightValue={highlightValue}
|
||||
data={item} />
|
||||
);
|
||||
}}
|
||||
</VList>
|
||||
);
|
||||
}}
|
||||
</SizeObserver>
|
||||
);
|
||||
}
|
||||
|
||||
export default VTree;
|
|
@ -1,15 +0,0 @@
|
|||
@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%);
|
||||
@hover-color: black;
|
||||
|
||||
@top-height: 2.625rem;
|
||||
@divider-width: 0.2px;
|
||||
@common-font-size: 12px;
|
||||
|
||||
@divider-style: @divider-color solid @divider-width;
|
|
@ -1,38 +0,0 @@
|
|||
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'));
|
||||
|
||||
// 监听来自页面的信息
|
||||
window.addEventListener('message', event => {
|
||||
// 只监听来自本页面的消息
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
if (checkMessage(data, DevToolHook)) {
|
||||
changeSource(data, DevToolContentScript);
|
||||
// 传递给background
|
||||
chrome.runtime.sendMessage(data);
|
||||
}
|
||||
}, false);
|
||||
|
||||
|
||||
|
||||
// 监听来自background的消息
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function (message, sender, sendResponse) {
|
||||
// 该方法可以监听页面 contentScript 和插件的消息
|
||||
// 没有 tab 信息说明消息来自插件
|
||||
if (!sender.tab && checkMessage(message, DevToolBackground)) {
|
||||
changeSource(message, DevToolContentScript);
|
||||
// 传递消息给页面
|
||||
window.postMessage(message, '*');
|
||||
}
|
||||
sendResponse({status: 'ok'});
|
||||
}
|
||||
);
|
|
@ -1,99 +0,0 @@
|
|||
/**
|
||||
* 用一个纯数据类型的对象 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';
|
||||
import { helper } from '../../../horizon/src/external/devtools';
|
||||
|
||||
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(helper.travelVNodeTree, 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);
|
|
@ -1,22 +0,0 @@
|
|||
import { Component } from 'horizon';
|
||||
|
||||
const defaultState = {
|
||||
name: 'jenny',
|
||||
boolean: true,
|
||||
};
|
||||
|
||||
export default class MockClassComponent extends Component<{fruit: string}, typeof defaultState> {
|
||||
|
||||
state = defaultState;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => (this.setState({name: 'pika'}))} >update state</button>
|
||||
{this.state.name}
|
||||
{this.props?.fruit}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import { createContext } from 'horizon';
|
||||
|
||||
export const MockContext = createContext({value: 'default context value'});
|
|
@ -1,38 +0,0 @@
|
|||
import { useState, useEffect, useRef, useContext, useReducer } from 'horizon';
|
||||
import { MockContext } from './MockContext';
|
||||
|
||||
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 [name, setName] = useState({test: 1});
|
||||
const domRef = useRef<HTMLDivElement>();
|
||||
const objRef = useRef({ str: 'string' });
|
||||
const context = useContext(MockContext);
|
||||
|
||||
useEffect(() => { }, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
age: {age}
|
||||
name: {name.test}
|
||||
<button onClick={() => setAge(age + 1)} >update age</button>
|
||||
count: {props.count}
|
||||
<div ref={domRef} />
|
||||
<div>{objRef.current.str}</div>
|
||||
<div>{context.ctx}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { render, useState } 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() {
|
||||
const [count, setCount] = useState(12);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => (setCount(count + 1))} >add count</button>
|
||||
<MockContext.Provider value={{ ctx: 'I am ctx' }}>
|
||||
<MockClassComponent fruit={'apple'} />
|
||||
<MockFunctionComponent count={count}/>
|
||||
</MockContext.Provider>
|
||||
abc
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<App />, root);
|
|
@ -1,28 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Horizon Mock Page</title>
|
||||
<script src="horizon.production.js"></script>
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,147 +0,0 @@
|
|||
// 过滤树的抽象逻辑
|
||||
// 需要知道渲染了哪些数据,过滤的字符串/正则表达式
|
||||
// 控制Tree组件位置跳转,告知匹配结果
|
||||
// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果
|
||||
//
|
||||
// 跳转搜索结果的交互逻辑:
|
||||
// 如果当前页面存在匹配项,页面不动
|
||||
// 如果当前页面不存在匹配项,页面跳转到第一个匹配项位置
|
||||
// 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开
|
||||
// 跳转到上一个匹配项或下一个匹配项时,如果匹配项被折叠,需要展开其父节点
|
||||
//
|
||||
// 寻找父节点:
|
||||
// 找到该节点的缩进值,和index值,在data中向上遍历,通过缩进值判断父节点
|
||||
|
||||
import { useState, useRef } from 'horizon';
|
||||
import { createRegExp } from '../utils/regExpUtils';
|
||||
|
||||
/**
|
||||
* 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组
|
||||
*
|
||||
* @param item 需要展开父节点的节点
|
||||
* @param data 全部数据
|
||||
* @param collapsedNodes 收起节点数据
|
||||
* @returns 新的收起节点数组
|
||||
*/
|
||||
function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: BaseType[]): BaseType[] {
|
||||
const index = data.indexOf(item);
|
||||
let currentIndentation = item.indentation;
|
||||
// 不对原始数据进行修改
|
||||
const newCollapsedNodes = [...collapsedNodes];
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const lastData = data[i];
|
||||
const lastIndentation = lastData.indentation;
|
||||
// 缩进更小,找到了父节点
|
||||
if (lastIndentation < currentIndentation) {
|
||||
// 更新缩进值,只找父节点的父节点,避免修改父节点的兄弟节点的展开状态
|
||||
currentIndentation = lastIndentation;
|
||||
const cIndex = newCollapsedNodes.indexOf(lastData);
|
||||
if (cIndex !== -1) {
|
||||
newCollapsedNodes.splice(cIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newCollapsedNodes;
|
||||
}
|
||||
|
||||
type BaseType = {
|
||||
id: string,
|
||||
name: string,
|
||||
indentation: number,
|
||||
}
|
||||
|
||||
export function FilterTree<T extends BaseType>(props: { data: T[] }) {
|
||||
const { data } = props;
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [currentItem, setCurrentItem] = useState(null); // 当前选中的匹配项
|
||||
const showItemsRef = useRef([]); // 页面展示的 items
|
||||
const matchItemsRef = useRef([]); // 匹配过滤条件的 items
|
||||
const collapsedNodesRef = useRef([]); // 折叠节点,如果匹配 item 被折叠了,需要展开
|
||||
|
||||
const matchItems = matchItemsRef.current;
|
||||
const collapsedNodes = collapsedNodesRef.current;
|
||||
|
||||
const updateCollapsedNodes = (item: BaseType) => {
|
||||
const newCollapsedNodes = expandItemParent(item, data, collapsedNodes);
|
||||
// 如果新旧收起节点数组长度不一样,说明存在收起节点
|
||||
if (newCollapsedNodes.length !== collapsedNodes.length) {
|
||||
// 更新引用,确保 VTree 拿到新的 collapsedNodes
|
||||
collapsedNodesRef.current = newCollapsedNodes;
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSearchValue = (search: string) => {
|
||||
const reg = createRegExp(search);
|
||||
let newCurrentItem = null;
|
||||
let newMatchItems = [];
|
||||
if (search !== '') {
|
||||
const showItems: T[] = showItemsRef.current;
|
||||
newMatchItems = data.reduce((pre, current) => {
|
||||
const { name } = current;
|
||||
if (reg && name.match(reg)) {
|
||||
pre.push(current);
|
||||
// 如果当前页面显示的 item 存在匹配项,则把它设置为 currentItem
|
||||
if (newCurrentItem === null && showItems.includes(current)) {
|
||||
newCurrentItem = current;
|
||||
}
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
if (newMatchItems.length === 0) {
|
||||
setCurrentItem(null);
|
||||
} else {
|
||||
if (newCurrentItem === null) {
|
||||
const item = newMatchItems[0];
|
||||
// 不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
} else {
|
||||
setCurrentItem(newCurrentItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setCurrentItem(null);
|
||||
}
|
||||
matchItemsRef.current = newMatchItems;
|
||||
setFilterValue(search);
|
||||
};
|
||||
const onSelectNext = () => {
|
||||
const index = matchItems.indexOf(currentItem);
|
||||
const nextIndex = index + 1;
|
||||
const item = nextIndex < matchItemsRef.current.length ? matchItems[nextIndex] : matchItems[0];
|
||||
// 可能不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
};
|
||||
const onSelectLast = () => {
|
||||
const index = matchItems.indexOf(currentItem);
|
||||
const last = index - 1;
|
||||
const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1];
|
||||
// 可能不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
};
|
||||
const setShowItems = (items) => {
|
||||
showItemsRef.current = [...items];
|
||||
};
|
||||
const onClear = () => {
|
||||
onChangeSearchValue('');
|
||||
};
|
||||
const setCollapsedNodes = (items) => {
|
||||
// 不更新引用,避免子组件的重复渲染
|
||||
collapsedNodesRef.current.length = 0;
|
||||
collapsedNodesRef.current.push(...items);
|
||||
};
|
||||
return {
|
||||
filterValue,
|
||||
onChangeSearchValue,
|
||||
onClear,
|
||||
currentItem,
|
||||
matchItems,
|
||||
onSelectNext,
|
||||
onSelectLast,
|
||||
setShowItems,
|
||||
collapsedNodes,
|
||||
setCollapsedNodes,
|
||||
};
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
|
||||
import { packagePayload, checkMessage } from '../utils/transferTool';
|
||||
import {
|
||||
RequestAllVNodeTreeInfos,
|
||||
AllVNodeTreesInfos,
|
||||
RequestComponentAttrs,
|
||||
ComponentAttrs,
|
||||
DevToolHook,
|
||||
DevToolContentScript,
|
||||
ModifyAttrs,
|
||||
ModifyHooks,
|
||||
ModifyState,
|
||||
ModifyProps,
|
||||
InspectDom,
|
||||
LogComponentData
|
||||
} from '../utils/constants';
|
||||
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
|
||||
import { parseVNodeAttrs } 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(helper.travelVNodeTree ,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 = queryVNode(id);
|
||||
if (!vNode) {
|
||||
console.error('Do not find match vNode, this is a bug, please report us');
|
||||
return;
|
||||
}
|
||||
const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo);
|
||||
postMessage(ComponentAttrs, parsedAttrs);
|
||||
}
|
||||
|
||||
function calculateNextValue(editValue, value, attrPath) {
|
||||
let nextState;
|
||||
const editValueType = typeof editValue;
|
||||
if (editValueType === 'string' || editValueType === 'undefined' || editValueType === 'boolean') {
|
||||
nextState = value;
|
||||
} else if (editValueType === 'number') {
|
||||
const numValue = Number(value);
|
||||
nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字,用原值
|
||||
} else if(editValueType === 'object') {
|
||||
if (editValue === null) {
|
||||
nextState = value;
|
||||
} else {
|
||||
const newValue = Array.isArray(editValue) ? [...editValue] : {...editValue};
|
||||
// 遍历读取到直接指向需要修改值的对象
|
||||
let attr = newValue;
|
||||
for(let i = 0; i < attrPath.length - 1; i++) {
|
||||
attr = attr[attrPath[i]];
|
||||
}
|
||||
// 修改对象上的值
|
||||
attr[attrPath[attrPath.length - 1]] = value;
|
||||
nextState = newValue;
|
||||
}
|
||||
} else {
|
||||
console.error('The devTool tried to edit a non-editable value, this is a bug, please report', editValue);
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
function modifyVNodeAttrs(data) {
|
||||
const {type, id, value, path} = data;
|
||||
const vNode = queryVNode(id);
|
||||
if (!vNode) {
|
||||
console.error('Do not find match vNode, this is a bug, please report us');
|
||||
return;
|
||||
}
|
||||
if (type === ModifyProps) {
|
||||
const nextProps = calculateNextValue(vNode.props, value, path);
|
||||
helper.updateProps(vNode, nextProps);
|
||||
} else if (type === ModifyHooks) {
|
||||
const hooks = vNode.hooks;
|
||||
const editHook = hooks[path[0]];
|
||||
const hookInfo = helper.getHookInfo(editHook);
|
||||
if (hookInfo) {
|
||||
const editValue = hookInfo.value;
|
||||
// path 的第一个值指向 hIndex,从第二个值才开始指向具体属性访问路径
|
||||
const nextState = calculateNextValue(editValue, value, path.slice(1));
|
||||
helper.updateHooks(vNode, path[0], nextState);
|
||||
} else {
|
||||
console.error('The devTool tried to edit a non-editable hook, this is a bug, please report', hooks);
|
||||
}
|
||||
} else if (type === ModifyState) {
|
||||
const oldState = vNode.state || {};
|
||||
const nextState = {...oldState};
|
||||
let accessRef = nextState;
|
||||
for(let i = 0; i < path.length - 1; i++) {
|
||||
accessRef = accessRef[path[i]];
|
||||
}
|
||||
accessRef[path[path.length - 1]] = value;
|
||||
helper.updateState(vNode, nextState);
|
||||
}
|
||||
}
|
||||
|
||||
function logComponentData(id: number) {
|
||||
const vNode = queryVNode(id);
|
||||
if (vNode) {
|
||||
const info = helper.getComponentInfo(vNode);
|
||||
console.log('Component Info: ', info);
|
||||
}
|
||||
}
|
||||
|
||||
let helper;
|
||||
|
||||
function init(horizonHelper) {
|
||||
helper = horizonHelper;
|
||||
window.__HORIZON_DEV_HOOK__.isInit = true;
|
||||
}
|
||||
|
||||
function injectHook() {
|
||||
if (window.__HORIZON_DEV_HOOK__) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
|
||||
enumerable: false,
|
||||
value: {
|
||||
init,
|
||||
isInit: false,
|
||||
addIfNotInclude,
|
||||
send,
|
||||
deleteVNode,
|
||||
},
|
||||
});
|
||||
window.addEventListener('message', function (event) {
|
||||
// We only accept messages from ourselves
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
const request = event.data;
|
||||
if (checkMessage(request, DevToolContentScript)) {
|
||||
const { payload } = request;
|
||||
const { type, data } = payload;
|
||||
if (type === RequestAllVNodeTreeInfos) {
|
||||
send();
|
||||
} else if (type === RequestComponentAttrs) {
|
||||
parseCompAttrs(data);
|
||||
} else if (type === ModifyAttrs) {
|
||||
modifyVNodeAttrs(data);
|
||||
} else if (type === InspectDom) {
|
||||
console.log(data);
|
||||
} else if (type === LogComponentData) {
|
||||
logComponentData(data);
|
||||
} else {
|
||||
console.warn('unknown command', type);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
injectHook();
|
|
@ -1,7 +0,0 @@
|
|||
chrome.devtools.panels.create('Horizon',
|
||||
'',
|
||||
'panel.html',
|
||||
function(panel) {
|
||||
|
||||
}
|
||||
);
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
</div>
|
||||
<div>
|
||||
<p>Horizon dev tools!</p>
|
||||
</div>
|
||||
</body>
|
||||
<script src="main.js"></script>
|
||||
</html>
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"name": "Horizon dev tool",
|
||||
"description": "Horizon chrome dev extension",
|
||||
"version": "1.0",
|
||||
"minimum_chrome_version": "10.0",
|
||||
"manifest_version": 3,
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"permissions": ["storage", "activeTab", "scripting"],
|
||||
|
||||
"devtools_page": "main.html",
|
||||
"action": {},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["contentScript.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [ "injector.js", "background.js" ],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
@import '../components/assets.less';
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
font-size: @common-font-size;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.left_top {
|
||||
border-bottom: @divider-style;
|
||||
flex: 0 0 @top-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 0.4rem;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.searchResult{
|
||||
flex: 0 0 ;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
.searchAction {
|
||||
flex: 0 0 1rem;
|
||||
height: 1rem;
|
||||
color: @arrow-color;
|
||||
&:hover{
|
||||
color: @hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left_bottom {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 3;
|
||||
border-left: @divider-style;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
import { useState, useEffect, useRef } 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';
|
||||
import { FilterTree } from '../hooks/FilterTree';
|
||||
import Close from '../svgs/Close';
|
||||
import Arrow from './../svgs/Arrow';
|
||||
import {
|
||||
AllVNodeTreesInfos,
|
||||
RequestComponentAttrs,
|
||||
ComponentAttrs,
|
||||
} from '../utils/constants';
|
||||
import {
|
||||
addBackgroundMessageListener,
|
||||
initBackgroundConnection,
|
||||
postMessageToBackground, removeBackgroundMessageListener,
|
||||
} from '../panelConnection';
|
||||
import { IAttr } from '../parser/parseAttr';
|
||||
import { createLogger } from '../utils/logUtil';
|
||||
|
||||
const logger = createLogger('panelApp');
|
||||
|
||||
const parseVNodeData = (rawData, idToTreeNodeMap , nextIdToTreeNodeMap) => {
|
||||
const idIndentationMap: {
|
||||
[id: string]: number;
|
||||
} = {};
|
||||
const data: IData[] = [];
|
||||
let i = 0;
|
||||
while (i < rawData.length) {
|
||||
const id = rawData[i] as number;
|
||||
i++;
|
||||
const name = rawData[i] as string;
|
||||
i++;
|
||||
const parentId = rawData[i] as string;
|
||||
i++;
|
||||
const userKey = rawData[i] as string;
|
||||
i++;
|
||||
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
|
||||
idIndentationMap[id] = indentation;
|
||||
const lastItem = idToTreeNodeMap[id];
|
||||
if (lastItem) {
|
||||
// 由于 diff 算法限制,一个 vNode 的 name,userKey,indentation 属性不会发生变化
|
||||
// 但是在跳转到新页面时,id 值重置,此时原有 id 对应的节点都发生了变化,需要更新
|
||||
// 为了让架构尽可能简单,我们不区分是否是页面跳转,所以每次都需要重新赋值
|
||||
nextIdToTreeNodeMap[id] = lastItem;
|
||||
lastItem.name = name;
|
||||
lastItem.indentation = indentation;
|
||||
lastItem.userKey = userKey;
|
||||
data.push(lastItem);
|
||||
} else {
|
||||
const item = {
|
||||
id, name, indentation, userKey
|
||||
};
|
||||
nextIdToTreeNodeMap[id] = item;
|
||||
data.push(item);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
|
||||
const parents: IData[] = [];
|
||||
if (item) {
|
||||
const index = parsedVNodeData.indexOf(item);
|
||||
let indentation = item.indentation;
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const last = parsedVNodeData[i];
|
||||
const lastIndentation = last.indentation;
|
||||
if (lastIndentation < indentation) {
|
||||
parents.push(last);
|
||||
indentation = lastIndentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
interface IIdToNodeMap {
|
||||
[id: number]: IData;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||
const [componentAttrs, setComponentAttrs] = useState<{
|
||||
parsedProps?: IAttr[],
|
||||
parsedState?: IAttr[],
|
||||
parsedHooks?: IAttr[],
|
||||
}>({});
|
||||
const [selectComp, setSelectComp] = useState(null);
|
||||
const idToTreeNodeMapRef = useRef<IIdToNodeMap>({});
|
||||
|
||||
const {
|
||||
filterValue,
|
||||
onChangeSearchValue: setFilterValue,
|
||||
onClear,
|
||||
currentItem,
|
||||
matchItems,
|
||||
onSelectNext,
|
||||
onSelectLast,
|
||||
setShowItems,
|
||||
collapsedNodes,
|
||||
setCollapsedNodes,
|
||||
} = FilterTree({ data: parsedVNodeData });
|
||||
|
||||
useEffect(() => {
|
||||
if (isDev) {
|
||||
const nextIdToTreeNodeMap: IIdToNodeMap = {};
|
||||
const parsedData = parseVNodeData(mockParsedVNodeData, idToTreeNodeMapRef.current, nextIdToTreeNodeMap);
|
||||
idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
|
||||
setParsedVNodeData(parsedData);
|
||||
setComponentAttrs({
|
||||
parsedProps: parsedMockState,
|
||||
parsedState: parsedMockState,
|
||||
});
|
||||
} else {
|
||||
const handleBackgroundMessage = (message) => {
|
||||
const { payload } = message;
|
||||
// 对象数据只是记录了引用,内容可能在后续被修改,打印字符串可以获取当前真正内容,不被后续修改影响
|
||||
logger.info(JSON.stringify(payload));
|
||||
if (payload) {
|
||||
const { type, data } = payload;
|
||||
if (type === AllVNodeTreesInfos) {
|
||||
const idToTreeNodeMap = idToTreeNodeMapRef.current;
|
||||
const nextIdToTreeNodeMap: IIdToNodeMap = {};
|
||||
const allTreeData = data.reduce((pre, current) => {
|
||||
const parsedTreeData = parseVNodeData(current, idToTreeNodeMap, nextIdToTreeNodeMap);
|
||||
return pre.concat(parsedTreeData);
|
||||
}, []);
|
||||
idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
|
||||
setParsedVNodeData(allTreeData);
|
||||
} else if (type === ComponentAttrs) {
|
||||
const {parsedProps, parsedState, parsedHooks} = data;
|
||||
setComponentAttrs({
|
||||
parsedProps,
|
||||
parsedState,
|
||||
parsedHooks,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// 在页面渲染后初始化连接
|
||||
initBackgroundConnection();
|
||||
// 监听 background消息
|
||||
addBackgroundMessageListener(handleBackgroundMessage);
|
||||
return () => {
|
||||
removeBackgroundMessageListener(handleBackgroundMessage);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = (str: string) => {
|
||||
setFilterValue(str);
|
||||
};
|
||||
|
||||
const handleSelectComp = (item: IData) => {
|
||||
if (isDev) {
|
||||
setComponentAttrs({
|
||||
parsedProps: parsedMockState,
|
||||
parsedState: parsedMockState,
|
||||
});
|
||||
} else {
|
||||
postMessageToBackground(RequestComponentAttrs, item.id);
|
||||
}
|
||||
setSelectComp(item);
|
||||
};
|
||||
|
||||
const handleClickParent = (item: IData) => {
|
||||
setSelectComp(item);
|
||||
};
|
||||
|
||||
const onRendered = (info) => {
|
||||
setShowItems(info.visibleItems);
|
||||
};
|
||||
const parents = getParents(selectComp, parsedVNodeData);
|
||||
|
||||
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} value={filterValue} />
|
||||
</div>
|
||||
{filterValue !== '' && <>
|
||||
<span className={styles.searchResult}>{`${matchItems.indexOf(currentItem) + 1}/${matchItems.length}`}</span>
|
||||
<div className={styles.divider} />
|
||||
<button className={styles.searchAction} onClick={onSelectLast}><Arrow direction={'up'} /></button>
|
||||
<button className={styles.searchAction} onClick={onSelectNext}><Arrow direction={'down'} /></button>
|
||||
<button className={styles.searchAction} onClick={onClear}><Close /></button>
|
||||
</>}
|
||||
</div>
|
||||
<div className={styles.left_bottom}>
|
||||
<VTree
|
||||
data={parsedVNodeData}
|
||||
highlightValue={filterValue}
|
||||
onRendered={onRendered}
|
||||
collapsedNodes={collapsedNodes}
|
||||
onCollapseNode={setCollapsedNodes}
|
||||
scrollToItem={currentItem}
|
||||
selectItem={selectComp}
|
||||
onSelectItem={handleSelectComp} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<ComponentInfo
|
||||
name={selectComp ? selectComp.name : null}
|
||||
attrs={selectComp ? componentAttrs : {}}
|
||||
parents={parents}
|
||||
id={selectComp ? selectComp.id : null}
|
||||
onClickParent={handleClickParent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,7 +0,0 @@
|
|||
import { render } from 'horizon';
|
||||
import App from './App';
|
||||
|
||||
render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -1,33 +0,0 @@
|
|||
<!doctype html>
|
||||
<html style="display: flex">
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script src='horizon.development.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="panel.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,61 +0,0 @@
|
|||
import { packagePayload } from '../utils/transferTool';
|
||||
import { DevToolPanel, InitDevToolPageConnection } from '../utils/constants';
|
||||
|
||||
let connection;
|
||||
const callbacks = [];
|
||||
|
||||
export function addBackgroundMessageListener(fun: (message) => void) {
|
||||
callbacks.push(fun);
|
||||
}
|
||||
|
||||
export function removeBackgroundMessageListener(fun: (message) => void) {
|
||||
const index = callbacks.indexOf(fun);
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function initBackgroundConnection() {
|
||||
console.log(!isDev);
|
||||
if (!isDev) {
|
||||
try {
|
||||
connection = chrome.runtime.connect({ name: 'panel' });
|
||||
const notice = message => {
|
||||
callbacks.forEach(fun => {
|
||||
fun(message);
|
||||
});
|
||||
};
|
||||
// TODO: 我们需要删除 notice 吗?如果需要,在什么时候删除
|
||||
// 监听 background 消息
|
||||
connection.onMessage.addListener(notice);
|
||||
// 页面打开后发送初始化请求
|
||||
postMessageToBackground(InitDevToolPageConnection);
|
||||
} catch (e) {
|
||||
console.error('create connection failed');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reconnectTimes = 0;
|
||||
export function postMessageToBackground(type: string, data?: any) {
|
||||
try{
|
||||
const payLoad = data
|
||||
? { type, tabId: chrome.devtools.inspectedWindow.tabId, data }
|
||||
: { type, tabId: chrome.devtools.inspectedWindow.tabId };
|
||||
connection.postMessage(packagePayload(payLoad, DevToolPanel));
|
||||
} catch(err) {
|
||||
// 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
|
||||
if (reconnectTimes === 20) {
|
||||
reconnectTimes = 0;
|
||||
console.error('reconnect failed');
|
||||
return;
|
||||
}
|
||||
console.error(err);
|
||||
reconnectTimes++;
|
||||
// 重建连接
|
||||
initBackgroundConnection();
|
||||
// 初始化成功后才会重新发送消息
|
||||
postMessageToBackground(type, data);
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './PanelConnection';
|
|
@ -1,199 +0,0 @@
|
|||
|
||||
import { Hook } from '../../../horizon/src/renderer/hooks/HookType';
|
||||
import { ModifyHooks, ModifyProps, ModifyState } from '../utils/constants';
|
||||
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
|
||||
import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
|
||||
|
||||
// 展示值为 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 | number;
|
||||
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 !== undefined) {
|
||||
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, result);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseHooks(hooks: Hook<any, any>[], getHookInfo) {
|
||||
const result: IAttr[] = [];
|
||||
const indentation = 0;
|
||||
hooks.forEach(hook => {
|
||||
const hookInfo = getHookInfo(hook);
|
||||
if (hookInfo) {
|
||||
const {name, hIndex, value} = hookInfo;
|
||||
parseSubAttr(value, indentation, name, result, hIndex);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseVNodeAttrs(vNode: VNode, getHookInfo) {
|
||||
const tag = vNode.tag;
|
||||
if (tag === ClassComponent) {
|
||||
const { props, state } = vNode;
|
||||
const parsedProps = parseAttr(props);
|
||||
const parsedState = parseAttr(state);
|
||||
return {
|
||||
parsedProps,
|
||||
parsedState,
|
||||
};
|
||||
} else if (tag === FunctionComponent) {
|
||||
const { props, hooks } = vNode;
|
||||
const parsedProps = parseAttr(props);
|
||||
const parsedHooks = parseHooks(hooks, getHookInfo);
|
||||
return {
|
||||
parsedProps,
|
||||
parsedHooks,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性的访问顺序
|
||||
function calculateAttrAccessPath(item: IAttr, index: number, attrs: IAttr[], isHook: boolean) {
|
||||
let currentIndentation = item.indentation;
|
||||
const path = [item.name];
|
||||
let hookRootItem: IAttr = item;
|
||||
for(let i = index - 1; i >= 0; i--) {
|
||||
const lastItem = attrs[i];
|
||||
const lastIndentation = lastItem.indentation;
|
||||
if (lastIndentation < currentIndentation) {
|
||||
hookRootItem = lastItem;
|
||||
path.push(lastItem.name);
|
||||
currentIndentation = lastIndentation;
|
||||
}
|
||||
}
|
||||
path.reverse();
|
||||
if (isHook) {
|
||||
if (hookRootItem) {
|
||||
path[0] = hookRootItem.hIndex;
|
||||
} else {
|
||||
console.error('There is a bug, please report');
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function buildAttrModifyData(parsedAttrsType: string, attrs: IAttr[], value, item: IAttr, index: number, id: number) {
|
||||
let type;
|
||||
if (parsedAttrsType === 'parsedProps') {
|
||||
type = ModifyProps;
|
||||
} else if (parsedAttrsType === 'parsedState') {
|
||||
type = ModifyState;
|
||||
} else if (parsedAttrsType === 'parsedHooks') {
|
||||
type = ModifyHooks;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const path = calculateAttrAccessPath(item, index, attrs, parsedAttrsType === 'parsedHooks');
|
||||
return {
|
||||
id: id,
|
||||
type: type,
|
||||
value: value,
|
||||
path: path,
|
||||
};
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
|
||||
import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
|
||||
|
||||
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
|
||||
const VNodeToIdMap = new Map<VNode, number>();
|
||||
const IdToVNodeMap = new Map<number, VNode>();
|
||||
|
||||
let uid = 0;
|
||||
function generateUid (vNode: VNode) {
|
||||
const id = VNodeToIdMap.get(vNode);
|
||||
if (id !== undefined) {
|
||||
return id;
|
||||
}
|
||||
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(travelVNodeTree, treeRoot: VNode) {
|
||||
const result: any[] = [];
|
||||
travelVNodeTree(treeRoot, (node: VNode) => {
|
||||
const tag = node.tag;
|
||||
if (isUserComponent(tag)) {
|
||||
const id = generateUid(node);
|
||||
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);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function queryVNode(id: number): VNode|undefined {
|
||||
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;
|
|
@ -1,17 +0,0 @@
|
|||
interface IArrow {
|
||||
direction: 'up' | 'down'
|
||||
}
|
||||
|
||||
export default function Arrow({ direction: director }: IArrow) {
|
||||
let d: string;
|
||||
if (director === 'up') {
|
||||
d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z';
|
||||
} else if (director === 'down') {
|
||||
d = 'M5 5.5 L4 6.5 L8 10.5 L12 6.5 L11 5.5 L8 8.5z';
|
||||
}
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d={d} fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
export default function Close() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M4 3 L3 4 L7 8 L3 12 L4 13 L8 9 L12 13 L13 12 L9 8 L13 4 L12 3 L8 7z' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
interface IArrow {
|
||||
director: 'right' | 'down'
|
||||
}
|
||||
|
||||
export default function Triangle({ 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>
|
||||
);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
// 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 ModifyAttrs = 'modify attrs';
|
||||
|
||||
export const ModifyProps = 'modify props';
|
||||
|
||||
export const ModifyState = 'modify state';
|
||||
|
||||
export const ModifyHooks = 'modify hooks';
|
||||
|
||||
export const InspectDom = 'inspect component dom';
|
||||
|
||||
export const LogComponentData = 'log component data';
|
||||
|
||||
export const CopyComponentAttr = 'copy component attr';
|
||||
|
||||
// 传递消息来源标志
|
||||
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';
|
|
@ -1,19 +0,0 @@
|
|||
|
||||
function ifNullThrows(v) {
|
||||
if (v === null) {
|
||||
throw new Error('received a null');
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// 用于向页面注入脚本
|
||||
export function injectCode(src) {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = function () {
|
||||
// 加载完毕后需要移除
|
||||
script.remove();
|
||||
};
|
||||
|
||||
ifNullThrows(document.head || document.documentElement).appendChild(script);
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// chrome 通过 iframe 的方式将 panel 页面嵌入到开发者工具中,如果有报错是无法感知到的
|
||||
// 同时也无法在运行时打断点,需要适当的日志辅助开发和问题定位
|
||||
|
||||
interface loggerType {
|
||||
error: typeof console.error,
|
||||
info: typeof console.info,
|
||||
log: typeof console.log,
|
||||
warn: typeof console.warn,
|
||||
}
|
||||
|
||||
export function createLogger(id: string): loggerType {
|
||||
return ['error', 'info', 'log', 'warn'].reduce((pre, current) => {
|
||||
const prefix = `[horizon_dev_tool][${id}] `;
|
||||
pre[current] = (...data) => {
|
||||
console[current](prefix, ...data);
|
||||
};
|
||||
return pre;
|
||||
}, {} as loggerType);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
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,19 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"*": ["types/*"]
|
||||
}
|
||||
},
|
||||
"includes": [
|
||||
"./src/index.d.ts", "./src/*/*.ts", "./src/*/*.tsx"
|
||||
]
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const fs = require('fs');
|
||||
|
||||
function handleBuildDir() {
|
||||
const staticDir = path.join(__dirname, 'build');
|
||||
console.log('staticDir: ', staticDir);
|
||||
const isBuildExist = fs.existsSync(staticDir);
|
||||
console.log('isBuildExist: ', isBuildExist);
|
||||
if (!isBuildExist) {
|
||||
fs.mkdirSync(staticDir);
|
||||
}
|
||||
fs.copyFileSync(path.join(__dirname, 'src', 'panel', 'panel.html'),path.join(staticDir, 'panel.html'));
|
||||
fs.copyFileSync(path.join(__dirname, 'src', 'main', 'main.html'),path.join(staticDir, 'main.html'));
|
||||
fs.copyFileSync(path.join(__dirname, 'src', 'manifest.json'),path.join(staticDir, 'manifest.json'));
|
||||
}
|
||||
handleBuildDir();
|
||||
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
background: './src/background/index.ts',
|
||||
main: './src/main/index.ts',
|
||||
injector: './src/injector/index.ts',
|
||||
contentScript: './src/contentScript/index.ts',
|
||||
panel: './src/panel/index.tsx',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, './build'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.less/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
|
||||
}
|
||||
},
|
||||
'less-loader'],
|
||||
}]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
},
|
||||
externals: {
|
||||
'horizon': 'Horizon',
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
isDev: 'false',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
|
@ -1,62 +0,0 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// 用于 panel 页面开发
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
panel: path.join(__dirname, './src/panel/index.tsx'),
|
||||
mockPage: path.join(__dirname, './src/devtools/mockPage/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',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.less/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
|
||||
}
|
||||
},
|
||||
'less-loader'],
|
||||
}]
|
||||
},
|
||||
externals: {
|
||||
'horizon': 'Horizon',
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'build'),
|
||||
},
|
||||
open: 'panel.html',
|
||||
port: 9000,
|
||||
magicHtml: true,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
isDev: 'true',
|
||||
}),
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue