React Native 点击事件采集方案 | 数据采集

2023-05-29  本文已影响0人  涅槃快乐是金

一、前言

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 [1] 的方式实现 JavaScript 调用 Native 接口,神策分析的 React Native Module [2]在 v2.0 版本使用新方案实现了 React Native 全埋点功能。本文主要介绍神策分析 React Native Module 是如何实现 $AppClick(全埋点的点击事件) 功能的,内容以 iOS 项目为例。

二、原理分析

2.1 触发点击

在 React Native 中没有专门的按钮组件,为了让视图能够响应用户的点击事件,我们需要借助 Touchable 系列组件来包装我们的视图。

2.1.1 Touchable 系列组件

Touchable 系列组件中的四个组件都可以用来包装视图,从而响应用户的点击事件:

以上组件中前三者都是在 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 是如何实现点击功能的。

2.1.2 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 也会及时通知控件向用户提供反馈。

2.1.3 Touchable 状态变化

React Native 控件的触摸操作是会发生变化的,为了监听控件触摸状态的变化,React Native 在 Touchable 中声明了 StateSignal 类型来描述用户的触摸行为。

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 源码 [3])

从图 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 所示:

[图片上传失败...(image-3e99c3-1685460922460)]

图 2-2 控制台打印信息

至此,我们就找到了触发 $AppClick 事件的时机。

2.2 创建视图

上一节中我们已经找到了触发 $AppClick 事件的时机。但是,还存在一个问题:在 React Native 中是无法直接获取到触发点击事件对应的 View 对象。针对这一问题,我们可以通过 reactTag 来解决。

**2.2.1 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。

2.2.2 创建视图

在 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 保存起来。

2.2.3 方案简述

根据前面两节的内容可知,我们可以在 UIManager 创建视图时将可点击视图的 reactTag 保存起来,当控件触发点击时通过对比 reactTag 判断当前点击的视图是否为可点击,并通过 reactTag 找到对应的 View 对象触发 $AppClick 点击事件。

三、准备工作

3.1 创建项目

在实现 React Native 点击事件采集方案之前,我们首先创建一个演示项目。详细的安装步骤可以参考官网 environment-setup [4]部分,现在使用下面的命令创建一个 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 项目截图

3.2 集成神策分析

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. 将AwesomeProject.xcworkspace 打开(在 “ios 文件夹” 下),并在 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 事件。

3.3 创建 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.homepage     = "http://www.sensorsdata.cn"
  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 事件功能的关键步骤,下面来详细说明下代码的实现。

4.1 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];
    });
}

4.2 手动插入代码

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 触发的点击事件信息

4.3 自动插入代码

在上一节中,我们是手动插入了 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 事件的采集功能。

使用这种方案实现有如下优点:

但是这种方案也存在如下缺点:

在这里我们为了保证数据的准确性仍然使用此方案,并且在 Hook 代码中做了一定的代码保护,尽最大的努力减少数据埋点带来的风险性。

参考文献:

[1]https://reactnative.dev/docs/native-modules-setup

[2]https://manual.sensorsdata.cn/sa/latest/tech_sdk_client_three_react-7549534.html

[3]https://github.com/facebook/react-native/blob/master/Libraries/Components/Touchable/Touchable.js

[4]https://reactnative.dev/docs/environment-setup

上一篇下一篇

猜你喜欢

热点阅读