前言
React Native 是由 Facebook 推出的移动应用开发框架,可以用来开发 iOS、Android、Web 等跨平台应用程序,官网为:https://facebook.github.io/react-native/。
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 在 v2.0 版本使用新方案实现了 React Native 全埋点功能。本文主要介绍神策分析 React Native Module 是如何实现 $AppClick(全埋点的点击事件) 功能的,内容以 iOS 项目为例。
原理分析
触发点击
在 React Native 中没有专门的按钮组件,为了让视图能够响应用户的点击事件,我们需要借助 Touchable 系列组件来包装我们的视图。
Touchable 系列组件
Touchable 系列组件中的四个组件都可以用来包装视图,从而响应用户的点击事件:
- TouchableHighlight:在用户手指按下时背景会有变暗的效果;
- TouchableNativeFeedback:在 Android 上可以使用 TouchableNativeFeedback,它会在用户手指按下时形成类似水波纹的视觉效果。注意,此组件只支持 Android;
- TouchableOpacity:会在用户手指按下时降低按钮的透明度,而不会改变背景的颜色;
- TouchableWithoutFeedback:响应用户的点击事件,如果你想在处理点击事件的同时不显示任何视觉反馈,使用它是个不错的选择。
以上组件中 TouchableHighlight 、TouchableNativeFeedback 以及 TouchableOpacity 都是在 TouchableWithoutFeedback 的基础上做了一些扩展,我们从源码中可以看出:
TouchableHighlight
type Props = $ReadOnly<{| ...TouchableWithoutFeedbackProps, ...IOSProps, ...AndroidProps, activeOpacity?: ?number, underlayColor?: ?ColorValue, style?: ?ViewStyleProp, onShowUnderlay?: ?() => void, onHideUnderlay?: ?() => void, testOnly_pressed?: ?boolean, |}>; |
TouchableNativeFeedback
propTypes: { /* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment * suppresses an error found when Flow v0.89 was deployed. To see the * error, delete this comment and run Flow. */ ...TouchableWithoutFeedback.propTypes, |
TouchableOpacity
type Props = $ReadOnly<{| ...TouchableWithoutFeedbackProps, ...TVProps, activeOpacity?: ?number, style?: ?ViewStyleProp, |}>; |
因为 TouchableWithoutFeedback 有其他组件的共同属性,所以我们只需要来了解下 TouchableWithoutFeedback 是如何实现点击功能的。
Touchable 功能介绍
React Native 的响应系统用起来可能比较复杂,因此官方提供了一个抽象的 Touchable
实现,用来做 “可触控” 的组件。Touchable 系列组件相关文件都在 node_modules/react-native/Libraries/Components/Touchable 文件夹中。在 Touchable 文件夹下也提供了 Touchable.js 文件,点击功能的实现都是在此文件中。
React Native 对 Touchable.js 的描述如下:
* ====================== Touchable Tutorial =============================== * The `Touchable` mixin helps you handle the "press" interaction. It analyzes * the geometry of elements, and observes when another responder (scroll view * etc) has stolen the touch lock. It notifies your component when it should * give feedback to the user. (bouncing /highlighting/unhighlighting ). * * - When a touch was activated (typically you highlight) * - When a touch was deactivated (typically you unhighlight) * - When a touch was "pressed" - a touch ended while still within the geometry * of the element, and no other element (like scroller) has "stolen" touch * lock ( "responder" ) (Typically you bounce the element). |
从
描述中可以看出,Touchable 会帮助开发者处理触摸交互,当有其他响应者响应了触摸交互时,Touchable 也会及时通知控件向用户提供反馈。
Touchable 状态变化
React Native 控件的触摸操作是会发生变化的,为了监听控件触摸状态的变化,React Native 在 Touchable 中声明了 State 和 Signal 类型来描述用户的触摸行为。
State
type State = | typeof States.NOT_RESPONDER // 非响应者 | typeof States.RESPONDER_INACTIVE_PRESS_IN // 无效的按压 | typeof States.RESPONDER_INACTIVE_PRESS_OUT // 无效的抬起 | typeof States.RESPONDER_ACTIVE_PRESS_IN // 有效的按压 | typeof States.RESPONDER_ACTIVE_PRESS_OUT // 有效的抬起 | typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN // 有效的长按 | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT // 有效的长按后抬起 | typeof States.ERROR; // 错误 |
Signal
/** * Inputs to the state machine. */ const Signals = keyMirror({ DELAY: null, RESPONDER_GRANT: null, RESPONDER_RELEASE: null, RESPONDER_TERMINATED: null, ENTER_PRESS_RECT: null, LEAVE_PRESS_RECT: null, LONG_PRESS_DETECTED: null, }); type Signal = | typeof Signals.DELAY // 延迟触发信号 | typeof Signals.RESPONDER_GRANT // 开始触摸 | typeof Signals.RESPONDER_RELEASE // 触摸结束 | typeof Signals.RESPONDER_TERMINATED // 触摸中断 | typeof Signals.ENTER_PRESS_RECT // 进入按压范围内 | typeof Signals.LEAVE_PRESS_RECT // 离开按压范围 | typeof Signals.LONG_PRESS_DETECTED; // 检测是否为长按 |
交互流程如图 2-1 所示:
图 2-1 交互流程图(参考:React Native 源码)
从图 2-1 中可以看出,当 State 为 RESPONDER_ACTIVE_PRESS_IN 并且 Signal 为 RESPONDER_RELEASE 时,表示用户正在点击控件。因此,我们可以在这里触发控件的点击事件采集。_performSideEffectsForTransition 函数中已有此逻辑的判断,我们可以在这里添加打印信息来验证方案的可行性:
_performSideEffectsForTransition: function ( curState: State, nextState: State, signal: Signal, e: PressEvent, ) { // ... const shouldInvokePress = !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress; if (shouldInvokePress && this.touchableHandlePress) { if (!newIsHighlight && !curIsHighlight) { // we never highlighted because of delay, but we should highlight now this._startHighlight(e); this._endHighlight(e); } if (Platform.OS === 'android' && !this.props.touchSoundDisabled) { this._playTouchSound(); } console.log( "这里是按钮点击" ); this.touchableHandlePress(e); } } this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout); this.touchableDelayTimeout = null; }, |
在项目入口文件 App.js 中添加 Button 按钮并运行项目,点击 Button 按钮可以看到终端控制台打印内容 “这里是按钮点击”,如图 2-2 所示:
图 2-2 控制台打印信息
至此,我们就找到了触发 $AppClick 事件的时机。
创建视图
上一节中我们已经找到了触发 $AppClick 事件的时机。但是,还存在一个问题:在 React Native 中是无法直接获取到触发点击事件对应的 View 对象。针对这一问题,我们可以通过 reactTag 来解决。
reactTag
在 React Native 项目中会给每个 View 分配一个唯一的 id(reactTag)。reactTag 是一个递增的整型数字,我们可以通过 reactTag 来找到每一个 View 对象。
RCTRootView 作为整个 React Native 项目的入口,在初始化时会默认将 1 分配给 RCTRootView 作为 reactTag,即 RootTag 。
我们下面来看下 reactTag 的生成规则:
// Counter for uniquely identifying views. // % 10 === 1 means it is a rootTag. // % 2 === 0 means it is a Fabric tag. var nextReactTag = 3; function allocateTag() { var tag = nextReactTag; if (tag % 10 === 1) { tag += 2; } nextReactTag = tag + 2; return tag; } |
从上面的代码片段中可以看出,tag 以 +2 的方式递增,当 tag % 10 === 1 时会再做一次累加。因此,tag % 10 === 1 只会出现一次,即 RootTag。
创建视图
在 React Native 中所有的 View 都是通过 RCTUIManager 类来进行创建并管理的。RCTUIManager 类提供了如下方法来创建 View 对象:
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag viewName:(NSString *)viewName rootTag:(nonnull NSNumber *)rootTag props:(NSDictionary *)props) |
下面我们需要找到此方法是在哪里调用的,这样就可以知道在 JavaScript 端创建 View 的时机。经过在 react-native 源码中查找,定位到在 /node_modules/react-native/Renderer/implementations/ReactNativeRenderer-dev.js 中有如下代码片段:
ReactNativePrivateInterface.UIManager.createView( tag, // reactTag viewConfig.uiViewClassName, // viewName rootContainerInstance, // rootTag updatePayload // props ); |
可以看出,这里就是 JavaScript 端创建 View 的代码位置。我们可以在这里添加 Hook 代码将 View 的 reactTag 保存起来。
方案简述
根据前面两节的内容可知,我们可以在 UIManager 创建视图时将可点击视图的 reactTag 保存起来,当控件触发点击时通过对比 reactTag 判断当前点击的视图是否为可点击,并通过 reactTag 找到对应的 View 对象触发 $AppClick 点击事件。
准备工作
创建项目
在实现 React Native 点击事件采集方案之前,我们首先创建一个演示项目。详细的安装步骤可以参考官网 environment-setup 部分,现在使用下面的命令创建一个 React Native 项目。
react-native init AwesomeProject --version 0.61.5 cd AwesomeProject react-native run-ios |
0.62.x 及以上版本针对控件点击功能源码有部分改动,我们已在神策分析 React Native Module 后续版本中进行了兼容。这里为了演示效果,我们仍以 v0.61.5 版本来进行后续功能的说明。
通过以上命令我们已经创建了一个 AwesomeProject 的 React Native 项目,并可以成功运行项目。项目如图 3-1 所示:
图 3-1 React Native 项目截图
集成神策分析
1. 在项目目录下执行 “cd ios” 命令后再执行 “vim Podfile” 命令编辑 Podfile 文件。在文件中添加 ” pod ‘SensorsAnalyticsSDK’ ” 后保存,并执行 “pod install” 命令集成神策分析 SDK。Podfile 文件内容如下:
platform :ios, '9.0' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' target 'AwesomeProject' do # Pods for AwesomeProject # ...... Pod 'SensorsAnalyticsSDK' target 'AwesomeProjectTests' do inherit! :search_paths # Pods for testing end use_native_modules! end target 'AwesomeProject-tvOS' do # Pods for AwesomeProject-tvOS target 'AwesomeProject-tvOSTests' do inherit! :search_paths # Pods for testing end end |
2. 打开 “ios 文件夹” 下的 AwesomeProject.xcworkspace ,并在 AppDelegate 中初始化神策分析 SDK:
#import <SensorsAnalyticsSDK/SensorsAnalyticsSDK.h> @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { .... SAConfigOptions *options = [[SAConfigOptions alloc] initWithServerURL:@ "" launchOptions:launchOptions]; options.autoTrackEventType = SensorsAnalyticsEventTypeAppStart | SensorsAnalyticsEventTypeAppEnd | SensorsAnalyticsEventTypeAppClick | SensorsAnalyticsEventTypeAppViewScreen; options.enableLog = YES; [SensorsAnalyticsSDK startWithConfigOptions:options]; return YES; } |
完成初始化 SDK 后运行项目,可以看到控制台会打印出 $AppStart 事件。
创建 Module
集成神策分析 SDK 后我们还需要创建一个 React Native Module 用来将 Native 触发 $AppClick 的接口提供给 JavaScript 端调用。
1. 打开 Xcode 并选择 File → New → Project…,输入静态库名称 SensorsAnalyticsModule。如图 3-2 所示:
图 3-2 创建 Module
2. 在静态库项目文件夹下添加 SensorsAnalyticsModule.podspec 文件,文件内容如下:
Pod::Spec.new do |s| s.name = "SensorsAnalyticsModule" s.version = "0.0.1" s.summary = "The official React Native SDK of Sensors Analytics." s.license = { : type => "Apache License, Version 2.0" } s.author = { "Yuanyang Peng" => "pengyuanyang@sensorsdata.cn" } s. source = { :git => "https://github.com/sensorsdata/react-native-sensors-analytics" , :tag => "v#{s.version}" } s.platform = :ios, "7.0" s.source_files = "SensorsAnalyticsModule/*.{h,m}" s.requires_arc = true s.dependency "React" end |
3. 将创建的 SensorsAnalyticsModule 工程文件夹移动到演示项目根目录下,并在演示项目 “ios 文件夹” 下的 Podfile 文件中,添加 SensorsAnalyticsModule 引用:
platform :ios, '9.0' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' target 'AwesomeProject' do # Pods for AwesomeProject # ...... pod 'SensorsAnalyticsSDK' pod 'SensorsAnalyticsModule' , :path => '../SensorsAnalyticsModule/' target 'AwesomeProjectTests' do inherit! :search_paths # Pods for testing end use_native_modules! end target 'AwesomeProject-tvOS' do # Pods for AwesomeProject-tvOS target 'AwesomeProject-tvOSTests' do inherit! :search_paths # Pods for testing end end |
运行项目后可以正常工作,至此准备工作已完成。
代码实现
通过前面的介绍,我们已经知道了实现 $AppClick 事件功能的关键步骤,下面来详细说明下代码的实现。
Module
1. 在 SensorsAnalyticsModule.h 中添加 RCTBridgeModule 引用及实现协议内容:
#import <React/RCTBridgeModule.h> @interface SensorsAnalyticsModule : NSObject <RCTBridgeModule> @end |
2. 在 SensorsAnalyticsModule.m 中新增 reactTags 集合属性来保存可点击视图的 reactTag 信息:
#import <SensorsAnalyticsSDK/SensorsAnalyticsSDK.h> #import <React/RCTRootView.h> #import <React/RCTUIManager.h> @interface SensorsAnalyticsModule () @property (nonatomic, strong) NSMutableSet<NSNumber*> *reactTags; @end |
3. 在 SensorsAnalyticsModule.m 中添加 Module 声明,并添加 + sharedInstance 方法:
@implementation SensorsAnalyticsModule RCT_EXPORT_MODULE(SensorsAnalyticsModule) + (instancetype)sharedInstance { static dispatch_once_t onceToken; static SensorsAnalyticsModule *module; dispatch_once(&onceToken, ^{ module = [[SensorsAnalyticsModule alloc] init]; }); return module; } @end |
4. 新增 saveReactTag:clickable: 方法用来保存可点击视图的 reactTag,并将此方法通过 RCT_EXPORT_METHOD 提供给 JavaScript 端调用:
RCT_EXPORT_METHOD(saveReactTag:(NSInteger)reactTag clickable:(BOOL)clickable) { if (!clickable) { return ; } SensorsAnalyticsModule *module = [SensorsAnalyticsModule sharedInstance]; [module.reactTags addObject:@(reactTag)]; } |
5. 通过 reactTag 找到对应视图:
- (UIView *)viewForTag:(NSNumber *)reactTag { UIViewController *root = [[[UIApplication sharedApplication] keyWindow] rootViewController]; RCTRootView *rootView = [root rootView]; RCTUIManager *manager = rootView.bridge.uiManager; return [manager viewForReactTag:reactTag]; } |
6. 新增 trackViewClick: 方法用来触发 $AppClick 事件。在 trackViewClick: 方法中通过 reactTag 找到对应的视图后触发 $AppClick 事件:
RCT_EXPORT_METHOD(trackViewClick:(NSInteger)reactTag) { SensorsAnalyticsModule *module = [SensorsAnalyticsModule sharedInstance]; BOOL clickable = [module.reactTags containsObject:@(reactTag)]; if (!clickable) { return ; } dispatch_async(dispatch_get_main_queue(), ^{ UIView *view = [module viewForTag:@(reactTag)]; [[SensorsAnalyticsSDK sharedInstance] trackViewAppClick:view withProperties:nil]; }); } |
手动插入代码
1. 在 /node_modules/react-native/Renderer/implementations/ReactNativeRenderer-dev.js 的 “ReactNativePrivateInterface.UIManager.createView” 代码前插入 Hook 代码如下:
( function (thatThis){ try{ var clickable = false ; if (props.onStartShouldSetResponder){ clickable = true ; } var ReactNative = require( 'react-native' ); var dataModule = ReactNative.NativeModules.SensorsAnalyticsModule; dataModule && dataModule.saveReactTag && dataModule.saveReactTag(tag, clickable); } catch (error) { throw new Error( 'SensorsAnalyticsModule Hook Code 调用异常: ' + error); } })(this); /* SENSORSDATA HOOK */ ReactNativePrivateInterface.UIManager.createView( tag, // reactTag viewConfig.uiViewClassName, // viewName rootContainerInstance, // rootTag updatePayload // props ); // 在此方法前插入代码 ReactNativePrivateInterface.UIManager.createView( tag, // reactTag viewConfig.uiViewClassName, // viewName rootContainerInstance, // rootTag updatePayload // props ); |
2. 在 node_modules/react-native/Libraries/Components/Touchable/Touchable.js 的 “this.touchableHandlePress(e);” 代码前插入 Hook 代码如下:
( function (thatThis) { try { var ReactNative = require( 'react-native' ); var module = ReactNative.NativeModules.SensorsAnalyticsModule; thatThis.props.onPress && module && module.trackViewClick && module.trackViewClick(ReactNative.findNodeHandle(thatThis)); } catch (error) { throw new Error( 'SensorsData RN Hook Code 调用异常: ' + error); } })(this); /* SENSORSDATA HOOK */ // 在此方法前插入代码 this.touchableHandlePress(e); |
运行项目并点击 Button ,项目的控制台中已打印出 Button 的 $AppClick 事件信息。至此,完成了 React Native 全埋点的 $AppClick 事件采集功能。 如图 4-1 所示:
图 4-1 触发的点击事件信息
自动插入代码
在上一节中,我们是手动插入了 React Native JavaScript 端的 Hook 代码,这种方案并不利于后期代码的维护以及不同 React Native 版本的兼容。因此,在这里需要新增一个 Hook 文件用来实现源码的自动插入功能。
1. 新建 Hook.js 文件放在演示项目的根目录下,并添加系统变量和文件位置:
// 系统变量 var path = require( "path" ), fs = require( "fs" ), dir = path.resolve(__dirname, "node_modules/" ); // RN 点击事件 Touchable.js 源码文件 // 为了兼容不同的 React Native 版本,这里可以再添加路径 var RNClickFilePath = dir + '/react-native/Libraries/Components/Touchable/Touchable.js' ; var RNClickableFiles = [ dir + '/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js' , dir + '/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-prod.js' ]; |
2. 添加后续需要用到的工具类方法:
// 工具函数- add try catch addTryCatch = function (functionBody) { functionBody = functionBody.replace( /this/g , 'thatThis' ); return "(function(thatThis){\n" + " try{\n " + functionBody + " \n } catch (error) { throw new Error('SensorsData RN Hook Code 调用异常: ' + error);}\n" + "})(this); /* SENSORSDATA HOOK */" ; } // 工具函数 - 计算位置 function lastArgumentName(content, index) { --index; var lastComma = content.lastIndexOf( ',' , index); var lastParentheses = content.lastIndexOf( '(' , index); var start = Math.max(lastComma, lastParentheses); return content.substring(start + 1, index + 1); } |
3. 添加 Hook Touchable.js 文件的代码片段:
var sensorsdataClickHookCode = `( function (thatThis){ try { var ReactNative = require( 'react-native' ); var dataModule = ReactNative.NativeModules.SensorsAnalyticsModule; thatThis.props.onPress && dataModule && dataModule.trackViewClick && dataModule.trackViewClick(ReactNative.findNodeHandle(thatThis)) } catch (error) { throw new Error( 'SensorsData RN Hook Code 调用异常: ' + error); }})(this); /* SENSORSDATA HOOK */ `; sensorsdataHookClickRN = function () { // 读取文件内容 var fileContent = fs.readFileSync(RNClickFilePath, 'utf8' ); // 已经 hook 过了,不需要再次 hook if (fileContent.indexOf( 'SENSORSDATA HOOK' ) > -1) { return ; } // 获取 hook 的代码插入的位置 var hookIndex = fileContent.indexOf( "this.touchableHandlePress(" ); // 判断文件是否异常,不存在 touchableHandlePress 方法,导致无法 hook 点击事件 if (hookIndex == -1) { throw "Can't not find touchableHandlePress function" ; }; // 插入 hook 代码 var hookedContent = `${fileContent.substring(0, hookIndex)}\n${sensorsdataClickHookCode}\n${fileContent.substring(hookIndex)}`; // 备份 Touchable.js 源文件 fs.renameSync(RNClickFilePath, `${RNClickFilePath}_sensorsdata_backup`); // 重写 Touchable.js 文件 fs.writeFileSync(RNClickFilePath, hookedContent, 'utf8' ); console.log(`found and modify Touchable.js: ${RNClickFilePath}`); }; |
4. 添加 Hook 获取 reactTag 信息的代码片段:
// hook clickable sensorsdataHookClickableRN = function (reset = false ) { RNClickableFiles.forEach( function (onefile) { if (fs.existsSync(onefile)) { if (reset) { // 读取文件内容 var fileContent = fs.readFileSync(onefile, "utf8" ); // 未被 hook 过代码,不需要处理 if (fileContent.indexOf( 'SENSORSDATA HOOK' ) == -1) { return ; } // 检查备份文件是否存在 var backFilePath = `${onefile}_sensorsdata_backup`; if (!fs.existsSync(backFilePath)) { throw `File: ${backFilePath} not found, Please rm -rf node_modules and npm install again`; } // 将备份文件重命名恢复 + 自动覆盖被 hook 过的同名文件 fs.renameSync(backFilePath, onefile); } else { // 读取文件内容 var content = fs.readFileSync(onefile, 'utf8' ); // 已经 hook 过了,不需要再次 hook if (content.indexOf( 'SENSORSDATA HOOK' ) > -1) { return ; } // 获取 hook 的代码插入的位置 var newObjRe = /ReactNativePrivateInterface \.UIManager\.createView\([\s\S]{1,60}\.uiViewClassName,[\s\S]*?\)[,;]/ var match = newObjRe. exec (content); if (!match) { var objRe = /UIManager \.createView\([\s\S]{1,60}\.uiViewClassName,[\s\S]*?\)[,;]/ match = objRe. exec (content); } if (!match) throw "can't inject clickable js" ; var lastParentheses = content.lastIndexOf( ')' , match.index); var lastCommaIndex = content.lastIndexOf( ',' , lastParentheses); if (lastCommaIndex == -1) throw "can't inject clickable js,and lastCommaIndex is -1" ; var nextCommaIndex = content.indexOf( ',' , match.index); if (nextCommaIndex == -1) throw "can't inject clickable js, and nextCommaIndex is -1" ; var propsName = lastArgumentName(content, lastCommaIndex).trim(); var tagName = lastArgumentName(content, nextCommaIndex).trim(); var functionBody = `var clickable = false ; if (${propsName}.onStartShouldSetResponder){ clickable = true ; } var ReactNative = require( 'react-native' ); var dataModule = ReactNative.NativeModules.SensorsAnalyticsModule; dataModule && dataModule.saveReactTag && dataModule.saveReactTag(${tagName}, clickable); `; var call = addTryCatch(functionBody); var lastReturn = content.lastIndexOf( 'return' , match.index); var splitIndex = match.index; if (lastReturn > lastParentheses) { splitIndex = lastReturn; } var hookedContent = `${content.substring(0, splitIndex)}\n${call}\n${content.substring(splitIndex)}` // 备份源文件 fs.renameSync(onefile, `${onefile}_sensorsdata_backup`); // 重写文件 fs.writeFileSync(onefile, hookedContent, 'utf8' ); console.log(`found and modify clickable.js: ${onefile}`); } } }); }; |
5. 添加代码还原功能:
// 恢复被 hook 过的代码 sensorsdataResetRN = function (resetFilePath) { // 判断需要被恢复的文件是否存在 if (!fs.existsSync(resetFilePath)) { return ; } var fileContent = fs.readFileSync(resetFilePath, "utf8" ); // 未被 hook 过代码,不需要处理 if (fileContent.indexOf( 'SENSORSDATA HOOK' ) == -1) { return ; } // 检查备份文件是否存在 var backFilePath = `${resetFilePath}_sensorsdata_backup`; if (!fs.existsSync(backFilePath)) { throw `File: ${backFilePath} not found, Please rm -rf node_modules and npm install again`; } // 将备份文件重命名恢复 + 自动覆盖被 hook 过的同名 Touchable.js 文件 fs.renameSync(backFilePath, resetFilePath); }; |
6. 定义执行命令:
// 全部 hook 文件恢复 resetAllSensorsdataHookRN = function () { sensorsdataResetRN(RNClickFilePath); sensorsdataHookClickableRN( true ); }; // 全部 hook 文件 allSensorsdataHookRN = function () { sensorsdataHookClickRN(RNClickFilePath); sensorsdataHookClickableRN(); }; // 命令行 switch (process.argv[2]) { case '-run' : allSensorsdataHookRN(); break ; case '-reset' : resetAllSensorsdataHookRN(); break ; default: console.log( 'can not find this options: ' + process.argv[2]); } |
7. 删除手动插入的代码片段,在演示项目的根目录执行 “node Hook.js -run”,Hook 成功后会打印出插入代码的文件路径。运行项目测试 Button 点击,可以在控制台正常打印信息。如图 4-2 所示:
图 4-2 触发的点击事件
总结
总的来说,神策分析 React Native Module 在 v2.0 版本使用的方案是 Hook React Native JavaScript 端的源码,实现 $AppClick 事件的采集功能。
使用这种方案实现有如下优点:
- 点击控件采集的信息更准确(主要是 $screen_name 的准确性,这部分内容会在后续的 React Native 页面浏览全埋点方案中重点讲解);
- 和 Native SDK 解耦,不再需要 Native SDK 配合 React Native Module 版本更新。
但是这种方案也存在如下缺点:
- 对 React Native JavaScript 端源码进行改动,一定程度上会造成 React Native 代码的不稳定性。
在这里我们为了保证数据的准确性仍然使用此方案,并且在 Hook 代码中做了一定的代码保护,尽最大的努力减少数据埋点带来的风险性。