Match-id-6a372ad6721fedc80a0156db1ed2178aa1fe5b22
This commit is contained in:
commit
0c13790ea7
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|||
```
|
||||
|
||||
## 传输数据结构
|
||||
**<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 的引用也需要删除。
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(item))}>
|
||||
<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'
|
||||
? <input value={item.value} className={styles.attrValue}>{item.value}</input>
|
||||
: <span className={styles.attrValue}>{item.value}</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) {
|
||||
|
@ -74,10 +120,7 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
|
|||
return (
|
||||
<div className={styles.attrContainer}>
|
||||
<div className={styles.attrHead}>
|
||||
<span className={styles.attrType}>{name}</span>
|
||||
<span className={styles.attrCopy}>
|
||||
<Copy />
|
||||
</span>
|
||||
<span className={styles.attrType}>{attrsName}</span>
|
||||
</div>
|
||||
<div className={styles.attrDetail}>
|
||||
{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 (
|
||||
<div className={styles.infoContainer} >
|
||||
<div className={styles.componentInfoHead}>
|
||||
|
@ -95,19 +137,27 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
|
|||
<span className={styles.name}>
|
||||
{name}
|
||||
</span>
|
||||
<span className={styles.eye} >
|
||||
<span className={styles.eye} title={'Inspect dom element'} onClick={() => {
|
||||
postMessageToBackground(InspectDom, id);
|
||||
}}>
|
||||
<Eye />
|
||||
</span>
|
||||
<span className={styles.debug}>
|
||||
<span className={styles.debug} title={'Log this component data'} onClick={() => {
|
||||
postMessageToBackground(LogComponentData, id);
|
||||
}}>
|
||||
<Debug />
|
||||
</span>
|
||||
</>}
|
||||
</div>
|
||||
<div className={styles.componentInfoMain}>
|
||||
{context && <ComponentAttr name={'context'} attrs={context} />}
|
||||
{props && props.length !== 0 && <ComponentAttr name={'props'} attrs={props} />}
|
||||
{state && state.length !== 0 && <ComponentAttr name={'state'} attrs={state} />}
|
||||
{hooks && hooks.length !== 0 && <ComponentAttr name={'hook'} attrs={hooks} />}
|
||||
{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: {
|
||||
|
@ -122,4 +172,4 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { VList, renderInfoType } from './VList';
|
||||
export { VList } from './VList';
|
||||
export type { renderInfoType } from './VList';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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<HTMLDivElement>();
|
||||
const objRef = useRef({ str: 'string' });
|
||||
const context = useContext(MockContext);
|
||||
|
@ -26,6 +27,7 @@ export default function MockFunctionComponent(props) {
|
|||
return (
|
||||
<div>
|
||||
age: {age}
|
||||
name: {name.test}
|
||||
<button onClick={() => setAge(age + 1)} >update age</button>
|
||||
count: {props.count}
|
||||
<div ref={domRef} />
|
||||
|
@ -33,4 +35,4 @@ export default function MockFunctionComponent(props) {
|
|||
<div>{context.ctx}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
<button onClick={() => (setCount(count + 1))} >add count</button>
|
||||
<MockContext.Provider value={{ ctx: 'I am ctx' }}>
|
||||
<MockClassComponent fruit={'apple'} />
|
||||
<MockFunctionComponent />
|
||||
<MockFunctionComponent count={count}/>
|
||||
</MockContext.Provider>
|
||||
abc
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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<IIdToNodeMap>({});
|
||||
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html style="display: flex">
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Horizon</title>
|
||||
<script src='horizon.production.js'></script>
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
|
@ -24,11 +22,12 @@
|
|||
}
|
||||
|
||||
</style>
|
||||
<script src='horizon.development.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
<div id="root"></div>
|
||||
<script src="panel.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './PanelConnection';
|
|
@ -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<any, any>[]) {
|
||||
export function parseHooks(hooks: Hook<any, any>[], getHookInfo) {
|
||||
const result: IAttr[] = [];
|
||||
const indentation = 0;
|
||||
hooks.forEach(hook => {
|
||||
const { hIndex, state ,type } = hook;
|
||||
if (type === 'useState') {
|
||||
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'state', result, hIndex);
|
||||
} else if (type === 'useRef') {
|
||||
parseSubAttr((state as Ref<any>).current, indentation, 'ref', result, hIndex);
|
||||
} else if (type === 'useReducer') {
|
||||
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'reducer', result, hIndex);
|
||||
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,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<VNode, number>();
|
||||
const IdToVNodeMap = new Map<number, VNode>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
export default function Copy() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
export const DevToolHook = 'dev tool hook';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -47,7 +47,7 @@ module.exports = {
|
|||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'dist'),
|
||||
directory: path.join(__dirname, 'build'),
|
||||
},
|
||||
open: 'panel.html',
|
||||
port: 9000,
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
区分是否开发者模式
|
||||
*/
|
||||
declare var isDev: boolean;
|
||||
declare const __VERSION__: string;
|
|
@ -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,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"keywords": [
|
||||
"horizon"
|
||||
],
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"homepage": "",
|
||||
"bugs": "",
|
||||
"main": "index.js",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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<any, any>) => {
|
||||
const { hIndex, state } = hook;
|
||||
if ((state as Reducer<any, any>).trigger) {
|
||||
if ((state as Reducer<any, any>).isUseState) {
|
||||
return {name: 'state', hIndex, value: (state as Reducer<any, any>).stateValue};
|
||||
}
|
||||
} else if ((state as Ref<any>).current) {
|
||||
return {name: 'ref', hIndex, value: (state as Ref<any>).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<any, any>;
|
||||
// 暂时只支持更新 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<any, any>;
|
||||
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();
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import {EffectConstant} from './EffectConstant';
|
|||
export interface Hook<S, A> {
|
||||
state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>;
|
||||
hIndex: number;
|
||||
type?: 'useState' | 'useRef' | 'useReducer';
|
||||
}
|
||||
|
||||
export interface Reducer<S, A> {
|
||||
|
|
|
@ -87,7 +87,6 @@ export function useReducerForInit<S, A>(reducer, initArg, init, isUseState?: boo
|
|||
}
|
||||
|
||||
const hook = createHook();
|
||||
hook.type = isUseState ? 'useState' : 'useReducer';
|
||||
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
|
||||
hook.state = {
|
||||
stateValue: stateValue,
|
||||
|
|
|
@ -12,7 +12,6 @@ export function useRefImpl<V>(value: V): Ref<V> {
|
|||
if (stage === HookStage.Init) {
|
||||
hook = createHook();
|
||||
hook.state = {current: value};
|
||||
hook.type = 'useRef';
|
||||
} else if (stage === HookStage.Update) {
|
||||
hook = getCurrentHook();
|
||||
}
|
||||
|
|
|
@ -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 = ''; // 保存从根到本节点的路径
|
||||
|
|
|
@ -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
|
||||
|
|
15
package.json
15
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",
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<p>{this.state.num}</p>
|
||||
<p id="p">{this.state.price}</p>
|
||||
<button onClick={this.setNum.bind(this)} >button</button>
|
||||
<button id="btn" onClick={() => this.setPrice()} >button</button>
|
||||
<button onClick={this.setNum.bind(this)}>button</button>
|
||||
<button id="btn" onClick={() => this.setPrice()}>
|
||||
button
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App num={0} price={100} />, 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(
|
||||
<button onClick={handleClick} disabled={true}>
|
||||
<span ref={spanRef}>Click Me</span>
|
||||
</button>,
|
||||
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', <div
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseMoveCapture={onMouseMoveCapture}
|
||||
/>);
|
||||
test('mousemove', <div onMouseMove={onMouseMove} onMouseMoveCapture={onMouseMoveCapture} />);
|
||||
});
|
||||
|
||||
it('onMouseDown', () => {
|
||||
|
@ -106,10 +114,7 @@ describe('MouseEvent Test', () => {
|
|||
const onMousedownCapture = () => {
|
||||
LogUtils.log('mousedown capture');
|
||||
};
|
||||
test('mousedown', <div
|
||||
onMousedown={onMousedown}
|
||||
onMousedownCapture={onMousedownCapture}
|
||||
/>);
|
||||
test('mousedown', <div onMousedown={onMousedown} onMousedownCapture={onMousedownCapture} />);
|
||||
});
|
||||
|
||||
it('onMouseUp', () => {
|
||||
|
@ -119,10 +124,7 @@ describe('MouseEvent Test', () => {
|
|||
const onMouseUpCapture = () => {
|
||||
LogUtils.log('mouseup capture');
|
||||
};
|
||||
test('mouseup', <div
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseUpCapture={onMouseUpCapture}
|
||||
/>);
|
||||
test('mouseup', <div onMouseUp={onMouseUp} onMouseUpCapture={onMouseUpCapture} />);
|
||||
});
|
||||
|
||||
it('onMouseOut', () => {
|
||||
|
@ -132,10 +134,7 @@ describe('MouseEvent Test', () => {
|
|||
const onMouseOutCapture = () => {
|
||||
LogUtils.log('mouseout capture');
|
||||
};
|
||||
test('mouseout', <div
|
||||
onMouseOut={onMouseOut}
|
||||
onMouseOutCapture={onMouseOutCapture}
|
||||
/>);
|
||||
test('mouseout', <div onMouseOut={onMouseOut} onMouseOutCapture={onMouseOutCapture} />);
|
||||
});
|
||||
|
||||
it('onMouseOver', () => {
|
||||
|
@ -145,10 +144,7 @@ describe('MouseEvent Test', () => {
|
|||
const onMouseOverCapture = () => {
|
||||
LogUtils.log('mouseover capture');
|
||||
};
|
||||
test('mouseover', <div
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOverCapture={onMouseOverCapture}
|
||||
/>);
|
||||
test('mouseover', <div onMouseOver={onMouseOver} onMouseOverCapture={onMouseOverCapture} />);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,3 +5,4 @@ global.MessageChannel = function MessageChannel() {
|
|||
postMessage() { }
|
||||
};
|
||||
};
|
||||
global.__VERSION__ = require('../../../libs/horizon/package.json').version;
|
||||
|
|
|
@ -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}`));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
import fs from 'fs';
|
||||
|
||||
export default function copyFiles(copyPairs) {
|
||||
return {
|
||||
name: 'copy-files',
|
||||
generateBundle() {
|
||||
copyPairs.forEach(({ from, to }) => {
|
||||
console.log(`copy files: ${from} → ${to}`);
|
||||
fs.copyFileSync(from, to);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import path from 'path';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import copy from './copy-plugin';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import { version as horizonVersion } from '@cloudsop/horizon/package.json';
|
||||
|
||||
const extensions = ['.js', '.ts'];
|
||||
|
||||
const libDir = path.join(__dirname, '../../libs/horizon');
|
||||
const rootDir = path.join(__dirname, '../..');
|
||||
const outDir = path.join(rootDir, 'build', 'horizon');
|
||||
const outputResolve = (...p) => path.resolve(outDir, ...p);
|
||||
|
||||
function genConfig(mode) {
|
||||
const sourcemap = mode === 'development' ? 'inline' : false;
|
||||
return {
|
||||
input: path.resolve(libDir, 'index.ts'),
|
||||
output: [
|
||||
{
|
||||
file: outputResolve('cjs', `horizon.${mode}.js`),
|
||||
sourcemap,
|
||||
format: 'cjs',
|
||||
},
|
||||
{
|
||||
file: outputResolve('umd', `horizon.${mode}.js`),
|
||||
sourcemap,
|
||||
name: 'Horizon',
|
||||
format: 'umd',
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(__dirname, '../../babel.config.js'),
|
||||
babelHelpers: 'runtime',
|
||||
extensions,
|
||||
}),
|
||||
replace({
|
||||
values: {
|
||||
'process.env.NODE_ENV': `"${mode}"`,
|
||||
isDev: 'true',
|
||||
__VERSION__: `"${horizonVersion}"`,
|
||||
},
|
||||
preventAssignment: true,
|
||||
}),
|
||||
mode === 'production' && terser(),
|
||||
copy([
|
||||
{
|
||||
from: path.join(libDir, 'index.js'),
|
||||
to: path.join(outDir, 'index.js'),
|
||||
},
|
||||
{
|
||||
from: path.join(libDir, 'package.json'),
|
||||
to: path.join(outDir, 'package.json'),
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default [genConfig('development'), genConfig('production')];
|
|
@ -1,28 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
const libPath = path.join(__dirname, '../../libs/horizon');
|
||||
const baseConfig = {
|
||||
entry: path.resolve(libPath, 'index.ts'),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js)|ts$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts'],
|
||||
alias: {
|
||||
'horizon-external': path.join(libPath, './horizon-external'),
|
||||
'horizon': path.join(libPath, './horizon'),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = baseConfig;
|
|
@ -1,4 +0,0 @@
|
|||
const dev = require('./webpack.dev');
|
||||
const pro = require('./webpack.pro');
|
||||
|
||||
module.exports = [...dev, ...pro];
|
|
@ -1,43 +0,0 @@
|
|||
const webpack = require('webpack');
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
const baseConfig = require('./webpack.base');
|
||||
const path = require('path');
|
||||
|
||||
const mode = 'development';
|
||||
const devtool = 'inline-source-map';
|
||||
const filename = 'horizon.development.js';
|
||||
|
||||
const plugins = [
|
||||
new ESLintPlugin({fix: true}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
isDev: 'true',
|
||||
}),
|
||||
];
|
||||
|
||||
const umd = {
|
||||
...baseConfig,
|
||||
mode,
|
||||
devtool,
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../build/horizon/umd'),
|
||||
filename,
|
||||
libraryTarget: 'umd',
|
||||
library: 'Horizon',
|
||||
},
|
||||
plugins,
|
||||
};
|
||||
|
||||
const cjs = {
|
||||
...baseConfig,
|
||||
mode,
|
||||
devtool,
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../build/horizon/cjs'),
|
||||
filename,
|
||||
libraryTarget: 'commonjs',
|
||||
},
|
||||
plugins,
|
||||
};
|
||||
|
||||
module.exports = [umd, cjs];
|
|
@ -1,59 +0,0 @@
|
|||
const webpack = require('webpack');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const baseConfig = require('./webpack.base');
|
||||
const path = require('path');
|
||||
|
||||
const mode = 'production';
|
||||
const devtool = 'none';
|
||||
const filename = 'horizon.production.js';
|
||||
|
||||
const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
isDev: 'false',
|
||||
})
|
||||
];
|
||||
|
||||
const proBaseConfig = {
|
||||
...baseConfig,
|
||||
mode,
|
||||
devtool,
|
||||
plugins,
|
||||
optimization: {
|
||||
minimize: true
|
||||
},
|
||||
};
|
||||
|
||||
const umd = {
|
||||
...proBaseConfig,
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../build/horizon/umd'),
|
||||
filename,
|
||||
libraryTarget: 'umd',
|
||||
library: 'Horizon',
|
||||
},
|
||||
};
|
||||
|
||||
const cjs = {
|
||||
...proBaseConfig,
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../build/horizon/cjs'),
|
||||
filename,
|
||||
libraryTarget: 'commonjs',
|
||||
},
|
||||
plugins: [
|
||||
...plugins,
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.join(__dirname, '../../libs/horizon/index.js'),
|
||||
to: path.join(__dirname, '../../build/horizon/index.js'),
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../../libs/horizon/package.json'),
|
||||
to: path.join(__dirname, '../../build/horizon/package.json'),
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = [umd, cjs];
|
|
@ -34,7 +34,8 @@
|
|||
},
|
||||
"include": [
|
||||
"./libs/**/src/**/*.ts",
|
||||
"libs/horizon/index.d.ts"
|
||||
"./libs/**/*.ts",
|
||||
"./libs/horizon/global.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "**/*.spec.ts", "dev"],
|
||||
"types": ["node"]
|
||||
|
|
Loading…
Reference in New Issue