React Native 点击埋点思路

367 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前言

因为React Native没有H5那样可以直接给dom添加click监听,所以导致在业务中写代码时比较繁重。比如一段常见的业务场景代码

import report from '../util'

function clickHandle() {
    // 业务中本身的操作,比如跳转详情页等等
    report()
}

<TouchableOpacity
    onPress={clickHandle}>
    {/*  业务代码组件 */}
</TouchableOpacity>

以上代码让我在实际写业务时有一些困扰:

1 埋点方法总是需要引入到各个业务组件中

2 总是需要在业务代码里去添加一个埋点方法

3 偶尔埋点如果报错了,还可能阻塞整个程序的运行

基于以上问题,暂时想出了几种方案

方案一 自定义hooks

试想一下,如果我们需要做点击埋点时,只需通过自定义hooks添加一个ref即可,会不会方便很多,理想状态下应该是这样去使用,这里的灵感来自(React 进阶实践指南 - 我不是外星人) 文章里的useLog

const reportRef = useReport(null)

function clickHandle() {
    // 业务中本身的操作,比如跳转详情页等等
}

// 只需要简单给需要点击埋点的组件加上ref,就会自动去采集埋点,而且不会和本身组件上的点击事件冲突
<TouchableOpacity
    ref={reportRef}
    onPress={clickHandle}>
    {/*  业务代码组件 */}
</TouchableOpacity>

那我们开始来写这个hooks

// 模拟
function trackLog(eventBody) {
  console.log('埋点报告', eventBody)
}
// 首先从设计来看我们需要写什么
// 正常一个埋点应该需要有 trackId 用于记录埋点事件,trackBody 用于记录埋点的具体内容。
// 我们这里假设都是埋在同一个trackId上,就不做过多的代码了
// trackBody需要引入/或者也可以存入到props里直接取
function useReport(trackBody) {
    // 首先我们需要先设计一个ref,用于绑定到组件
    const trackRef = useRef(null)

    useEffect(() => {
        // trackRef.current.touchableHandlePress 里记录的是原本组件上通过onPress绑定的回调,需要先存起来
        const originPressHandle = trackRef.current.touchableHandlePress
        // 然后重写了回调,在回调里同时触发原有点击回调和埋点方法
        trackRef.current.touchableHandlePress = function() {
          trackLog(message)
          console.log('点击', trackRef.current)
          originPressHandle()
        }
     // 注销时我们应该把点击的回调归还
    return () => trackRef.current.touchableHandlePress = originPressHandle
   }, [trackRef, message])
    
    return trackRef
}

接下来看下如何使用

import { useReport } from 'hooks'

function SearchPage (props) {
    // 一些用户信息
    const { userId } = props
    const { item: { title, id, jumpUrl } } = props
    
    const clickReportRef = useReport({title, id})
    
    const jumpDetail = useCallback(() => {
       // 通过app桥接方法去跳转
       Bridge.dispatcher(jumpUrl)
    }, [jumpUrl])
    
    return (
        <TouchableOpacity
            ref={clickReportRef}
            onPress={jumpDetail}>
            {/* 一些组件内代码 */}
        </TouchableOpacity>
    )
}

场景二 列表下多个item需要绑定埋点

上述的方式解决了一部分场景的问题,但如果我们还有一种场景,一个列表遍历出多个item需要去埋点,简化代码如下:

function ContentList (props) {
    const { list = [] } = props
    
    const renderItem = (item, index) => {
        const {id, title} = item
        return (
            <View key={id} onPress={jump}>
                {title}
            </View>
        )
    }
    
   return (
       <View>
           { list.map(renderItem) }
       </View>
   )
}

上面场景如果在父组件ContentList处理点击埋点,我们就没法去动态创建 useReport 了,有一种处理方式是把 renderItem 里的内容提出来,单独写成一个子组件,在这个子组件里单独写 useReport。我们也可以使用另外一种思路来处理。

我们可以想象这样的处理方式

function ContentList (props) {
    const { list = [] } = props
    
    const renderItem = (item, index) => {
        const {id, title} = item
        return (
            <View key={id} onPress={jump}>
                {title}
            </View>
        )
    }
    
   return (
       <View>
           {/* 添加这个组件后,内部第一次会自动添加一个点击埋点 */}
           <TrackWrapper>
           { list.map(renderItem) }
           </TrackWrapper>
       </View>
   )
}

接下来我们实现下这个 TrackWrapper

// 模拟埋点方法
function track(i) {
  console.log('埋点触发了', i)
}

// 埋点容器
function TrackWrapper (props) {

  const children = React.Children.map(props.children, child => {
    console.log('点击埋点', child)
    // 通过  displayName 判断是否已经是点击组件
    const { type: { displayName } } = child
    if (displayName.startsWith('Touch')) {
      // onPress 是原有的埋点回调函数
      const { props: { onPress } } = child
      // 为其新增一个埋点方法
      return React.cloneElement(child, { onPress: () => {
        typeof onPress === 'function' && onPress()
        track({ title: '埋点报告' })
      }})
    } else {
      // 如果没有点击组件,需要为其增加一层
      return <TouchableOpacity
        onPress={() => track({ title: '埋点报告' })}>
          {child}
      </TouchableOpacity>
    }
  })

  console.log(children)

  return children
}

// 使用模拟
<TrackWrapper>
  {[1,2,3,4].map((item, index) => {
    if (index % 2 === 0) {
      return <View>
        <Text>{item}</Text>
      </View>
    } else {
      return <TouchableOpacity onPress={() => console.log(2323)}>
        <View>
          <Text>{item}</Text>
        </View>
      </TouchableOpacity>
    }
  })}
</TrackWrapper>

通过上述方法,我们就实现了一个埋点容器,会自动遍历子组件,并根据子组件不同的类型去添加点击埋点,当然代码没有考虑太多边界问题,只是提供一种思路。我们还可以在此基础做一些其他的增强:

1, 可以在 TrackWrapper props 上增加 fn 来定义具体使用哪个report方法,不必写死在 TrackWrapper 组件中。 2, TrackWrapper props 上增加埋点映射,可以自动从子组件中去取,比如 map = {title: 'itemTitle', id: 'itemID'}。子组件上是有props的,我们就可以从item里去取对应的值,并存成 {title, id} 这样的结构。

其他的思路大家也可以根据实际的业务场景去考虑。

总结

自此我们使用了2种不同的方式去简化点击埋点:

1, 自定义hooks。

2, render props + React.Children 来增强子组件的功能。