1. 前言
React Native 是由 Facebook 推出的挪动利用开发框架,能够用来开发 iOS、Android、Web 等跨平台应用程序,官网为:https://facebook.github.io/re…。
React Native 和传统的 Hybrid 利用最大的区别就是它抛开了 WebView 控件。React Native 产出的并不是“网页利用”、“HTML5 利用”或者“混合利用”,而是一个真正的挪动利用,从应用感触上和用 Objective-C 或 Java 编写的利用相比简直是没有区别的。React Native 所应用的根底 UI 组件和原生利用完全一致。咱们要做的就是把这些根底组件应用 JavaScript 和 React 的形式组合起来。React Native 是一个十分优良的跨平台框架。
React Native 能够通过自定义 Module 的形式实现 JavaScript 调用 Native 接口,神策剖析的 React Native Module 应用新计划实现了 React Native 全埋点性能。
本文以 Android 我的项目为例,介绍了神策剖析 React Native Module 是如何通过 React Navigation 来实现全埋点的页面浏览事件采集。
2. React Navigation
2.1. 简介
React Navigation 的诞生源于 React Native 社区对基于 JavaScript 的导航组件可扩大和易用性的需要。
React Navigation 是 Facebook,Expo 和 React 社区的开发者们单干的后果:它取代并改良了 React Native 生态系统中的多个导航库,包含 Ex-Navigation、React Native 的 Navigator 和 NavigationExperimental 组件。
2.2. 装置
上面以 npm 形式为例介绍下 React Navigation 的装置流程:
- 导入必须包
在 React Native 我的项目中装置 React Navigation 包:
npm install @react-navigation/native
在 React Native 我的项目中装置依赖包:
npm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
- 导入可选包
React Navigation 反对三种类型的导航器,别离是 StackNavigator、TabNavigator 和 DrawerNavigator。
StackNavigator
一次只渲染一个页面,并提供页面之间跳转的办法。当关上一个新的页面时,它被搁置在堆栈的顶部。
引入形式如下:
npm install @react-navigation/stack
TabNavigator
渲染一个选项卡,让用户能够在几个页面之间切换。
引入形式:
npm install @react-navigation/bottom-tabs
DrawerNavigator
提供一个从屏幕左侧滑入的抽屉。
引入形式:
npm install @react-navigation/drawer
2.3. 应用形式
通过 NavigationContainer 包裹须要应用的导航器 Stack.Navigator、Tab.Navigator、Drawer.Navigator,如下所示:
—————————–Stack————————————const Stack = createStackNavigator(); function App() {return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name=”Home” component={HomeScreen} /> <Stack.Screen name=”Store” component={StoreScreen} /> </Stack.Navigator> </NavigationContainer> );}—————————-Tab————————————-const Tab = createBottomTabNavigator();function App() {return ( <NavigationContainer> <Tab.Navigator> <Tab.Screen name=”Home” component={HomeScreen} /> <Tab.Screen name=”Store” component={StoreScreen} /> </Tab.Navigator> </NavigationContainer> );}—————————–Drawer————————————const Drawer = createDrawerNavigator(); function App() {return ( <NavigationContainer> <Drawer.Navigator> <Drawer.Screen name=”Home” component={HomeScreen} /> <Drawer.Screen name=”Store” component={StoreScreen} /> </Drawer.Navigator> </NavigationContainer> );}
3. 具体实现
因为 React Native 我的项目无奈从零碎层级标识页面,所以通过 React Navigation 的 RouteName 来进行页面的惟一标识。
3.1. NavigationContainer 解析
3.1.1. BaseNavigationContainer
所有的导航都包裹在 NavigationContainer 中,其中 BaseNavigationContainer 通过 React.useEffect 监听了 state:
BaseNavigationContainer
const BaseNavigationContainer = React.forwardRef(function BaseNavigationContainer( { initialState, onStateChange, independent, children,}: NavigationContainerProps, ref?: React.Ref<NavigationContainerRef> ) {… React.useEffect(() => {if (process.env.NODE_ENV !== ‘production’) {if ( state !== undefined && !isSerializable(state) && !hasWarnedForSerialization ) {hasWarnedForSerialization = true; console.warn( “Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use ‘navigation.setOptions’ instead. See https://reactnavigation.org/d… for more details.”); } } emitter.emit({type: ‘state’, data: { state} }); if (!isFirstMountRef.current && onStateChangeRef.current) {onStateChangeRef.current(getRootState()); } isFirstMountRef.current = false; }, [getRootState, emitter, state]); return (<ScheduleUpdateContext.Provider value={scheduleContext}> <NavigationBuilderContext.Provider value={builderContext}> <NavigationStateContext.Provider value={context}> <EnsureSingleNavigator>{children}</EnsureSingleNavigator> </NavigationStateContext.Provider> </NavigationBuilderContext.Provider> </ScheduleUpdateContext.Provider> ); } export default BaseNavigationContainer;
3.1.2. state
state 是一个 NavigationState 对象,一个 NavigationState 对象中保留了已渲染的路由树。而当任何一个页面从新渲染时,都会变更 NavigationState 中的信息,此时就会回调到 BaseNavigationContainer 中:
NavigationState
export type NavigationState = Readonly<{…/ Index of the currently focused route./index: number; / List of rendered routes./routes: (Route<string> & {state?: NavigationState | PartialState<NavigationState>;})[];…}>;
下面咱们介绍了 React Navigation 的相干信息,上面咱们通过一个 Demo 来看下是如何实现 React Native 全埋点的页面浏览事件采集。
3.2. 获取 RouteName
咱们先来看下 Demo 首页的代码实现:
import BottomTabNavigator from ‘./BottomTabNavigator’;import DrawerNavigator from ‘./DrawerNavigator’;import Intro from ‘../screen/Intro’;import MaterialBottomTabNavigator from ‘./MaterialBottomTabNavigator’;import MaterialTopTabNavigator from ‘./MaterialTopTabNavigator’; const Stack = createNativeStackNavigator(); function RootNavigator(): React.ReactElement {const { theme} = useThemeContext(); return ( <NavigationContainer> <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: theme.background,}, headerTitleStyle: {color: theme.fontColor}, headerTintColor: theme.tintColor, }} > <Stack.Screen name=”Intro” component={Intro} /> <Stack.Screen name=”StackNavigator” component={StackNavigator} /> <Stack.Screen name=”DrawerNavigator” component={DrawerNavigator} /> <Stack.Screen name=”BottomTabNavigator” component={BottomTabNavigator} /> <Stack.Screen name=”MaterialTopTabNavigator” component={MaterialTopTabNavigator} /> <Stack.Screen name=”MaterialBottomTabNavigator” component={MaterialBottomTabNavigator} /> </Stack.Navigator> </NavigationContainer> );}
首页是一个 StackNavigator,默认展现 Intro 这个导航组件,如图 3-1 所示:
图 3-1 Intro 导航组件
咱们来看下 Intro 导航组件的 NavigationState 信息:
能够看到 routes(路由树)中有个 name 为 Intro 的 route,通过这个 name 就能够拿到以后展现路由组件的 RouteName。然而,如果是 Tab 或者 Drawer 这种嵌套类型的导航组件呢?
当初咱们来看下 TabNavigator 导航组件的代码实现:
function BottomTabNavigator(): ReactElement { return ( <Tab.Navigator screenOptions={{ tabBarIcon: ({ focused}): React.ReactElement => TabBarIcon(focused), }} > <Tab.Screen name=”Screen1″ component={Screen1} options={{tabBarLabel: ‘Screen1’, tabBarIcon: ({ focused}): React.ReactElement => TabBarIcon(focused), }} /> <Tab.Screen name=”Screen2″ component={Screen2} /> <Tab.Screen name=”Screen3″ component={Screen3} /> <Tab.Screen name=”Screen4″ component={Screen4} /> </Tab.Navigator> );}
接着跳转到该组件,能够看到 Screen1 这个组件,如图 3-2 所示:
图 3-2 Screen1 导航组件
咱们来看下 Screen1 的 NavigationState 信息:
从下面能够看到以后页面的 NavigationState 只有 BottomTabNavigator,并没有 Screen1 的 NavigationState 信息,咱们再看下 NavigationState 的获取形式:
const getCurrentRoute = React.useCallback(() => { let state = getRootState(); if (state === undefined) {return undefined;}while (state.routes[state.index].state !== undefined) {state = state.routes[state.index].state as NavigationState;} return state.routes[state.index];}, [getRootState]);
能够看到是其实是通过 RootState 获取,咱们来看下 RootState 的信息:
能够看到在 RootState 中岂但有 BottomTabNavigator 的 NavigationState 也有子导航组件 Screen1、Screen2 等 NavigationState 信息,这样咱们就能够依据 index 获取以后组件的 RouteName,而 Drawer 的 NavigationState 其实和 Tab 的相似,这里不再赘述。
至此,咱们曾经能够获取到 Stack、Tab 和 Drawer 类型的 RouteName 了。
3.3. 全埋点的页面浏览事件
神策 React Native Module 中提供了原生与 JavaScript 交互的 Module,其中有一个 trackViewScreen 办法:
/* 导出 trackViewScreen 办法给 RN 应用. <p> 此办法用于 RN 中切换页面的时候调用,用于记录 $AppViewScreen 事件. * * @param url 页面的 url 记录到 $url 字段中. @param properties 页面的属性. <p> 注:为保障记录到的 $AppViewScreen 事件和 Auto Track 采集的统一,* 须要传入 $title(页面的题目)、$screen_name(页面的名称,即 包名. 类名)字段. * <p> * RN 中应用示例:* <Button * title=”Button” * onPress={()=> * RNSensorsAnalyticsModule.trackViewScreen(url, {“$title”:”RN 主页 ”,”$screen_name”:”cn.sensorsdata.demo.RNHome”})}> </Button> */@ReactMethodpublic void trackViewScreen(String url, ReadableMap properties) {try { RNAgent.trackViewScreen(url, RNUtils.convertToJSONObject(properties), false); } catch (Exception e) {e.printStackTrace(); Log.e(LOGTAG, e.toString() + “”); }}
那咱们是否能够在页面跳转时主动调用 trackViewScreen 办法,将获取到的 RouteName 作为页面标识呢?答案是必定的。这里通过 node 命令执行 JavaScript 办法,将获取 RouteName 和调用 trackViewScreen 办法的代码插入到 BaseNavigationContanier 中,上面咱们来看下如何实现。
3.3.1. hook 文件生成
- 创立 hook.js 文件,放到我的项目的根目录下,减少须要批改文件的门路:
// 零碎变量 var path = require(“path”), fs = require(“fs”), dir = path.resolve(__dirname, “node_modules/”);var reactNavigationPath5X = dir + ‘/@react-navigation/core/src/BaseNavigationContainer.tsx’;
- 须要插入的代码实现:
12345678910111213141516171819202122232425262728293031323334353637383940
var sensorsdataNavigation5ImportHookCode =”import ReactNative from ‘react-native’;\n”;var sensorsdataNavigation5HookCode = “function getParams(state:any):any{\n” +” if(!state){\n” +” return null;\n” +”}\n” +” var route = state.routes[state.index];\n” +” var params = route.params;\n” +” if(route.state){\n” +” var p = getParams(route.state);\n” +” if(p){\n” +” params = p;\n” +”}\n” +” }\n” +” return params;\n” +”}\n” +”function trackViewScreen(state: any): void {\n” +” if (!state) {\n” +” return;\n” +”}\n” +” var route = state.routes[state.index];\n” +” if (route.name === ‘Root’) {\n” +” trackViewScreen(route.state);\n” +” return;\n” +” }\n” +” var screenName = getCurrentRoute()?.name;\n” +” var params = getParams(state);\n” +” if (params) {\n” +” if (!params.sensorsdataurl) {\n” +” params.sensorsdataurl = screenName;\n” +”}\n” +” } else {\n” +” params = {\n” +” sensorsdataurl: screenName,\n” +”};\n” +” }\n” +” var dataModule = ReactNative?.NativeModules?.RNSensorsDataModule;\n” +” dataModule?.trackViewScreen && dataModule.trackViewScreen(params);\n” +”}\n” +”trackViewScreen(getRootState());\n” +”/ SENSORSDATA HOOK /\n”;
- 找到插入地位并插入代码:
1234567891011121314151617181920212223242526272829303132333435
// hook navigation 5.xsensorsdataHookNavigation5 = function () { if (fs.existsSync(reactNavigationPath5X)) {// 读取文件内容 var fileContent = fs.readFileSync(reactNavigationPath5X, ‘utf8’); // 曾经 hook 过了,不须要再次 hook if (fileContent.indexOf(‘SENSORSDATA HOOK’) > -1) {return;} // 获取 hook 的代码插入的地位 var scriptStr = ‘isFirstMountRef.current = false;’; var hookIndex = fileContent.lastIndexOf(scriptStr); // 判断文件是否异样,不存在代码,导致无奈 hook 点击事件 if (hookIndex == -1) {throw “navigation Can’t not find isFirstMountRef.current = false;
code”; } // 插入 hook 代码 var hookedContent = ${fileContent.substring( 0, hookIndex)}\n${sensorsdataNavigation5HookCode}\n${fileContent.substring(hookIndex)}
; // BaseNavigationContainer.tsx fs.renameSync(reactNavigationPath5X, ${reactNavigationPath5X}_sensorsdata_backup
); hookedContent = sensorsdataNavigation5ImportHookCode+hookedContent; // BaseNavigationContainer.tsx fs.writeFileSync(reactNavigationPath5X, hookedContent, ‘utf8’); console.log(found and modify BaseNavigationContainer.tsx: ${reactNavigationPath5X}
); }};
- 编写 node 执行代码命令:
switch (process.argv[2]) {case ‘-run’: sensorsdataHookNavigation5(); break; case ‘-reset’: sensorsdataResetRN(reactNavigationPath5X); break; default: console.log(‘can not find this options: ‘ + process.argv[2]);}
这样,代码插入的 JavaScript 文件就实现了。
3.3.2. 代码插入
进行代码插入只须要在控制台执行 node 命令:
node hook.js -run
3.4. 后果验证
再次关上 BaseNavigationContainer.tsx,能够看到在“isFirstMountRef.current = false;”这行代码前插入了咱们在 hook.js 中实现的办法:
123456789101112131415161718192021222324252627282930313233343536373839404142
function getParams(state:any):any{if(!state){return null;} var route = state.routes[state.index]; var params = route.params; if(route.state){var p = getParams(route.state); if(p){params = p;} } return params;}function trackViewScreen(state: any): void {if (!state) {return;} var route = state.routes[state.index]; if (route.name === ‘Root’) {trackViewScreen(route.state); return; } var screenName = getCurrentRoute()?.name; var params = getParams(state); if (params) {if (!params.sensorsdataurl) {params.sensorsdataurl = screenName;} } else {params = { sensorsdataurl: screenName,}; } var dataModule = ReactNative?.NativeModules?.RNSensorsDataModule; dataModule?.trackViewScreen && dataModule.trackViewScreen(params);}console.log(getRootState());trackViewScreen(getRootState());/ SENSORSDATA HOOK / isFirstMountRef.current = false;
再次运行 demo,看到曾经正确触发页面浏览事件了:
4. 总结
总的来说,神策剖析 React Native Module 应用的计划是 Hook React Navigation 的源码,实现页面浏览事件($AppViewScreen)的采集性能。
应用这种计划实现具备如下长处:
能够主动采集页面浏览事件;
计划的实现较为简单。
然而这种计划也存在如下毛病:
对 React Navigation 源码进行改变,肯定水平上会影响我的项目的稳定性;
可能存在的兼容性问题:指标文件的门路变更或指标代码的改变、反复会造成 hook 代码无奈插入或插入地位谬误。
为了实现 React Native 全埋点的页面浏览事件采集,咱们调研了多种实现计划,相对而言此种计划是最优的。同时,咱们也在继续优化,尽可能保障版本的兼容性和稳定性。
文章起源:神策技术社区