From 093df86bec61236a36a26f1ff550f57a53b23d52 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 11 Apr 2022 11:25:18 +0800 Subject: [PATCH 01/18] Match-id-e9676d550fe11b724574bfa46e70222982263490 --- libs/extension/src/{components => hooks}/FilterTree.ts | 6 +++--- libs/extension/src/panel/App.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename libs/extension/src/{components => hooks}/FilterTree.ts (96%) diff --git a/libs/extension/src/components/FilterTree.ts b/libs/extension/src/hooks/FilterTree.ts similarity index 96% rename from libs/extension/src/components/FilterTree.ts rename to libs/extension/src/hooks/FilterTree.ts index 446bd05d..178b9155 100644 --- a/libs/extension/src/components/FilterTree.ts +++ b/libs/extension/src/hooks/FilterTree.ts @@ -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/panel/App.tsx b/libs/extension/src/panel/App.tsx index 7cea62e9..0c62fb19 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -5,7 +5,7 @@ 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'; From dadf05a1d656167f08d422f449cd5d16fd0a7a7b Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 14 Apr 2022 21:15:09 +0800 Subject: [PATCH 02/18] Match-id-f18761040a003c118e4e62955f56f2d47035b29e --- libs/horizon/src/renderer/diff/nodeDiffComparator.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 2b97612f..b897f800 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -402,8 +402,9 @@ function diffArrayNodesHandler( // 从Map删除,后面不会deleteVNode leftChildrenMap.delete(newNode.key || leftIdx); } - - if (oldNodeFromMap !== null) { + if (newNode.isCreated) { + FlagUtils.setAddition(newNode); + } else if (oldNodeFromMap !== null) { const eIndex = newNode.eIndex; eIndexes.push(eIndex); last = eIndexes[result[result.length - 1]]; From 49fac0e85f91814ed97088c55e4047e9c9a43e48 Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 15 Apr 2022 11:17:30 +0800 Subject: [PATCH 03/18] Match-id-93516717c91c5e50211d023e098455978ca8a2c4 --- .../src/renderer/diff/nodeDiffComparator.ts | 138 ++++++++---------- 1 file changed, 64 insertions(+), 74 deletions(-) diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index b897f800..fb0a4990 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) { @@ -364,7 +358,9 @@ function diffArrayNodesHandler( newNode = getNewNode(parentNode, newChildren[leftIdx], null); if (newNode !== null) { - if (isComparing) { + if (parentNode.tag === DomPortal) { + FlagUtils.setAddition(newNode); + } else if (!parentNode.isCreated) { FlagUtils.setAddition(newNode); } if (isDirectAdd) { @@ -398,42 +394,39 @@ function diffArrayNodesHandler( oldNodeFromMap = getOldNodeFromMap(leftChildrenMap, leftIdx, newChildren[leftIdx]); newNode = getNewNode(parentNode, newChildren[leftIdx], oldNodeFromMap); if (newNode !== null) { - if (isComparing && !newNode.isCreated) { - // 从Map删除,后面不会deleteVNode - leftChildrenMap.delete(newNode.key || leftIdx); - } if (newNode.isCreated) { + // 新VNode,直接打上标签新增,不参与到复用,旧的VNode会在后面打上delete标签 FlagUtils.setAddition(newNode); - } else 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; + } 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 (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; @@ -441,28 +434,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); @@ -490,7 +481,6 @@ function diffIteratorNodesHandler( parentNode: VNode, firstChild: VNode | null, newChildrenIterable: Iterable, - isComparing: boolean ): VNode | null { const iteratorFn = getIteratorFn(newChildrenIterable); const iteratorObj = iteratorFn.call(newChildrenIterable); @@ -503,7 +493,7 @@ function diffIteratorNodesHandler( result = iteratorObj.next(); } - return diffArrayNodesHandler(parentNode, firstChild, childrenArray, isComparing); + return diffArrayNodesHandler(parentNode, firstChild, childrenArray); } // 新节点是字符串类型 @@ -644,12 +634,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是对象类型 From 31a9a329bd43bbf9a3b1625906eabdb4944e7700 Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 15 Apr 2022 14:19:19 +0800 Subject: [PATCH 04/18] Match-id-d742d2229ce0f137b3281986d7894b39b4201860 --- libs/horizon/src/renderer/diff/nodeDiffComparator.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index fb0a4990..8e029fb6 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -354,13 +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 (parentNode.tag === DomPortal) { - FlagUtils.setAddition(newNode); - } else if (!parentNode.isCreated) { + if (isAddition) { FlagUtils.setAddition(newNode); } if (isDirectAdd) { From 9e797bc1f420ab16e0d79b05abf16b9963a53dce Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 15 Apr 2022 16:11:15 +0800 Subject: [PATCH 05/18] Match-id-677dd16a7549e021386e3dccc19510d6948e6a32 --- libs/horizon/package.json | 2 +- scripts/gen3rdLib.js | 2 +- scripts/template.ejs | 1406 +++++++++++++++++++------------------ 3 files changed, 716 insertions(+), 694 deletions(-) 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/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" + diff --git a/libs/extension/src/manifest.json b/libs/extension/src/manifest.json new file mode 100644 index 00000000..3db7b374 --- /dev/null +++ b/libs/extension/src/manifest.json @@ -0,0 +1,22 @@ +{ + "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": [] +} 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/webpack.config.js b/libs/extension/webpack.config.js index 3910117e..f712808b 100644 --- a/libs/extension/webpack.config.js +++ b/libs/extension/webpack.config.js @@ -7,7 +7,6 @@ const config = { injector: './src/injector/index.ts', contentScript: './src/contentScript/index.ts', panel: './src/panel/index.tsx', - popup: './src/popup/index.ts', }, output: { path: path.resolve(__dirname, './build'), From 2d7fc647a4cddd6e283a07faa440fd89380e54c2 Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 15 Apr 2022 17:58:40 +0800 Subject: [PATCH 08/18] Match-id-527aac42c8125a83b35e3ab8d4307eacc69da99a --- libs/extension/src/components/ComponentInfo.tsx | 2 +- libs/extension/src/components/ComponentsInfo.less | 5 ----- libs/extension/src/components/ResizeEvent.ts | 2 +- libs/extension/src/components/SizeObserver.tsx | 4 ++-- libs/extension/src/components/VList.tsx | 4 +++- libs/extension/src/components/VTree.tsx | 4 ++-- libs/extension/src/hooks/FilterTree.ts | 2 +- libs/extension/src/parser/parseAttr.ts | 8 ++------ 8 files changed, 12 insertions(+), 19 deletions(-) diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx index 10a1f637..7d78c814 100644 --- a/libs/extension/src/components/ComponentInfo.tsx +++ b/libs/extension/src/components/ComponentInfo.tsx @@ -18,7 +18,7 @@ type IComponentInfo = { onClickParent: (item: IData) => void; }; -type IAttr = { +export type IAttr = { name: string; type: string; value: string | boolean; 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..aeee688a 100644 --- a/libs/extension/src/components/VList.tsx +++ b/libs/extension/src/components/VList.tsx @@ -1,3 +1,5 @@ +// TODO:当前的 item 渲染效果较差,每次滚动所有项在数组中的位置都会发生变更。 +// 建议修改成选项增加减少时,未变更项在原数组中位置不变更 import { useState, useRef, useEffect } from 'horizon'; import styles from './VList.less'; @@ -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.tsx b/libs/extension/src/components/VTree.tsx index a286e411..6b990dc1 100644 --- a/libs/extension/src/components/VTree.tsx +++ b/libs/extension/src/components/VTree.tsx @@ -1,7 +1,7 @@ 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'; @@ -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/hooks/FilterTree.ts b/libs/extension/src/hooks/FilterTree.ts index 178b9155..faa789f0 100644 --- a/libs/extension/src/hooks/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'; /** * 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组 diff --git a/libs/extension/src/parser/parseAttr.ts b/libs/extension/src/parser/parseAttr.ts index c52b1891..f710141e 100644 --- a/libs/extension/src/parser/parseAttr.ts +++ b/libs/extension/src/parser/parseAttr.ts @@ -1,12 +1,8 @@ +import { IAttr } from "../components/ComponentInfo"; // 将状态的值解析成固定格式 export function parseAttr(rootAttr: any) { - const result: { - name: string, - type: string, - value: string, - indentation: number - }[] = []; + const result: IAttr[] = []; const indentation = 0; const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => { const stateType = typeof attr; From 756b00d72633d2cb16790b1be1259e5700925732 Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 15 Apr 2022 18:00:14 +0800 Subject: [PATCH 09/18] Match-id-e50f481637ca8c0ab67910ed72ea82ae08e26261 --- .../devtools/mockPage/MockClassComponent.tsx | 22 +++++++++++++++ .../mockPage/MockFunctionComponent.tsx | 22 +++++++++++++++ .../extension/src/devtools/mockPage/index.tsx | 18 ++++++++++++ .../src/devtools/mockPage/mockPage.html | 28 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 libs/extension/src/devtools/mockPage/MockClassComponent.tsx create mode 100644 libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx create mode 100644 libs/extension/src/devtools/mockPage/index.tsx create mode 100644 libs/extension/src/devtools/mockPage/mockPage.html 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/MockFunctionComponent.tsx b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx new file mode 100644 index 00000000..48a76e93 --- /dev/null +++ b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx @@ -0,0 +1,22 @@ +import { useState, useEffect, useRef, createContext } from 'horizon'; + +const Ctx = createContext(); + +export default function MockFunctionComponent(props) { + const [age, setAge] = useState(0); + const domRef = useRef(); + const objRef = useRef({ str: 'string' }); + + useEffect(() => { }, []); + + return ( +
+ age: {age} + + count: {props.count} +
+
{objRef.current.str}
+ +
+ ); +} \ 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..9b5332e3 --- /dev/null +++ b/libs/extension/src/devtools/mockPage/index.tsx @@ -0,0 +1,18 @@ +import { render } from 'horizon'; +import MockClassComponent from './MockClassComponent'; +import MockFunctionComponent from './MockFunctionComponent'; + +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 + + + + + + + + + From ed8d86f538b10817c3b2b1fa861a94cc8e147b6e Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 17:50:12 +0800 Subject: [PATCH 10/18] Match-id-64ca0140120735cae09f6abda4732cb3c0165d8c --- libs/extension/src/manifest.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/extension/src/manifest.json b/libs/extension/src/manifest.json index 3db7b374..0679215a 100644 --- a/libs/extension/src/manifest.json +++ b/libs/extension/src/manifest.json @@ -18,5 +18,10 @@ "run_at": "document_start" } ], - "web_accessible_resources": [] + "web_accessible_resources": [ + { + "resources": [ "injector.js", "background.js" ], + "matches": [""] + } + ] } From be5b864736a0613fcdb54ec038cc35239d719d5c Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 17:50:38 +0800 Subject: [PATCH 11/18] Match-id-0a5df1caf20a3855a1bc4052117f5835e113fd74 --- libs/extension/webpack.config.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libs/extension/webpack.config.js b/libs/extension/webpack.config.js index f712808b..25dab94b 100644 --- a/libs/extension/webpack.config.js +++ b/libs/extension/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const webpack = require('webpack'); const config = { entry: { @@ -45,6 +46,12 @@ const config = { externals: { 'horizon': 'Horizon', }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"development"', + isDev: 'false', + }), + ], }; module.exports = config; From 48342612539c104e9367ed9152f62bb001648931 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 17:51:07 +0800 Subject: [PATCH 12/18] Match-id-dcc1f7995d057864df9b055c29397e3166c23599 --- libs/extension/readme.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/libs/extension/readme.md b/libs/extension/readme.md index ee9ec4cd..52e952cc 100644 --- a/libs/extension/readme.md +++ b/libs/extension/readme.md @@ -27,25 +27,45 @@ 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树传递过来,传递对象太大,最好将数据进行压缩然后传递。 - 相同的组件名可以进行压缩 @@ -59,5 +79,13 @@ sequenceDiagram ## 滚动动态渲染 Tree 考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。 +## 开发者页面打开场景 +- 先有页面,然后打开开发者工具:工具建立连接,发送通知,页面hook收到后发送VNode树信息给工具页面 +- 已经打开开发者工具,然后打开页面:业务页面渲染完毕,发送VNode树信息给工具页面 + +## 开发者工具页面响应组件树变更 +组件树变更会带来新旧两个组件树信息数组,新旧数组存在数据一致而引用不一致的情况,而VTree和VList组件中相关信息的计算依赖引用而非数据本身,在收到新的组件树信息后需要对数据本身进行判断,将新数组中的相同数据使用旧对象代替。 + ## 测试框架 jest测试框架不提供浏览器插件的相关 api,我们在封装好相关 api 后需要模拟这些 api 的行为从而展开测试工作。 + From e9b9f69269099bd74a2da63e9b63de26056de5d7 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 18 Apr 2022 19:45:55 +0800 Subject: [PATCH 13/18] Match-id-645d6786f413f96296f5fbf6cc123242c12adf82 --- libs/extension/src/background/index.ts | 53 ++++++++++++-------- libs/extension/src/contentScript/index.ts | 21 ++++---- libs/extension/src/injector/index.ts | 59 +++++++++++++++++------ libs/extension/src/panel/App.tsx | 52 +++++++++++++++++--- libs/extension/src/panel/index.tsx | 2 +- libs/extension/src/parser/parseVNode.ts | 8 +++ libs/extension/src/utils/constants.ts | 12 +++++ libs/extension/src/utils/transferTool.ts | 21 ++++++++ libs/horizon/src/renderer/TreeBuilder.ts | 5 ++ 9 files changed, 176 insertions(+), 57 deletions(-) create mode 100644 libs/extension/src/utils/constants.ts create mode 100644 libs/extension/src/utils/transferTool.ts diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts index 2745c61a..cc724452 100644 --- a/libs/extension/src/background/index.ts +++ b/libs/extension/src/background/index.ts @@ -1,30 +1,41 @@ +import { checkData, packagePayload } from '../utils/transferTool'; +import { requestAllVNodeTreeInfos, initDevToolPageConnection } from '../utils/constants'; + // 多个页面、tab页共享一个 background,需要建立连接池,给每个tab建立连接 const connections = {}; // panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数 chrome.runtime.onConnect.addListener(function (port) { - - // The original connection event doesn't include the tab ID of the - // DevTools page, so we need to send it explicitly. - function extensionListener(message, sender, sendResponse) { - // 在backgroundPageConnection创建后会发送初始化请求,这样就可以获取tabId,给连接编号 - if (message.name === 'init') { - // 获取 panel 所在 tab 页的tabId - connections[message.tabId] = port; - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { - chrome.tabs.sendMessage(tabs[0].id, {tag: 'init horizon info'}, function(response) { - console.log(response.farewell); - }); + function extensionListener(message) { + const isHorizonMessage = checkData(message); + if (isHorizonMessage) { + console.log('received message', message); + 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 }); + } else { + passMessage = message; + } + console.log('post message:', passMessage); + // 查询参数有 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'); + } }); - return; } - - if (message.name === 'update') { - return; - } - // other message handling } - // Listen to messages sent from the DevTools page port.onMessage.addListener(extensionListener); @@ -42,11 +53,11 @@ chrome.runtime.onConnect.addListener(function (port) { }); // 监听来自 content script 的消息,并将消息发送给对应的 devTools page,也就是 panel -chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { // Messages from content scripts should have sender.tab set if (sender.tab) { const tabId = sender.tab.id; - if (tabId in connections) { + if (tabId in connections && checkData(request)) { connections[tabId].postMessage(request); } else { console.log('Tab not found in connection list.'); diff --git a/libs/extension/src/contentScript/index.ts b/libs/extension/src/contentScript/index.ts index 2236496a..9357f69f 100644 --- a/libs/extension/src/contentScript/index.ts +++ b/libs/extension/src/contentScript/index.ts @@ -1,4 +1,5 @@ import { injectCode } from '../utils/injectUtils'; +import { checkData } from '../utils/transferTool'; // 页面的window对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入hook injectCode(chrome.runtime.getURL('/injector.js')); @@ -10,12 +11,10 @@ window.addEventListener('message', event => { return; } - if (event.data.type && (event.data.type === 'HORIZON_DEV_TOOLS')) { - console.log('Content script received: ' + JSON.stringify(event.data.vNode)); + const data = event.data; + if (checkData(data)) { // 传递给background - chrome.runtime.sendMessage(event.data.vNode, function (response) { - console.log(response); - }); + chrome.runtime.sendMessage(data); } }, false); @@ -23,14 +22,12 @@ window.addEventListener('message', event => { // 监听来自background的消息 chrome.runtime.onMessage.addListener( - function (request, sender, sendResponse) { - console.log(sender.tab ? - 'from a content script:' + sender.tab.url : - 'from the extension'); - if (request.tag === 'init horizon info') { + function (request, sender) { + // 该方法可以监听页面 contentScript 和插件的消息 + // 没有 tab 信息说明消息来自插件 + if (!sender.tab && checkData(request)) { // 传递消息给页面 - console.log('start pass info to webpage'); - window.postMessage({type: 'HORIZON_DEV_TOOLS', id: 1}, '*'); + window.postMessage(request, '*'); } } ); diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts index 178cf608..d1920678 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -1,4 +1,8 @@ -import parseTreeRoot from "../parser/parseVNode"; +import parseTreeRoot, { deleteVNode } from '../parser/parseVNode'; +import { packagePayload, checkData } from './../utils/transferTool'; +import { oneVNodeTreeInfos, allVNodeTreesInfos, requestAllVNodeTreeInfos } from './../utils/constants'; + +const roots = []; function injectHook() { if (window.__HORIZON_DEV_HOOK__) { @@ -7,26 +11,49 @@ function injectHook() { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { enumerable: false, value: { - roots: [], + addIfNotInclude: function( treeRoot: any) { + if (!roots.includes(treeRoot)) { + roots.push(treeRoot); + } + }, send: function (vNode: any) { const result = parseTreeRoot(vNode); - window.postMessage({ - type: 'HORIZON_DEV_TOOLS', vNode: result - }, '*'); + window.postMessage(packagePayload({ + data: result, + type: oneVNodeTreeInfos + }), '*'); }, - listen: function (id: number) { - window.addEventListener('message', function(event) { - // We only accept messages from ourselves - if (event.source !== window) { - return; - } - - if (event.data.type && (event.data.type === 'HORIZON_DEV_TOOLS') && event.data.id === id) { - console.log('todo'); - } - }); + delete: function (vNode: any) { + // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除 + deleteVNode(vNode); + const index = roots.indexOf(vNode); + if (index !== -1) { + roots.splice(index, 1); + } } }, }); + window.addEventListener('message', function (event) { + // We only accept messages from ourselves + if (event.source !== window) { + return; + } + const request = event.data; + if (checkData(request)) { + const { payload } = request; + const { type, data } = payload; + if (type === requestAllVNodeTreeInfos) { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(current); + pre.push(info); + return pre; + }, []); + window.postMessage(packagePayload({ + data: result, + type: allVNodeTreesInfos + }), '*'); + } + } + }); } injectHook(); diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index 0c62fb19..7ba1151f 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -1,4 +1,4 @@ -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'; @@ -8,6 +8,8 @@ import { mockParsedVNodeData, parsedMockState } from '../devtools/mock'; import { FilterTree } from '../hooks/FilterTree'; import Close from '../svgs/Close'; import Arrow from './../svgs/Arrow'; +import { initDevToolPageConnection, allVNodeTreesInfos, requestComponentAttrs } from './../utils/constants'; +import { packagePayload } from './../utils/transferTool'; const parseVNodeData = (rawData) => { const idIndentationMap: { @@ -55,6 +57,7 @@ function App() { const [parsedVNodeData, setParsedVNodeData] = useState([]); const [componentAttrs, setComponentAttrs] = useState({}); const [selectComp, setSelectComp] = useState(null); + const treeRootInfos = useRef<{[id: string]: number}>({}); // 记录保存的根节点 id 和长度 const { filterValue, @@ -77,6 +80,34 @@ function App() { state: parsedMockState, props: parsedMockState, }); + } else { + const connection = chrome.runtime.connect({ + name: 'panel' + }); + // 页面打开后发送初始化请求 + connection.postMessage(packagePayload({ + type: initDevToolPageConnection, + data: 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; + if (length) { + const treeRoot = parsedTreeData[0]; + treeRootInfos.current[treeRoot.id] = length; + } + return pre.concat(parsedTreeData); + }, []); + setParsedVNodeData(allTreeData); + } + } + }); } }, []); @@ -85,10 +116,17 @@ function App() { }; const handleSelectComp = (item: IData) => { - setComponentAttrs({ - state: parsedMockState, - props: parsedMockState, - }); + if (isDev) { + setComponentAttrs({ + state: parsedMockState, + props: parsedMockState, + }); + } else { + connection.postMessage({ + name: requestComponentAttrs, + data: item.id + }); + } setSelectComp(item); }; @@ -134,8 +172,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/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts index 91104058..813f9a73 100644 --- a/libs/extension/src/parser/parseVNode.ts +++ b/libs/extension/src/parser/parseVNode.ts @@ -57,4 +57,12 @@ function parseTreeRoot(treeRoot: VNode) { return result; } +export function deleteVNode(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..dd6ce7cc --- /dev/null +++ b/libs/extension/src/utils/constants.ts @@ -0,0 +1,12 @@ +// 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'; \ No newline at end of file diff --git a/libs/extension/src/utils/transferTool.ts b/libs/extension/src/utils/transferTool.ts new file mode 100644 index 00000000..655be4e7 --- /dev/null +++ b/libs/extension/src/utils/transferTool.ts @@ -0,0 +1,21 @@ +const devTools = 'HORIZON_DEV_TOOLS'; + +interface payLoadType { + type: string, + data?: any, +} + +export function packagePayload(payload: payLoadType) { + return { + type: devTools, + payload, + }; +} + +export function checkData(data: any) { + if (data?.type === devTools) { + return true; + } + return false; +} + 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; } From 18a5f366049ec985d4ac93c1af354eb91659e1fd Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 19 Apr 2022 15:28:54 +0800 Subject: [PATCH 14/18] Match-id-9696388f05085d0c1eb211cd745a75e2c2a6ab46 --- libs/extension/src/background/index.ts | 21 ++++---- libs/extension/src/components/VList.tsx | 4 +- libs/extension/src/components/VTree.tsx | 2 +- libs/extension/src/contentScript/index.ts | 14 ++++-- libs/extension/src/injector/index.ts | 61 ++++++++++++++++------- libs/extension/src/panel/App.tsx | 51 +++++++++++++------ libs/extension/src/parser/parseVNode.ts | 4 ++ libs/extension/src/utils/constants.ts | 22 +++++--- libs/extension/src/utils/transferTool.ts | 17 +++++-- 9 files changed, 136 insertions(+), 60 deletions(-) diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts index cc724452..a750c1bb 100644 --- a/libs/extension/src/background/index.ts +++ b/libs/extension/src/background/index.ts @@ -1,5 +1,6 @@ -import { checkData, packagePayload } from '../utils/transferTool'; -import { requestAllVNodeTreeInfos, initDevToolPageConnection } from '../utils/constants'; +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 = {}; @@ -7,22 +8,21 @@ const connections = {}; // panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数 chrome.runtime.onConnect.addListener(function (port) { function extensionListener(message) { - const isHorizonMessage = checkData(message); + const isHorizonMessage = checkMessage(message, DevToolPanel); if (isHorizonMessage) { - console.log('received message', message); const { payload } = message; const { type, data } = payload; let passMessage; - if (type === initDevToolPageConnection) { + if (type === InitDevToolPageConnection) { if (!connections[data]) { // 获取 panel 所在 tab 页的tabId connections[data] = port; } - passMessage = packagePayload({ type: requestAllVNodeTreeInfos }); + passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground); } else { passMessage = message; + changeSource(passMessage, DevToolBackground); } - console.log('post message:', passMessage); // 查询参数有 active 和 currentWindow, 如果开发者工具与页面分离,会导致currentWindow为false才能找到 // 所以只用 active 参数查找,但不确定这么写是否会引发查询错误的情况 // 或许需要用不同的查询参数查找两次 @@ -53,12 +53,13 @@ chrome.runtime.onConnect.addListener(function (port) { }); // 监听来自 content script 的消息,并将消息发送给对应的 devTools page,也就是 panel -chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { +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 && checkData(request)) { - connections[tabId].postMessage(request); + if (tabId in connections && checkMessage(message, DevToolContentScript)) { + changeSource(message, DevToolBackground); + connections[tabId].postMessage(message); } else { console.log('Tab not found in connection list.'); } diff --git a/libs/extension/src/components/VList.tsx b/libs/extension/src/components/VList.tsx index aeee688a..73036fae 100644 --- a/libs/extension/src/components/VList.tsx +++ b/libs/extension/src/components/VList.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useEffect } from 'horizon'; import styles from './VList.less'; -interface IProps { +interface IProps { data: T[], width: number, // 暂时未用到,当需要支持横向滚动时使用 height: number, // VList 的高度 @@ -20,7 +20,7 @@ export type renderInfoType = { skipItemCountBeforeScrollItem: number, }; -export function VList(props: IProps) { +export function VList(props: IProps) { const { data, height, diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx index 6b990dc1..05c9d4e3 100644 --- a/libs/extension/src/components/VTree.tsx +++ b/libs/extension/src/components/VTree.tsx @@ -6,7 +6,7 @@ import { SizeObserver } from './SizeObserver'; import { renderInfoType, VList } from './VList'; export interface IData { - id: string; + id: number; name: string; indentation: number; userKey: string; diff --git a/libs/extension/src/contentScript/index.ts b/libs/extension/src/contentScript/index.ts index 9357f69f..92c7150f 100644 --- a/libs/extension/src/contentScript/index.ts +++ b/libs/extension/src/contentScript/index.ts @@ -1,5 +1,7 @@ import { injectCode } from '../utils/injectUtils'; -import { checkData } from '../utils/transferTool'; +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')); @@ -12,7 +14,8 @@ window.addEventListener('message', event => { } const data = event.data; - if (checkData(data)) { + if (checkMessage(data, DevToolHook)) { + changeSource(data, DevToolContentScript); // 传递给background chrome.runtime.sendMessage(data); } @@ -22,12 +25,13 @@ window.addEventListener('message', event => { // 监听来自background的消息 chrome.runtime.onMessage.addListener( - function (request, sender) { + function (message, sender) { // 该方法可以监听页面 contentScript 和插件的消息 // 没有 tab 信息说明消息来自插件 - if (!sender.tab && checkData(request)) { + if (!sender.tab && checkMessage(message, DevToolBackground)) { + changeSource(message, DevToolContentScript); // 传递消息给页面 - window.postMessage(request, '*'); + window.postMessage(message, '*'); } } ); diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts index d1920678..58432849 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -1,6 +1,23 @@ -import parseTreeRoot, { deleteVNode } from '../parser/parseVNode'; -import { packagePayload, checkData } from './../utils/transferTool'; -import { oneVNodeTreeInfos, allVNodeTreesInfos, requestAllVNodeTreeInfos } from './../utils/constants'; +import parseTreeRoot, { deleteVNode, 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 } from '../parser/parseAttr'; + +function postMessage(type: string, data) { + window.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolHook), '*'); +} const roots = []; @@ -11,19 +28,20 @@ function injectHook() { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { enumerable: false, value: { - addIfNotInclude: function( treeRoot: any) { + addIfNotInclude: function (treeRoot: VNode) { if (!roots.includes(treeRoot)) { roots.push(treeRoot); } }, - send: function (vNode: any) { - const result = parseTreeRoot(vNode); - window.postMessage(packagePayload({ - data: result, - type: oneVNodeTreeInfos - }), '*'); + send: function () { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(current); + pre.push(info); + return pre; + }, []); + postMessage(AllVNodeTreesInfos, result); }, - delete: function (vNode: any) { + delete: function (vNode: VNode) { // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除 deleteVNode(vNode); const index = roots.indexOf(vNode); @@ -39,19 +57,28 @@ function injectHook() { return; } const request = event.data; - if (checkData(request)) { + if (checkMessage(request, DevToolContentScript)) { const { payload } = request; const { type, data } = payload; - if (type === requestAllVNodeTreeInfos) { + if (type === RequestAllVNodeTreeInfos) { const result = roots.reduce((pre, current) => { const info = parseTreeRoot(current); pre.push(info); return pre; }, []); - window.postMessage(packagePayload({ - data: result, - type: allVNodeTreesInfos - }), '*'); + postMessage(AllVNodeTreesInfos, result); + } else if (type === RequestComponentAttrs) { + const vNode: VNode = queryVNode(data); + const tag = vNode.tag; + if (tag === ClassComponent) { + const { props, state } = vNode; + const parsedProps = parseAttr(props); + const parsedState = parseAttr(state); + postMessage(ComponentAttrs, { + parsedProps, + parsedState, + }); + } } } }); diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index 7ba1151f..d5283843 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -8,7 +8,13 @@ import { mockParsedVNodeData, parsedMockState } from '../devtools/mock'; import { FilterTree } from '../hooks/FilterTree'; import Close from '../svgs/Close'; import Arrow from './../svgs/Arrow'; -import { initDevToolPageConnection, allVNodeTreesInfos, requestComponentAttrs } from './../utils/constants'; +import { + InitDevToolPageConnection, + AllVNodeTreesInfos, + RequestComponentAttrs, + ComponentAttrs, + DevToolPanel, +} from './../utils/constants'; import { packagePayload } from './../utils/transferTool'; const parseVNodeData = (rawData) => { @@ -18,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++; @@ -53,11 +59,26 @@ const getParents = (item: IData | null, parsedVNodeData: IData[]) => { return parents; }; +let connection; +if (!isDev) { + // 与 background 的唯一连接 + connection = chrome.runtime.connect({ + name: 'panel' + }); +} + +function postMessage(type: string, data: any) { + connection.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolPanel)); +} + function App() { const [parsedVNodeData, setParsedVNodeData] = useState([]); const [componentAttrs, setComponentAttrs] = useState({}); const [selectComp, setSelectComp] = useState(null); - const treeRootInfos = useRef<{[id: string]: number}>({}); // 记录保存的根节点 id 和长度 + const treeRootInfos = useRef<{id: number, length: number}[]>([]); // 记录保存的根节点 id 和长度, const { filterValue, @@ -81,30 +102,31 @@ function App() { props: parsedMockState, }); } else { - const connection = chrome.runtime.connect({ - name: 'panel' - }); // 页面打开后发送初始化请求 - connection.postMessage(packagePayload({ - type: initDevToolPageConnection, - data: chrome.devtools.inspectedWindow.tabId - })); + postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId); // 监听 background消息 connection.onMessage.addListener(function (message) { const { payload } = message; if (payload) { const { type, data } = payload; - if (type === allVNodeTreesInfos) { + 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[treeRoot.id] = length; + treeRootInfos.current.push({id: treeRoot.id, length: length}); } return pre.concat(parsedTreeData); }, []); setParsedVNodeData(allTreeData); + } else if (type === ComponentAttrs) { + const {parsedProps, parsedState} = data; + setComponentAttrs({ + state: parsedProps, + props: parsedState, + }); } } }); @@ -122,10 +144,7 @@ function App() { props: parsedMockState, }); } else { - connection.postMessage({ - name: requestComponentAttrs, - data: item.id - }); + postMessage(RequestComponentAttrs, item.id); } setSelectComp(item); }; diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts index 813f9a73..e4ecc276 100644 --- a/libs/extension/src/parser/parseVNode.ts +++ b/libs/extension/src/parser/parseVNode.ts @@ -57,6 +57,10 @@ function parseTreeRoot(treeRoot: VNode) { return result; } +export function queryVNode(id: number) { + return IdToVNodeMap.get(id); +} + export function deleteVNode(vNode: VNode) { if (VNodeToIdMap.has(vNode)) { const id = VNodeToIdMap.get(vNode); diff --git a/libs/extension/src/utils/constants.ts b/libs/extension/src/utils/constants.ts index dd6ce7cc..e779a143 100644 --- a/libs/extension/src/utils/constants.ts +++ b/libs/extension/src/utils/constants.ts @@ -1,12 +1,22 @@ // panel 页面打开后初始化连接标志 -export const initDevToolPageConnection = 'init dev tool page connection'; +export const InitDevToolPageConnection = 'init dev tool page connection'; // background 解析全部 root VNodes 标志 -export const requestAllVNodeTreeInfos = 'request all vNodes tree infos'; +export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos'; // vNodes 全部树解析结果标志 -export const allVNodeTreesInfos = 'vNode trees Infos'; +export const AllVNodeTreesInfos = 'vNode trees Infos'; // 一棵树的解析 -export const oneVNodeTreeInfos = 'one vNode tree'; +export const OneVNodeTreeInfos = 'one vNode tree'; // 获取组件属性 -export const requestComponentAttrs = 'get component attrs'; +export const RequestComponentAttrs = 'get component attrs'; // 返回组件属性 -export const componentAttrs = 'component attrs'; \ No newline at end of file +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/transferTool.ts b/libs/extension/src/utils/transferTool.ts index 655be4e7..c25fc4d7 100644 --- a/libs/extension/src/utils/transferTool.ts +++ b/libs/extension/src/utils/transferTool.ts @@ -5,17 +5,28 @@ interface payLoadType { data?: any, } -export function packagePayload(payload: payLoadType) { +interface message { + type: typeof devTools, + payload: payLoadType, + from: string, +} + +export function packagePayload(payload: payLoadType, from: string): message { return { type: devTools, payload, + from, }; } -export function checkData(data: any) { - if (data?.type === devTools) { +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; +} + From d31bbc937d4a6ae4221637482183e8f253057de3 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 19 Apr 2022 16:15:02 +0800 Subject: [PATCH 15/18] Match-id-094551ccf405a03b88a0a038c149dac31ae48e12 --- libs/extension/src/injector/index.ts | 86 +++++++++++++------------ libs/extension/src/parser/parseVNode.ts | 2 +- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/libs/extension/src/injector/index.ts b/libs/extension/src/injector/index.ts index 58432849..0d1ad023 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -1,4 +1,4 @@ -import parseTreeRoot, { deleteVNode, queryVNode } from '../parser/parseVNode'; +import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode'; import { packagePayload, checkMessage } from './../utils/transferTool'; import { RequestAllVNodeTreeInfos, @@ -12,6 +12,32 @@ import { VNode } from './../../../horizon/src/renderer/vnode/VNode'; import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags'; import { parseAttr } from '../parser/parseAttr'; +const roots = []; + +function addIfNotInclude(treeRoot: VNode) { + if (!roots.includes(treeRoot)) { + roots.push(treeRoot); + } +} + +function send() { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(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, @@ -19,7 +45,19 @@ function postMessage(type: string, data) { }, DevToolHook), '*'); } -const roots = []; +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, + }); + } +} function injectHook() { if (window.__HORIZON_DEV_HOOK__) { @@ -28,27 +66,9 @@ function injectHook() { Object.defineProperty(window, '__HORIZON_DEV_HOOK__', { enumerable: false, value: { - addIfNotInclude: function (treeRoot: VNode) { - if (!roots.includes(treeRoot)) { - roots.push(treeRoot); - } - }, - send: function () { - const result = roots.reduce((pre, current) => { - const info = parseTreeRoot(current); - pre.push(info); - return pre; - }, []); - postMessage(AllVNodeTreesInfos, result); - }, - delete: function (vNode: VNode) { - // 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除 - deleteVNode(vNode); - const index = roots.indexOf(vNode); - if (index !== -1) { - roots.splice(index, 1); - } - } + addIfNotInclude, + send, + deleteVNode, }, }); window.addEventListener('message', function (event) { @@ -61,26 +81,12 @@ function injectHook() { const { payload } = request; const { type, data } = payload; if (type === RequestAllVNodeTreeInfos) { - const result = roots.reduce((pre, current) => { - const info = parseTreeRoot(current); - pre.push(info); - return pre; - }, []); - postMessage(AllVNodeTreesInfos, result); + send(); } else if (type === RequestComponentAttrs) { - const vNode: VNode = queryVNode(data); - const tag = vNode.tag; - if (tag === ClassComponent) { - const { props, state } = vNode; - const parsedProps = parseAttr(props); - const parsedState = parseAttr(state); - postMessage(ComponentAttrs, { - parsedProps, - parsedState, - }); - } + parseCompAttrs(data); } } }); } + injectHook(); diff --git a/libs/extension/src/parser/parseVNode.ts b/libs/extension/src/parser/parseVNode.ts index e4ecc276..fbf4b0bd 100644 --- a/libs/extension/src/parser/parseVNode.ts +++ b/libs/extension/src/parser/parseVNode.ts @@ -61,7 +61,7 @@ export function queryVNode(id: number) { return IdToVNodeMap.get(id); } -export function deleteVNode(vNode: VNode) { +export function clearVNode(vNode: VNode) { if (VNodeToIdMap.has(vNode)) { const id = VNodeToIdMap.get(vNode); VNodeToIdMap.delete(vNode); From a5af9c4c9e218889eea71a43453ed22fd02b0c6e Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 19 Apr 2022 19:29:56 +0800 Subject: [PATCH 16/18] Match-id-daf7043d5b9449b93aa1cee20aaf82cd485b8c9f --- libs/extension/src/background/index.ts | 3 ++- libs/extension/src/contentScript/index.ts | 3 ++- libs/extension/src/panel/App.tsx | 20 ++++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/libs/extension/src/background/index.ts b/libs/extension/src/background/index.ts index a750c1bb..ed9698fb 100644 --- a/libs/extension/src/background/index.ts +++ b/libs/extension/src/background/index.ts @@ -66,5 +66,6 @@ chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { } else { console.log('sender.tab not defined.'); } - return true; + // 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received + sendResponse({status: 'ok'}); }); diff --git a/libs/extension/src/contentScript/index.ts b/libs/extension/src/contentScript/index.ts index 92c7150f..9a7fe2e8 100644 --- a/libs/extension/src/contentScript/index.ts +++ b/libs/extension/src/contentScript/index.ts @@ -25,7 +25,7 @@ window.addEventListener('message', event => { // 监听来自background的消息 chrome.runtime.onMessage.addListener( - function (message, sender) { + function (message, sender, sendResponse) { // 该方法可以监听页面 contentScript 和插件的消息 // 没有 tab 信息说明消息来自插件 if (!sender.tab && checkMessage(message, DevToolBackground)) { @@ -33,5 +33,6 @@ chrome.runtime.onMessage.addListener( // 传递消息给页面 window.postMessage(message, '*'); } + sendResponse({status: 'ok'}); } ); diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index d5283843..a0455831 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -68,10 +68,22 @@ if (!isDev) { } function postMessage(type: string, data: any) { - connection.postMessage(packagePayload({ - type: type, - data: data, - }, DevToolPanel)); + try { + connection.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolPanel)); + } catch(err) { + // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性 + console.error(err); + connection = chrome.runtime.connect({ + name: 'panel' + }); + connection.postMessage(packagePayload({ + type: type, + data: data, + }, DevToolPanel)); + } } function App() { From ec0f0dd2c4451eeb6de2486da9e98d9e1617a0d2 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 21 Apr 2022 14:54:34 +0800 Subject: [PATCH 17/18] Match-id-aad38a74387ab110540aeb594a643b1be7c42a17 --- .../src/components/ComponentInfo.tsx | 23 +- libs/extension/src/components/VTree.less | 2 +- libs/extension/src/injector/index.ts | 11 +- libs/extension/src/panel/App.tsx | 24 ++- libs/extension/src/parser/parseAttr.ts | 196 +++++++++++------- libs/horizon/src/renderer/hooks/HookType.ts | 1 + .../src/renderer/hooks/UseReducerHook.ts | 1 + libs/horizon/src/renderer/hooks/UseRefHook.ts | 1 + 8 files changed, 167 insertions(+), 92 deletions(-) diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx index 7d78c814..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; }; -export 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/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/injector/index.ts b/libs/extension/src/injector/index.ts index 0d1ad023..e0372d10 100644 --- a/libs/extension/src/injector/index.ts +++ b/libs/extension/src/injector/index.ts @@ -10,7 +10,8 @@ import { } from './../utils/constants'; import { VNode } from './../../../horizon/src/renderer/vnode/VNode'; import { ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags'; -import { parseAttr } from '../parser/parseAttr'; +import { parseAttr, parseHooks } from '../parser/parseAttr'; +import { FunctionComponent } from './../../../horizon/src/renderer/vnode/VNodeTags'; const roots = []; @@ -56,6 +57,14 @@ function parseCompAttrs(id: number) { parsedProps, parsedState, }); + } else if (tag === FunctionComponent) { + const { props, hooks } = vNode; + const parsedProps = parseAttr(props); + const parsedHooks = parseHooks(hooks); + postMessage(ComponentAttrs, { + parsedProps, + parsedHooks, + }); } } diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index a0455831..af6e5d9a 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -67,6 +67,8 @@ if (!isDev) { }); } +let reconnectTimes = 0; + function postMessage(type: string, data: any) { try { connection.postMessage(packagePayload({ @@ -75,14 +77,21 @@ function postMessage(type: string, data: any) { }, DevToolPanel)); } catch(err) { // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性 + if (reconnectTimes === 20) { + reconnectTimes = 0; + console.error('reconnect failed'); + return; + } console.error(err); + reconnectTimes++; + // 重建连接 connection = chrome.runtime.connect({ name: 'panel' }); - connection.postMessage(packagePayload({ - type: type, - data: data, - }, DevToolPanel)); + // 重新发送初始化消息 + postMessage(InitDevToolPageConnection, chrome.devtools.inspectedWindow.tabId); + // 初始化成功后才会重新发送消息 + postMessage(type, data); } } @@ -134,10 +143,11 @@ function App() { }, []); setParsedVNodeData(allTreeData); } else if (type === ComponentAttrs) { - const {parsedProps, parsedState} = data; + const {parsedProps, parsedState, parsedHooks} = data; setComponentAttrs({ - state: parsedProps, - props: parsedState, + props: parsedProps, + state: parsedState, + hooks: parsedHooks, }); } } diff --git a/libs/extension/src/parser/parseAttr.ts b/libs/extension/src/parser/parseAttr.ts index f710141e..e18f9bad 100644 --- a/libs/extension/src/parser/parseAttr.ts +++ b/libs/extension/src/parser/parseAttr.ts @@ -1,79 +1,133 @@ -import { IAttr } from "../components/ComponentInfo"; -// 将状态的值解析成固定格式 +import { Hook, Reducer, Ref } from './../../../horizon/src/renderer/hooks/HookType'; + +// 展示值为 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; - 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({ - name: attrName, - type: showType, - value, - indentation: parentIndentation + 1, - }); - if (addSubState) { - addSubState(); - } - }; + 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/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(); } From 54ad44525e96ec6d3ee516f85233110609655297 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 21 Apr 2022 14:56:02 +0800 Subject: [PATCH 18/18] Match-id-63ab7802bc94b2c6034d615683e1c3dc915e8db9 --- .../src/devtools/mockPage/MockContext.ts | 3 +++ .../mockPage/MockFunctionComponent.tsx | 22 +++++++++++++++---- .../extension/src/devtools/mockPage/index.tsx | 10 +++++---- 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 libs/extension/src/devtools/mockPage/MockContext.ts 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 index 48a76e93..41437e38 100644 --- a/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx +++ b/libs/extension/src/devtools/mockPage/MockFunctionComponent.tsx @@ -1,12 +1,26 @@ -import { useState, useEffect, useRef, createContext } from 'horizon'; +import { useState, useEffect, useRef, useContext, useReducer } from 'horizon'; +import { MockContext } from './MockContext'; -const Ctx = createContext(); +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 ( @@ -16,7 +30,7 @@ export default function MockFunctionComponent(props) { 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 index 9b5332e3..57c96b44 100644 --- a/libs/extension/src/devtools/mockPage/index.tsx +++ b/libs/extension/src/devtools/mockPage/index.tsx @@ -1,18 +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); +render(, root);