Match-id-8a6dc862b98dc7dff46c0212e3dad45d895985f3
This commit is contained in:
commit
3630ac5f19
|
@ -0,0 +1,15 @@
|
||||||
|
module.exports = api => {
|
||||||
|
const isTest = api.env('test');
|
||||||
|
console.log('isTest', isTest);
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-env',
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
['@babel/preset-react', {
|
||||||
|
runtime: 'classic',
|
||||||
|
'pragma': 'Horizon.createElement',
|
||||||
|
'pragmaFrag': 'Horizon.Fragment',
|
||||||
|
}]],
|
||||||
|
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "extension",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config ./webpack.config.js",
|
||||||
|
"watch": "webpack --config ./webpack.config.js --watch",
|
||||||
|
"build-dev": "webpack --config ./webpack.dev.js",
|
||||||
|
"start": "webpack serve --config ./webpack.dev.js ",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.12.3",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
|
"@babel/preset-env": "7.12.1",
|
||||||
|
"@babel/preset-react": "7.12.1",
|
||||||
|
"@babel/preset-typescript": "^7.16.7",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"babel-loader": "8.1.0",
|
||||||
|
"css-loader": "^6.7.1",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"less": "^4.1.2",
|
||||||
|
"less-loader": "^10.2.0",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"ts-jest": "^27.1.4",
|
||||||
|
"webpack": "^5.70.0",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "^4.7.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
devtools_page: devtool主页面
|
devtools_page: devtool主页面
|
||||||
default_popup: 拓展图标点击时弹窗页面
|
default_popup: 拓展图标点击时弹窗页面
|
||||||
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
|
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
|
||||||
web_accessible_resources: 注入全局变量代码
|
|
||||||
|
|
||||||
## 打开 panel 页面调试面板的方式
|
## 打开 panel 页面调试面板的方式
|
||||||
|
|
||||||
|
@ -28,30 +27,65 @@ sequenceDiagram
|
||||||
participant panel
|
participant panel
|
||||||
|
|
||||||
Note over web_page: window.postMessage
|
Note over web_page: window.postMessage
|
||||||
web_page ->> script_content : {}
|
web_page ->> script_content : data
|
||||||
Note over script_content: window.addEventListener
|
Note over script_content: window.addEventListener
|
||||||
Note over script_content: chrome.runtime.sendMessage
|
Note over script_content: chrome.runtime.sendMessage
|
||||||
script_content ->> background : {}
|
script_content ->> background : data
|
||||||
Note over background: chrome.runtime.onMessage
|
Note over background: chrome.runtime.onMessage
|
||||||
Note over background: port.postMessage
|
Note over background: port.postMessage
|
||||||
background ->> panel : {}
|
background ->> panel : data
|
||||||
Note over panel: connection.onMessage.addListener
|
Note over panel: connection.onMessage.addListener
|
||||||
Note over panel: connection.postMessage
|
Note over panel: connection.postMessage
|
||||||
panel ->> background : {}
|
panel ->> background : data
|
||||||
Note over background: port.onMessage.addListener
|
Note over background: port.onMessage.addListener
|
||||||
Note over background: chrome.tabs.sendMessage
|
Note over background: chrome.tabs.sendMessage
|
||||||
background ->> script_content : {}
|
background ->> script_content : data
|
||||||
Note over script_content: chrome.runtime.onMessage
|
Note over script_content: chrome.runtime.onMessage
|
||||||
Note over script_content: window.postMessage
|
Note over script_content: window.postMessage
|
||||||
script_content ->> web_page : {}
|
script_content ->> web_page : data
|
||||||
Note over web_page: window.addEventListener
|
Note over web_page: window.addEventListener
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 传输数据结构
|
||||||
|
```ts
|
||||||
|
type passData = {
|
||||||
|
type: 'HORIZON_DEV_TOOLS',
|
||||||
|
payload: {
|
||||||
|
type: string,
|
||||||
|
data: any,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## horizon和devTools的主要交互
|
||||||
|
- 页面初始渲染
|
||||||
|
- 页面更新
|
||||||
|
- 页面销毁
|
||||||
|
- devTools触发组件属性更新
|
||||||
|
|
||||||
|
## VNode的清理
|
||||||
|
全局 hook 中保存了root VNode,在解析 VNode 树的时候也会保存 VNode 的引用,在清理VNode的时候这些 VNode 的引用也需要删除。
|
||||||
|
|
||||||
## 数据压缩
|
## 数据压缩
|
||||||
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
|
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
|
||||||
- 相同的组件名可以进行压缩
|
- 相同的组件名可以进行压缩
|
||||||
- 每个vNode有唯一的 path 属性,可以作为标识使用
|
- 每个vNode有唯一的 path 属性,可以作为标识使用
|
||||||
- 通过解析 path 值可以分析出组件树的结构
|
- 通过解析 path 值可以分析出组件树的结构
|
||||||
|
|
||||||
|
## 组件props/state/hook等数据的传输和解析
|
||||||
|
将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串,他们的解析和 props/state 的解析同样存在差异。
|
||||||
|
|
||||||
|
|
||||||
## 滚动动态渲染 Tree
|
## 滚动动态渲染 Tree
|
||||||
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
|
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
|
||||||
|
|
||||||
|
## 开发者页面打开场景
|
||||||
|
- 先有页面,然后打开开发者工具:工具建立连接,发送通知,页面hook收到后发送VNode树信息给工具页面
|
||||||
|
- 已经打开开发者工具,然后打开页面:业务页面渲染完毕,发送VNode树信息给工具页面
|
||||||
|
|
||||||
|
## 开发者工具页面响应组件树变更
|
||||||
|
组件树变更会带来新旧两个组件树信息数组,新旧数组存在数据一致而引用不一致的情况,而VTree和VList组件中相关信息的计算依赖引用而非数据本身,在收到新的组件树信息后需要对数据本身进行判断,将新数组中的相同数据使用旧对象代替。
|
||||||
|
|
||||||
|
## 测试框架
|
||||||
|
jest测试框架不提供浏览器插件的相关 api,我们在封装好相关 api 后需要模拟这些 api 的行为从而展开测试工作。
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { checkMessage, packagePayload, changeSource } from '../utils/transferTool';
|
||||||
|
import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants';
|
||||||
|
import { DevToolPanel, DevToolContentScript } from './../utils/constants';
|
||||||
|
|
||||||
|
// 多个页面、tab页共享一个 background,需要建立连接池,给每个tab建立连接
|
||||||
|
const connections = {};
|
||||||
|
|
||||||
|
// panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数
|
||||||
|
chrome.runtime.onConnect.addListener(function (port) {
|
||||||
|
function extensionListener(message) {
|
||||||
|
const isHorizonMessage = checkMessage(message, DevToolPanel);
|
||||||
|
if (isHorizonMessage) {
|
||||||
|
const { payload } = message;
|
||||||
|
const { type, data } = payload;
|
||||||
|
let passMessage;
|
||||||
|
if (type === InitDevToolPageConnection) {
|
||||||
|
if (!connections[data]) {
|
||||||
|
// 获取 panel 所在 tab 页的tabId
|
||||||
|
connections[data] = port;
|
||||||
|
}
|
||||||
|
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
|
||||||
|
} else {
|
||||||
|
passMessage = message;
|
||||||
|
changeSource(passMessage, DevToolBackground);
|
||||||
|
}
|
||||||
|
// 查询参数有 active 和 currentWindow, 如果开发者工具与页面分离,会导致currentWindow为false才能找到
|
||||||
|
// 所以只用 active 参数查找,但不确定这么写是否会引发查询错误的情况
|
||||||
|
// 或许需要用不同的查询参数查找两次
|
||||||
|
chrome.tabs.query({ active: true }, function (tabs) {
|
||||||
|
if (tabs.length) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, passMessage);
|
||||||
|
console.log('post message end');
|
||||||
|
} else {
|
||||||
|
console.log('do not find message');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Listen to messages sent from the DevTools page
|
||||||
|
port.onMessage.addListener(extensionListener);
|
||||||
|
|
||||||
|
port.onDisconnect.addListener(function (port) {
|
||||||
|
port.onMessage.removeListener(extensionListener);
|
||||||
|
|
||||||
|
const tabs = Object.keys(connections);
|
||||||
|
for (let i = 0, len = tabs.length; i < len; i++) {
|
||||||
|
if (connections[tabs[i]] == port) {
|
||||||
|
delete connections[tabs[i]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听来自 content script 的消息,并将消息发送给对应的 devTools page,也就是 panel
|
||||||
|
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
||||||
|
// Messages from content scripts should have sender.tab set
|
||||||
|
if (sender.tab) {
|
||||||
|
const tabId = sender.tab.id;
|
||||||
|
if (tabId in connections && checkMessage(message, DevToolContentScript)) {
|
||||||
|
changeSource(message, DevToolBackground);
|
||||||
|
connections[tabId].postMessage(message);
|
||||||
|
} else {
|
||||||
|
console.log('Tab not found in connection list.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('sender.tab not defined.');
|
||||||
|
}
|
||||||
|
// 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received
|
||||||
|
sendResponse({status: 'ok'});
|
||||||
|
});
|
|
@ -3,8 +3,9 @@ import Eye from '../svgs/Eye';
|
||||||
import Debug from '../svgs/Debug';
|
import Debug from '../svgs/Debug';
|
||||||
import Copy from '../svgs/Copy';
|
import Copy from '../svgs/Copy';
|
||||||
import Triangle from '../svgs/Triangle';
|
import Triangle from '../svgs/Triangle';
|
||||||
import { useState } from 'horizon';
|
import { useState, useEffect } from 'horizon';
|
||||||
import { IData } from './VTree';
|
import { IData } from './VTree';
|
||||||
|
import { IAttr } from '../parser/parseAttr';
|
||||||
|
|
||||||
type IComponentInfo = {
|
type IComponentInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -18,13 +19,6 @@ type IComponentInfo = {
|
||||||
onClickParent: (item: IData) => void;
|
onClickParent: (item: IData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IAttr = {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
value: string | boolean;
|
|
||||||
indentation: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collapseAllNodes(attrs: IAttr[]) {
|
function collapseAllNodes(attrs: IAttr[]) {
|
||||||
return attrs.filter((item, index) => {
|
return attrs.filter((item, index) => {
|
||||||
const nextItem = attrs[index + 1];
|
const nextItem = attrs[index + 1];
|
||||||
|
@ -34,6 +28,9 @@ function collapseAllNodes(attrs: IAttr[]) {
|
||||||
|
|
||||||
function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
|
function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
|
||||||
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
|
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
|
||||||
|
useEffect(() => {
|
||||||
|
setCollapsedNode(collapseAllNodes(attrs));
|
||||||
|
}, [attrs]);
|
||||||
const handleCollapse = (item: IAttr) => {
|
const handleCollapse = (item: IAttr) => {
|
||||||
const nodes = [...collapsedNode];
|
const nodes = [...collapsedNode];
|
||||||
const i = nodes.indexOf(item);
|
const i = nodes.indexOf(item);
|
||||||
|
@ -64,7 +61,9 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
|
||||||
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
|
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||||
<span className={styles.attrName}>{`${item.name}`}</span>
|
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||||
{' :'}
|
{' :'}
|
||||||
<span className={styles.attrValue}>{item.value}</span>
|
{item.type === 'string' || item.type === 'number'
|
||||||
|
? <input value={item.value} className={styles.attrValue}>{item.value}</input>
|
||||||
|
: <span className={styles.attrValue}>{item.value}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
|
@ -106,9 +105,9 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.componentInfoMain}>
|
<div className={styles.componentInfoMain}>
|
||||||
{context && <ComponentAttr name={'context'} attrs={context} />}
|
{context && <ComponentAttr name={'context'} attrs={context} />}
|
||||||
{props && <ComponentAttr name={'props'} attrs={props} />}
|
{props && props.length !== 0 && <ComponentAttr name={'props'} attrs={props} />}
|
||||||
{state && <ComponentAttr name={'state'} attrs={state} />}
|
{state && state.length !== 0 && <ComponentAttr name={'state'} attrs={state} />}
|
||||||
{hooks && <ComponentAttr name={'hook'} attrs={hooks} />}
|
{hooks && hooks.length !== 0 && <ComponentAttr name={'hook'} attrs={hooks} />}
|
||||||
<div className={styles.parentsInfo}>
|
<div className={styles.parentsInfo}>
|
||||||
{name && <div>
|
{name && <div>
|
||||||
parents: {
|
parents: {
|
||||||
|
|
|
@ -36,13 +36,8 @@
|
||||||
border-bottom: unset;
|
border-bottom: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
>:first-child {
|
|
||||||
padding: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
border-bottom: @divider-style;
|
border-bottom: @divider-style;
|
||||||
padding: 0.5rem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attrContainer {
|
.attrContainer {
|
||||||
|
|
|
@ -60,7 +60,7 @@ export function addResizeListener(element: any, fn: any) {
|
||||||
observer.data = 'about:blank';
|
observer.data = 'about:blank';
|
||||||
observer.onload = loadObserver;
|
observer.onload = loadObserver;
|
||||||
observer.type = 'text/html';
|
observer.type = 'text/html';
|
||||||
observer.__observeElement__ = element;
|
observer['__observeElement__'] = element;
|
||||||
element.__observer__ = observer;
|
element.__observer__ = observer;
|
||||||
element.appendChild(observer);
|
element.appendChild(observer);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { addResizeListener, removeResizeListener } from './ResizeEvent';
|
||||||
|
|
||||||
export function SizeObserver(props) {
|
export function SizeObserver(props) {
|
||||||
const { children, ...rest } = props;
|
const { children, ...rest } = props;
|
||||||
const containerRef = useRef();
|
const containerRef = useRef<HTMLDivElement>();
|
||||||
const [size, setSize] = useState();
|
const [size, setSize] = useState<{width: number, height: number}>();
|
||||||
const notifyChild = (element) => {
|
const notifyChild = (element) => {
|
||||||
setSize({
|
setSize({
|
||||||
width: element.offsetWidth,
|
width: element.offsetWidth,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
// TODO:当前的 item 渲染效果较差,每次滚动所有项在数组中的位置都会发生变更。
|
||||||
|
// 建议修改成选项增加减少时,未变更项在原数组中位置不变更
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'horizon';
|
import { useState, useRef, useEffect } from 'horizon';
|
||||||
import styles from './VList.less';
|
import styles from './VList.less';
|
||||||
|
|
||||||
interface IProps<T extends { id: string }> {
|
interface IProps<T extends { id: number | string }> {
|
||||||
data: T[],
|
data: T[],
|
||||||
width: number, // 暂时未用到,当需要支持横向滚动时使用
|
width: number, // 暂时未用到,当需要支持横向滚动时使用
|
||||||
height: number, // VList 的高度
|
height: number, // VList 的高度
|
||||||
|
@ -18,7 +20,7 @@ export type renderInfoType<T> = {
|
||||||
skipItemCountBeforeScrollItem: number,
|
skipItemCountBeforeScrollItem: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VList<T extends { id: string }>(props: IProps<T>) {
|
export function VList<T extends { id: number | string }>(props: IProps<T>) {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
height,
|
height,
|
||||||
|
@ -30,7 +32,7 @@ export function VList<T extends { id: string }>(props: IProps<T>) {
|
||||||
} = props;
|
} = props;
|
||||||
const [scrollTop, setScrollTop] = useState(data.indexOf(scrollToItem) * itemHeight);
|
const [scrollTop, setScrollTop] = useState(data.indexOf(scrollToItem) * itemHeight);
|
||||||
const renderInfoRef: { current: renderInfoType<T> } = useRef({ visibleItems: [], skipItemCountBeforeScrollItem: 0 });
|
const renderInfoRef: { current: renderInfoType<T> } = useRef({ visibleItems: [], skipItemCountBeforeScrollItem: 0 });
|
||||||
const containerRef = useRef();
|
const containerRef = useRef<HTMLDivElement>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onRendered(renderInfoRef.current);
|
onRendered(renderInfoRef.current);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
.treeItem {
|
.treeItem {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
line-height: 18px;
|
line-height: 1.125rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: @select-color;
|
background-color: @select-color;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useState, useEffect } from 'horizon';
|
import { useState, useEffect } from 'horizon';
|
||||||
import styles from './VTree.less';
|
import styles from './VTree.less';
|
||||||
import Triangle from '../svgs/Triangle';
|
import Triangle from '../svgs/Triangle';
|
||||||
import { createRegExp } from './../utils';
|
import { createRegExp } from '../utils/regExpUtils';
|
||||||
import { SizeObserver } from './SizeObserver';
|
import { SizeObserver } from './SizeObserver';
|
||||||
import { renderInfoType, VList } from './VList';
|
import { renderInfoType, VList } from './VList';
|
||||||
|
|
||||||
export interface IData {
|
export interface IData {
|
||||||
id: string;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
indentation: number;
|
indentation: number;
|
||||||
userKey: string;
|
userKey: string;
|
||||||
|
@ -102,7 +102,7 @@ function VTree(props: {
|
||||||
onRendered: (renderInfo: renderInfoType<IData>) => void,
|
onRendered: (renderInfo: renderInfoType<IData>) => void,
|
||||||
collapsedNodes?: IData[],
|
collapsedNodes?: IData[],
|
||||||
onCollapseNode?: (item: IData[]) => void,
|
onCollapseNode?: (item: IData[]) => void,
|
||||||
selectItem: IData[],
|
selectItem: IData,
|
||||||
onSelectItem: (item: IData) => void,
|
onSelectItem: (item: IData) => void,
|
||||||
}) {
|
}) {
|
||||||
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
|
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { injectCode } from '../utils/injectUtils';
|
||||||
|
import { checkMessage } from '../utils/transferTool';
|
||||||
|
import { DevToolContentScript, DevToolHook, DevToolBackground } from './../utils/constants';
|
||||||
|
import { changeSource } from './../utils/transferTool';
|
||||||
|
|
||||||
|
// 页面的window对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入hook
|
||||||
|
injectCode(chrome.runtime.getURL('/injector.js'));
|
||||||
|
|
||||||
|
// 监听来自页面的信息
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
// 只监听来自本页面的消息
|
||||||
|
if (event.source !== window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
if (checkMessage(data, DevToolHook)) {
|
||||||
|
changeSource(data, DevToolContentScript);
|
||||||
|
// 传递给background
|
||||||
|
chrome.runtime.sendMessage(data);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 监听来自background的消息
|
||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
function (message, sender, sendResponse) {
|
||||||
|
// 该方法可以监听页面 contentScript 和插件的消息
|
||||||
|
// 没有 tab 信息说明消息来自插件
|
||||||
|
if (!sender.tab && checkMessage(message, DevToolBackground)) {
|
||||||
|
changeSource(message, DevToolContentScript);
|
||||||
|
// 传递消息给页面
|
||||||
|
window.postMessage(message, '*');
|
||||||
|
}
|
||||||
|
sendResponse({status: 'ok'});
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Component } from 'horizon';
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
name: 'jenny',
|
||||||
|
boolean: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class MockClassComponent extends Component<{fruit: string}, typeof defaultState> {
|
||||||
|
|
||||||
|
state = defaultState;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => (this.setState({name: 'pika'}))} >update state</button>
|
||||||
|
{this.state.name}
|
||||||
|
{this.props?.fruit}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { createContext } from 'horizon';
|
||||||
|
|
||||||
|
export const MockContext = createContext({value: 'default context value'});
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useState, useEffect, useRef, useContext, useReducer } from 'horizon';
|
||||||
|
import { MockContext } from './MockContext';
|
||||||
|
|
||||||
|
const initialState = {count: 0};
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'increment':
|
||||||
|
return {count: state.count + 1};
|
||||||
|
case 'decrement':
|
||||||
|
return {count: state.count - 1};
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MockFunctionComponent(props) {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
const [age, setAge] = useState(0);
|
||||||
|
const domRef = useRef<HTMLDivElement>();
|
||||||
|
const objRef = useRef({ str: 'string' });
|
||||||
|
const context = useContext(MockContext);
|
||||||
|
|
||||||
|
useEffect(() => { }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
age: {age}
|
||||||
|
<button onClick={() => setAge(age + 1)} >update age</button>
|
||||||
|
count: {props.count}
|
||||||
|
<div ref={domRef} />
|
||||||
|
<div>{objRef.current.str}</div>
|
||||||
|
<div>{context.ctx}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { render } from 'horizon';
|
||||||
|
import MockClassComponent from './MockClassComponent';
|
||||||
|
import MockFunctionComponent from './MockFunctionComponent';
|
||||||
|
import { MockContext } from './MockContext';
|
||||||
|
|
||||||
|
const root = document.createElement('div');
|
||||||
|
document.body.append(root);
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MockContext.Provider value={{ ctx: 'I am ctx' }}>
|
||||||
|
<MockClassComponent fruit={'apple'} />
|
||||||
|
<MockFunctionComponent />
|
||||||
|
</MockContext.Provider>
|
||||||
|
abc
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, root);
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<title>Horizon Mock Page</title>
|
||||||
|
<script src="horizon.production.js"></script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -13,7 +13,7 @@
|
||||||
// 找到该节点的缩进值,和index值,在data中向上遍历,通过缩进值判断父节点
|
// 找到该节点的缩进值,和index值,在data中向上遍历,通过缩进值判断父节点
|
||||||
|
|
||||||
import { useState, useRef } from 'horizon';
|
import { useState, useRef } from 'horizon';
|
||||||
import { createRegExp } from '../utils';
|
import { createRegExp } from '../utils/regExpUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组
|
* 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组
|
||||||
|
@ -62,11 +62,11 @@ export function FilterTree<T extends BaseType>(props: { data: T[] }) {
|
||||||
const collapsedNodes = collapsedNodesRef.current;
|
const collapsedNodes = collapsedNodesRef.current;
|
||||||
|
|
||||||
const updateCollapsedNodes = (item: BaseType) => {
|
const updateCollapsedNodes = (item: BaseType) => {
|
||||||
const newcollapsedNodes = expandItemParent(item, data, collapsedNodes);
|
const newCollapsedNodes = expandItemParent(item, data, collapsedNodes);
|
||||||
// 如果新旧收起节点数组长度不一样,说明存在收起节点
|
// 如果新旧收起节点数组长度不一样,说明存在收起节点
|
||||||
if (newcollapsedNodes.length !== collapsedNodes.length) {
|
if (newCollapsedNodes.length !== collapsedNodes.length) {
|
||||||
// 更新引用,确保 VTree 拿到新的 collapsedNodes
|
// 更新引用,确保 VTree 拿到新的 collapsedNodes
|
||||||
collapsedNodesRef.current = newcollapsedNodes;
|
collapsedNodesRef.current = newCollapsedNodes;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
|
||||||
|
import { packagePayload, checkMessage } from './../utils/transferTool';
|
||||||
|
import {
|
||||||
|
RequestAllVNodeTreeInfos,
|
||||||
|
AllVNodeTreesInfos,
|
||||||
|
RequestComponentAttrs,
|
||||||
|
ComponentAttrs,
|
||||||
|
DevToolHook,
|
||||||
|
DevToolContentScript
|
||||||
|
} from './../utils/constants';
|
||||||
|
import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
|
||||||
|
import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
|
||||||
|
import { parseAttr, parseHooks } from '../parser/parseAttr';
|
||||||
|
import { FunctionComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
|
||||||
|
|
||||||
|
const roots = [];
|
||||||
|
|
||||||
|
function addIfNotInclude(treeRoot: VNode) {
|
||||||
|
if (!roots.includes(treeRoot)) {
|
||||||
|
roots.push(treeRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
const result = roots.reduce((pre, current) => {
|
||||||
|
const info = parseTreeRoot(current);
|
||||||
|
pre.push(info);
|
||||||
|
return pre;
|
||||||
|
}, []);
|
||||||
|
postMessage(AllVNodeTreesInfos, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteVNode(vNode: VNode) {
|
||||||
|
// 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除
|
||||||
|
clearVNode(vNode);
|
||||||
|
const index = roots.indexOf(vNode);
|
||||||
|
if (index !== -1) {
|
||||||
|
roots.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postMessage(type: string, data) {
|
||||||
|
window.postMessage(packagePayload({
|
||||||
|
type: type,
|
||||||
|
data: data,
|
||||||
|
}, DevToolHook), '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCompAttrs(id: number) {
|
||||||
|
const vNode: VNode = queryVNode(id);
|
||||||
|
const tag = vNode.tag;
|
||||||
|
if (tag === ClassComponent) {
|
||||||
|
const { props, state } = vNode;
|
||||||
|
const parsedProps = parseAttr(props);
|
||||||
|
const parsedState = parseAttr(state);
|
||||||
|
postMessage(ComponentAttrs, {
|
||||||
|
parsedProps,
|
||||||
|
parsedState,
|
||||||
|
});
|
||||||
|
} else if (tag === FunctionComponent) {
|
||||||
|
const { props, hooks } = vNode;
|
||||||
|
const parsedProps = parseAttr(props);
|
||||||
|
const parsedHooks = parseHooks(hooks);
|
||||||
|
postMessage(ComponentAttrs, {
|
||||||
|
parsedProps,
|
||||||
|
parsedHooks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectHook() {
|
||||||
|
if (window.__HORIZON_DEV_HOOK__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
|
||||||
|
enumerable: false,
|
||||||
|
value: {
|
||||||
|
addIfNotInclude,
|
||||||
|
send,
|
||||||
|
deleteVNode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.addEventListener('message', function (event) {
|
||||||
|
// We only accept messages from ourselves
|
||||||
|
if (event.source !== window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const request = event.data;
|
||||||
|
if (checkMessage(request, DevToolContentScript)) {
|
||||||
|
const { payload } = request;
|
||||||
|
const { type, data } = payload;
|
||||||
|
if (type === RequestAllVNodeTreeInfos) {
|
||||||
|
send();
|
||||||
|
} else if (type === RequestComponentAttrs) {
|
||||||
|
parseCompAttrs(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
injectHook();
|
|
@ -0,0 +1,7 @@
|
||||||
|
chrome.devtools.panels.create('Horizon',
|
||||||
|
'',
|
||||||
|
'panel.html',
|
||||||
|
function(panel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Horizon dev tools!</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</html>
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "Horizon dev tool",
|
||||||
|
"description": "Horizon chrome dev extension",
|
||||||
|
"version": "1.0",
|
||||||
|
"minimum_chrome_version": "10.0",
|
||||||
|
"manifest_version": 3,
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"permissions": ["storage", "activeTab", "scripting"],
|
||||||
|
|
||||||
|
"devtools_page": "main.html",
|
||||||
|
"action": {},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["contentScript.js"],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [ "injector.js", "background.js" ],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,13 +1,21 @@
|
||||||
import { useState, useEffect } from 'horizon';
|
import { useState, useEffect, useRef } from 'horizon';
|
||||||
import VTree, { IData } from '../components/VTree';
|
import VTree, { IData } from '../components/VTree';
|
||||||
import Search from '../components/Search';
|
import Search from '../components/Search';
|
||||||
import ComponentInfo from '../components/ComponentInfo';
|
import ComponentInfo from '../components/ComponentInfo';
|
||||||
import styles from './App.less';
|
import styles from './App.less';
|
||||||
import Select from '../svgs/Select';
|
import Select from '../svgs/Select';
|
||||||
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
||||||
import { FilterTree } from '../components/FilterTree';
|
import { FilterTree } from '../hooks/FilterTree';
|
||||||
import Close from '../svgs/Close';
|
import Close from '../svgs/Close';
|
||||||
import Arrow from './../svgs/Arrow';
|
import Arrow from './../svgs/Arrow';
|
||||||
|
import {
|
||||||
|
InitDevToolPageConnection,
|
||||||
|
AllVNodeTreesInfos,
|
||||||
|
RequestComponentAttrs,
|
||||||
|
ComponentAttrs,
|
||||||
|
DevToolPanel,
|
||||||
|
} from './../utils/constants';
|
||||||
|
import { packagePayload } from './../utils/transferTool';
|
||||||
|
|
||||||
const parseVNodeData = (rawData) => {
|
const parseVNodeData = (rawData) => {
|
||||||
const idIndentationMap: {
|
const idIndentationMap: {
|
||||||
|
@ -16,7 +24,7 @@ const parseVNodeData = (rawData) => {
|
||||||
const data: IData[] = [];
|
const data: IData[] = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < rawData.length) {
|
while (i < rawData.length) {
|
||||||
const id = rawData[i] as string;
|
const id = rawData[i] as number;
|
||||||
i++;
|
i++;
|
||||||
const name = rawData[i] as string;
|
const name = rawData[i] as string;
|
||||||
i++;
|
i++;
|
||||||
|
@ -51,10 +59,47 @@ const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
|
||||||
return parents;
|
return parents;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
if (!isDev) {
|
||||||
|
// 与 background 的唯一连接
|
||||||
|
connection = chrome.runtime.connect({
|
||||||
|
name: 'panel'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let reconnectTimes = 0;
|
||||||
|
|
||||||
|
function postMessage(type: string, data: any) {
|
||||||
|
try {
|
||||||
|
connection.postMessage(packagePayload({
|
||||||
|
type: type,
|
||||||
|
data: data,
|
||||||
|
}, DevToolPanel));
|
||||||
|
} catch(err) {
|
||||||
|
// 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
|
||||||
|
if (reconnectTimes === 20) {
|
||||||
|
reconnectTimes = 0;
|
||||||
|
console.error('reconnect failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(err);
|
||||||
|
reconnectTimes++;
|
||||||
|
// 重建连接
|
||||||
|
connection = chrome.runtime.connect({
|
||||||
|
name: 'panel'
|
||||||
|
});
|
||||||
|
// 重新发送初始化消息
|
||||||
|
postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
|
||||||
|
// 初始化成功后才会重新发送消息
|
||||||
|
postMessage(type, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||||
const [componentAttrs, setComponentAttrs] = useState({});
|
const [componentAttrs, setComponentAttrs] = useState({});
|
||||||
const [selectComp, setSelectComp] = useState(null);
|
const [selectComp, setSelectComp] = useState(null);
|
||||||
|
const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度,
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filterValue,
|
filterValue,
|
||||||
|
@ -77,6 +122,36 @@ function App() {
|
||||||
state: parsedMockState,
|
state: parsedMockState,
|
||||||
props: parsedMockState,
|
props: parsedMockState,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// 页面打开后发送初始化请求
|
||||||
|
postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId);
|
||||||
|
// 监听 background消息
|
||||||
|
connection.onMessage.addListener(function (message) {
|
||||||
|
const { payload } = message;
|
||||||
|
if (payload) {
|
||||||
|
const { type, data } = payload;
|
||||||
|
if (type === AllVNodeTreesInfos) {
|
||||||
|
const allTreeData = data.reduce((pre, current) => {
|
||||||
|
const parsedTreeData = parseVNodeData(current);
|
||||||
|
const length = parsedTreeData.length;
|
||||||
|
treeRootInfos.current.length = 0;
|
||||||
|
if (length) {
|
||||||
|
const treeRoot = parsedTreeData[0];
|
||||||
|
treeRootInfos.current.push({id: treeRoot.id, length: length});
|
||||||
|
}
|
||||||
|
return pre.concat(parsedTreeData);
|
||||||
|
}, []);
|
||||||
|
setParsedVNodeData(allTreeData);
|
||||||
|
} else if (type === ComponentAttrs) {
|
||||||
|
const {parsedProps, parsedState, parsedHooks} = data;
|
||||||
|
setComponentAttrs({
|
||||||
|
props: parsedProps,
|
||||||
|
state: parsedState,
|
||||||
|
hooks: parsedHooks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -85,10 +160,14 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectComp = (item: IData) => {
|
const handleSelectComp = (item: IData) => {
|
||||||
setComponentAttrs({
|
if (isDev) {
|
||||||
state: parsedMockState,
|
setComponentAttrs({
|
||||||
props: parsedMockState,
|
state: parsedMockState,
|
||||||
});
|
props: parsedMockState,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
postMessage(RequestComponentAttrs, item.id);
|
||||||
|
}
|
||||||
setSelectComp(item);
|
setSelectComp(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,8 +213,8 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.right}>
|
<div className={styles.right}>
|
||||||
<ComponentInfo
|
<ComponentInfo
|
||||||
name={selectComp ? selectComp.name: null}
|
name={selectComp ? selectComp.name : null}
|
||||||
attrs={selectComp ? componentAttrs: {}}
|
attrs={selectComp ? componentAttrs : {}}
|
||||||
parents={parents}
|
parents={parents}
|
||||||
onClickParent={handleClickParent} />
|
onClickParent={handleClickParent} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,4 +4,4 @@ import App from './App';
|
||||||
render(
|
render(
|
||||||
<App />,
|
<App />,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,83 +1,133 @@
|
||||||
|
|
||||||
// 将状态的值解析成固定格式
|
import { Hook, Reducer, Ref } from './../../../horizon/src/renderer/hooks/HookType';
|
||||||
export function parseAttr(rootAttr: any) {
|
|
||||||
const result: {
|
|
||||||
name: string,
|
|
||||||
type: string,
|
|
||||||
value: string,
|
|
||||||
indentation: number
|
|
||||||
}[] = [];
|
|
||||||
const indentation = 0;
|
|
||||||
const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => {
|
|
||||||
const stateType = typeof attr;
|
|
||||||
let value: any;
|
|
||||||
let showType;
|
|
||||||
let addSubState;
|
|
||||||
if (stateType === 'boolean' ||
|
|
||||||
stateType === 'number' ||
|
|
||||||
stateType === 'string' ||
|
|
||||||
stateType === 'undefined') {
|
|
||||||
value = attr;
|
|
||||||
showType = stateType;
|
|
||||||
} else if (stateType === 'function') {
|
|
||||||
const funName = attr.name;
|
|
||||||
value = `f() ${funName}{}`;
|
|
||||||
} else if (stateType === 'symbol') {
|
|
||||||
value = attr.description;
|
|
||||||
} else if (stateType === 'object') {
|
|
||||||
if (attr === null) {
|
|
||||||
showType = 'null';
|
|
||||||
} else if (attr instanceof Map) {
|
|
||||||
showType = 'map';
|
|
||||||
const size = attr.size;
|
|
||||||
value = `Map(${size})`;
|
|
||||||
addSubState = () => {
|
|
||||||
attr.forEach((value, key) => {
|
|
||||||
parseSubAttr(value, parentIndentation + 2, key);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else if (attr instanceof Set) {
|
|
||||||
showType = 'set';
|
|
||||||
const size = attr.size;
|
|
||||||
value = `Set(${size})`;
|
|
||||||
addSubState = () => {
|
|
||||||
let i = 0;
|
|
||||||
attr.forEach((value) => {
|
|
||||||
parseSubAttr(value, parentIndentation + 2, String(i));
|
|
||||||
});
|
|
||||||
i++;
|
|
||||||
};
|
|
||||||
} else if (Array.isArray(attr)) {
|
|
||||||
showType = 'array';
|
|
||||||
value = `Array(${attr.length})`;
|
|
||||||
addSubState = () => {
|
|
||||||
attr.forEach((value, index) => {
|
|
||||||
parseSubAttr(value, parentIndentation + 2, String(index));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
showType = stateType;
|
|
||||||
value = '{...}';
|
|
||||||
addSubState = () => {
|
|
||||||
Object.keys(attr).forEach((key) => {
|
|
||||||
parseSubAttr(attr[key], parentIndentation + 2, key);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push({
|
// 展示值为 string 的可编辑类型
|
||||||
name: attrName,
|
type editableStringType = 'string' | 'number' | 'undefined' | 'null';
|
||||||
type: showType,
|
// 展示值为 string 的不可编辑类型
|
||||||
value,
|
type unEditableStringType = 'function' | 'symbol' | 'object' | 'map' | 'set' | 'array'
|
||||||
indentation: parentIndentation + 1,
|
| 'dom' // 值为 dom 元素的 ref 类型
|
||||||
});
|
| 'ref'; // 值为其他数据的 ref 类型
|
||||||
if (addSubState) {
|
|
||||||
addSubState();
|
type showAsStringType = editableStringType | unEditableStringType;
|
||||||
|
|
||||||
|
|
||||||
|
export type IAttr = {
|
||||||
|
name: string;
|
||||||
|
indentation: number;
|
||||||
|
hIndex?: number; // 用于记录 hook 的 hIndex 值
|
||||||
|
} & ({
|
||||||
|
type: showAsStringType;
|
||||||
|
value: string;
|
||||||
|
} | {
|
||||||
|
type: 'boolean';
|
||||||
|
value: boolean;
|
||||||
|
})
|
||||||
|
|
||||||
|
type showType = showAsStringType | 'boolean';
|
||||||
|
|
||||||
|
const parseSubAttr = (
|
||||||
|
attr: any,
|
||||||
|
parentIndentation: number,
|
||||||
|
attrName: string,
|
||||||
|
result: IAttr[],
|
||||||
|
hIndex?: number) => {
|
||||||
|
const attrType = typeof attr;
|
||||||
|
let value: any;
|
||||||
|
let showType: showType;
|
||||||
|
let addSubState;
|
||||||
|
if (attrType === 'boolean' ||
|
||||||
|
attrType === 'number' ||
|
||||||
|
attrType === 'string' ||
|
||||||
|
attrType === 'undefined') {
|
||||||
|
value = attr;
|
||||||
|
showType = attrType;
|
||||||
|
} else if (attrType === 'function') {
|
||||||
|
const funName = attr.name;
|
||||||
|
value = `f() ${funName}{}`;
|
||||||
|
} else if (attrType === 'symbol') {
|
||||||
|
value = attr.description;
|
||||||
|
} else if (attrType === 'object') {
|
||||||
|
if (attr === null) {
|
||||||
|
showType = 'null';
|
||||||
|
} else if (attr instanceof Map) {
|
||||||
|
showType = 'map';
|
||||||
|
const size = attr.size;
|
||||||
|
value = `Map(${size})`;
|
||||||
|
addSubState = () => {
|
||||||
|
attr.forEach((value, key) => {
|
||||||
|
parseSubAttr(value, parentIndentation + 2, key, result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else if (attr instanceof Set) {
|
||||||
|
showType = 'set';
|
||||||
|
const size = attr.size;
|
||||||
|
value = `Set(${size})`;
|
||||||
|
addSubState = () => {
|
||||||
|
let i = 0;
|
||||||
|
attr.forEach((value) => {
|
||||||
|
parseSubAttr(value, parentIndentation + 2, String(i), result);
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(attr)) {
|
||||||
|
showType = 'array';
|
||||||
|
value = `Array(${attr.length})`;
|
||||||
|
addSubState = () => {
|
||||||
|
attr.forEach((value, index) => {
|
||||||
|
parseSubAttr(value, parentIndentation + 2, String(index), result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else if (attr instanceof Element) {
|
||||||
|
showType = 'dom';
|
||||||
|
value = attr.tagName;
|
||||||
|
} else {
|
||||||
|
showType = attrType;
|
||||||
|
value = '{...}';
|
||||||
|
addSubState = () => {
|
||||||
|
Object.keys(attr).forEach((key) => {
|
||||||
|
parseSubAttr(attr[key], parentIndentation + 2, key, result);
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const item: IAttr = {
|
||||||
|
name: attrName,
|
||||||
|
type: showType,
|
||||||
|
value,
|
||||||
|
indentation: parentIndentation + 1,
|
||||||
};
|
};
|
||||||
|
if (hIndex) {
|
||||||
|
item.hIndex = hIndex;
|
||||||
|
}
|
||||||
|
result.push(item);
|
||||||
|
if (addSubState) {
|
||||||
|
addSubState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将属性的值解析成固定格式,props 和 类组件的 state 必须是一个对象
|
||||||
|
export function parseAttr(rootAttr: any) {
|
||||||
|
const result: IAttr[] = [];
|
||||||
|
const indentation = 0;
|
||||||
|
if (typeof rootAttr === 'object' && rootAttr !== null)
|
||||||
Object.keys(rootAttr).forEach(key => {
|
Object.keys(rootAttr).forEach(key => {
|
||||||
parseSubAttr(rootAttr[key], indentation, key);
|
parseSubAttr(rootAttr[key], indentation, key, result);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHooks(hooks: Hook<any, any>[]) {
|
||||||
|
const result: IAttr[] = [];
|
||||||
|
const indentation = 0;
|
||||||
|
hooks.forEach(hook => {
|
||||||
|
const { hIndex, state ,type } = hook;
|
||||||
|
if (type === 'useState') {
|
||||||
|
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'state', result, hIndex);
|
||||||
|
} else if (type === 'useRef') {
|
||||||
|
parseSubAttr((state as Ref<any>).current, indentation, 'ref', result, hIndex);
|
||||||
|
} else if (type === 'useReducer') {
|
||||||
|
parseSubAttr((state as Reducer<any, any>).stateValue, indentation, 'reducer', result, hIndex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,4 +57,16 @@ function parseTreeRoot(treeRoot: VNode) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function queryVNode(id: number) {
|
||||||
|
return IdToVNodeMap.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearVNode(vNode: VNode) {
|
||||||
|
if (VNodeToIdMap.has(vNode)) {
|
||||||
|
const id = VNodeToIdMap.get(vNode);
|
||||||
|
VNodeToIdMap.delete(vNode);
|
||||||
|
IdToVNodeMap.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default parseTreeRoot;
|
export default parseTreeRoot;
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// panel 页面打开后初始化连接标志
|
||||||
|
export const InitDevToolPageConnection = 'init dev tool page connection';
|
||||||
|
// background 解析全部 root VNodes 标志
|
||||||
|
export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos';
|
||||||
|
// vNodes 全部树解析结果标志
|
||||||
|
export const AllVNodeTreesInfos = 'vNode trees Infos';
|
||||||
|
// 一棵树的解析
|
||||||
|
export const OneVNodeTreeInfos = 'one vNode tree';
|
||||||
|
// 获取组件属性
|
||||||
|
export const RequestComponentAttrs = 'get component attrs';
|
||||||
|
// 返回组件属性
|
||||||
|
export const ComponentAttrs = 'component attrs';
|
||||||
|
|
||||||
|
|
||||||
|
// 传递消息来源标志
|
||||||
|
export const DevToolPanel = 'dev tool panel';
|
||||||
|
|
||||||
|
export const DevToolBackground = 'dev tool background';
|
||||||
|
|
||||||
|
export const DevToolContentScript = 'dev tool content script';
|
||||||
|
|
||||||
|
export const DevToolHook = 'dev tool hook';
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
function ifNullThrows(v) {
|
||||||
|
if (v === null) {
|
||||||
|
throw new Error('received a null');
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于向页面注入脚本
|
||||||
|
export function injectCode(src) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = src;
|
||||||
|
script.onload = function () {
|
||||||
|
// 加载完毕后需要移除
|
||||||
|
script.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
ifNullThrows(document.head || document.documentElement).appendChild(script);
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
const devTools = 'HORIZON_DEV_TOOLS';
|
||||||
|
|
||||||
|
interface payLoadType {
|
||||||
|
type: string,
|
||||||
|
data?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface message {
|
||||||
|
type: typeof devTools,
|
||||||
|
payload: payLoadType,
|
||||||
|
from: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function packagePayload(payload: payLoadType, from: string): message {
|
||||||
|
return {
|
||||||
|
type: devTools,
|
||||||
|
payload,
|
||||||
|
from,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkMessage(data: any, from: string) {
|
||||||
|
if (data?.type === devTools && data?.from === from) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeSource(message: message, from: string) {
|
||||||
|
message.from = from;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"module": "es6",
|
||||||
|
"target": "es5",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"allowJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"*": ["types/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"includes": [
|
||||||
|
"./src/index.d.ts", "./src/*/*.ts", "./src/*/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
entry: {
|
||||||
|
background: './src/background/index.ts',
|
||||||
|
main: './src/main/index.ts',
|
||||||
|
injector: './src/injector/index.ts',
|
||||||
|
contentScript: './src/contentScript/index.ts',
|
||||||
|
panel: './src/panel/index.tsx',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, './build'),
|
||||||
|
filename: '[name].js',
|
||||||
|
},
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.less/i,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'less-loader'],
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
'horizon': 'Horizon',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': '"development"',
|
||||||
|
isDev: 'false',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
|
@ -1,5 +1,4 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
// 用于 panel 页面开发
|
// 用于 panel 页面开发
|
||||||
|
@ -8,6 +7,7 @@ module.exports = {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
entry: {
|
entry: {
|
||||||
panel: path.join(__dirname, './src/panel/index.tsx'),
|
panel: path.join(__dirname, './src/panel/index.tsx'),
|
||||||
|
mockPage: path.join(__dirname, './src/devtools/mockPage/index.tsx'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
|
@ -15,7 +15,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.tsx', '.js']
|
extensions: ['.ts', '.tsx', '.js'],
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -25,16 +25,6 @@ module.exports = {
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
|
||||||
presets: ['@babel/preset-env',
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
['@babel/preset-react', {
|
|
||||||
runtime: 'classic',
|
|
||||||
'pragma': 'Horizon.createElement',
|
|
||||||
'pragmaFrag': 'Horizon.Fragment',
|
|
||||||
}]],
|
|
||||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -64,10 +54,6 @@ module.exports = {
|
||||||
magicHtml: true,
|
magicHtml: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: 'panel.html',
|
|
||||||
template: './src/panel/panel.html'
|
|
||||||
}),
|
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.NODE_ENV': '"development"',
|
'process.env.NODE_ENV': '"development"',
|
||||||
isDev: 'true',
|
isDev: 'true',
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"horizon"
|
"horizon"
|
||||||
],
|
],
|
||||||
"version": "1.0.0",
|
"version": "0.0.6",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"bugs": "",
|
"bugs": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
@ -275,6 +275,11 @@ function renderFromRoot(treeRoot) {
|
||||||
// 2. 提交变更
|
// 2. 提交变更
|
||||||
submitToRender(treeRoot);
|
submitToRender(treeRoot);
|
||||||
|
|
||||||
|
if (window.__HORIZON_DEV_HOOK__) {
|
||||||
|
const hook = window.__HORIZON_DEV_HOOK__;
|
||||||
|
hook.addIfNotInclude(treeRoot);
|
||||||
|
hook.send(treeRoot);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,11 +96,8 @@ function getNodeType(newChild: any): string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置vNode的flag
|
// 设置vNode的flag
|
||||||
function setVNodeAdditionFlag(newNode: VNode, lastPosition: number, isComparing: boolean): number {
|
function setVNodeAdditionFlag(newNode: VNode, lastPosition: number): number {
|
||||||
let position = lastPosition;
|
let position = lastPosition;
|
||||||
if (!isComparing) {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newNode.isCreated || newNode.eIndex < lastPosition) { // 位置 小于 上一个复用的位置
|
if (newNode.isCreated || newNode.eIndex < lastPosition) { // 位置 小于 上一个复用的位置
|
||||||
// 标记为新增
|
// 标记为新增
|
||||||
|
@ -221,7 +218,6 @@ function diffArrayNodesHandler(
|
||||||
parentNode: VNode,
|
parentNode: VNode,
|
||||||
firstChild: VNode | null,
|
firstChild: VNode | null,
|
||||||
newChildren: Array<any>,
|
newChildren: Array<any>,
|
||||||
isComparing: boolean
|
|
||||||
): VNode | null {
|
): VNode | null {
|
||||||
let resultingFirstChild: VNode | null = null;
|
let resultingFirstChild: VNode | null = null;
|
||||||
|
|
||||||
|
@ -273,11 +269,11 @@ function diffArrayNodesHandler(
|
||||||
}
|
}
|
||||||
|
|
||||||
// diff过程中,需要将现有的节点清除掉,如果是创建,则不需要处理(因为没有现存节点)
|
// diff过程中,需要将现有的节点清除掉,如果是创建,则不需要处理(因为没有现存节点)
|
||||||
if (isComparing && oldNode && newNode.isCreated) {
|
if (oldNode && newNode.isCreated) {
|
||||||
deleteVNode(parentNode, oldNode);
|
deleteVNode(parentNode, oldNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
theLastPosition = setVNodeAdditionFlag(newNode, theLastPosition, isComparing);
|
theLastPosition = setVNodeAdditionFlag(newNode, theLastPosition);
|
||||||
newNode.eIndex = leftIdx;
|
newNode.eIndex = leftIdx;
|
||||||
appendNode(newNode);
|
appendNode(newNode);
|
||||||
oldNode = nextOldNode;
|
oldNode = nextOldNode;
|
||||||
|
@ -319,11 +315,11 @@ function diffArrayNodesHandler(
|
||||||
rightNewNode = newNode;
|
rightNewNode = newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComparing && rightOldNode && newNode.isCreated) {
|
if (rightOldNode && newNode.isCreated) {
|
||||||
deleteVNode(parentNode, rightOldNode);
|
deleteVNode(parentNode, rightOldNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
setVNodeAdditionFlag(newNode, theLastPosition, isComparing);
|
setVNodeAdditionFlag(newNode, theLastPosition);
|
||||||
newNode.eIndex = rightIdx - 1;
|
newNode.eIndex = rightIdx - 1;
|
||||||
rightOldIndex--;
|
rightOldIndex--;
|
||||||
rightEndOldNode = rightOldNode;
|
rightEndOldNode = rightOldNode;
|
||||||
|
@ -332,13 +328,11 @@ function diffArrayNodesHandler(
|
||||||
|
|
||||||
// 3. 新节点已经处理完成
|
// 3. 新节点已经处理完成
|
||||||
if (leftIdx === rightIdx) {
|
if (leftIdx === rightIdx) {
|
||||||
if (isComparing) {
|
if (firstChild && parentNode.tag === DomComponent && newChildren.length === 0) {
|
||||||
if (firstChild && parentNode.tag === DomComponent && newChildren.length === 0) {
|
FlagUtils.markClear(parentNode);
|
||||||
FlagUtils.markClear(parentNode);
|
parentNode.clearChild = firstChild;
|
||||||
parentNode.clearChild = firstChild;
|
} else {
|
||||||
} else {
|
deleteVNodes(parentNode, oldNode, rightEndOldNode);
|
||||||
deleteVNodes(parentNode, oldNode, rightEndOldNode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightNewNode) {
|
if (rightNewNode) {
|
||||||
|
@ -360,11 +354,12 @@ function diffArrayNodesHandler(
|
||||||
rightIdx - leftIdx === newChildren.length) {
|
rightIdx - leftIdx === newChildren.length) {
|
||||||
isDirectAdd = true;
|
isDirectAdd = true;
|
||||||
}
|
}
|
||||||
|
const isAddition = parentNode.tag === DomPortal || !parentNode.isCreated;
|
||||||
for (; leftIdx < rightIdx; leftIdx++) {
|
for (; leftIdx < rightIdx; leftIdx++) {
|
||||||
newNode = getNewNode(parentNode, newChildren[leftIdx], null);
|
newNode = getNewNode(parentNode, newChildren[leftIdx], null);
|
||||||
|
|
||||||
if (newNode !== null) {
|
if (newNode !== null) {
|
||||||
if (isComparing) {
|
if (isAddition) {
|
||||||
FlagUtils.setAddition(newNode);
|
FlagUtils.setAddition(newNode);
|
||||||
}
|
}
|
||||||
if (isDirectAdd) {
|
if (isDirectAdd) {
|
||||||
|
@ -398,41 +393,39 @@ function diffArrayNodesHandler(
|
||||||
oldNodeFromMap = getOldNodeFromMap(leftChildrenMap, leftIdx, newChildren[leftIdx]);
|
oldNodeFromMap = getOldNodeFromMap(leftChildrenMap, leftIdx, newChildren[leftIdx]);
|
||||||
newNode = getNewNode(parentNode, newChildren[leftIdx], oldNodeFromMap);
|
newNode = getNewNode(parentNode, newChildren[leftIdx], oldNodeFromMap);
|
||||||
if (newNode !== null) {
|
if (newNode !== null) {
|
||||||
if (isComparing && !newNode.isCreated) {
|
if (newNode.isCreated) {
|
||||||
// 从Map删除,后面不会deleteVNode
|
// 新VNode,直接打上标签新增,不参与到复用,旧的VNode会在后面打上delete标签
|
||||||
|
FlagUtils.setAddition(newNode);
|
||||||
|
} else {
|
||||||
|
// 从Map删除,后面不会deleteVNode,就可以实现复用
|
||||||
leftChildrenMap.delete(newNode.key || leftIdx);
|
leftChildrenMap.delete(newNode.key || leftIdx);
|
||||||
}
|
if (oldNodeFromMap !== null) {
|
||||||
|
const eIndex = newNode.eIndex;
|
||||||
if (oldNodeFromMap !== null) {
|
eIndexes.push(eIndex);
|
||||||
const eIndex = newNode.eIndex;
|
last = eIndexes[result[result.length - 1]];
|
||||||
eIndexes.push(eIndex);
|
if (eIndex > last || last === undefined) { // 大的 eIndex直接放在最后
|
||||||
last = eIndexes[result[result.length - 1]];
|
preIndex[i] = result[result.length - 1];
|
||||||
if (eIndex > last || last === undefined) { // 大的 eIndex直接放在最后
|
result.push(i);
|
||||||
preIndex[i] = result[result.length - 1];
|
} else {
|
||||||
result.push(i);
|
let start = 0;
|
||||||
} else {
|
let end = result.length - 1;
|
||||||
let start = 0;
|
let middle;
|
||||||
let end = result.length - 1;
|
// 二分法找到需要替换的值
|
||||||
let middle;
|
while (start < end) {
|
||||||
// 二分法找到需要替换的值
|
middle = Math.floor((start + end) / 2);
|
||||||
while (start < end) {
|
if (eIndexes[result[middle]] > eIndex) {
|
||||||
middle = Math.floor((start + end) / 2);
|
end = middle;
|
||||||
if (eIndexes[result[middle]] > eIndex) {
|
} else {
|
||||||
end = middle;
|
start = middle + 1;
|
||||||
} else {
|
}
|
||||||
start = middle + 1;
|
}
|
||||||
|
if (eIndex < eIndexes[result[start]]) {
|
||||||
|
preIndex[i] = result[start - 1];
|
||||||
|
result[start] = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (eIndex < eIndexes[result[start]]) {
|
i++;
|
||||||
preIndex[i] = result[start - 1];
|
reuseNodes.push(newNode); // 记录所有复用的节点
|
||||||
result[start] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
reuseNodes.push(newNode); // 记录所有复用的节点
|
|
||||||
} else {
|
|
||||||
if (isComparing) {
|
|
||||||
FlagUtils.setAddition(newNode); // 新增节点直接打上add标签
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newNode.eIndex = leftIdx;
|
newNode.eIndex = leftIdx;
|
||||||
|
@ -440,28 +433,26 @@ function diffArrayNodesHandler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComparing) {
|
// 向前回溯找到正确的结果
|
||||||
// 向前回溯找到正确的结果
|
let length = result.length;
|
||||||
let length = result.length;
|
let prev = result[length - 1];
|
||||||
let prev = result[length - 1];
|
while (length-- > 0) {
|
||||||
while (length-- > 0) {
|
result[length] = prev;
|
||||||
result[length] = prev;
|
prev = preIndex[result[length]];
|
||||||
prev = preIndex[result[length]];
|
|
||||||
}
|
|
||||||
result.forEach(idx => {
|
|
||||||
// 把需要复用的节点从 restNodes 中清理掉,因为不需要打 add 标记,直接复用 dom 节点
|
|
||||||
reuseNodes[idx] = null;
|
|
||||||
});
|
|
||||||
reuseNodes.forEach(node => {
|
|
||||||
if (node !== null) {
|
|
||||||
// 没有被清理的节点打上 add 标记,通过dom的append操作实现位置移动
|
|
||||||
FlagUtils.setAddition(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
leftChildrenMap.forEach(child => {
|
|
||||||
deleteVNode(parentNode, child);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
result.forEach(idx => {
|
||||||
|
// 把需要复用的节点从 restNodes 中清理掉,因为不需要打 add 标记,直接复用 dom 节点
|
||||||
|
reuseNodes[idx] = null;
|
||||||
|
});
|
||||||
|
reuseNodes.forEach(node => {
|
||||||
|
if (node !== null) {
|
||||||
|
// 没有被清理的节点打上 add 标记,通过dom的append操作实现位置移动
|
||||||
|
FlagUtils.setAddition(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
leftChildrenMap.forEach(child => {
|
||||||
|
deleteVNode(parentNode, child);
|
||||||
|
});
|
||||||
|
|
||||||
if (rightNewNode) {
|
if (rightNewNode) {
|
||||||
appendNode(rightNewNode);
|
appendNode(rightNewNode);
|
||||||
|
@ -489,7 +480,6 @@ function diffIteratorNodesHandler(
|
||||||
parentNode: VNode,
|
parentNode: VNode,
|
||||||
firstChild: VNode | null,
|
firstChild: VNode | null,
|
||||||
newChildrenIterable: Iterable<any>,
|
newChildrenIterable: Iterable<any>,
|
||||||
isComparing: boolean
|
|
||||||
): VNode | null {
|
): VNode | null {
|
||||||
const iteratorFn = getIteratorFn(newChildrenIterable);
|
const iteratorFn = getIteratorFn(newChildrenIterable);
|
||||||
const iteratorObj = iteratorFn.call(newChildrenIterable);
|
const iteratorObj = iteratorFn.call(newChildrenIterable);
|
||||||
|
@ -502,7 +492,7 @@ function diffIteratorNodesHandler(
|
||||||
result = iteratorObj.next();
|
result = iteratorObj.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
return diffArrayNodesHandler(parentNode, firstChild, childrenArray, isComparing);
|
return diffArrayNodesHandler(parentNode, firstChild, childrenArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新节点是字符串类型
|
// 新节点是字符串类型
|
||||||
|
@ -643,12 +633,12 @@ export function createChildrenByDiff(
|
||||||
|
|
||||||
// 3. newChild是数组类型
|
// 3. newChild是数组类型
|
||||||
if (Array.isArray(newChild)) {
|
if (Array.isArray(newChild)) {
|
||||||
return diffArrayNodesHandler(parentNode, firstChild, newChild, isComparing);
|
return diffArrayNodesHandler(parentNode, firstChild, newChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. newChild是迭代器类型
|
// 4. newChild是迭代器类型
|
||||||
if (isIteratorType(newChild)) {
|
if (isIteratorType(newChild)) {
|
||||||
return diffIteratorNodesHandler(parentNode, firstChild, newChild, isComparing);
|
return diffIteratorNodesHandler(parentNode, firstChild, newChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. newChild是对象类型
|
// 5. newChild是对象类型
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {EffectConstant} from './EffectConstant';
|
||||||
export interface Hook<S, A> {
|
export interface Hook<S, A> {
|
||||||
state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>;
|
state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>;
|
||||||
hIndex: number;
|
hIndex: number;
|
||||||
|
type?: 'useState' | 'useRef' | 'useReducer';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Reducer<S, A> {
|
export interface Reducer<S, A> {
|
||||||
|
|
|
@ -87,6 +87,7 @@ export function useReducerForInit<S, A>(reducer, initArg, init, isUseState?: boo
|
||||||
}
|
}
|
||||||
|
|
||||||
const hook = createHook();
|
const hook = createHook();
|
||||||
|
hook.type = isUseState ? 'useState' : 'useReducer';
|
||||||
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
|
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
|
||||||
hook.state = {
|
hook.state = {
|
||||||
stateValue: stateValue,
|
stateValue: stateValue,
|
||||||
|
|
|
@ -12,6 +12,7 @@ export function useRefImpl<V>(value: V): Ref<V> {
|
||||||
if (stage === HookStage.Init) {
|
if (stage === HookStage.Init) {
|
||||||
hook = createHook();
|
hook = createHook();
|
||||||
hook.state = {current: value};
|
hook.state = {current: value};
|
||||||
|
hook.type = 'useRef';
|
||||||
} else if (stage === HookStage.Update) {
|
} else if (stage === HookStage.Update) {
|
||||||
hook = getCurrentHook();
|
hook = getCurrentHook();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const readLib = (lib) => {
|
||||||
ejs.renderFile(path.resolve(__dirname, './template.ejs'), {
|
ejs.renderFile(path.resolve(__dirname, './template.ejs'), {
|
||||||
Horizon: readLib(`horizon.${suffix}`),
|
Horizon: readLib(`horizon.${suffix}`),
|
||||||
}, null, function(err, result) {
|
}, null, function(err, result) {
|
||||||
const common3rdLibPath = path.resolve(__dirname, `${libPathPrefix}/common3rdlib.min.js`)
|
const common3rdLibPath = path.resolve(__dirname, `${libPathPrefix}/horizonCommon3rdlib.min.js`)
|
||||||
rimRaf(common3rdLibPath, e => {
|
rimRaf(common3rdLibPath, e => {
|
||||||
if (e) {
|
if (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|
1406
scripts/template.ejs
1406
scripts/template.ejs
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue