diff --git a/libs/extension/package.json b/libs/extension/package.json
index ade577c7..6562c624 100644
--- a/libs/extension/package.json
+++ b/libs/extension/package.json
@@ -7,7 +7,7 @@
"build": "webpack --config ./webpack.config.js",
"watch": "webpack --config ./webpack.config.js --watch",
"build-dev": "webpack --config ./webpack.dev.js",
- "start": "webpack serve --config ./webpack.dev.js ",
+ "start": "npm run build && webpack serve --config ./webpack.dev.js ",
"test": "jest"
},
"keywords": [],
@@ -15,21 +15,21 @@
"license": "ISC",
"devDependencies": {
"@babel/core": "7.12.3",
- "@babel/plugin-proposal-class-properties": "^7.16.7",
+ "@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/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",
+ "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"
}
}
diff --git a/libs/extension/readme.md b/libs/extension/readme.md
index b54fecf2..fecc8f4f 100644
--- a/libs/extension/readme.md
+++ b/libs/extension/readme.md
@@ -1,3 +1,60 @@
+## 为什么要做 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: 拓展图标点击时弹窗页面
@@ -15,9 +72,6 @@ Optional: Feel free to dock the developer tools again if you had undocked it at
## 全局变量注入
通过content_scripts在document初始化时给页面添加script脚本,在新添加的脚本中给window注入全局变量
-## horizon页面判断
-在页面完成渲染后往全局变量中添加信息,并传递 tabId 给 background 告知这是 horizon 页面
-
## 通信方式:
```mermaid
sequenceDiagram
@@ -47,22 +101,37 @@ sequenceDiagram
```
## 传输数据结构
+**限制:chrome.runtime.sendMessage只能传递 JSON-serializable 数据**
+
+
```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 的引用也需要删除。
@@ -73,7 +142,7 @@ type passData = {
- 通过解析 path 值可以分析出组件树的结构
## 组件props/state/hook等数据的传输和解析
-将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串,他们的解析和 props/state 的解析同样存在差异。
+将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串或者 dom 元素,他们的解析和 props/state 的解析同样存在差异,需要单独处理。
## 滚动动态渲染 Tree
diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts
index ed9698fb..01e455f8 100644
--- a/libs/extension/src/background/index.ts
+++ b/libs/extension/src/background/index.ts
@@ -1,6 +1,6 @@
import { checkMessage, packagePayload, changeSource } from '../utils/transferTool';
import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants';
-import { DevToolPanel, DevToolContentScript } from './../utils/constants';
+import { DevToolPanel, DevToolContentScript } from '../utils/constants';
// 多个页面、tab页共享一个 background,需要建立连接池,给每个tab建立连接
const connections = {};
@@ -11,29 +11,18 @@ chrome.runtime.onConnect.addListener(function (port) {
const isHorizonMessage = checkMessage(message, DevToolPanel);
if (isHorizonMessage) {
const { payload } = message;
- const { type, data } = payload;
+ // tabId 值指当前浏览器分配给 web_page 的 id 值。是panel页面查询得到,指定向该页面发送消息
+ const { type, data, tabId } = payload;
let passMessage;
if (type === InitDevToolPageConnection) {
- if (!connections[data]) {
- // 获取 panel 所在 tab 页的tabId
- connections[data] = port;
- }
+ // 记录 panel 所在 tab 页的tabId,如果已经记录了,覆盖原有port,因为原有port可能关闭了
+ // 可能这次是 panel 发起的重新建立请求
+ connections[tabId] = port;
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
} else {
- passMessage = message;
- changeSource(passMessage, DevToolBackground);
+ passMessage = packagePayload({type, data}, DevToolBackground);
}
- // 查询参数有 active 和 currentWindow, 如果开发者工具与页面分离,会导致currentWindow为false才能找到
- // 所以只用 active 参数查找,但不确定这么写是否会引发查询错误的情况
- // 或许需要用不同的查询参数查找两次
- chrome.tabs.query({ active: true }, function (tabs) {
- if (tabs.length) {
- chrome.tabs.sendMessage(tabs[0].id, passMessage);
- console.log('post message end');
- } else {
- console.log('do not find message');
- }
- });
+ chrome.tabs.sendMessage(tabId, passMessage);
}
}
// Listen to messages sent from the DevTools page
@@ -57,10 +46,12 @@ 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 {
diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx
index f5ff0051..844da1ba 100644
--- a/libs/extension/src/components/ComponentInfo.tsx
+++ b/libs/extension/src/components/ComponentInfo.tsx
@@ -1,21 +1,22 @@
import styles from './ComponentsInfo.less';
import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
-import Copy from '../svgs/Copy';
import Triangle from '../svgs/Triangle';
import { useState, useEffect } from 'horizon';
import { IData } from './VTree';
-import { IAttr } from '../parser/parseAttr';
+import { buildAttrModifyData, IAttr } from '../parser/parseAttr';
+import { postMessageToBackground } from '../panelConnection';
+import { InspectDom, LogComponentData, ModifyAttrs } from '../utils/constants';
type IComponentInfo = {
name: string;
attrs: {
- props?: IAttr[];
- context?: IAttr[];
- state?: IAttr[];
- hooks?: IAttr[];
+ parsedProps?: IAttr[],
+ parsedState?: IAttr[],
+ parsedHooks?: IAttr[],
};
parents: IData[];
+ id: number;
onClickParent: (item: IData) => void;
};
@@ -26,11 +27,18 @@ function collapseAllNodes(attrs: IAttr[]) {
});
}
-function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
+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);
@@ -44,7 +52,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
const showAttr = [];
let currentIndentation = null;
- attrs.forEach((item, index) => {
+ editableAttrs.forEach((item, index) => {
const indentation = item.indentation;
if (currentIndentation !== null) {
if (indentation > currentIndentation) {
@@ -53,17 +61,55 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
currentIndentation = null;
}
}
- const nextItem = attrs[index + 1];
+ const nextItem = editableAttrs[index + 1];
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
const isCollapsed = collapsedNode.includes(item);
showAttr.push(
-
(handleCollapse(item))}>
+
handleCollapse(item)}>
{hasChild && }
{`${item.name}`}
{' :'}
- {item.type === 'string' || item.type === 'number'
- ? {item.value}
- : {item.value}}
+ {item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null'
+ ? {
+ 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'
+ ? {
+ 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);
+ }
+ }}/>
+ : {item.value}
+ )}
);
if (isCollapsed) {
@@ -74,10 +120,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
return (
- {name}
-
-
-
+ {attrsName}
{showAttr}
@@ -86,8 +129,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
);
}
-export default function ComponentInfo({ name, attrs, parents, onClickParent }: IComponentInfo) {
- const { state, props, context, hooks } = attrs;
+export default function ComponentInfo({ name, attrs, parents, id, onClickParent }: IComponentInfo) {
return (
@@ -95,19 +137,27 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
{name}
-
+ {
+ postMessageToBackground(InspectDom, id);
+ }}>
-
+ {
+ postMessageToBackground(LogComponentData, id);
+ }}>
>}
- {context &&
}
- {props && props.length !== 0 &&
}
- {state && state.length !== 0 &&
}
- {hooks && hooks.length !== 0 &&
}
+ {Object.keys(attrs).map(attrsType => {
+ const parsedAttrs = attrs[attrsType];
+ if (parsedAttrs && parsedAttrs.length !== 0) {
+ const attrsName = attrsType.slice(6); // parsedState => State
+ return
;
+ }
+ return null;
+ })}
{name &&
parents: {
@@ -122,4 +172,4 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
);
-}
\ No newline at end of file
+}
diff --git a/libs/extension/src/components/ComponentsInfo.less b/libs/extension/src/components/ComponentsInfo.less
index 853d4209..d1fd0de1 100644
--- a/libs/extension/src/components/ComponentsInfo.less
+++ b/libs/extension/src/components/ComponentsInfo.less
@@ -20,11 +20,13 @@
.eye {
flex: 0 0 1rem;
padding-right: 1rem;
+ cursor: pointer;
}
.debug {
flex: 0 0 1rem;
padding-right: 1rem;
+ cursor: pointer;
}
}
@@ -71,7 +73,7 @@
}
.attrValue {
- margin-left: 4px;
+ margin: 0 0 0 4px;
}
}
}
diff --git a/libs/extension/src/components/VList/VList.tsx b/libs/extension/src/components/VList/VList.tsx
index 0879fbf4..edf4b16b 100644
--- a/libs/extension/src/components/VList/VList.tsx
+++ b/libs/extension/src/components/VList/VList.tsx
@@ -2,7 +2,7 @@
// data 数组更新后不修改滚动位置,
// 只有修改scrollToItem才会修改滚动位置
-import { useState, useRef, useEffect, useMemo } from 'libs/extension/src/components/VList/node_modules/horizon';
+import { useState, useRef, useEffect, useMemo } from 'horizon';
import styles from './VList.less';
import ItemMap from './ItemMap';
diff --git a/libs/extension/src/components/VList/index.ts b/libs/extension/src/components/VList/index.ts
index d0154206..f30db06a 100644
--- a/libs/extension/src/components/VList/index.ts
+++ b/libs/extension/src/components/VList/index.ts
@@ -1 +1,2 @@
-export { VList, renderInfoType } from './VList';
+export { VList } from './VList';
+export type { renderInfoType } from './VList';
diff --git a/libs/extension/src/devtools/mock.ts b/libs/extension/src/devtools/mock.ts
index 3a3eef88..1c7447dc 100644
--- a/libs/extension/src/devtools/mock.ts
+++ b/libs/extension/src/devtools/mock.ts
@@ -5,8 +5,9 @@
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 { 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'];
@@ -50,7 +51,7 @@ addOneThousandNode(tree);
/**
* 将mock数据转变为 VNode 树
- *
+ *
* @param node 树节点
* @param vNode VNode节点
*/
@@ -78,7 +79,7 @@ function getMockVNodeTree(node: IMockTree, vNode: VNode) {
const rootVNode = MockVNode(tree.tag);
getMockVNodeTree(tree, rootVNode);
-export const mockParsedVNodeData = parseTreeRoot(rootVNode);
+export const mockParsedVNodeData = parseTreeRoot(helper.travelVNodeTree, rootVNode);
const mockState = {
str: 'jenny',
diff --git a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
index 41437e38..9495cd60 100644
--- a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
+++ b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
@@ -17,6 +17,7 @@ function reducer(state, action) {
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();
const objRef = useRef({ str: 'string' });
const context = useContext(MockContext);
@@ -26,6 +27,7 @@ export default function MockFunctionComponent(props) {
return (
age: {age}
+ name: {name.test}
count: {props.count}
@@ -33,4 +35,4 @@ export default function MockFunctionComponent(props) {
{context.ctx}
);
-}
\ No newline at end of file
+}
diff --git a/libs/extension/src/devtools/mockPage/index.tsx b/libs/extension/src/devtools/mockPage/index.tsx
index 57c96b44..f59908ff 100644
--- a/libs/extension/src/devtools/mockPage/index.tsx
+++ b/libs/extension/src/devtools/mockPage/index.tsx
@@ -1,4 +1,4 @@
-import { render } from 'horizon';
+import { render, useState } from 'horizon';
import MockClassComponent from './MockClassComponent';
import MockFunctionComponent from './MockFunctionComponent';
import { MockContext } from './MockContext';
@@ -6,11 +6,13 @@ import { MockContext } from './MockContext';
const root = document.createElement('div');
document.body.append(root);
function App() {
+ const [count, setCount] = useState(12);
return (
+
-
+
abc
diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts
index e0372d10..73692427 100644
--- a/libs/extension/src/injector/index.ts
+++ b/libs/extension/src/injector/index.ts
@@ -1,17 +1,21 @@
import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
-import { packagePayload, checkMessage } from './../utils/transferTool';
+import { packagePayload, checkMessage } from '../utils/transferTool';
import {
RequestAllVNodeTreeInfos,
AllVNodeTreesInfos,
RequestComponentAttrs,
ComponentAttrs,
DevToolHook,
- DevToolContentScript
-} from './../utils/constants';
-import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
-import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
-import { parseAttr, parseHooks } from '../parser/parseAttr';
-import { FunctionComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
+ 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 = [];
@@ -23,7 +27,7 @@ function addIfNotInclude(treeRoot: VNode) {
function send() {
const result = roots.reduce((pre, current) => {
- const info = parseTreeRoot(current);
+ const info = parseTreeRoot(helper.travelVNodeTree ,current);
pre.push(info);
return pre;
}, []);
@@ -47,25 +51,90 @@ function postMessage(type: string, data) {
}
function parseCompAttrs(id: number) {
- const vNode: VNode = queryVNode(id);
- const tag = vNode.tag;
- if (tag === ClassComponent) {
- const { props, state } = vNode;
- const parsedProps = parseAttr(props);
- const parsedState = parseAttr(state);
- postMessage(ComponentAttrs, {
- parsedProps,
- parsedState,
- });
- } else if (tag === FunctionComponent) {
- const { props, hooks } = vNode;
- const parsedProps = parseAttr(props);
- const parsedHooks = parseHooks(hooks);
- postMessage(ComponentAttrs, {
- parsedProps,
- parsedHooks,
- });
+ 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() {
@@ -75,6 +144,8 @@ function injectHook() {
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
enumerable: false,
value: {
+ init,
+ isInit: false,
addIfNotInclude,
send,
deleteVNode,
@@ -93,6 +164,14 @@ function injectHook() {
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);
}
}
});
diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx
index af6e5d9a..22994dcc 100644
--- a/libs/extension/src/panel/App.tsx
+++ b/libs/extension/src/panel/App.tsx
@@ -9,15 +9,21 @@ import { FilterTree } from '../hooks/FilterTree';
import Close from '../svgs/Close';
import Arrow from './../svgs/Arrow';
import {
- InitDevToolPageConnection,
AllVNodeTreesInfos,
RequestComponentAttrs,
ComponentAttrs,
- DevToolPanel,
-} from './../utils/constants';
-import { packagePayload } from './../utils/transferTool';
+} from '../utils/constants';
+import {
+ addBackgroundMessageListener,
+ initBackgroundConnection,
+ postMessageToBackground, removeBackgroundMessageListener,
+} from '../panelConnection';
+import { IAttr } from '../parser/parseAttr';
+import { createLogger } from '../utils/logUtil';
-const parseVNodeData = (rawData) => {
+const logger = createLogger('panelApp');
+
+const parseVNodeData = (rawData, idToTreeNodeMap , nextIdToTreeNodeMap) => {
const idIndentationMap: {
[id: string]: number;
} = {};
@@ -34,10 +40,23 @@ const parseVNodeData = (rawData) => {
i++;
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
idIndentationMap[id] = indentation;
- const item = {
- id, name, indentation, userKey
- };
- data.push(item);
+ 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;
};
@@ -59,47 +78,19 @@ const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
return parents;
};
-let connection;
-if (!isDev) {
- // 与 background 的唯一连接
- connection = chrome.runtime.connect({
- name: 'panel'
- });
-}
-
-let reconnectTimes = 0;
-
-function postMessage(type: string, data: any) {
- try {
- connection.postMessage(packagePayload({
- type: type,
- data: data,
- }, DevToolPanel));
- } catch(err) {
- // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
- if (reconnectTimes === 20) {
- reconnectTimes = 0;
- console.error('reconnect failed');
- return;
- }
- console.error(err);
- reconnectTimes++;
- // 重建连接
- connection = chrome.runtime.connect({
- name: 'panel'
- });
- // 重新发送初始化消息
- postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
- // 初始化成功后才会重新发送消息
- postMessage(type, data);
- }
+interface IIdToNodeMap {
+ [id: number]: IData;
}
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
- const [componentAttrs, setComponentAttrs] = useState({});
+ const [componentAttrs, setComponentAttrs] = useState<{
+ parsedProps?: IAttr[],
+ parsedState?: IAttr[],
+ parsedHooks?: IAttr[],
+ }>({});
const [selectComp, setSelectComp] = useState(null);
- const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度,
+ const idToTreeNodeMapRef = useRef({});
const {
filterValue,
@@ -116,42 +107,47 @@ function App() {
useEffect(() => {
if (isDev) {
- const parsedData = parseVNodeData(mockParsedVNodeData);
+ const nextIdToTreeNodeMap: IIdToNodeMap = {};
+ const parsedData = parseVNodeData(mockParsedVNodeData, idToTreeNodeMapRef.current, nextIdToTreeNodeMap);
+ idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(parsedData);
setComponentAttrs({
- state: parsedMockState,
- props: parsedMockState,
+ parsedProps: parsedMockState,
+ parsedState: parsedMockState,
});
} else {
- // 页面打开后发送初始化请求
- postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
- // 监听 background消息
- connection.onMessage.addListener(function (message) {
+ 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);
- const length = parsedTreeData.length;
- treeRootInfos.current.length = 0;
- if (length) {
- const treeRoot = parsedTreeData[0];
- treeRootInfos.current.push({id: treeRoot.id, length: length});
- }
+ 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({
- props: parsedProps,
- state: parsedState,
- hooks: parsedHooks,
+ parsedProps,
+ parsedState,
+ parsedHooks,
});
}
}
- });
+ };
+ // 在页面渲染后初始化连接
+ initBackgroundConnection();
+ // 监听 background消息
+ addBackgroundMessageListener(handleBackgroundMessage);
+ return () => {
+ removeBackgroundMessageListener(handleBackgroundMessage);
+ };
}
}, []);
@@ -162,11 +158,11 @@ function App() {
const handleSelectComp = (item: IData) => {
if (isDev) {
setComponentAttrs({
- state: parsedMockState,
- props: parsedMockState,
+ parsedProps: parsedMockState,
+ parsedState: parsedMockState,
});
} else {
- postMessage(RequestComponentAttrs, item.id);
+ postMessageToBackground(RequestComponentAttrs, item.id);
}
setSelectComp(item);
};
@@ -216,6 +212,7 @@ function App() {
name={selectComp ? selectComp.name : null}
attrs={selectComp ? componentAttrs : {}}
parents={parents}
+ id={selectComp ? selectComp.id : null}
onClickParent={handleClickParent} />
diff --git a/libs/extension/src/panel/panel.html b/libs/extension/src/panel/panel.html
index ae944454..705a2536 100644
--- a/libs/extension/src/panel/panel.html
+++ b/libs/extension/src/panel/panel.html
@@ -1,10 +1,8 @@
-
+
-
Horizon
-
+
-
-
+
+
diff --git a/libs/extension/src/panelConnection/PanelConnection.ts b/libs/extension/src/panelConnection/PanelConnection.ts
new file mode 100644
index 00000000..27bcc4ac
--- /dev/null
+++ b/libs/extension/src/panelConnection/PanelConnection.ts
@@ -0,0 +1,61 @@
+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);
+ }
+}
diff --git a/libs/extension/src/panelConnection/index.ts b/libs/extension/src/panelConnection/index.ts
new file mode 100644
index 00000000..b2d3e64d
--- /dev/null
+++ b/libs/extension/src/panelConnection/index.ts
@@ -0,0 +1 @@
+export * from './PanelConnection';
diff --git a/libs/extension/src/parser/parseAttr.ts b/libs/extension/src/parser/parseAttr.ts
index e18f9bad..0719771d 100644
--- a/libs/extension/src/parser/parseAttr.ts
+++ b/libs/extension/src/parser/parseAttr.ts
@@ -1,5 +1,8 @@
-import { Hook, Reducer, Ref } from './../../../horizon/src/renderer/hooks/HookType';
+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';
@@ -12,7 +15,7 @@ type showAsStringType = editableStringType | unEditableStringType;
export type IAttr = {
- name: string;
+ name: string | number;
indentation: number;
hIndex?: number; // 用于记录 hook 的 hIndex 值
} & ({
@@ -96,7 +99,7 @@ const parseSubAttr = (
value,
indentation: parentIndentation + 1,
};
- if (hIndex) {
+ if (hIndex !== undefined) {
item.hIndex = hIndex;
}
result.push(item);
@@ -116,18 +119,81 @@ export function parseAttr(rootAttr: any) {
return result;
}
-export function parseHooks(hooks: Hook
[]) {
+export function parseHooks(hooks: Hook[], getHookInfo) {
const result: IAttr[] = [];
const indentation = 0;
hooks.forEach(hook => {
- const { hIndex, state ,type } = hook;
- if (type === 'useState') {
- parseSubAttr((state as Reducer).stateValue, indentation, 'state', result, hIndex);
- } else if (type === 'useRef') {
- parseSubAttr((state as Ref).current, indentation, 'ref', result, hIndex);
- } else if (type === 'useReducer') {
- parseSubAttr((state as Reducer).stateValue, indentation, 'reducer', result, hIndex);
+ 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,
+ };
+}
diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts
index fbf4b0bd..a70e7189 100644
--- a/libs/extension/src/parser/parseVNode.ts
+++ b/libs/extension/src/parser/parseVNode.ts
@@ -1,13 +1,16 @@
-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';
+import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
+import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
const VNodeToIdMap = new Map();
const IdToVNodeMap = new Map();
let uid = 0;
-function generateUid () {
+function generateUid (vNode: VNode) {
+ const id = VNodeToIdMap.get(vNode);
+ if (id !== undefined) {
+ return id;
+ }
uid++;
return uid;
}
@@ -28,12 +31,12 @@ function getParentUserComponent(node: VNode) {
return parent;
}
-function parseTreeRoot(treeRoot: VNode) {
+function parseTreeRoot(travelVNodeTree, treeRoot: VNode) {
const result: any[] = [];
travelVNodeTree(treeRoot, (node: VNode) => {
const tag = node.tag;
if (isUserComponent(tag)) {
- const id = generateUid();
+ const id = generateUid(node);
result.push(id);
const name = node.type.name;
result.push(name);
@@ -53,11 +56,11 @@ function parseTreeRoot(treeRoot: VNode) {
VNodeToIdMap.set(node, id);
IdToVNodeMap.set(id, node);
}
- }, null, treeRoot, null);
+ });
return result;
}
-export function queryVNode(id: number) {
+export function queryVNode(id: number): VNode|undefined {
return IdToVNodeMap.get(id);
}
diff --git a/libs/extension/src/svgs/Copy.tsx b/libs/extension/src/svgs/Copy.tsx
deleted file mode 100644
index 483d9ece..00000000
--- a/libs/extension/src/svgs/Copy.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-
-export default function Copy() {
- return (
-
- );
-}
diff --git a/libs/extension/src/utils/constants.ts b/libs/extension/src/utils/constants.ts
index e779a143..e448bc2b 100644
--- a/libs/extension/src/utils/constants.ts
+++ b/libs/extension/src/utils/constants.ts
@@ -11,6 +11,19 @@ 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';
@@ -19,4 +32,4 @@ export const DevToolBackground = 'dev tool background';
export const DevToolContentScript = 'dev tool content script';
-export const DevToolHook = 'dev tool hook';
\ No newline at end of file
+export const DevToolHook = 'dev tool hook';
diff --git a/libs/extension/src/utils/logUtil.ts b/libs/extension/src/utils/logUtil.ts
new file mode 100644
index 00000000..d57e8bfe
--- /dev/null
+++ b/libs/extension/src/utils/logUtil.ts
@@ -0,0 +1,19 @@
+// 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);
+}
diff --git a/libs/extension/webpack.config.js b/libs/extension/webpack.config.js
index 25dab94b..86450201 100644
--- a/libs/extension/webpack.config.js
+++ b/libs/extension/webpack.config.js
@@ -1,5 +1,21 @@
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: {
diff --git a/libs/extension/webpack.dev.js b/libs/extension/webpack.dev.js
index b3332077..fc29852f 100644
--- a/libs/extension/webpack.dev.js
+++ b/libs/extension/webpack.dev.js
@@ -47,7 +47,7 @@ module.exports = {
},
devServer: {
static: {
- directory: path.join(__dirname, 'dist'),
+ directory: path.join(__dirname, 'build'),
},
open: 'panel.html',
port: 9000,
diff --git a/libs/horizon/index.d.ts b/libs/horizon/global.d.ts
similarity index 65%
rename from libs/horizon/index.d.ts
rename to libs/horizon/global.d.ts
index bab6684d..0ebd7135 100644
--- a/libs/horizon/index.d.ts
+++ b/libs/horizon/global.d.ts
@@ -2,3 +2,4 @@
区分是否开发者模式
*/
declare var isDev: boolean;
+declare const __VERSION__: string;
diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts
index cd194973..71d104a9 100644
--- a/libs/horizon/index.ts
+++ b/libs/horizon/index.ts
@@ -13,6 +13,7 @@ import { createContext } from './src/renderer/components/context/CreateContext';
import { lazy } from './src/renderer/components/Lazy';
import { forwardRef } from './src/renderer/components/ForwardRef';
import { memo } from './src/renderer/components/Memo';
+import './src/external/devtools';
import {
useCallback,
@@ -82,6 +83,7 @@ const Horizon = {
_getProcessingVNode,
};
+export const version = __VERSION__;
export {
Children,
createRef,
@@ -114,7 +116,6 @@ export {
findDOMNode,
unmountComponentAtNode,
act,
-
// 暂时给HorizonX使用
_launchUpdateFromVNode,
_getProcessingVNode,
diff --git a/libs/horizon/package.json b/libs/horizon/package.json
index f3ffb19d..dedb9445 100644
--- a/libs/horizon/package.json
+++ b/libs/horizon/package.json
@@ -4,7 +4,7 @@
"keywords": [
"horizon"
],
- "version": "0.0.6",
+ "version": "0.0.7",
"homepage": "",
"bugs": "",
"main": "index.js",
diff --git a/libs/horizon/src/event/ListenerGetter.ts b/libs/horizon/src/event/ListenerGetter.ts
index 53e14ee9..d6860f35 100644
--- a/libs/horizon/src/event/ListenerGetter.ts
+++ b/libs/horizon/src/event/ListenerGetter.ts
@@ -1,14 +1,32 @@
-import {VNode} from '../renderer/Types';
-import {DomComponent} from '../renderer/vnode/VNodeTags';
-import {EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE} from './const';
-import {AnyNativeEvent, ListenerUnitList} from './Types';
+import { VNode } from '../renderer/Types';
+import { DomComponent } from '../renderer/vnode/VNodeTags';
+import { EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE } from './const';
+import { AnyNativeEvent, ListenerUnitList } from './Types';
+
+// 从vnode属性中获取事件listener
+function getListenerFromVNode(vNode: VNode, eventName: string): Function | null {
+ const props = vNode.props;
+ const mouseEvents = ['onClick', 'onDoubleClick', 'onMouseDown', 'onMouseMove', 'onMouseUp', 'onMouseEnter'];
+ const formElements = ['button', 'input', 'select', 'textarea'];
+
+ // 是否应该阻止禁用的表单元素触发鼠标事件
+ const shouldPreventMouseEvent =
+ mouseEvents.includes(eventName) && props.disabled && formElements.includes(vNode.type);
+
+ const listener = props[eventName];
+ if (shouldPreventMouseEvent) {
+ return null;
+ } else {
+ return listener;
+ }
+}
// 获取监听事件
export function getListenersFromTree(
targetVNode: VNode | null,
horizonEvtName: string | null,
nativeEvent: AnyNativeEvent,
- eventType: string,
+ eventType: string
): ListenerUnitList {
if (!horizonEvtName) {
return [];
@@ -20,11 +38,11 @@ export function getListenersFromTree(
// 从目标节点到根节点遍历获取listener
while (vNode !== null) {
- const {realNode, tag} = vNode;
+ const { realNode, tag } = vNode;
if (tag === DomComponent && realNode !== null) {
if (eventType === EVENT_TYPE_ALL || eventType === EVENT_TYPE_CAPTURE) {
const captureName = horizonEvtName + EVENT_TYPE_CAPTURE;
- const captureListener = vNode.props[captureName];
+ const captureListener = getListenerFromVNode(vNode, captureName);
if (captureListener) {
listeners.unshift({
vNode,
@@ -36,7 +54,7 @@ export function getListenersFromTree(
}
if (eventType === EVENT_TYPE_ALL || eventType === EVENT_TYPE_BUBBLE) {
- const bubbleListener = vNode.props[horizonEvtName];
+ const bubbleListener = getListenerFromVNode(vNode, horizonEvtName);
if (bubbleListener) {
listeners.push({
vNode,
@@ -52,6 +70,3 @@ export function getListenersFromTree(
return listeners;
}
-
-
-
diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts
new file mode 100644
index 00000000..10e70e80
--- /dev/null
+++ b/libs/horizon/src/external/devtools.ts
@@ -0,0 +1,84 @@
+import { travelVNodeTree } from '../renderer/vnode/VNodeUtils';
+import { Hook, Reducer, Ref } from '../renderer/hooks/HookType';
+import { VNode } from '../renderer/vnode/VNode';
+import { launchUpdateFromVNode } from '../renderer/TreeBuilder';
+import { DomComponent } from '../renderer/vnode/VNodeTags';
+
+export const helper = {
+ travelVNodeTree: (rootVNode, fun) => {
+ travelVNodeTree(rootVNode, fun, null, rootVNode, null);
+ },
+ // 获取 hook 名,hIndex值和存储的值
+ // 目前只处理 useState和useRef
+ getHookInfo:(hook: Hook) => {
+ const { hIndex, state } = hook;
+ if ((state as Reducer).trigger) {
+ if ((state as Reducer).isUseState) {
+ return {name: 'state', hIndex, value: (state as Reducer).stateValue};
+ }
+ } else if ((state as Ref).current) {
+ return {name: 'ref', hIndex, value: (state as Ref).current};
+ }
+ return null;
+ },
+ updateProps: (vNode: VNode, props: any) =>{
+ vNode.devProps = props;
+ launchUpdateFromVNode(vNode);
+ },
+ updateState: (vNode: VNode, nextState) => {
+ const instance = vNode.realNode;
+ instance.setState(nextState);
+ },
+ updateHooks: (vNode: VNode, hIndex, nextState) => {
+ const hooks = vNode.hooks;
+ if (hooks) {
+ const editHook = hooks[hIndex];
+ const editState = editHook.state as Reducer;
+ // 暂时只支持更新 useState 的值
+ if (editState.trigger && editState.isUseState) {
+ editState.trigger(nextState);
+ }
+ } else {
+ console.error('Target vNode is not a hook vNode: ', vNode);
+ }
+ },
+ getComponentInfo: (vNode: VNode) => {
+ const { props, state, hooks } = vNode;
+ const info:any = {};
+ if (props && Object.keys(props).length !== 0) {
+ info['Props'] = props;
+ }
+ if (state && Object.keys(state).length !== 0) {
+ info['State'] = state;
+ }
+ if (hooks && hooks.length !== 0) {
+ const logHookInfo: any[] = [];
+ hooks.forEach((hook) =>{
+ const state = hook.state as Reducer;
+ if (state.trigger && state.isUseState) {
+ logHookInfo.push(state.stateValue);
+ }
+ });
+ info['Hooks'] = logHookInfo;
+ }
+ travelVNodeTree(vNode, (node: VNode) => {
+ if (node.tag === DomComponent) {
+ // 找到组件的第一个dom元素,返回它所在父节点的全部子节点
+ const dom = node.realNode;
+ info['Nodes'] = dom?.parentNode?.childNodes;
+ return true;
+ }
+ return false;
+ }, null, vNode, null);
+ return info;
+ },
+};
+
+export function injectUpdater() {
+ const hook = window.__HORIZON_DEV_HOOK__;
+ if (hook) {
+ hook.init(helper);
+ }
+}
+
+injectUpdater();
diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts
index 394319e0..923b4275 100644
--- a/libs/horizon/src/renderer/TreeBuilder.ts
+++ b/libs/horizon/src/renderer/TreeBuilder.ts
@@ -36,6 +36,7 @@ import {
updateShouldUpdateOfTree
} from './vnode/VNodeShouldUpdate';
import { getPathArr } from './utils/vNodePath';
+import { injectUpdater } from '../external/devtools';
// 不可恢复错误
let unrecoverableErrorDuringBuild: any = null;
@@ -236,7 +237,11 @@ function buildVNodeTree(treeRoot: VNode) {
// 重置环境变量,为重新进行深度遍历做准备
resetProcessingVariables(startVNode);
-
+ // devProps 用于插件手动更新props值
+ if (startVNode.devProps !== undefined) {
+ startVNode.props = startVNode.devProps;
+ startVNode.devProps = undefined;
+ }
while (processing !== null) {
try {
while (processing !== null) {
@@ -277,6 +282,11 @@ function renderFromRoot(treeRoot) {
if (window.__HORIZON_DEV_HOOK__) {
const hook = window.__HORIZON_DEV_HOOK__;
+ // injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量
+ // Horizon 代码初次加载时不会初始化 helper
+ if (!hook.isInit) {
+ injectUpdater();
+ }
hook.addIfNotInclude(treeRoot);
hook.send(treeRoot);
}
diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts
index cb8be892..e965fdf1 100644
--- a/libs/horizon/src/renderer/hooks/HookType.ts
+++ b/libs/horizon/src/renderer/hooks/HookType.ts
@@ -3,7 +3,6 @@ import {EffectConstant} from './EffectConstant';
export interface Hook {
state: Reducer | Effect | Memo | CallBack | Ref;
hIndex: number;
- type?: 'useState' | 'useRef' | 'useReducer';
}
export interface Reducer {
diff --git a/libs/horizon/src/renderer/hooks/UseReducerHook.ts b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
index 480f43bb..52399713 100644
--- a/libs/horizon/src/renderer/hooks/UseReducerHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
@@ -87,7 +87,6 @@ export function useReducerForInit(reducer, initArg, init, isUseState?: boo
}
const hook = createHook();
- hook.type = isUseState ? 'useState' : 'useReducer';
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
hook.state = {
stateValue: stateValue,
diff --git a/libs/horizon/src/renderer/hooks/UseRefHook.ts b/libs/horizon/src/renderer/hooks/UseRefHook.ts
index 381ef61e..754a16d2 100644
--- a/libs/horizon/src/renderer/hooks/UseRefHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseRefHook.ts
@@ -12,7 +12,6 @@ export function useRefImpl(value: V): Ref {
if (stage === HookStage.Init) {
hook = createHook();
hook.state = {current: value};
- hook.type = 'useRef';
} else if (stage === HookStage.Update) {
hook = getCurrentHook();
}
diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts
index 29fb008b..812d2b8e 100644
--- a/libs/horizon/src/renderer/vnode/VNode.ts
+++ b/libs/horizon/src/renderer/vnode/VNode.ts
@@ -70,7 +70,7 @@ export class VNode {
oldRef: RefType | ((handle: any) => void) | null = null;
oldChild: VNode | null = null;
promiseResolve: boolean; // suspense的promise是否resolve
-
+ devProps: any; // 用于dev插件临时保存更新props值
suspenseState: SuspenseState;
path = ''; // 保存从根到本节点的路径
diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts
index 2875a125..ef60b566 100644
--- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts
+++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts
@@ -97,6 +97,10 @@ export function clearVNode(vNode: VNode) {
vNode.toUpdateNodes = null;
vNode.belongClassVNode = null;
+ if (window.__HORIZON_DEV_HOOK__) {
+ const hook = window.__HORIZON_DEV_HOOK__;
+ hook.delete(vNode);
+ }
}
// 是dom类型的vNode
diff --git a/package.json b/package.json
index 5ef9d520..58bf2f9f 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,10 @@
],
"scripts": {
"lint": "eslint . --ext .ts",
- "build": " webpack --config ./scripts/webpack/webpack.config.js",
+ "build": " rollup --config ./scripts/rollup/rollup.config.js",
"build-3rdLib": "node ./scripts/gen3rdLib.js",
"build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev",
+ "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon",
"debug-test": "yarn test --debug",
"test": "jest --config=jest.config.js",
"watch-test": "yarn test --watch --dev"
@@ -50,6 +51,9 @@
"@babel/register": "^7.14.5",
"@babel/traverse": "^7.11.0",
"@mattiasbuelens/web-streams-polyfill": "^0.3.2",
+ "@rollup/plugin-babel": "^5.3.1",
+ "@rollup/plugin-node-resolve": "^13.3.0",
+ "@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^26.0.24",
"@types/node": "^17.0.18",
"@typescript-eslint/eslint-plugin": "^5.15.0",
@@ -57,13 +61,11 @@
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.3",
"babel-jest": "^27.5.1",
- "babel-loader": "^8.2.2",
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"chalk": "^3.0.0",
"confusing-browser-globals": "^1.0.9",
- "copy-webpack-plugin": "5.0.4",
"core-js": "^3.6.4",
"danger": "^9.2.10",
"ejs": "^3.1.6",
@@ -76,7 +78,6 @@
"eslint-plugin-no-for-of-loops": "^1.0.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
"eslint-plugin-react": "^6.7.1",
- "eslint-webpack-plugin": "^3.0.1",
"jest": "^25.5.4",
"jest-cli": "^25.2.7",
"jest-diff": "^25.2.6",
@@ -89,9 +90,9 @@
"react-lifecycles-compat": "^3.0.4",
"regenerator-runtime": "^0.13.9",
"rimraf": "^3.0.0",
- "typescript": "^3.9.7",
- "webpack": "^4.46.0",
- "webpack-cli": "^4.7.2"
+ "rollup": "^2.75.5",
+ "rollup-plugin-terser": "^7.0.2",
+ "typescript": "^3.9.7"
},
"engines": {
"node": ">=10.x",
diff --git a/scripts/__tests__/EventTest/MouseEvent.test.js b/scripts/__tests__/EventTest/MouseEvent.test.js
index a0972a05..030e25de 100644
--- a/scripts/__tests__/EventTest/MouseEvent.test.js
+++ b/scripts/__tests__/EventTest/MouseEvent.test.js
@@ -2,8 +2,8 @@ import * as Horizon from '@cloudsop/horizon/index.ts';
import { getLogUtils } from '../jest/testUtils';
describe('MouseEvent Test', () => {
- const LogUtils =getLogUtils();
-
+ const LogUtils = getLogUtils();
+
describe('onClick Test', () => {
it('绑定this', () => {
class App extends Horizon.Component {
@@ -11,7 +11,7 @@ describe('MouseEvent Test', () => {
super(props);
this.state = {
num: this.props.num,
- price: this.props.price
+ price: this.props.price,
};
}
@@ -19,21 +19,24 @@ describe('MouseEvent Test', () => {
this.setState({ num: this.state.num + 1 });
}
- setPrice = (e) => {
+ setPrice = e => {
this.setState({ num: this.state.price + 1 });
- }
+ };
render() {
return (
<>
{this.state.num}
{this.state.price}
-
-
+
+
>
);
}
}
+
Horizon.render(, container);
expect(container.querySelector('p').innerHTML).toBe('0');
expect(container.querySelector('#p').innerHTML).toBe('100');
@@ -55,6 +58,20 @@ describe('MouseEvent Test', () => {
}
expect(handleClick).toHaveBeenCalledTimes(6);
});
+
+ it('disable不触发click', () => {
+ const handleClick = jest.fn();
+ const spanRef = Horizon.createRef();
+ Horizon.render(
+ ,
+ container
+ );
+ spanRef.current.click();
+
+ expect(handleClick).toHaveBeenCalledTimes(0);
+ });
});
const test = (name, config) => {
@@ -62,27 +79,21 @@ describe('MouseEvent Test', () => {
let event = new MouseEvent(name, {
relatedTarget: null,
bubbles: true,
- screenX: 1
+ screenX: 1,
});
node.dispatchEvent(event);
- expect(LogUtils.getAndClear()).toEqual([
- `${name} capture`,
- `${name} bubble`
- ]);
+ expect(LogUtils.getAndClear()).toEqual([`${name} capture`, `${name} bubble`]);
event = new MouseEvent(name, {
relatedTarget: null,
bubbles: true,
- screenX: 2
+ screenX: 2,
});
node.dispatchEvent(event);
// 再次触发新事件
- expect(LogUtils.getAndClear()).toEqual([
- `${name} capture`,
- `${name} bubble`
- ]);
+ expect(LogUtils.getAndClear()).toEqual([`${name} capture`, `${name} bubble`]);
};
describe('合成鼠标事件', () => {
@@ -93,10 +104,7 @@ describe('MouseEvent Test', () => {
const onMouseMoveCapture = () => {
LogUtils.log('mousemove capture');
};
- test('mousemove', );
+ test('mousemove', );
});
it('onMouseDown', () => {
@@ -106,10 +114,7 @@ describe('MouseEvent Test', () => {
const onMousedownCapture = () => {
LogUtils.log('mousedown capture');
};
- test('mousedown', );
+ test('mousedown', );
});
it('onMouseUp', () => {
@@ -119,10 +124,7 @@ describe('MouseEvent Test', () => {
const onMouseUpCapture = () => {
LogUtils.log('mouseup capture');
};
- test('mouseup', );
+ test('mouseup', );
});
it('onMouseOut', () => {
@@ -132,10 +134,7 @@ describe('MouseEvent Test', () => {
const onMouseOutCapture = () => {
LogUtils.log('mouseout capture');
};
- test('mouseout', );
+ test('mouseout', );
});
it('onMouseOver', () => {
@@ -145,10 +144,7 @@ describe('MouseEvent Test', () => {
const onMouseOverCapture = () => {
LogUtils.log('mouseover capture');
};
- test('mouseover', );
+ test('mouseover', );
});
});
});
diff --git a/scripts/__tests__/jest/jestEnvironment.js b/scripts/__tests__/jest/jestEnvironment.js
index d19a3b87..824f8a9d 100644
--- a/scripts/__tests__/jest/jestEnvironment.js
+++ b/scripts/__tests__/jest/jestEnvironment.js
@@ -5,3 +5,4 @@ global.MessageChannel = function MessageChannel() {
postMessage() { }
};
};
+global.__VERSION__ = require('../../../libs/horizon/package.json').version;
diff --git a/scripts/gen3rdLib.js b/scripts/gen3rdLib.js
index 7e33dfec..f2953691 100644
--- a/scripts/gen3rdLib.js
+++ b/scripts/gen3rdLib.js
@@ -1,4 +1,4 @@
-'use strict'
+'use strict';
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
@@ -9,26 +9,31 @@ const argv = require('minimist')(process.argv.slice(2));
const libPathPrefix = '../build';
const suffix = argv.dev ? 'development.js' : 'production.js';
-
-const readLib = (lib) => {
+const template = argv.type === 'horizon' ? 'horizon3rdTemplate.ejs' : 'template.ejs';
+const readLib = lib => {
const libName = lib.split('.')[0];
const libPath = path.resolve(__dirname, `${libPathPrefix}/${libName}/umd/${lib}`);
if (fs.existsSync(libPath)) {
- return fs.readFileSync(libPath,'utf-8');
+ return fs.readFileSync(libPath, 'utf-8');
} else {
- console.log(chalk.red(`Error: "${libPath}" 文件不存在\n先运行 npm run build`))
+ console.log(chalk.red(`Error: "${libPath}" 文件不存在\n先运行 npm run build`));
}
};
-ejs.renderFile(path.resolve(__dirname, './template.ejs'), {
- Horizon: readLib(`horizon.${suffix}`),
-}, null, function(err, result) {
- const common3rdLibPath = path.resolve(__dirname, `${libPathPrefix}/horizonCommon3rdlib.min.js`)
- rimRaf(common3rdLibPath, e => {
- if (e) {
- console.log(e)
- }
- fs.writeFileSync(common3rdLibPath, result);
- console.log(chalk.green(`成功生成: ${common3rdLibPath}`))
- })
-});
+ejs.renderFile(
+ path.resolve(__dirname, `./${template}`),
+ {
+ Horizon: readLib(`horizon.${suffix}`),
+ },
+ null,
+ function(err, result) {
+ const common3rdLibPath = path.resolve(__dirname, `${libPathPrefix}/horizonCommon3rdlib.min.js`);
+ rimRaf(common3rdLibPath, e => {
+ if (e) {
+ console.log(e);
+ }
+ fs.writeFileSync(common3rdLibPath, result);
+ console.log(chalk.green(`成功生成: ${common3rdLibPath}`));
+ });
+ }
+);
diff --git a/scripts/horizon3rdTemplate.ejs b/scripts/horizon3rdTemplate.ejs
new file mode 100644
index 00000000..889a6283
--- /dev/null
+++ b/scripts/horizon3rdTemplate.ejs
@@ -0,0 +1,17568 @@
+!function(t, r) {
+"object" == typeof exports && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports ? exports.ie = r() : t.ie = r()
+}(window, (function() {
+return function(t) {
+var r = {};
+function e(n) {
+if (r[n])
+return r[n].exports;
+var o = r[n] = {
+i: n,
+l: !1,
+exports: {}
+};
+return t[n].call(o.exports, o, o.exports, e),
+o.l = !0,
+o.exports
+}
+return e.m = t,
+e.c = r,
+e.d = function(t, r, n) {
+e.o(t, r) || Object.defineProperty(t, r, {
+enumerable: !0,
+get: n
+})
+}
+,
+e.r = function(t) {
+"undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(t, Symbol.toStringTag, {
+value: "Module"
+}),
+Object.defineProperty(t, "__esModule", {
+value: !0
+})
+}
+,
+e.t = function(t, r) {
+if (1 & r && (t = e(t)),
+8 & r)
+return t;
+if (4 & r && "object" == typeof t && t && t.__esModule)
+return t;
+var n = Object.create(null);
+if (e.r(n),
+Object.defineProperty(n, "default", {
+enumerable: !0,
+value: t
+}),
+2 & r && "string" != typeof t)
+for (var o in t)
+e.d(n, o, function(r) {
+return t[r]
+}
+.bind(null, o));
+return n
+}
+,
+e.n = function(t) {
+var r = t && t.__esModule ? function() {
+return t.default
+}
+: function() {
+return t
+}
+;
+return e.d(r, "a", r),
+r
+}
+,
+e.o = function(t, r) {
+return Object.prototype.hasOwnProperty.call(t, r)
+}
+,
+e.p = "",
+e(e.s = 192)
+}([function(t, r, e) {
+var n = e(1)
+, o = e(23).f
+, i = e(25)
+, a = e(17)
+, u = e(114)
+, c = e(90)
+, s = e(73);
+t.exports = function(t, r) {
+var e, f, l, h, p, v = t.target, g = t.global, d = t.stat;
+if (e = g ? n : d ? n[v] || u(v, {}) : (n[v] || {}).prototype)
+for (f in r) {
+if (h = r[f],
+l = t.noTargetGet ? (p = o(e, f)) && p.value : e[f],
+!s(g ? f : v + (d ? "." : "#") + f, t.forced) && void 0 !== l) {
+if (typeof h == typeof l)
+continue;
+c(h, l)
+}
+(t.sham || l && l.sham) && i(h, "sham", !0),
+a(e, f, h, t)
+}
+}
+}
+, function(t, r, e) {
+(function(r) {
+var e = function(t) {
+return t && t.Math == Math && t
+};
+t.exports = e("object" == typeof globalThis && globalThis) || e("object" == typeof window && window) || e("object" == typeof self && self) || e("object" == typeof r && r) || function() {
+return this
+}() || Function("return this")()
+}
+).call(this, e(195))
+}
+, function(t, r) {
+t.exports = function(t) {
+try {
+return !!t()
+} catch (t) {
+return !0
+}
+}
+}
+, function(t, r, e) {
+var n = e(68)
+, o = Function.prototype
+, i = o.bind
+, a = o.call
+, u = n && i.bind(a, a);
+t.exports = n ? function(t) {
+return t && u(t)
+}
+: function(t) {
+return t && function() {
+return a.apply(t, arguments)
+}
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(6)
+, i = n.String
+, a = n.TypeError;
+t.exports = function(t) {
+if (o(t))
+return t;
+throw a(i(t) + " is not an object")
+}
+}
+, function(t, r, e) {
+var n = e(2);
+t.exports = !n((function() {
+return 7 != Object.defineProperty({}, 1, {
+get: function() {
+return 7
+}
+})[1]
+}
+))
+}
+, function(t, r, e) {
+var n = e(9);
+t.exports = function(t) {
+return "object" == typeof t ? null !== t : n(t)
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(86)
+, i = e(12)
+, a = e(60)
+, u = e(112)
+, c = e(142)
+, s = o("wks")
+, f = n.Symbol
+, l = f && f.for
+, h = c ? f : f && f.withoutSetter || a;
+t.exports = function(t) {
+if (!i(s, t) || !u && "string" != typeof s[t]) {
+var r = "Symbol." + t;
+u && i(f, t) ? s[t] = f[t] : s[t] = c && l ? l(r) : h(r)
+}
+return s[t]
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(52)
+, i = n.String;
+t.exports = function(t) {
+if ("Symbol" === o(t))
+throw TypeError("Cannot convert a Symbol value to a string");
+return i(t)
+}
+}
+, function(t, r) {
+t.exports = function(t) {
+return "function" == typeof t
+}
+}
+, function(t, r, e) {
+"use strict";
+var n, o, i, a = e(128), u = e(5), c = e(1), s = e(9), f = e(6), l = e(12), h = e(52), p = e(70), v = e(25), g = e(17), d = e(13).f, y = e(29), m = e(37), b = e(39), x = e(7), w = e(60), E = c.Int8Array, S = E && E.prototype, A = c.Uint8ClampedArray, O = A && A.prototype, R = E && m(E), T = S && m(S), I = Object.prototype, M = c.TypeError, j = x("toStringTag"), P = w("TYPED_ARRAY_TAG"), k = w("TYPED_ARRAY_CONSTRUCTOR"), _ = a && !!b && "Opera" !== h(c.opera), L = !1, N = {
+Int8Array: 1,
+Uint8Array: 1,
+Uint8ClampedArray: 1,
+Int16Array: 2,
+Uint16Array: 2,
+Int32Array: 4,
+Uint32Array: 4,
+Float32Array: 4,
+Float64Array: 8
+}, D = {
+BigInt64Array: 8,
+BigUint64Array: 8
+}, U = function(t) {
+if (!f(t))
+return !1;
+var r = h(t);
+return l(N, r) || l(D, r)
+};
+for (n in N)
+(i = (o = c[n]) && o.prototype) ? v(i, k, o) : _ = !1;
+for (n in D)
+(i = (o = c[n]) && o.prototype) && v(i, k, o);
+if ((!_ || !s(R) || R === Function.prototype) && (R = function() {
+throw M("Incorrect invocation")
+}
+,
+_))
+for (n in N)
+c[n] && b(c[n], R);
+if ((!_ || !T || T === I) && (T = R.prototype,
+_))
+for (n in N)
+c[n] && b(c[n].prototype, T);
+if (_ && m(O) !== T && b(O, T),
+u && !l(T, j))
+for (n in L = !0,
+d(T, j, {
+get: function() {
+return f(this) ? this[P] : void 0
+}
+}),
+N)
+c[n] && v(c[n], P, n);
+t.exports = {
+NATIVE_ARRAY_BUFFER_VIEWS: _,
+TYPED_ARRAY_CONSTRUCTOR: k,
+TYPED_ARRAY_TAG: L && P,
+aTypedArray: function(t) {
+if (U(t))
+return t;
+throw M("Target is not a typed array")
+},
+aTypedArrayConstructor: function(t) {
+if (s(t) && (!b || y(R, t)))
+return t;
+throw M(p(t) + " is not a typed array constructor")
+},
+exportTypedArrayMethod: function(t, r, e, n) {
+if (u) {
+if (e)
+for (var o in N) {
+var i = c[o];
+if (i && l(i.prototype, t))
+try {
+delete i.prototype[t]
+} catch (e) {
+try {
+i.prototype[t] = r
+} catch (t) {}
+}
+}
+T[t] && !e || g(T, t, e ? r : _ && S[t] || r, n)
+}
+},
+exportTypedArrayStaticMethod: function(t, r, e) {
+var n, o;
+if (u) {
+if (b) {
+if (e)
+for (n in N)
+if ((o = c[n]) && l(o, t))
+try {
+delete o[t]
+} catch (t) {}
+if (R[t] && !e)
+return;
+try {
+return g(R, t, e ? r : _ && R[t] || r)
+} catch (t) {}
+}
+for (n in N)
+!(o = c[n]) || o[t] && !e || g(o, t, r)
+}
+},
+isView: function(t) {
+if (!f(t))
+return !1;
+var r = h(t);
+return "DataView" === r || l(N, r) || l(D, r)
+},
+isTypedArray: U,
+TypedArray: R,
+TypedArrayPrototype: T
+}
+}
+, function(t, r, e) {
+var n = e(68)
+, o = Function.prototype.call;
+t.exports = n ? o.bind(o) : function() {
+return o.apply(o, arguments)
+}
+}
+, function(t, r, e) {
+var n = e(3)
+, o = e(14)
+, i = n({}.hasOwnProperty);
+t.exports = Object.hasOwn || function(t, r) {
+return i(o(t), r)
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(5)
+, i = e(144)
+, a = e(145)
+, u = e(4)
+, c = e(49)
+, s = n.TypeError
+, f = Object.defineProperty
+, l = Object.getOwnPropertyDescriptor;
+r.f = o ? a ? function(t, r, e) {
+if (u(t),
+r = c(r),
+u(e),
+"function" == typeof t && "prototype" === r && "value"in e && "writable"in e && !e.writable) {
+var n = l(t, r);
+n && n.writable && (t[r] = e.value,
+e = {
+configurable: "configurable"in e ? e.configurable : n.configurable,
+enumerable: "enumerable"in e ? e.enumerable : n.enumerable,
+writable: !1
+})
+}
+return f(t, r, e)
+}
+: f : function(t, r, e) {
+if (u(t),
+r = c(r),
+u(e),
+i)
+try {
+return f(t, r, e)
+} catch (t) {}
+if ("get"in e || "set"in e)
+throw s("Accessors not supported");
+return "value"in e && (t[r] = e.value),
+t
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(18)
+, i = n.Object;
+t.exports = function(t) {
+return i(o(t))
+}
+}
+, function(t, r, e) {
+var n = e(30);
+t.exports = function(t) {
+return n(t.length)
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(9)
+, i = function(t) {
+return o(t) ? t : void 0
+};
+t.exports = function(t, r) {
+return arguments.length < 2 ? i(n[t]) : n[t] && n[t][r]
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(9)
+, i = e(12)
+, a = e(25)
+, u = e(114)
+, c = e(88)
+, s = e(19)
+, f = e(61).CONFIGURABLE
+, l = s.get
+, h = s.enforce
+, p = String(String).split("String");
+(t.exports = function(t, r, e, c) {
+var s, l = !!c && !!c.unsafe, v = !!c && !!c.enumerable, g = !!c && !!c.noTargetGet, d = c && void 0 !== c.name ? c.name : r;
+o(e) && ("Symbol(" === String(d).slice(0, 7) && (d = "[" + String(d).replace(/^Symbol\(([^)]*)\)/, "$1") + "]"),
+(!i(e, "name") || f && e.name !== d) && a(e, "name", d),
+(s = h(e)).source || (s.source = p.join("string" == typeof d ? d : ""))),
+t !== n ? (l ? !g && t[r] && (v = !0) : delete t[r],
+v ? t[r] = e : a(t, r, e)) : v ? t[r] = e : u(r, e)
+}
+)(Function.prototype, "toString", (function() {
+return o(this) && l(this).source || c(this)
+}
+))
+}
+, function(t, r, e) {
+var n = e(1).TypeError;
+t.exports = function(t) {
+if (null == t)
+throw n("Can't call method on " + t);
+return t
+}
+}
+, function(t, r, e) {
+var n, o, i, a = e(146), u = e(1), c = e(3), s = e(6), f = e(25), l = e(12), h = e(113), p = e(89), v = e(71), g = u.TypeError, d = u.WeakMap;
+if (a || h.state) {
+var y = h.state || (h.state = new d)
+, m = c(y.get)
+, b = c(y.has)
+, x = c(y.set);
+n = function(t, r) {
+if (b(y, t))
+throw new g("Object already initialized");
+return r.facade = t,
+x(y, t, r),
+r
+}
+,
+o = function(t) {
+return m(y, t) || {}
+}
+,
+i = function(t) {
+return b(y, t)
+}
+} else {
+var w = p("state");
+v[w] = !0,
+n = function(t, r) {
+if (l(t, w))
+throw new g("Object already initialized");
+return r.facade = t,
+f(t, w, r),
+r
+}
+,
+o = function(t) {
+return l(t, w) ? t[w] : {}
+}
+,
+i = function(t) {
+return l(t, w)
+}
+}
+t.exports = {
+set: n,
+get: o,
+has: i,
+enforce: function(t) {
+return i(t) ? o(t) : n(t, {})
+},
+getterFor: function(t) {
+return function(r) {
+var e;
+if (!s(r) || (e = o(r)).type !== t)
+throw g("Incompatible receiver, " + t + " required");
+return e
+}
+}
+}
+}
+, function(t, r) {
+var e = Math.ceil
+, n = Math.floor;
+t.exports = function(t) {
+var r = +t;
+return r != r || 0 === r ? 0 : (r > 0 ? n : e)(r)
+}
+}
+, function(t, r) {
+t.exports = !1
+}
+, function(t, r, e) {
+var n = e(38)
+, o = e(3)
+, i = e(69)
+, a = e(14)
+, u = e(15)
+, c = e(77)
+, s = o([].push)
+, f = function(t) {
+var r = 1 == t
+, e = 2 == t
+, o = 3 == t
+, f = 4 == t
+, l = 6 == t
+, h = 7 == t
+, p = 5 == t || l;
+return function(v, g, d, y) {
+for (var m, b, x = a(v), w = i(x), E = n(g, d), S = u(w), A = 0, O = y || c, R = r ? O(v, S) : e || h ? O(v, 0) : void 0; S > A; A++)
+if ((p || A in w) && (b = E(m = w[A], A, x),
+t))
+if (r)
+R[A] = b;
+else if (b)
+switch (t) {
+case 3:
+return !0;
+case 5:
+return m;
+case 6:
+return A;
+case 2:
+s(R, m)
+}
+else
+switch (t) {
+case 4:
+return !1;
+case 7:
+s(R, m)
+}
+return l ? -1 : o || f ? f : R
+}
+};
+t.exports = {
+forEach: f(0),
+map: f(1),
+filter: f(2),
+some: f(3),
+every: f(4),
+find: f(5),
+findIndex: f(6),
+filterReject: f(7)
+}
+}
+, function(t, r, e) {
+var n = e(5)
+, o = e(11)
+, i = e(85)
+, a = e(35)
+, u = e(26)
+, c = e(49)
+, s = e(12)
+, f = e(144)
+, l = Object.getOwnPropertyDescriptor;
+r.f = n ? l : function(t, r) {
+if (t = u(t),
+r = c(r),
+f)
+try {
+return l(t, r)
+} catch (t) {}
+if (s(t, r))
+return a(!o(i.f, t, r), t[r])
+}
+}
+, function(t, r, e) {
+var n = e(1)
+, o = e(9)
+, i = e(70)
+, a = n.TypeError;
+t.exports = function(t) {
+if (o(t))
+return t;
+throw a(i(t) + " is not a function")
+}
+}
+, function(t, r, e) {
+var n = e(5)
+, o = e(13)
+, i = e(35);
+t.exports = n ? function(t, r, e) {
+return o.f(t, r, i(1, e))
+}
+: function(t, r, e) {
+return t[r] = e,
+t
+}
+}
+, function(t, r, e) {
+var n = e(69)
+, o = e(18);
+t.exports = function(t) {
+return n(o(t))
+}
+}
+, function(t, r, e) {
+var n = e(150)
+, o = e(12)
+, i = e(149)
+, a = e(13).f;
+t.exports = function(t) {
+var r = n.Symbol || (n.Symbol = {});
+o(r, t) || a(r, t, {
+value: i.f(t)
+})
+}
+}
+, function(t, r, e) {
+var n = e(3)
+, o = n({}.toString)
+, i = n("".slice);
+t.exports = function(t) {
+return i(o(t), 8, -1)
+}
+}
+, function(t, r, e) {
+var n = e(3);
+t.exports = n({}.isPrototypeOf)
+}
+, function(t, r, e) {
+var n = e(20)
+, o = Math.min;
+t.exports = function(t) {
+return t > 0 ? o(n(t), 9007199254740991) : 0
+}
+}
+, function(t, r, e) {
+var n = e(68)
+, o = Function.prototype
+, i = o.apply
+, a = o.call;
+t.exports = "object" == typeof Reflect && Reflect.apply || (n ? a.bind(i) : function() {
+return a.apply(i, arguments)
+}
+)
+}
+, function(t, r, e) {
+var n, o = e(4), i = e(74), a = e(116), u = e(71), c = e(148), s = e(87), f = e(89), l = f("IE_PROTO"), h = function() {}, p = function(t) {
+return "