美文网首页
React Native 点击事件采集方案 | 数据采集

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

作者: 涅槃快乐是金 | 来源:发表于2023-05-29 23:43 被阅读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 系列组件中的四个组件都可以用来包装视图,从而响应用户的点击事件:

    • TouchableHighlight:在用户手指按下时背景会有变暗的效果;

    • TouchableNativeFeedback:在 Android 上可以使用 TouchableNativeFeedback,它会在用户手指按下时形成类似水波纹的视觉效果。注意,此组件只支持 Android;

    • TouchableOpacity:会在用户手指按下时降低按钮的透明度,而不会改变背景的颜色;

    • TouchableWithoutFeedback:响应用户的点击事件,如果你想在处理点击事件的同时不显示任何视觉反馈,使用它是个不错的选择。

    以上组件中前三者都是在 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 事件的采集功能。

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

    • 点击控件采集到的信息更准确(主要是 $screen_name 的准确性,这部分内容会在后续的 React Native 页面浏览全埋点方案中重点讲解);

    • 和 Native SDK 解耦,不再需要 Native SDK 配合 React Native Module 版本更新。

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

    • 对 React Native JavaScript 端源码进行改动,一定程度上会造成 React Native 代码的不稳定性。

    在这里我们为了保证数据的准确性仍然使用此方案,并且在 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

    相关文章

      网友评论

          本文标题:React Native 点击事件采集方案 | 数据采集

          本文链接:https://www.haomeiwen.com/subject/yfesedtx.html