携手创作,共同成长!这是我参与「掘金日新计划 · 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 来增强子组件的功能。