基于Fiber的前端埋点方案

128 阅读3分钟

利用React的Fiber层级结构特点,从页面中自动提取需要上报的信息。非侵入,易扩展,移植性强 需求:记录按钮和菜单的点击

记录信息:

什么人,什么时间

在什么页面(用面包屑表示)

在什么层级(比如在某个Tab下被点击)

什么元素(元素是什么类型,里面有什么文案)

思路:

1.  在body上绑定click事件,用于拦截页面中的所有的click事件
2.  在事件处理器中,获取到事件->事件源->fiber。
3.  判断该fiber是否需要上报
4.  如果该fiber需要上报,则根据fiber的return属性,查找页面中的TabPane元素,Modal元素等元素形成层级信息
5.  根据菜单配置,获取对应的面包屑
6.  在requestIdleCallback中执行b,c,d,e步骤完成手动上报。
  1. 在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);
  1. 获取点击事件发生时所在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);
}
  1. 判断该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';
}
  1. 获取需要上报fiber的文案

获取HostComponent对应fiber中的文案

function getTextFromHostComponentFiber(fiber: any) {
  const { stateNode } = fiber;
  return stateNode.textContent;
}
  1. 获取发生点击事件所在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;
}
  1. 从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;
}
  1. 异常:对于点击事件发生时,被卸载的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>