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

This commit is contained in:
13659257719 2023-11-13 16:52:51 +08:00
parent 8f33376722
commit 559a6fc3aa
2 changed files with 581 additions and 0 deletions

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 };
}