!78 [inula-dev-tools]<feat> injector 逻辑合入

Merge pull request !78 from 涂旭辉/master
This commit is contained in:
openInula-robot 2023-11-13 08:53:54 +00:00 committed by Gitee
commit a13330ce1c
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
3 changed files with 855 additions and 0 deletions

View File

@ -0,0 +1,274 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import assign from 'object-assign';
import { VNode } from '../../../inula/src/renderer/vnode/VNode';
const overlayStyles = {
background: 'rgba(120, 170, 210, 0.7)',
padding: 'rgba(77, 200, 0, 0.3)',
margin: 'rgba(255, 155, 0, 0.3)',
border: 'rgba(255, 200, 50, 0.3)'
};
type Rect = {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
};
function setBoxStyle(eleStyle, boxArea, node) {
assign(node.style, {
borderTopWidth: eleStyle[boxArea + 'Top'] + 'px',
borderLeftWidth: eleStyle[boxArea + 'Left'] + 'px',
borderRightWidth: eleStyle[boxArea + 'Right'] + 'px',
borderBottomWidth: eleStyle[boxArea + 'Bottom'] + 'px',
});
}
function getOwnerWindow(node: Element): typeof window | null {
if (!node.ownerDocument) {
return null;
}
return node.ownerDocument.defaultView;
}
function getOwnerIframe(node: Element): Element | null {
const nodeWindow = getOwnerWindow(node);
if (nodeWindow) {
return nodeWindow.frameElement;
}
return null;
}
function getElementStyle(domElement: Element) {
const style = window.getComputedStyle(domElement);
return{
marginLeft: parseInt(style.marginLeft, 10),
marginRight: parseInt(style.marginRight, 10),
marginTop: parseInt(style.marginTop, 10),
marginBottom: parseInt(style.marginBottom, 10),
borderLeft: parseInt(style.borderLeftWidth, 10),
borderRight: parseInt(style.borderRightWidth, 10),
borderTop: parseInt(style.borderTopWidth, 10),
borderBottom: parseInt(style.borderBottomWidth, 10),
paddingLeft: parseInt(style.paddingLeft, 10),
paddingRight: parseInt(style.paddingRight, 10),
paddingTop: parseInt(style.paddingTop, 10),
paddingBottom: parseInt(style.paddingBottom, 10)
};
}
function mergeRectOffsets(rects: Array<Rect>): Rect {
return rects.reduce((previousRect, rect) => {
if (previousRect == null) {
return rect;
}
return {
top: previousRect.top + rect.top,
left: previousRect.left + rect.left,
width: previousRect.width + rect.width,
height: previousRect.height + rect.height,
bottom: previousRect.bottom + rect.bottom,
right: previousRect.right + rect.right
};
});
}
function getBoundingClientRectWithBorderOffset(node: Element) {
const dimensions = getElementStyle(node);
return mergeRectOffsets([
node.getBoundingClientRect(),
{
top: dimensions.borderTop,
left: dimensions.borderLeft,
bottom: dimensions.borderBottom,
right:dimensions.borderRight,
// 高度和宽度不会被使用
width: 0,
height: 0
}
]);
}
function getNestedBoundingClientRect(
node: HTMLElement,
boundaryWindow
): Rect {
const ownerIframe = getOwnerIframe(node);
if (ownerIframe && ownerIframe !== boundaryWindow) {
const rects = [node.getBoundingClientRect()] as Rect[];
let currentIframe = ownerIframe;
let onlyOneMore = false;
while (currentIframe) {
const rect = getBoundingClientRectWithBorderOffset(currentIframe);
rects.push(rect);
currentIframe = getOwnerIframe(currentIframe);
if (onlyOneMore) {
break;
}
if (currentIframe &&getOwnerWindow(currentIframe) === boundaryWindow) {
onlyOneMore = true;
}
}
return mergeRectOffsets(rects);
} else {
return node.getBoundingClientRect();
}
}
// 用来遮罩
class OverlayRect {
node: HTMLElement;
border: HTMLElement;
padding: HTMLElement;
content: HTMLElement;
constructor(doc: Document, container: HTMLElement) {
this.node = doc.createElement('div');
this.border = doc.createElement('div');
this.padding = doc.createElement('div');
this.content = doc.createElement('div');
this.border.style.borderColor = overlayStyles.border;
this.padding.style.borderColor = overlayStyles.padding;
this.content.style.backgroundColor = overlayStyles.background;
assign(this.node.style, {
borderColor: overlayStyles.margin,
pointerEvents: 'none',
position: 'fixed'
});
this.node.style.zIndex = '10000000';
this.node.appendChild(this.border);
this.border.appendChild(this.padding);
this.padding.appendChild(this.content);
container.appendChild(this.node);
}
remove() {
if (this.node.parentNode) {
this.node.parentNode.removeChild(this.node);
}
}
update(boxRect: Rect, eleStyle: any) {
setBoxStyle(eleStyle, 'margin', this.node);
setBoxStyle(eleStyle, 'border', this.border);
setBoxStyle(eleStyle, 'padding', this.padding);
assign(this.content.style, {
height: boxRect.height - eleStyle.borderTop - eleStyle.borderBottom - eleStyle.paddingTop - eleStyle.paddingBottom + 'px',
width: boxRect.width - eleStyle.borderLeft - eleStyle.borderRight - eleStyle.paddingLeft - eleStyle.paddingRight + 'px'
});
assign(this.node.style, {
top: boxRect.top - eleStyle.marginTop + 'px',
left: boxRect.left - eleStyle.marginLeft + 'px'
});
}
}
class ElementOverlay {
window: typeof window;
container: HTMLElement;
rects: Array<OverlayRect>;
constructor() {
this.window = window;
const doc = window.document;
this.container = doc.createElement('div');
this.container.style.zIndex = '10000000';
this.rects = [];
doc.body.appendChild(this.container);
}
remove() {
this.rects.forEach(rect => {
rect.remove();
});
this.rects.length = 0;
if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
}
execute(nodes: Array<VNode>) {
const elements = nodes.filter(node => node.tag === 'DomComponent');
// 有几个 element 就添加几个 OverlayRect
while (this.rects.length > elements.length) {
const rect = this.rects.pop();
rect.remove();
}
if (elements.length === 0) {
return;
}
while (this.rects.length < elements.length) {
this.rects.push(new OverlayRect(this.window.document, this.container));
}
const outerBox = {
top: Number.POSITIVE_INFINITY,
right: Number.NEGATIVE_INFINITY,
bottom: Number.NEGATIVE_INFINITY,
left: Number.POSITIVE_INFINITY
};
elements.forEach((element, index) => {
const eleStyle = getElementStyle(element.realNode);
const boxRect = getNestedBoundingClientRect(element.realNode, this.window);
outerBox.top = Math.min(outerBox.top, boxRect.top - eleStyle.marginTop);
outerBox.right = Math.max(outerBox.right, boxRect.left + boxRect.width + eleStyle.marginRight);
outerBox.bottom = Math.max(outerBox.bottom, boxRect.top + boxRect.height + eleStyle.marginBottom);
outerBox.left = Math.min(outerBox.left, boxRect.left - eleStyle.marginLeft);
const rect = this.rects[index];
rect.update(boxRect, eleStyle);
});
}
}
let elementOverlay: ElementOverlay | null = null;
export function hideHighlight() {
if (elementOverlay !== null) {
elementOverlay.remove();
elementOverlay = null;
}
}
export function showHighlight(elements: Array<VNode> | null) {
if (window.document == null || elements == null) {
return;
}
if (elementOverlay === null) {
elementOverlay = new ElementOverlay();
}
elementOverlay.execute(elements);
}

View File

@ -0,0 +1,485 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import parseTreeRoot, { clearVNode, queryVNode, VNodeToIdMap } from '../parser/parseVNode';
import { packagePayload, checkMessage } from '../utils/transferUtils';
import {
RequestAllVNodeTreeInfos,
AllVNodeTreeInfos,
RequestComponentAttrs,
ComponentAttrs,
DevToolHook,
DevToolContentScript,
ModifyAttrs,
ModifyHooks,
ModifyState,
ModifyProps,
InspectDom,
LogComponentData,
Highlight,
RemoveHighlight,
ViewSource,
PickElement,
StopPickElement,
CopyToConsole,
StorageValue,
} from '../utils/constants';
import { VNode } from '../../../inula/src/renderer/vnode/VNode';
import { parseVNodeAttrs } from '../parser/parseAttr';
import { showHighlight, hideHighlight } from '../highlight';
import {
FunctionComponent,
ClassComponent,
IncompleteClassComponent,
ForwardRef,
MemoComponent
} from '../../../inula/src/renderer/vnode/VNodeTags';
import { pickElement } from './pickElement';
const roots = [];
let storeDataCount = 0;
function addIfNotInclude(treeRoot: VNode) {
if (!roots.includes(treeRoot)) {
roots.push(treeRoot);
}
}
function send() {
const result = roots.reduce((pre, current) => {
const info = parseTreeRoot(helper.travelVNodeTree, current);
pre.push(info);
return pre;
}, []);
postMessage(AllVNodeTreeInfos, result);
}
function deleteVNode(vNode: VNode) {
// 开发工具中保存了 vNode 的引用,在清理 vNode 的时候需要一并删除
clearVNode(vNode);
const index = roots.indexOf(vNode);
if (index !== -1) {
roots.splice(index, 1);
}
}
export function postMessage(type: string, data) {
window.postMessage(
packagePayload(
{
type: type,
data: data,
},
DevToolHook
),
'*'
);
}
function parseCompAttrs(id: number) {
const vNode = queryVNode(id);
if (!vNode) {
console.error('Do not find match vNode, this is a bug, please report us.');
return;
}
const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo);
postMessage(ComponentAttrs, parsedAttrs);
}
function calculateNextValue(editValue, value, attrPath) {
let nextState;
const editValueType = typeof editValue;
if (
editValueType === 'string' ||
editValueType === 'undefined' ||
editValueType === 'boolean'
) {
nextState = value;
} else if (editValueType === 'number') {
const numValue = Number(value);
nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字旧用原值
} else if (editValueType === 'object') {
if (editValue === null) {
nextState = value;
} else {
const newValue = Array.isArray(editValue) ? [...editValue] : { ...editValue };
// 遍历读取到直接指向需要修改值的对象
let attr = newValue;
for (let i = 0; i < attrPath.length - 1; i++) {
attr = attr[attrPath[i]];
}
// 修改对象上的值
attr[attrPath[attrPath.length - 1]] = value;
nextState = newValue;
}
} else {
console.error(
'The dev tools tried to edit a non-editable value, this is a bug, please report.',
editValue
);
}
return nextState;
}
function modifyVNodeAttrs(data) {
const { type, id, value, path } = data;
const vNode = queryVNode(id);
if (!vNode) {
console.error('Do not find match vNode, this is a bug, please report us.');
return;
}
if (type === ModifyProps) {
const nextProps = calculateNextValue(vNode.props, value, path);
helper.updateProps(vNode, nextProps);
} else if (type === ModifyHooks) {
const hooks = vNode.hooks;
const editHook = hooks[path[0]];
const hookInfo = helper.getHookInfo(editHook);
if (hookInfo) {
const editValue = hookInfo.value;
// path 的第一个指向 hIndex从第二个值才开始指向具体属性访问路径
const nextState = calculateNextValue(editValue, value, path.slice(1));
helper.updateHooks(vNode, path[0], nextState);
} else {
console.error(
'The dev tools tried to edit a non-editable hook, this is a bug, please report.',
hooks
);
}
} else if (type === ModifyState) {
const oldState = vNode.state || {};
const nextState = { ...oldState };
let accessRef = nextState;
for (let i = 0; i < path.length - 1; i++) {
accessRef = accessRef[path[i]];
}
accessRef[path[path.length - 1]] = value;
helper.updateState(vNode, nextState);
}
}
function logComponentData(id: number) {
const vNode = queryVNode(id);
if (vNode == null) {
console.warn(`Could not find vNode with id "${id}"`);
return null;
}
if (vNode) {
const info = helper.getComponentInfo(vNode);
console.log('vNode: ', vNode);
console.log('Component Info: ', info);
}
}
/**
* path vNode
*
* @param {VNode} vNode dom
* @param {Array<string | number>} path
* @param {string} attrsName props hooks
*/
const getValueByPath = (
vNode: VNode,
path: Array<string | number>,
attrsName: string
) => {
if (attrsName === 'Props') {
return path.reduce((previousValue, currentValue) => {
return previousValue[currentValue];
}, vNode.props);
} else {
// attrsName 为 Hooks
if (path.length > 1) {
return path.reduce((previousValue, currentValue) => {
return previousValue[currentValue];
}, vNode.hooks);
}
return vNode.hooks[path[0]];
}
};
/**
* path vNode
*
* @param {number} id idToVNodeMap key id VNode
* @param {string} itemName
* @param {Array<string | number>} path
* @param {string} attrsName
*/
function logDataWithPath(
id: number,
itemName: string,
path: Array<string | number>,
attrsName: string
) {
const vNode = queryVNode(id);
if (vNode === null) {
console.warn(`Could not find vNode with id "${id}"`);
return null;
}
if (vNode) {
const value = getValueByPath(vNode, path, attrsName);
if (attrsName === 'Hooks') {
console.log(itemName, value);
} else {
console.log(`${path[path.length - 1]}`, value);
}
}
}
/**
* path vNode
*
* @param {number} id idToVNodeMap key id VNode
* @param {Array<string |number>} path
* @param {string} attrsName
*/
function storeDataWithPath(
id: number,
path: Array<string | number>,
attrsName: string
) {
const vNode = queryVNode(id);
if (vNode === null) {
console.warn(`Could not find vNode with id "${id}"`);
return null;
}
if (vNode) {
const value = getValueByPath(vNode, path, attrsName);
const key = `$InulaTemp${storeDataCount++}`;
window[key] = value;
console.log(key);
console.log(value);
}
}
export let helper;
function init(inulaHelper) {
helper = inulaHelper;
(window as any).__INULA_DEV_HOOK__.isInit = true;
}
export function getElement(travelVNodeTree, treeRoot: VNode) {
const result: any[] = [];
travelVNodeTree(
treeRoot,
(node: VNode) => {
if (node.realNode) {
if (Object.keys(node.realNode).length > 0 || node.realNode.size > 0) {
result.push(node);
}
}
},
(node: VNode) =>
node.realNode != null &&
(Object.keys(node.realNode).length > 0 || node.realNode.size > 0)
);
return result;
}
// dev tools 点击眼睛图标功能
const inspectDom = data => {
const { id } = data;
const vNode = queryVNode(id);
if (vNode == null) {
console.warn(`Could not find vNode with id "${id}"`);
return null;
}
const info = getElement(helper.travelVNodeTree, vNode);
if (info) {
showHighlight(info);
(window as any).__INULA_DEV_HOOK__.$0 = info[0];
}
};
const picker = pickElement(window);
const actions = new Map([
// 请求左树所有数据
[
RequestAllVNodeTreeInfos,
() => {
send();
},
],
// 请求某个节点的 propshooks
[
RequestComponentAttrs,
data => {
parseCompAttrs(data);
},
],
// 修改 propshooks
[
ModifyAttrs,
data => {
modifyVNodeAttrs(data);
},
],
// 找到节点对应 element
[
InspectDom,
data => {
inspectDom(data);
},
],
// 打印节点数据
[
LogComponentData,
data => {
logComponentData(data);
},
],
// 高亮
[
Highlight,
data => {
const node = queryVNode(data.id);
if (node == null) {
console.warn(`Could not find vNode with id "${data.id}"`);
return null;
}
const info = getElement(helper.travelVNodeTree, node);
showHighlight(info);
},
],
// 移出高亮
[
RemoveHighlight,
() => {
hideHighlight();
},
],
// 查看节点源代码位置
[
ViewSource,
data => {
const node = queryVNode(data.id);
if (node == null) {
console.warn(`Could not find vNode with id "${data.id}"`);
return null;
}
showSource(node);
},
],
// 选中页面元素对应 dev tools 节点
[
PickElement,
() => {
picker.startPick();
},
],
[
StopPickElement,
() => {
picker.stopPick();
},
],
// 在控制台打印 Props Hooks State 值
[
CopyToConsole,
data => {
const node = queryVNode(data.id);
if (node == null) {
console.warn(`Could not find vNode with id "${data.id}"`);
return null;
}
logDataWithPath(data.id, data.itemName, data.path, data.attrsName);
},
],
// 把 Props Hooks State 值存为全局变量
[
StorageValue,
data => {
const node = queryVNode(data.id);
if (node == null) {
console.warn(`Could not find vNode with id "${data.id}"`);
return null;
}
storeDataWithPath(data.id, data.path, data.attrsName);
},
],
]);
const showSource = (node: VNode) => {
switch (node.tag) {
case ClassComponent:
case IncompleteClassComponent:
case FunctionComponent:
global.$type = node.type;
break;
case ForwardRef:
global.$type = node.type.render;
break;
case MemoComponent:
global.$type = node.type.type;
break;
default:
global.$type = null;
break;
}
};
const handleRequest = (type: string, data) => {
const action = actions.get(type);
if (action) {
action.call(this, data);
return null;
}
console.warn('unknown command', type);
};
function injectHook() {
if ((window as any).__INULA_DEV_HOOK__) {
return;
}
Object.defineProperty(window, '__INULA_DEV_HOOK__', {
enumerable: false,
value: {
$0: null,
init,
isInit: false,
addIfNotInclude,
send,
deleteVNode,
// inulaX 使用
getVNodeId: vNode => {
return VNodeToIdMap.get(vNode);
},
},
});
window.addEventListener('message', function (event) {
// 只接收我们自己的消息
if (event.source !== window) {
return;
}
const request = event.data;
if (checkMessage(request, DevToolContentScript)) {
const { payload } = request;
const { type, data } = payload;
// 忽略 inulaX 的 actions
if (type.startsWith('inulax')) {
return;
}
handleRequest(type, data);
}
});
}
injectHook();

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { PickElement, StopPickElement } from '../utils/constants';
import { getElement, helper, postMessage } from './index';
import { queryVNode, VNodeToIdMap } from '../parser/parseVNode';
import { isUserComponent } from '../parser/parseVNode';
import { throttle } from 'lodash';
import { hideHighlight, showHighlight } from '../highlight';
// 判断鼠标移入节点是否为 dev tools 上的节点,如果不是则找父节点
function getUserComponent(target) {
if (target.tag && isUserComponent(target.tag)) {
return target;
}
while (target.tag && !isUserComponent(target.tag)) {
if (target.parent) {
target = target.parent;
}
}
return target;
}
function onMouseEvent(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
}
function onMouseMove(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const target = (event.target as any)._inula_VNode;
if (target) {
const id = VNodeToIdMap.get(getUserComponent(target));
const vNode = queryVNode(id);
if (vNode == null) {
console.warn(`Could not find vNode with id "${id}"`);
return null;
}
const info = getElement(helper.travelVNodeTree, vNode);
if (info) {
showHighlight(info);
}
// 0.5 秒内在节流结束后只触发一次
throttle(
() => {
postMessage(PickElement, id);
},
500,
{ leading: false, trailing: true }
)();
}
}
export function pickElement(window: Window) {
function onClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
stopPick();
postMessage(StopPickElement, null);
}
const startPick = () => {
if (window && typeof window.addEventListener === 'function') {
window.addEventListener('click', onClick, true);
window.addEventListener('mousedown', onMouseEvent, true);
window.addEventListener('mousemove', onMouseMove, true);
window.addEventListener('mouseup', onMouseEvent, true);
}
};
const stopPick = () => {
hideHighlight();
window.removeEventListener('click', onClick, true);
window.removeEventListener('mousedown', onMouseEvent, true);
window.removeEventListener('mousemove', onMouseMove, true);
window.removeEventListener('mouseup', onMouseEvent, true);
};
return { startPick, stopPick };
}