在react中,对于公共组件,与组件职责无关的埋点参数需要通过props深入传递,影响代码可读性和耦合度。本文介绍了一种通过HOC在祖先组件中添加埋点信息,并在埋点组件中读取的方式。通过独立数据流,提高了代码的可读性,降低耦合度。
问题示例
需求
两个业务组件:
- 音乐列表组件 - MusicList
- 音乐列表项组件 - MusicItem
有两个业务页面:
播放列表页面 - PlayListPage
收藏列表页面 - CollectionListPage
组织结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
function MusicList({fetchList}) { const [list, setList] = useState([]) useEffect(() => { fetchList.then(res => {setList{res}}) }, []) const musicItems = list.map(item => <MusicItem key={item.id} item={item} />) return <div>{musicItems}</div> }
function MusicItem({item}) => { return <div>{item.name}</div> }
function PlayListPage() { return <MusicList fetchList={fetchPlayList} /> }
function CollectionListPage() { return <MusicList fetchList={fetchCollectionList} /> }
|
现在我们需要对以上页面进行埋点,埋点信息格式如下:
1 2 3 4 5 6 7 8 9 10 11 12
| const pageImpressLog = { page: 'play_list' or 'collection_list', type: 'impress', }
const itemClickLog = { page: 'play_list' or 'collection_list', type: 'click', id: '5486234' }
|
问题
对于页面曝光埋点,我们可以直接在对应的页面上进行埋点。但是对于公共业务组件,埋点就显得比较繁琐:我们需要在通过props把page的信息传入到MusicList中,再由它传至MusicItem。这会带来两个问题:
- page信息实际上和MusicList / MusicItem 组件无关,这影响了组件的独立性。
- 当公共组件内有多层嵌套的时候,这个无关信息需要深入传递,增加了组件的复杂度。
分析
对于以上例子透露的问题,以及对需求进行分析,我们可以发现:
页面曝光和音乐列表项点击的埋点存在公共字段(如:page
),可以复用,且使用公共字段的两个组件存在逻辑上的嵌套关系。
公共组件可以使用props中的信息,对属于自己的特殊字段进行埋点(如:音乐列表项点击埋点中的id
字段可以从通过item.id
从props中提取)。
我们希望使用独立于props之外的独立数据流来提供埋点信息,降低埋点代码对业务代码的污染。
解决方案
- 在祖先组件中注入和该组件相关的埋点信息
- 在后代组件中合并并使用所有祖先组件中注入的埋点信息
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| ////////// 组件 //////////
// 音乐列表 function MusicList({fetchList}) { const [list, setList] = useState([]) useEffect(() => { fetchList.then(res => {setList{res}}) }, []) const musicItems = list.map(item => <MusicItem key={item.id} item={item} />) return <div>{musicItems}</div> }
// 音乐列表项 function MusicItem({item}) { + const log = useLog() const onClick = () => { + sendLog({ ...log, type: 'click', id: item.id }) } return <div >{item.name}</div> }
////////// 页面 //////////
// 播放列表页面 + const PlayListPage = injectLog({ + page: 'play_list' + })( () => { const log = useLog() useEffect(() => { sendLog({ ...log, type: 'impress' }) }, []) return <MusicList fetchList={fetchPlayList} /> } )
// 收藏列表页面 function CollectionListPage = injectLog({ page: 'collection_list' })( () => { const log = useLog() useEffect(() => { sendLog({ ...log, type: 'impress' }) }, []) return <MusicList fetchList={fetchCollectionList} /> } )
|
从高亮行可以看到这种解决方案的示例:
- 在页面中使用
injectLog
注入公共埋点信息。
- 在埋点发送处使用
useLog
获取合并后的埋点信息,并发送。
一次失败的尝试
由于react使用DFS进行渲染,我的第一个想法是使用一个stack来存储所有祖先节点的渲染信息。injectLog
HOC的简单版实现如下:
1 2 3 4 5 6 7 8 9 10 11 12
| const stack = []
function injectLog(log){ return (Component) => { return (props) => { stack.push(log) const component = React.createElement(Component, props) stack.pop(log) return component } } }
|
但是这里执行stack pop操作的位置有问题。因为React.createElement并不会执行render函数。它返回的是一个Element,是一个所渲染的元素的描述,可以理解为一个plain object。React会通过render函数返回的描述自己控制渲染的执行。实际上,react profiler提供了onRender API。在render执行结束后会调用该回调函数。但是由于profiler会影响性能,官方不建议在生产环境使用。
注入和读取
使用react context,独立与组件之外对埋点信息进行存储。通过provider和consumer实现了脱离props传参之外的数据流。