利用React的Fiber层级结构特点,从页面中自动提取需要上报的信息。非侵入,易扩展,移植性强 需求:记录按钮和菜单的点击
记录信息:
什么人,什么时间
在什么页面(用面包屑表示)
在什么层级(比如在某个Tab下被点击)
什么元素(元素是什么类型,里面有什么文案)
思路:
1. 在body上绑定click事件,用于拦截页面中的所有的click事件
2. 在事件处理器中,获取到事件->事件源->fiber。
3. 判断该fiber是否需要上报
4. 如果该fiber需要上报,则根据fiber的return属性,查找页面中的TabPane元素,Modal元素等元素形成层级信息
5. 根据菜单配置,获取对应的面包屑
6. 在requestIdleCallback中执行b,c,d,e步骤完成手动上报。
- 在body上绑定点击事件
function addBuryingPointListening(e: Event) {
requestIdleCallback(deadline => {
if (deadline.timeRemaining() > 0) {
const fiber = getFiberFromEvent(e);
if (fiber) {
// 获取fiber相关的信息
const info = getFiberInfo(fiber);
if (info) {
// 获取面包屑信息
const breadcrumb = getBreadcrumbFromLocation();
// 获取用户信息
const userInfo = getUserInfo();
// 获取url信息
const { href, pathname } = location as any;
// 手动调用函数完成上报
buriedPointFun({
name: 'page_function_usage',
params: {
...info,
userName: userInfo?.name,
userEmail: userInfo?.email,
url: href,
pathname,
breadcrumb: breadcrumb ? breadcrumb.join('/') : '',
},
});
}
}
}
});
}
document.body.addEventListener('click', addBuryingPointListening);
- 获取点击事件发生时所在fiber的代码
function getFiberFromDom(dom: any) {
let fiber = null;
if (dom) {
const keys = Object.keys(dom);
for (const key of keys) {
if (key.includes('__reactFiber')) {
fiber = dom[key];
break;
}
}
}
return fiber;
}
function getFiberFromEvent(e: Event) {
const { target } = e;
return getFiberFromDom(target);
}
- 判断该fiber是否需要上报
判断是否是button类型的fiber
function isButtonFiber(fiber: any) {
return fiber?.type === 'button';
}
判断是否是MenuItem类型的fiber
function isMenuItemFiber(fiber: any) {
return isObject(fiber?.type) && fiber.type.menuType === 'MenuItem';
}
- 获取需要上报fiber的文案
获取HostComponent对应fiber中的文案
function getTextFromHostComponentFiber(fiber: any) {
const { stateNode } = fiber;
return stateNode.textContent;
}
- 获取发生点击事件所在fiber对应的层级
function getLevelsFromFiber(fiber: any) {
const levels = [] as any[];
let node = fiber.return;
while (node) {
const { type, memoizedProps } = node;
if (isTabPaneFiber(node)) {
const title = getTextFromJSX(memoizedProps.title);
levels.unshift(`Tab-(${title})`);
} else if (isModalFiber(node)) {
const title =
getTextFromJSX(memoizedProps.title) ||
getTextFromJSX(memoizedProps.title.props.title);
levels.unshift(`Modal-(${title})`);
} else if (isDrawerFiber(node)) {
const title = getTextFromJSX(memoizedProps.title);
levels.unshift(`Drawer-(${title})`);
} else if (isCustomFiber(node)) {
const { levels: originLevels = [] } = memoizedProps;
for (let i = originLevels.length - 1; i >= 0; i--) {
levels.unshift(originLevels[i]);
}
} else if (isSubMenuFiber(node)) {
const title = getTextFromJSX(memoizedProps.title);
levels.unshift(`SubMenu-(${title})`);
} else if (isMenuFiber(node)) {
levels.unshift(`Menu`);
} else if (isTooltipFiber(node)) {
const maybePopconfirmFiber = node.return;
if (isPopconfirmFiber(maybePopconfirmFiber)) {
const title = getTextFromJSX(memoizedProps.content);
levels.unshift(`Popconfirm-(${title})`);
node = maybePopconfirmFiber;
}
} else if (typeof type === 'function' && type.isBuryingPointRedirect) {
const { redirect } = memoizedProps;
if (redirect.current) {
const dom = redirect.current;
node = getFiberFromDom(dom);
continue;
}
}
node = node.return;
}
return levels;
}
- 从fiber中提取需要上报的信息,包括fiber的类型,文本,层级
function getFiberInfo(fiber: any) {
let node = fiber;
while (node) {
if (isFormFieldFiber(node)) {
const { memoizedProps } = node;
const text = getTextFromJSX(memoizedProps.label);
return {
type: 'FormField',
text,
levels: getLevelsFromFiber(node),
};
} else if (isCustomFiber(node)) {
const { type, levels = [], text } = getInfoFromCustomFiber(node);
if (type) {
return {
type,
levels: [...levels, ...getLevelsFromFiber(node)],
text,
};
}
} else if (isSwitchFiber(node)) {
return {
type: 'Switch',
levels: getLevelsFromFiber(node),
};
} else if (isTooltipContainButtonFiber(node)) {
const toolTipFiber = getTooltipFiberFromTooltipContainButtonFiber(node);
return {
type: 'TooltipContainButton',
text:
getTextFromHostComponentFiber(node) ||
getTextFromTooltipFiber(toolTipFiber),
levels: getLevelsFromFiber(toolTipFiber),
};
} else if (isButtonFiber(node)) {
return {
type: 'Button',
text: getTextFromHostComponentFiber(node),
levels: getLevelsFromFiber(node),
};
} else if (isDropdownMenuItemFiber(node)) {
return {
type: 'DropdownMenuItem',
text: getTextFromDropdownMenuItemFiber(node),
levels: getLevelsFromFiber(node),
};
} else if (isMenuItemFiber(node)) {
return {
type: 'MenuItem',
text: getTextFromMenuItemFiber(node),
levels: getLevelsFromFiber(node),
};
} else if (isLinkFiber(node)) {
return {
type: 'Link',
text: getTextFromLinkFiber(node),
levels: getLevelsFromFiber(node),
};
} else if (
typeof node?.type === 'function' &&
node?.type.isBuryingPointRedirect
) {
const { redirect } = node.memoizedProps;
if (redirect.current) {
const dom = redirect.current;
node = getFiberFromDom(dom);
continue;
}
}
node = node.return;
}
return null;
}
- 异常:对于点击事件发生时,被卸载的fiber如何处理?想自定义元素类型和层级怎么处理。
const BuryingPointContainer = (props: {
levels?: string[];
type?: string;
text?: string;
children: any;
}) => {
const { children } = props;
return children;
};
BuryingPointContainer.isBuryingPointContainer = true;
const BuryingPointRedirect = (props: {
children: any;
redirect: {
current: any;
};
}) => {
const { children } = props;
return children;
};
BuryingPointRedirect.isBuryingPointRedirect = true;
使用方式如下:
const menuRef = useRef(null);
return (
<Dropdown.Button
droplist={
<Menu ref={menuRef}>
{items.map(item => (
<Menu.Item>
<BuryingPointRedirect redirect={menuRef}>
<BuryingPointContainer
title={item.title}
type="DropdownMenuItem"
>
<span>
{item.title }
</span>
</BuryingPointContainer>
</BuryingPointRedirect>
</Menu.Item>
))}
</Menu>
}
>
{text}
</Dropdown.Button>
);
<BuryingPointContainer levels={levels}>
<FormPopup
{...modalSetting}
listContext={getContext()}
ref={modalRef}
/>
{finalComponent}
</BuryingPointContainer>