diff --git a/libs/extension/babel.config.js b/libs/extension/babel.config.js
new file mode 100644
index 00000000..c358bb46
--- /dev/null
+++ b/libs/extension/babel.config.js
@@ -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'],
+ };
+};
\ No newline at end of file
diff --git a/libs/extension/package.json b/libs/extension/package.json
new file mode 100644
index 00000000..ade577c7
--- /dev/null
+++ b/libs/extension/package.json
@@ -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"
+ }
+}
diff --git a/libs/extension/readme.md b/libs/extension/readme.md
index d7e98884..52e952cc 100644
--- a/libs/extension/readme.md
+++ b/libs/extension/readme.md
@@ -2,7 +2,6 @@
devtools_page: devtool主页面
default_popup: 拓展图标点击时弹窗页面
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
-web_accessible_resources: 注入全局变量代码
## 打开 panel 页面调试面板的方式
@@ -28,30 +27,65 @@ sequenceDiagram
participant panel
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: chrome.runtime.sendMessage
- script_content ->> background : {}
+ script_content ->> background : data
Note over background: chrome.runtime.onMessage
Note over background: port.postMessage
- background ->> panel : {}
+ background ->> panel : data
Note over panel: connection.onMessage.addListener
Note over panel: connection.postMessage
- panel ->> background : {}
+ panel ->> background : data
Note over background: port.onMessage.addListener
Note over background: chrome.tabs.sendMessage
- background ->> script_content : {}
+ background ->> script_content : data
Note over script_content: chrome.runtime.onMessage
Note over script_content: window.postMessage
- script_content ->> web_page : {}
+ script_content ->> web_page : data
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有唯一的 path 属性,可以作为标识使用
- 通过解析 path 值可以分析出组件树的结构
+## 组件props/state/hook等数据的传输和解析
+将数据格式进行转换后进行传递。对于 props 和 类组件的 state,他们都是对象,可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串,他们的解析和 props/state 的解析同样存在差异。
+
+
## 滚动动态渲染 Tree
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
+
+## 开发者页面打开场景
+- 先有页面,然后打开开发者工具:工具建立连接,发送通知,页面hook收到后发送VNode树信息给工具页面
+- 已经打开开发者工具,然后打开页面:业务页面渲染完毕,发送VNode树信息给工具页面
+
+## 开发者工具页面响应组件树变更
+组件树变更会带来新旧两个组件树信息数组,新旧数组存在数据一致而引用不一致的情况,而VTree和VList组件中相关信息的计算依赖引用而非数据本身,在收到新的组件树信息后需要对数据本身进行判断,将新数组中的相同数据使用旧对象代替。
+
+## 测试框架
+jest测试框架不提供浏览器插件的相关 api,我们在封装好相关 api 后需要模拟这些 api 的行为从而展开测试工作。
+
diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts
new file mode 100644
index 00000000..ed9698fb
--- /dev/null
+++ b/libs/extension/src/background/index.ts
@@ -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'});
+});
diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx
index 10a1f637..f5ff0051 100644
--- a/libs/extension/src/components/ComponentInfo.tsx
+++ b/libs/extension/src/components/ComponentInfo.tsx
@@ -3,8 +3,9 @@ import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
import Copy from '../svgs/Copy';
import Triangle from '../svgs/Triangle';
-import { useState } from 'horizon';
+import { useState, useEffect } from 'horizon';
import { IData } from './VTree';
+import { IAttr } from '../parser/parseAttr';
type IComponentInfo = {
name: string;
@@ -18,13 +19,6 @@ type IComponentInfo = {
onClickParent: (item: IData) => void;
};
-type IAttr = {
- name: string;
- type: string;
- value: string | boolean;
- indentation: number;
-}
-
function collapseAllNodes(attrs: IAttr[]) {
return attrs.filter((item, index) => {
const nextItem = attrs[index + 1];
@@ -34,6 +28,9 @@ function collapseAllNodes(attrs: IAttr[]) {
function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
+ useEffect(() => {
+ setCollapsedNode(collapseAllNodes(attrs));
+ }, [attrs]);
const handleCollapse = (item: IAttr) => {
const nodes = [...collapsedNode];
const i = nodes.indexOf(item);
@@ -64,7 +61,9 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
{hasChild && }
{`${item.name}`}
{' :'}
- {item.value}
+ {item.type === 'string' || item.type === 'number'
+ ? {item.value}
+ : {item.value}}
);
if (isCollapsed) {
@@ -106,9 +105,9 @@ export default function ComponentInfo({ name, attrs, parents, onClickParent }: I
{context &&
}
- {props &&
}
- {state &&
}
- {hooks &&
}
+ {props && props.length !== 0 &&
}
+ {state && state.length !== 0 &&
}
+ {hooks && hooks.length !== 0 &&
}
{name &&
parents: {
diff --git a/libs/extension/src/components/ComponentsInfo.less b/libs/extension/src/components/ComponentsInfo.less
index 9a52e2fb..853d4209 100644
--- a/libs/extension/src/components/ComponentsInfo.less
+++ b/libs/extension/src/components/ComponentsInfo.less
@@ -36,13 +36,8 @@
border-bottom: unset;
}
- >:first-child {
- padding: unset;
- }
-
>div {
border-bottom: @divider-style;
- padding: 0.5rem
}
.attrContainer {
diff --git a/libs/extension/src/components/ResizeEvent.ts b/libs/extension/src/components/ResizeEvent.ts
index 0150d0ad..69f6c1aa 100644
--- a/libs/extension/src/components/ResizeEvent.ts
+++ b/libs/extension/src/components/ResizeEvent.ts
@@ -60,7 +60,7 @@ export function addResizeListener(element: any, fn: any) {
observer.data = 'about:blank';
observer.onload = loadObserver;
observer.type = 'text/html';
- observer.__observeElement__ = element;
+ observer['__observeElement__'] = element;
element.__observer__ = observer;
element.appendChild(observer);
} else {
diff --git a/libs/extension/src/components/SizeObserver.tsx b/libs/extension/src/components/SizeObserver.tsx
index 3d430093..47348bb6 100644
--- a/libs/extension/src/components/SizeObserver.tsx
+++ b/libs/extension/src/components/SizeObserver.tsx
@@ -4,8 +4,8 @@ import { addResizeListener, removeResizeListener } from './ResizeEvent';
export function SizeObserver(props) {
const { children, ...rest } = props;
- const containerRef = useRef();
- const [size, setSize] = useState();
+ const containerRef = useRef
();
+ const [size, setSize] = useState<{width: number, height: number}>();
const notifyChild = (element) => {
setSize({
width: element.offsetWidth,
diff --git a/libs/extension/src/components/VList.tsx b/libs/extension/src/components/VList.tsx
index c932a492..73036fae 100644
--- a/libs/extension/src/components/VList.tsx
+++ b/libs/extension/src/components/VList.tsx
@@ -1,8 +1,10 @@
+// TODO:当前的 item 渲染效果较差,每次滚动所有项在数组中的位置都会发生变更。
+// 建议修改成选项增加减少时,未变更项在原数组中位置不变更
import { useState, useRef, useEffect } from 'horizon';
import styles from './VList.less';
-interface IProps {
+interface IProps {
data: T[],
width: number, // 暂时未用到,当需要支持横向滚动时使用
height: number, // VList 的高度
@@ -18,7 +20,7 @@ export type renderInfoType = {
skipItemCountBeforeScrollItem: number,
};
-export function VList(props: IProps) {
+export function VList(props: IProps) {
const {
data,
height,
@@ -30,7 +32,7 @@ export function VList(props: IProps) {
} = props;
const [scrollTop, setScrollTop] = useState(data.indexOf(scrollToItem) * itemHeight);
const renderInfoRef: { current: renderInfoType } = useRef({ visibleItems: [], skipItemCountBeforeScrollItem: 0 });
- const containerRef = useRef();
+ const containerRef = useRef();
useEffect(() => {
onRendered(renderInfoRef.current);
});
diff --git a/libs/extension/src/components/VTree.less b/libs/extension/src/components/VTree.less
index 0f34f9cd..a95f6986 100644
--- a/libs/extension/src/components/VTree.less
+++ b/libs/extension/src/components/VTree.less
@@ -6,7 +6,7 @@
.treeItem {
width: 100%;
position: absolute;
- line-height: 18px;
+ line-height: 1.125rem;
&:hover {
background-color: @select-color;
diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx
index a286e411..05c9d4e3 100644
--- a/libs/extension/src/components/VTree.tsx
+++ b/libs/extension/src/components/VTree.tsx
@@ -1,12 +1,12 @@
import { useState, useEffect } from 'horizon';
import styles from './VTree.less';
import Triangle from '../svgs/Triangle';
-import { createRegExp } from './../utils';
+import { createRegExp } from '../utils/regExpUtils';
import { SizeObserver } from './SizeObserver';
import { renderInfoType, VList } from './VList';
export interface IData {
- id: string;
+ id: number;
name: string;
indentation: number;
userKey: string;
@@ -102,7 +102,7 @@ function VTree(props: {
onRendered: (renderInfo: renderInfoType) => void,
collapsedNodes?: IData[],
onCollapseNode?: (item: IData[]) => void,
- selectItem: IData[],
+ selectItem: IData,
onSelectItem: (item: IData) => void,
}) {
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
diff --git a/libs/extension/src/contentScript/index.ts b/libs/extension/src/contentScript/index.ts
new file mode 100644
index 00000000..9a7fe2e8
--- /dev/null
+++ b/libs/extension/src/contentScript/index.ts
@@ -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'});
+ }
+);
diff --git a/libs/extension/src/devtools/mockPage/MockClassComponent.tsx b/libs/extension/src/devtools/mockPage/MockClassComponent.tsx
new file mode 100644
index 00000000..c59385ed
--- /dev/null
+++ b/libs/extension/src/devtools/mockPage/MockClassComponent.tsx
@@ -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 (
+
+
+ {this.state.name}
+ {this.props?.fruit}
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/libs/extension/src/devtools/mockPage/MockContext.ts b/libs/extension/src/devtools/mockPage/MockContext.ts
new file mode 100644
index 00000000..68bd8d1e
--- /dev/null
+++ b/libs/extension/src/devtools/mockPage/MockContext.ts
@@ -0,0 +1,3 @@
+import { createContext } from 'horizon';
+
+export const MockContext = createContext({value: 'default context value'});
diff --git a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
new file mode 100644
index 00000000..41437e38
--- /dev/null
+++ b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx
@@ -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();
+ const objRef = useRef({ str: 'string' });
+ const context = useContext(MockContext);
+
+ useEffect(() => { }, []);
+
+ return (
+
+ age: {age}
+
+ count: {props.count}
+
+
{objRef.current.str}
+
{context.ctx}
+
+ );
+}
\ No newline at end of file
diff --git a/libs/extension/src/devtools/mockPage/index.tsx b/libs/extension/src/devtools/mockPage/index.tsx
new file mode 100644
index 00000000..57c96b44
--- /dev/null
+++ b/libs/extension/src/devtools/mockPage/index.tsx
@@ -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 (
+
+
+
+
+
+ abc
+
+ );
+}
+
+render(, root);
diff --git a/libs/extension/src/devtools/mockPage/mockPage.html b/libs/extension/src/devtools/mockPage/mockPage.html
new file mode 100644
index 00000000..5e6b02c8
--- /dev/null
+++ b/libs/extension/src/devtools/mockPage/mockPage.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Horizon Mock Page
+
+
+
+
+
+
+
+
+
diff --git a/libs/extension/src/components/FilterTree.ts b/libs/extension/src/hooks/FilterTree.ts
similarity index 95%
rename from libs/extension/src/components/FilterTree.ts
rename to libs/extension/src/hooks/FilterTree.ts
index 446bd05d..faa789f0 100644
--- a/libs/extension/src/components/FilterTree.ts
+++ b/libs/extension/src/hooks/FilterTree.ts
@@ -13,7 +13,7 @@
// 找到该节点的缩进值,和index值,在data中向上遍历,通过缩进值判断父节点
import { useState, useRef } from 'horizon';
-import { createRegExp } from '../utils';
+import { createRegExp } from '../utils/regExpUtils';
/**
* 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组
@@ -62,11 +62,11 @@ export function FilterTree(props: { data: T[] }) {
const collapsedNodes = collapsedNodesRef.current;
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
- collapsedNodesRef.current = newcollapsedNodes;
+ collapsedNodesRef.current = newCollapsedNodes;
}
};
diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts
new file mode 100644
index 00000000..e0372d10
--- /dev/null
+++ b/libs/extension/src/injector/index.ts
@@ -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();
diff --git a/libs/extension/src/main/index.ts b/libs/extension/src/main/index.ts
new file mode 100644
index 00000000..b81f544c
--- /dev/null
+++ b/libs/extension/src/main/index.ts
@@ -0,0 +1,7 @@
+chrome.devtools.panels.create('Horizon',
+ '',
+ 'panel.html',
+ function(panel) {
+
+ }
+);
\ No newline at end of file
diff --git a/libs/extension/src/main/main.html b/libs/extension/src/main/main.html
new file mode 100644
index 00000000..540f18ae
--- /dev/null
+++ b/libs/extension/src/main/main.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/extension/src/manifest.json b/libs/extension/src/manifest.json
new file mode 100644
index 00000000..0679215a
--- /dev/null
+++ b/libs/extension/src/manifest.json
@@ -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": ["
"],
+ "js": ["contentScript.js"],
+ "run_at": "document_start"
+ }
+ ],
+ "web_accessible_resources": [
+ {
+ "resources": [ "injector.js", "background.js" ],
+ "matches": [""]
+ }
+ ]
+}
diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx
index 7cea62e9..af6e5d9a 100644
--- a/libs/extension/src/panel/App.tsx
+++ b/libs/extension/src/panel/App.tsx
@@ -1,13 +1,21 @@
-import { useState, useEffect } from 'horizon';
+import { useState, useEffect, useRef } from 'horizon';
import VTree, { IData } from '../components/VTree';
import Search from '../components/Search';
import ComponentInfo from '../components/ComponentInfo';
import styles from './App.less';
import Select from '../svgs/Select';
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
-import { FilterTree } from '../components/FilterTree';
+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';
const parseVNodeData = (rawData) => {
const idIndentationMap: {
@@ -16,7 +24,7 @@ const parseVNodeData = (rawData) => {
const data: IData[] = [];
let i = 0;
while (i < rawData.length) {
- const id = rawData[i] as string;
+ const id = rawData[i] as number;
i++;
const name = rawData[i] as string;
i++;
@@ -51,10 +59,47 @@ 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);
+ }
+}
+
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentAttrs, setComponentAttrs] = useState({});
const [selectComp, setSelectComp] = useState(null);
+ const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度,
const {
filterValue,
@@ -77,6 +122,36 @@ function App() {
state: 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) => {
- setComponentAttrs({
- state: parsedMockState,
- props: parsedMockState,
- });
+ if (isDev) {
+ setComponentAttrs({
+ state: parsedMockState,
+ props: parsedMockState,
+ });
+ } else {
+ postMessage(RequestComponentAttrs, item.id);
+ }
setSelectComp(item);
};
@@ -134,8 +213,8 @@ function App() {
diff --git a/libs/extension/src/panel/index.tsx b/libs/extension/src/panel/index.tsx
index a6174e37..2f97d3ce 100644
--- a/libs/extension/src/panel/index.tsx
+++ b/libs/extension/src/panel/index.tsx
@@ -4,4 +4,4 @@ import App from './App';
render(
,
document.getElementById('root')
-);
\ No newline at end of file
+);
diff --git a/libs/extension/src/parser/parseAttr.ts b/libs/extension/src/parser/parseAttr.ts
index c52b1891..e18f9bad 100644
--- a/libs/extension/src/parser/parseAttr.ts
+++ b/libs/extension/src/parser/parseAttr.ts
@@ -1,83 +1,133 @@
-// 将状态的值解析成固定格式
-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);
- });
- };
- }
- }
+import { Hook, Reducer, Ref } from './../../../horizon/src/renderer/hooks/HookType';
- result.push({
- name: attrName,
- type: showType,
- value,
- indentation: parentIndentation + 1,
- });
- if (addSubState) {
- addSubState();
+// 展示值为 string 的可编辑类型
+type editableStringType = 'string' | 'number' | 'undefined' | 'null';
+// 展示值为 string 的不可编辑类型
+type unEditableStringType = 'function' | 'symbol' | 'object' | 'map' | 'set' | 'array'
+ | 'dom' // 值为 dom 元素的 ref 类型
+ | 'ref'; // 值为其他数据的 ref 类型
+
+type showAsStringType = editableStringType | unEditableStringType;
+
+
+export type IAttr = {
+ name: string;
+ 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 => {
- parseSubAttr(rootAttr[key], indentation, key);
+ parseSubAttr(rootAttr[key], indentation, key, result);
+ });
+ return result;
+}
+
+export function parseHooks(hooks: Hook
[]) {
+ const result: IAttr[] = [];
+ const indentation = 0;
+ hooks.forEach(hook => {
+ const { hIndex, state ,type } = hook;
+ if (type === 'useState') {
+ parseSubAttr((state as Reducer).stateValue, indentation, 'state', result, hIndex);
+ } else if (type === 'useRef') {
+ parseSubAttr((state as Ref).current, indentation, 'ref', result, hIndex);
+ } else if (type === 'useReducer') {
+ parseSubAttr((state as Reducer).stateValue, indentation, 'reducer', result, hIndex);
+ }
});
return result;
}
diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts
index 91104058..fbf4b0bd 100644
--- a/libs/extension/src/parser/parseVNode.ts
+++ b/libs/extension/src/parser/parseVNode.ts
@@ -57,4 +57,16 @@ function parseTreeRoot(treeRoot: VNode) {
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;
diff --git a/libs/extension/src/utils/constants.ts b/libs/extension/src/utils/constants.ts
new file mode 100644
index 00000000..e779a143
--- /dev/null
+++ b/libs/extension/src/utils/constants.ts
@@ -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';
\ No newline at end of file
diff --git a/libs/extension/src/utils/injectUtils.ts b/libs/extension/src/utils/injectUtils.ts
new file mode 100644
index 00000000..583eb5ea
--- /dev/null
+++ b/libs/extension/src/utils/injectUtils.ts
@@ -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);
+}
diff --git a/libs/extension/src/utils.ts b/libs/extension/src/utils/regExpUtils.ts
similarity index 100%
rename from libs/extension/src/utils.ts
rename to libs/extension/src/utils/regExpUtils.ts
diff --git a/libs/extension/src/utils/transferTool.ts b/libs/extension/src/utils/transferTool.ts
new file mode 100644
index 00000000..c25fc4d7
--- /dev/null
+++ b/libs/extension/src/utils/transferTool.ts
@@ -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;
+}
+
diff --git a/libs/extension/tsconfig.json b/libs/extension/tsconfig.json
new file mode 100644
index 00000000..e2effe5e
--- /dev/null
+++ b/libs/extension/tsconfig.json
@@ -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"
+ ]
+}
\ No newline at end of file
diff --git a/libs/extension/webpack.config.js b/libs/extension/webpack.config.js
new file mode 100644
index 00000000..25dab94b
--- /dev/null
+++ b/libs/extension/webpack.config.js
@@ -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;
diff --git a/libs/extension/webpack.dev.js b/libs/extension/webpack.dev.js
index 1c468b0c..b3332077 100644
--- a/libs/extension/webpack.dev.js
+++ b/libs/extension/webpack.dev.js
@@ -1,5 +1,4 @@
const path = require('path');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
// 用于 panel 页面开发
@@ -8,6 +7,7 @@ module.exports = {
mode: 'development',
entry: {
panel: path.join(__dirname, './src/panel/index.tsx'),
+ mockPage: path.join(__dirname, './src/devtools/mockPage/index.tsx'),
},
output: {
path: path.join(__dirname, 'dist'),
@@ -15,7 +15,7 @@ module.exports = {
},
devtool: 'source-map',
resolve: {
- extensions: ['.ts', '.tsx', '.js']
+ extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
@@ -25,16 +25,6 @@ module.exports = {
use: [
{
loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env',
- '@babel/preset-typescript',
- ['@babel/preset-react', {
- runtime: 'classic',
- 'pragma': 'Horizon.createElement',
- 'pragmaFrag': 'Horizon.Fragment',
- }]],
- plugins: ['@babel/plugin-proposal-class-properties'],
- }
}
]
},
@@ -64,10 +54,6 @@ module.exports = {
magicHtml: true,
},
plugins: [
- new HtmlWebpackPlugin({
- filename: 'panel.html',
- template: './src/panel/panel.html'
- }),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"',
isDev: 'true',
diff --git a/libs/horizon/package.json b/libs/horizon/package.json
index 388ee500..f3ffb19d 100644
--- a/libs/horizon/package.json
+++ b/libs/horizon/package.json
@@ -4,7 +4,7 @@
"keywords": [
"horizon"
],
- "version": "1.0.0",
+ "version": "0.0.6",
"homepage": "",
"bugs": "",
"main": "index.js",
diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts
index 1680c949..394319e0 100644
--- a/libs/horizon/src/renderer/TreeBuilder.ts
+++ b/libs/horizon/src/renderer/TreeBuilder.ts
@@ -275,6 +275,11 @@ function renderFromRoot(treeRoot) {
// 2. 提交变更
submitToRender(treeRoot);
+ if (window.__HORIZON_DEV_HOOK__) {
+ const hook = window.__HORIZON_DEV_HOOK__;
+ hook.addIfNotInclude(treeRoot);
+ hook.send(treeRoot);
+ }
return null;
}
diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts
index 2b97612f..8e029fb6 100644
--- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts
+++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts
@@ -96,11 +96,8 @@ function getNodeType(newChild: any): string | null {
}
// 设置vNode的flag
-function setVNodeAdditionFlag(newNode: VNode, lastPosition: number, isComparing: boolean): number {
+function setVNodeAdditionFlag(newNode: VNode, lastPosition: number): number {
let position = lastPosition;
- if (!isComparing) {
- return position;
- }
if (newNode.isCreated || newNode.eIndex < lastPosition) { // 位置 小于 上一个复用的位置
// 标记为新增
@@ -221,7 +218,6 @@ function diffArrayNodesHandler(
parentNode: VNode,
firstChild: VNode | null,
newChildren: Array,
- isComparing: boolean
): VNode | null {
let resultingFirstChild: VNode | null = null;
@@ -273,11 +269,11 @@ function diffArrayNodesHandler(
}
// diff过程中,需要将现有的节点清除掉,如果是创建,则不需要处理(因为没有现存节点)
- if (isComparing && oldNode && newNode.isCreated) {
+ if (oldNode && newNode.isCreated) {
deleteVNode(parentNode, oldNode);
}
- theLastPosition = setVNodeAdditionFlag(newNode, theLastPosition, isComparing);
+ theLastPosition = setVNodeAdditionFlag(newNode, theLastPosition);
newNode.eIndex = leftIdx;
appendNode(newNode);
oldNode = nextOldNode;
@@ -319,11 +315,11 @@ function diffArrayNodesHandler(
rightNewNode = newNode;
}
- if (isComparing && rightOldNode && newNode.isCreated) {
+ if (rightOldNode && newNode.isCreated) {
deleteVNode(parentNode, rightOldNode);
}
- setVNodeAdditionFlag(newNode, theLastPosition, isComparing);
+ setVNodeAdditionFlag(newNode, theLastPosition);
newNode.eIndex = rightIdx - 1;
rightOldIndex--;
rightEndOldNode = rightOldNode;
@@ -332,13 +328,11 @@ function diffArrayNodesHandler(
// 3. 新节点已经处理完成
if (leftIdx === rightIdx) {
- if (isComparing) {
- if (firstChild && parentNode.tag === DomComponent && newChildren.length === 0) {
- FlagUtils.markClear(parentNode);
- parentNode.clearChild = firstChild;
- } else {
- deleteVNodes(parentNode, oldNode, rightEndOldNode);
- }
+ if (firstChild && parentNode.tag === DomComponent && newChildren.length === 0) {
+ FlagUtils.markClear(parentNode);
+ parentNode.clearChild = firstChild;
+ } else {
+ deleteVNodes(parentNode, oldNode, rightEndOldNode);
}
if (rightNewNode) {
@@ -360,11 +354,12 @@ function diffArrayNodesHandler(
rightIdx - leftIdx === newChildren.length) {
isDirectAdd = true;
}
+ const isAddition = parentNode.tag === DomPortal || !parentNode.isCreated;
for (; leftIdx < rightIdx; leftIdx++) {
newNode = getNewNode(parentNode, newChildren[leftIdx], null);
if (newNode !== null) {
- if (isComparing) {
+ if (isAddition) {
FlagUtils.setAddition(newNode);
}
if (isDirectAdd) {
@@ -398,41 +393,39 @@ function diffArrayNodesHandler(
oldNodeFromMap = getOldNodeFromMap(leftChildrenMap, leftIdx, newChildren[leftIdx]);
newNode = getNewNode(parentNode, newChildren[leftIdx], oldNodeFromMap);
if (newNode !== null) {
- if (isComparing && !newNode.isCreated) {
- // 从Map删除,后面不会deleteVNode
+ if (newNode.isCreated) {
+ // 新VNode,直接打上标签新增,不参与到复用,旧的VNode会在后面打上delete标签
+ FlagUtils.setAddition(newNode);
+ } else {
+ // 从Map删除,后面不会deleteVNode,就可以实现复用
leftChildrenMap.delete(newNode.key || leftIdx);
- }
-
- if (oldNodeFromMap !== null) {
- const eIndex = newNode.eIndex;
- eIndexes.push(eIndex);
- last = eIndexes[result[result.length - 1]];
- if (eIndex > last || last === undefined) { // 大的 eIndex直接放在最后
- preIndex[i] = result[result.length - 1];
- result.push(i);
- } else {
- let start = 0;
- let end = result.length - 1;
- let middle;
- // 二分法找到需要替换的值
- while (start < end) {
- middle = Math.floor((start + end) / 2);
- if (eIndexes[result[middle]] > eIndex) {
- end = middle;
- } else {
- start = middle + 1;
+ if (oldNodeFromMap !== null) {
+ const eIndex = newNode.eIndex;
+ eIndexes.push(eIndex);
+ last = eIndexes[result[result.length - 1]];
+ if (eIndex > last || last === undefined) { // 大的 eIndex直接放在最后
+ preIndex[i] = result[result.length - 1];
+ result.push(i);
+ } else {
+ let start = 0;
+ let end = result.length - 1;
+ let middle;
+ // 二分法找到需要替换的值
+ while (start < end) {
+ middle = Math.floor((start + end) / 2);
+ if (eIndexes[result[middle]] > eIndex) {
+ end = middle;
+ } else {
+ start = middle + 1;
+ }
+ }
+ if (eIndex < eIndexes[result[start]]) {
+ preIndex[i] = result[start - 1];
+ result[start] = i;
}
}
- if (eIndex < eIndexes[result[start]]) {
- preIndex[i] = result[start - 1];
- result[start] = i;
- }
- }
- i++;
- reuseNodes.push(newNode); // 记录所有复用的节点
- } else {
- if (isComparing) {
- FlagUtils.setAddition(newNode); // 新增节点直接打上add标签
+ i++;
+ reuseNodes.push(newNode); // 记录所有复用的节点
}
}
newNode.eIndex = leftIdx;
@@ -440,28 +433,26 @@ function diffArrayNodesHandler(
}
}
- if (isComparing) {
- // 向前回溯找到正确的结果
- let length = result.length;
- let prev = result[length - 1];
- while (length-- > 0) {
- result[length] = prev;
- 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);
- });
+ // 向前回溯找到正确的结果
+ let length = result.length;
+ let prev = result[length - 1];
+ while (length-- > 0) {
+ result[length] = prev;
+ 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);
+ });
if (rightNewNode) {
appendNode(rightNewNode);
@@ -489,7 +480,6 @@ function diffIteratorNodesHandler(
parentNode: VNode,
firstChild: VNode | null,
newChildrenIterable: Iterable,
- isComparing: boolean
): VNode | null {
const iteratorFn = getIteratorFn(newChildrenIterable);
const iteratorObj = iteratorFn.call(newChildrenIterable);
@@ -502,7 +492,7 @@ function diffIteratorNodesHandler(
result = iteratorObj.next();
}
- return diffArrayNodesHandler(parentNode, firstChild, childrenArray, isComparing);
+ return diffArrayNodesHandler(parentNode, firstChild, childrenArray);
}
// 新节点是字符串类型
@@ -643,12 +633,12 @@ export function createChildrenByDiff(
// 3. newChild是数组类型
if (Array.isArray(newChild)) {
- return diffArrayNodesHandler(parentNode, firstChild, newChild, isComparing);
+ return diffArrayNodesHandler(parentNode, firstChild, newChild);
}
// 4. newChild是迭代器类型
if (isIteratorType(newChild)) {
- return diffIteratorNodesHandler(parentNode, firstChild, newChild, isComparing);
+ return diffIteratorNodesHandler(parentNode, firstChild, newChild);
}
// 5. newChild是对象类型
diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts
index e965fdf1..cb8be892 100644
--- a/libs/horizon/src/renderer/hooks/HookType.ts
+++ b/libs/horizon/src/renderer/hooks/HookType.ts
@@ -3,6 +3,7 @@ import {EffectConstant} from './EffectConstant';
export interface Hook {
state: Reducer | Effect | Memo | CallBack | Ref;
hIndex: number;
+ type?: 'useState' | 'useRef' | 'useReducer';
}
export interface Reducer {
diff --git a/libs/horizon/src/renderer/hooks/UseReducerHook.ts b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
index 52399713..480f43bb 100644
--- a/libs/horizon/src/renderer/hooks/UseReducerHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseReducerHook.ts
@@ -87,6 +87,7 @@ export function useReducerForInit(reducer, initArg, init, isUseState?: boo
}
const hook = createHook();
+ hook.type = isUseState ? 'useState' : 'useReducer';
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
hook.state = {
stateValue: stateValue,
diff --git a/libs/horizon/src/renderer/hooks/UseRefHook.ts b/libs/horizon/src/renderer/hooks/UseRefHook.ts
index 754a16d2..381ef61e 100644
--- a/libs/horizon/src/renderer/hooks/UseRefHook.ts
+++ b/libs/horizon/src/renderer/hooks/UseRefHook.ts
@@ -12,6 +12,7 @@ export function useRefImpl(value: V): Ref {
if (stage === HookStage.Init) {
hook = createHook();
hook.state = {current: value};
+ hook.type = 'useRef';
} else if (stage === HookStage.Update) {
hook = getCurrentHook();
}
diff --git a/scripts/gen3rdLib.js b/scripts/gen3rdLib.js
index 66b54d69..7e33dfec 100644
--- a/scripts/gen3rdLib.js
+++ b/scripts/gen3rdLib.js
@@ -23,7 +23,7 @@ const readLib = (lib) => {
ejs.renderFile(path.resolve(__dirname, './template.ejs'), {
Horizon: readLib(`horizon.${suffix}`),
}, 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 => {
if (e) {
console.log(e)
diff --git a/scripts/template.ejs b/scripts/template.ejs
index 7ead2d31..3f426b7e 100644
--- a/scripts/template.ejs
+++ b/scripts/template.ejs
@@ -1,742 +1,764 @@
-if(!window["Horizon"]) {
- <%- Horizon %>
+<%- Horizon %>
+
+!function(t,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports.ie=r():t.ie=r()}(window,(function(){return function(t){var r={};function e(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{enumerable:!0,get:n})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,r){if(1&r&&(t=e(t)),8&r)return t;if(4&r&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(e.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&r&&"string"!=typeof t)for(var o in t)e.d(n,o,function(r){return t[r]}.bind(null,o));return n},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,r){return Object.prototype.hasOwnProperty.call(t,r)},e.p="",e(e.s=192)}([function(t,r,e){var n=e(1),o=e(23).f,i=e(25),a=e(17),u=e(114),c=e(90),s=e(73);t.exports=function(t,r){var e,f,l,h,p,v=t.target,g=t.global,d=t.stat;if(e=g?n:d?n[v]||u(v,{}):(n[v]||{}).prototype)for(f in r){if(h=r[f],l=t.noTargetGet?(p=o(e,f))&&p.value:e[f],!s(g?f:v+(d?".":"#")+f,t.forced)&&void 0!==l){if(typeof h==typeof l)continue;c(h,l)}(t.sham||l&&l.sham)&&i(h,"sham",!0),a(e,f,h,t)}}},function(t,r,e){(function(r){var e=function(t){return t&&t.Math==Math&&t};t.exports=e("object"==typeof globalThis&&globalThis)||e("object"==typeof window&&window)||e("object"==typeof self&&self)||e("object"==typeof r&&r)||function(){return this}()||Function("return this")()}).call(this,e(195))},function(t,r){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,r,e){var n=e(68),o=Function.prototype,i=o.bind,a=o.call,u=n&&i.bind(a,a);t.exports=n?function(t){return t&&u(t)}:function(t){return t&&function(){return a.apply(t,arguments)}}},function(t,r,e){var n=e(1),o=e(6),i=n.String,a=n.TypeError;t.exports=function(t){if(o(t))return t;throw a(i(t)+" is not an object")}},function(t,r,e){var n=e(2);t.exports=!n((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},function(t,r,e){var n=e(9);t.exports=function(t){return"object"==typeof t?null!==t:n(t)}},function(t,r,e){var n=e(1),o=e(86),i=e(12),a=e(60),u=e(112),c=e(142),s=o("wks"),f=n.Symbol,l=f&&f.for,h=c?f:f&&f.withoutSetter||a;t.exports=function(t){if(!i(s,t)||!u&&"string"!=typeof s[t]){var r="Symbol."+t;u&&i(f,t)?s[t]=f[t]:s[t]=c&&l?l(r):h(r)}return s[t]}},function(t,r,e){var n=e(1),o=e(52),i=n.String;t.exports=function(t){if("Symbol"===o(t))throw TypeError("Cannot convert a Symbol value to a string");return i(t)}},function(t,r){t.exports=function(t){return"function"==typeof t}},function(t,r,e){"use strict";var n,o,i,a=e(128),u=e(5),c=e(1),s=e(9),f=e(6),l=e(12),h=e(52),p=e(70),v=e(25),g=e(17),d=e(13).f,y=e(29),m=e(37),b=e(39),x=e(7),w=e(60),E=c.Int8Array,S=E&&E.prototype,A=c.Uint8ClampedArray,O=A&&A.prototype,R=E&&m(E),T=S&&m(S),I=Object.prototype,M=c.TypeError,j=x("toStringTag"),P=w("TYPED_ARRAY_TAG"),k=w("TYPED_ARRAY_CONSTRUCTOR"),_=a&&!!b&&"Opera"!==h(c.opera),L=!1,N={Int8Array:1,Uint8Array:1,Uint8ClampedArray:1,Int16Array:2,Uint16Array:2,Int32Array:4,Uint32Array:4,Float32Array:4,Float64Array:8},D={BigInt64Array:8,BigUint64Array:8},U=function(t){if(!f(t))return!1;var r=h(t);return l(N,r)||l(D,r)};for(n in N)(i=(o=c[n])&&o.prototype)?v(i,k,o):_=!1;for(n in D)(i=(o=c[n])&&o.prototype)&&v(i,k,o);if((!_||!s(R)||R===Function.prototype)&&(R=function(){throw M("Incorrect invocation")},_))for(n in N)c[n]&&b(c[n],R);if((!_||!T||T===I)&&(T=R.prototype,_))for(n in N)c[n]&&b(c[n].prototype,T);if(_&&m(O)!==T&&b(O,T),u&&!l(T,j))for(n in L=!0,d(T,j,{get:function(){return f(this)?this[P]:void 0}}),N)c[n]&&v(c[n],P,n);t.exports={NATIVE_ARRAY_BUFFER_VIEWS:_,TYPED_ARRAY_CONSTRUCTOR:k,TYPED_ARRAY_TAG:L&&P,aTypedArray:function(t){if(U(t))return t;throw M("Target is not a typed array")},aTypedArrayConstructor:function(t){if(s(t)&&(!b||y(R,t)))return t;throw M(p(t)+" is not a typed array constructor")},exportTypedArrayMethod:function(t,r,e,n){if(u){if(e)for(var o in N){var i=c[o];if(i&&l(i.prototype,t))try{delete i.prototype[t]}catch(e){try{i.prototype[t]=r}catch(t){}}}T[t]&&!e||g(T,t,e?r:_&&S[t]||r,n)}},exportTypedArrayStaticMethod:function(t,r,e){var n,o;if(u){if(b){if(e)for(n in N)if((o=c[n])&&l(o,t))try{delete o[t]}catch(t){}if(R[t]&&!e)return;try{return g(R,t,e?r:_&&R[t]||r)}catch(t){}}for(n in N)!(o=c[n])||o[t]&&!e||g(o,t,r)}},isView:function(t){if(!f(t))return!1;var r=h(t);return"DataView"===r||l(N,r)||l(D,r)},isTypedArray:U,TypedArray:R,TypedArrayPrototype:T}},function(t,r,e){var n=e(68),o=Function.prototype.call;t.exports=n?o.bind(o):function(){return o.apply(o,arguments)}},function(t,r,e){var n=e(3),o=e(14),i=n({}.hasOwnProperty);t.exports=Object.hasOwn||function(t,r){return i(o(t),r)}},function(t,r,e){var n=e(1),o=e(5),i=e(144),a=e(145),u=e(4),c=e(49),s=n.TypeError,f=Object.defineProperty,l=Object.getOwnPropertyDescriptor;r.f=o?a?function(t,r,e){if(u(t),r=c(r),u(e),"function"==typeof t&&"prototype"===r&&"value"in e&&"writable"in e&&!e.writable){var n=l(t,r);n&&n.writable&&(t[r]=e.value,e={configurable:"configurable"in e?e.configurable:n.configurable,enumerable:"enumerable"in e?e.enumerable:n.enumerable,writable:!1})}return f(t,r,e)}:f:function(t,r,e){if(u(t),r=c(r),u(e),i)try{return f(t,r,e)}catch(t){}if("get"in e||"set"in e)throw s("Accessors not supported");return"value"in e&&(t[r]=e.value),t}},function(t,r,e){var n=e(1),o=e(18),i=n.Object;t.exports=function(t){return i(o(t))}},function(t,r,e){var n=e(30);t.exports=function(t){return n(t.length)}},function(t,r,e){var n=e(1),o=e(9),i=function(t){return o(t)?t:void 0};t.exports=function(t,r){return arguments.length<2?i(n[t]):n[t]&&n[t][r]}},function(t,r,e){var n=e(1),o=e(9),i=e(12),a=e(25),u=e(114),c=e(88),s=e(19),f=e(61).CONFIGURABLE,l=s.get,h=s.enforce,p=String(String).split("String");(t.exports=function(t,r,e,c){var s,l=!!c&&!!c.unsafe,v=!!c&&!!c.enumerable,g=!!c&&!!c.noTargetGet,d=c&&void 0!==c.name?c.name:r;o(e)&&("Symbol("===String(d).slice(0,7)&&(d="["+String(d).replace(/^Symbol\(([^)]*)\)/,"$1")+"]"),(!i(e,"name")||f&&e.name!==d)&&a(e,"name",d),(s=h(e)).source||(s.source=p.join("string"==typeof d?d:""))),t!==n?(l?!g&&t[r]&&(v=!0):delete t[r],v?t[r]=e:a(t,r,e)):v?t[r]=e:u(r,e)})(Function.prototype,"toString",(function(){return o(this)&&l(this).source||c(this)}))},function(t,r,e){var n=e(1).TypeError;t.exports=function(t){if(null==t)throw n("Can't call method on "+t);return t}},function(t,r,e){var n,o,i,a=e(146),u=e(1),c=e(3),s=e(6),f=e(25),l=e(12),h=e(113),p=e(89),v=e(71),g=u.TypeError,d=u.WeakMap;if(a||h.state){var y=h.state||(h.state=new d),m=c(y.get),b=c(y.has),x=c(y.set);n=function(t,r){if(b(y,t))throw new g("Object already initialized");return r.facade=t,x(y,t,r),r},o=function(t){return m(y,t)||{}},i=function(t){return b(y,t)}}else{var w=p("state");v[w]=!0,n=function(t,r){if(l(t,w))throw new g("Object already initialized");return r.facade=t,f(t,w,r),r},o=function(t){return l(t,w)?t[w]:{}},i=function(t){return l(t,w)}}t.exports={set:n,get:o,has:i,enforce:function(t){return i(t)?o(t):n(t,{})},getterFor:function(t){return function(r){var e;if(!s(r)||(e=o(r)).type!==t)throw g("Incompatible receiver, "+t+" required");return e}}}},function(t,r){var e=Math.ceil,n=Math.floor;t.exports=function(t){var r=+t;return r!=r||0===r?0:(r>0?n:e)(r)}},function(t,r){t.exports=!1},function(t,r,e){var n=e(38),o=e(3),i=e(69),a=e(14),u=e(15),c=e(77),s=o([].push),f=function(t){var r=1==t,e=2==t,o=3==t,f=4==t,l=6==t,h=7==t,p=5==t||l;return function(v,g,d,y){for(var m,b,x=a(v),w=i(x),E=n(g,d),S=u(w),A=0,O=y||c,R=r?O(v,S):e||h?O(v,0):void 0;S>A;A++)if((p||A in w)&&(b=E(m=w[A],A,x),t))if(r)R[A]=b;else if(b)switch(t){case 3:return!0;case 5:return m;case 6:return A;case 2:s(R,m)}else switch(t){case 4:return!1;case 7:s(R,m)}return l?-1:o||f?f:R}};t.exports={forEach:f(0),map:f(1),filter:f(2),some:f(3),every:f(4),find:f(5),findIndex:f(6),filterReject:f(7)}},function(t,r,e){var n=e(5),o=e(11),i=e(85),a=e(35),u=e(26),c=e(49),s=e(12),f=e(144),l=Object.getOwnPropertyDescriptor;r.f=n?l:function(t,r){if(t=u(t),r=c(r),f)try{return l(t,r)}catch(t){}if(s(t,r))return a(!o(i.f,t,r),t[r])}},function(t,r,e){var n=e(1),o=e(9),i=e(70),a=n.TypeError;t.exports=function(t){if(o(t))return t;throw a(i(t)+" is not a function")}},function(t,r,e){var n=e(5),o=e(13),i=e(35);t.exports=n?function(t,r,e){return o.f(t,r,i(1,e))}:function(t,r,e){return t[r]=e,t}},function(t,r,e){var n=e(69),o=e(18);t.exports=function(t){return n(o(t))}},function(t,r,e){var n=e(150),o=e(12),i=e(149),a=e(13).f;t.exports=function(t){var r=n.Symbol||(n.Symbol={});o(r,t)||a(r,t,{value:i.f(t)})}},function(t,r,e){var n=e(3),o=n({}.toString),i=n("".slice);t.exports=function(t){return i(o(t),8,-1)}},function(t,r,e){var n=e(3);t.exports=n({}.isPrototypeOf)},function(t,r,e){var n=e(20),o=Math.min;t.exports=function(t){return t>0?o(n(t),9007199254740991):0}},function(t,r,e){var n=e(68),o=Function.prototype,i=o.apply,a=o.call;t.exports="object"==typeof Reflect&&Reflect.apply||(n?a.bind(i):function(){return a.apply(i,arguments)})},function(t,r,e){var n,o=e(4),i=e(74),a=e(116),u=e(71),c=e(148),s=e(87),f=e(89),l=f("IE_PROTO"),h=function(){},p=function(t){return"