HamronyOS开发5.0【埋点】方案讲解

news2024/9/21 18:41:45

大多数软件产品上线前,都会采用有规则的日志来对软件进行相关数据的采集,这个过程称为:[埋点],采集的数据主要用于产品分析。

埋点技术已在PC端, 移动端非常成熟,并且有大批量以此为生的公司。

本篇将探究一下HarmonyOS中的埋点,目标是统计用户浏览页面轨迹

准备

  1. 了解移动端的埋点技术方案
  2. 了解HarmonyOS页面生命周期

声明周期

先回顾一下有关页面显示的生命周期

UIAbility

在HarmonyOS中这个算是一个页面容器,因为它仅仅加载带@Entry装饰的自定义组件,几乎所有和业务相关的逻辑都是从自定义组件中触发。

这个容器共有四个状态,创建(Create),回到前台(Foreground), 回到后台(Background), 销毁(Destrory)

状态CreateForegroundBackgroundDestroy
API接口onCreateonForeground()onBackground()onDestroy()

被@Entry修饰的自定义组件

在HarmonyOS中的业务页面,实际上指的就是这个。这个对移动端的Web开发人员 ,React Native开发人员,Flutter开发人员比容易接受。

注意:这种自定义组件的生命周期,容易产生混淆

被 @Entry 修饰

总共有三个生命周期接口

  1. [onPageShow],页面每次显示时触发一次
  2. [onPageHide],页面每次隐藏时触发一次
  3. [onBackPress],当用户点击返回按钮时触发

被 @Component 修饰

  1. [aboutToAppear],组件即将出现时回调该接口
  2. [aboutToDisappear],自定义组件析构销毁之前执行

预研小结

  • 对于UIAbility的生命周期监测,可以监听事件‘[abilityLifecycle]‘事件,进而实现应用全局监测
  • 对于@Entry修饰的组件生命周期监测,目前还没有可统一监听的事件,只能手动在相应的方法中添加埋点

本篇探究的对象就是针对@Entry修饰的组件,实现生命周期的统一监听

探究

1)注解/装饰器方案

HarmonyOS 应用研发语言ArkTS,是基于TypeScript扩展而来,因此,理论上是可以自定义装饰器来完成对函数执行时的统计。

准备代码

定义一个统计方法
export function Harvey(params?: string) {
  return function(target:any, methodName:any, desc:any){

    console.log(params);

    console.log(JSON.stringify(target));
    console.log(JSON.stringify(methodName));
    console.log(JSON.stringify(desc));

  }
}
布局测试页面
......
//引入自定义方法装饰器文件
import { Harvey } from './HarveyEventTrack';


@Entry
@Component
struct RadomIndex {
 
  @Harvey('注解-aboutToAppear')
  aboutToAppear(){
     console.log('方法内-aboutToAppear')
  }

  @Harvey('注解-aboutToDisappear')
  aboutToDisappear(){
    console.log('方法内-aboutToDisappear')
  }

  @Harvey('注解-onPageShow')
  onPageShow(){
    console.log('方法内-onPageShow')
  }

  @Harvey('注解-onPageHide')
  onPageHide(){
    console.log('方法内-onPageHide')
  }

  @Harvey('注解-onBackPress')
  onBackPress(){
    console.log('方法内-onBackPress')
  }

  @Harvey('注解-build')
  build() {
     ......
  }

}

运行效果

日志分析

  1. 所有的生命周期上的装饰器方法全部跑了一遍,即 “注解-” 开头的日志
  2. 生命周期API最后运行,即 “方法内-” 开头的日志

5
6

结论

自定义装饰器没法满足统一埋点需求的

2)TypeScript AST

结论

这种方案暂时没有尝试成功

3) 脚本硬插入代码

这个方案比较原始,属于最笨的方法。

  • 适用编译场景: 打包机编译
  • 原因:编译前会直接修改源文件

大概流程如下 7

最终效果

8

9

尝试

创建埋点文件
  1. 在项目项目根目录下创建一个“Project”的文件夹
  2. Project文件夹下创建埋点文件

import common from '@ohos.app.ability.common';

export default class PageLifecycle{

  public static record(uiContext: common.UIAbilityContext, fileName: string,  funName: string){
    console.log('埋点:' + uiContext.abilityInfo.bundleName + ' -> ' + uiContext.abilityInfo.moduleName + ' -> '+
    uiContext.abilityInfo.name + ' -> ' + fileName + ' ' +
    '-> ' +
    funName)
  }

}
插入时机
  • entry 模块中的** hvigorfile.ts**
    注意: hvigorfile.ts 文件中提示文件不能修改,暂时不用去关心它
脚本代码
import * as fs from 'fs';
import * as path from 'path';

const INSERT_FUNCTION: string[] = [
  'aboutToAppear',
  'aboutToDisappear',
  'onPageShow',
  'onPageHide',
  'onBackPress',
]

const PAGELIFECYCLE_NAME = 'PageLifecycle.ets'

//开始复制埋点文件
copyConfigFile(process.cwd() + `/Project/${PAGELIFECYCLE_NAME}`, __dirname + `/src/main/ets/${PAGELIFECYCLE_NAME}`)

//遍历所有带@Entry装饰器的自定义组件
findAllPagesFiles(__dirname + '/src/main/ets/', __dirname + '/src/main/ets/', PAGELIFECYCLE_NAME);

/**
 * 文件遍历方法
 * @param filePath 需要遍历的文件路径
 */
function findAllPagesFiles(codeRootPath: string, filePath: string, configFileName: string) {
  // 根据文件路径读取文件,返回一个文件列表
  fs.readdir(filePath, (err, files) => {
    if (err) {
      console.error(err);
      return;
    }

    // 遍历读取到的文件列表
    files.forEach(filename => {

      // path.join得到当前文件的绝对路径
      const filepath: string = path.join(filePath, filename);

      // 根据文件路径获取文件信息
      fs.stat(filepath, (error, stats) => {

        if (error) {
          console.warn('获取文件stats失败');
          return;
        }

        const isFile = stats.isFile();
        const isDir = stats.isDirectory();
        if (isFile) {

          let checkPages: boolean = false

          let config: string = fs.readFileSync(__dirname + '/src/main/resources/base/profile/main_pages.json','utf8');

          let temps = JSON.parse(config)

          temps.src.forEach( (value) => {
            if(filepath.endsWith(value+'.ets') || filepath.endsWith(value+'.ts')){
              checkPages = true
              return
            }
          })

          if(!checkPages){
            return
          }

          fs.readFile(filepath, 'utf-8', (err, data) => {
            if (err) throw err;

            let content = (data as string)

            content = formatCode(content)

            //开始计算相对路径
            let tempFilePath: string = filepath.substring(codeRootPath.length+1)

            let slashCount: number = 0

            for(let char of tempFilePath){
              if(char == '/'){
                slashCount++
              }
            }

            //导入PageLife.ts文件
            if(configFileName.indexOf('.') != -1){
              configFileName = configFileName.substring(0, configFileName.indexOf('.'))
            }

            let importPath: string = 'import ' + configFileName + ' from ''
            for(let k = 0; k < slashCount; k++){
              importPath += '../'
            }

            importPath += configFileName + '''

            content = insertImport(content, importPath)

            //导入@ohos.app.ability.common
            content = insertImport(content, "import common from '@ohos.app.ability.common'", '@ohos.app.ability.common')

            content = insertVariable(content, "private  autoContext = getContext(this) as common.UIAbilityContext")

            INSERT_FUNCTION.forEach( value => {
              content = insertTargetFunction(content, value, `PageLifecycle.record(this.autoContext, '${filename}', '${value}')`)
            })

            fs.writeFile(filepath, content, (err) => {
              if (err) throw err;
            });

          });

        }

        if (isDir) {
          findAllPagesFiles(codeRootPath, filepath, configFileName);
        }

      });
    });
  });
}

/**
 * 复制埋点入口文件至目标地址
 *
 * @param originFile
 * @param targetFilePath
 */
function copyConfigFile(originFile: string, targetFilePath: string){
  let config = fs.readFileSync(originFile,'utf8');
  console.log(config)

  fs.writeFileSync(targetFilePath, config)
}


/**
 * 格式化代码,用于删除所有注释
 * @param inputContent
 * @returns
 */
function formatCode(inputContent: string): string{
  inputContent = deleteMulComments(inputContent)
  inputContent = deleteSingleComments(inputContent)
  return inputContent
}

/**
 * 删除多行注释
 * @param inputContent
 * @returns
 */
function deleteMulComments(inputContent: string): string{
  //删除注释
  let mulLinesStart = -1
  let mulLinesEnd = -1

  mulLinesStart = inputContent.indexOf('/*')

  if(mulLinesStart != -1){
    mulLinesEnd = inputContent.indexOf('*/', mulLinesStart)
    if(mulLinesEnd != -1){
      inputContent = inputContent.substring(0, mulLinesStart) + inputContent.substring(mulLinesEnd+'*/'.length)

      return deleteMulComments(inputContent)
    }
  }

  return inputContent
}

/**
 * 删除单行注释
 * @param inputContent
 * @returns
 */
function deleteSingleComments(inputContent: string): string{
  //删除注释
  let mulLinesStart = -1
  let mulLinesEnd = -1

  let splitContent = inputContent.split(/\r?\n/)

  inputContent = ''

  splitContent.forEach( value => {
    // console.log('输入 >> ' + value)

    let tempvalue = value.trim()

    //第一种注释, 单行后边没有跟注释
    // m = 6
    if(tempvalue.indexOf('//') == -1){
      if(tempvalue.length != 0){
        inputContent = inputContent + value + '\n'
      }
      //第二种注释,一整行都为注释内容
      //这是一个演示注释
    } else if(tempvalue.startsWith('//')){
      // inputContent = inputContent + '\n'
    } else {

      //第三种注释
      // m = 'h//' + "//ell" + `o` //https://www.baidu.com

      let lineContentIndex = -1

      let next: number = 0

      let label: string[] = []
      label.push(''')
      label.push("`")
      label.push(""")

      let shunxu: number[] = []

      while (true) {

        for(let k = 0; k < label.length; k++){
          let a = tempvalue.indexOf(label[k], next)
          let b = tempvalue.indexOf(label[k], a+1)
          if(a != -1 && b != -1){
            shunxu.push(a)
          }
        }

        //第四种注释
        // m = 2 //这是一个演示注释
        if(shunxu.length == 0){
          if(tempvalue.indexOf('//', next) != -1){
            inputContent = inputContent +  value.substring(0, value.indexOf('//', next)) + '\n'
          } else {
            inputContent = inputContent +  value.substring(0) + '\n'
          }
          break
        } else {

          //获取最先出现的
          let position = Math.min(...shunxu);
          let currentChar = tempvalue.charAt(position)
          let s = tempvalue.indexOf(currentChar, next)
          let e = tempvalue.indexOf(currentChar, s+1)

          if(s != -1 && e != -1 ){
            next = e + 1
          }

          while (shunxu.length != 0){
            shunxu.pop()
          }

        }

      }
    }

  })

  while (splitContent.length != 0){
    splitContent.pop()
  }
  splitContent = null

  return inputContent
}

function insertImport(inputContent: string, insertContent: string, keyContent?: string): string{
  let insertContentIndex: number = inputContent.indexOf(insertContent)

  if(keyContent){
    insertContentIndex = inputContent.indexOf(keyContent)
  }

  if(insertContentIndex == -1){
    inputContent = insertContent + '\n' + inputContent
  }

  return inputContent
}

function insertVariable(inputContent: string, insertContent: string): string{
  if(inputContent.indexOf(insertContent) == -1){
    let tempIndex = inputContent.indexOf('@Entry')
    tempIndex = inputContent.indexOf('{', tempIndex)
    inputContent = inputContent.substring(0, tempIndex+1) + '\n'  + insertContent + '\n' + inputContent.substring(tempIndex+1)
  }

  return inputContent
}

function insertTargetFunction(inputContent: string, funName: string, insertContent: string): string{
  let funNameIndex: number = inputContent.indexOf(funName)

  if(funNameIndex != -1){
    let funStartLabelIndex: number = inputContent.indexOf('{', funNameIndex)

    let funEndLabelIndex: number = findBrace(inputContent, funStartLabelIndex).endIndex

    if(funEndLabelIndex != -1){
      let funContent: string = inputContent.substring(funStartLabelIndex, funEndLabelIndex)

      let insertContentIndex: number = funContent.indexOf(insertContent)

      if(insertContentIndex == -1){
        inputContent = inputContent.substring(0, funStartLabelIndex+1)
        + '\n'
        + insertContent
        + '\n'
        + inputContent.substring(funStartLabelIndex+1)
      }
    }

  } else {

    let findEntryIndex = inputContent.indexOf('@Entry')

    findEntryIndex = inputContent.indexOf('{', findEntryIndex)

    let codeEndIndex = findBrace(inputContent, findEntryIndex).endIndex

    if(codeEndIndex != -1){

      inputContent = inputContent.substring(0, codeEndIndex)
      + '\n'
      + funName +'(){'
      + '\n'
      + insertContent
      + '\n'
      + '}'
      + '\n'
      + inputContent.substring(codeEndIndex)

    } else {
      throw Error('解析错误')
    }
  }

  return inputContent
}


function findBrace(inputContent: string, currentIndex: number): BraceIndex{
  let computer: BraceIndex = new BraceIndex()
  computer.startIndex = currentIndex

  let count: number = 0

  if(currentIndex != -1){
    count++
    currentIndex++
  }

  let tempChar: string = ''

  while(count != 0){

    tempChar = inputContent.charAt(currentIndex)

    if(tempChar == '}'){
      count--
    } else if(tempChar == '{'){
      count++
    }

    if(count == 0){
      computer.endIndex = currentIndex
      break
    }

    currentIndex++
  }

  return computer

}

class BraceIndex{
  public startIndex: number = 0
  public endIndex: number = 0
}

以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线,展示如下:
1

除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下

内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!

鸿蒙【北向应用开发+南向系统层开发】文档

鸿蒙【基础+实战项目】视频

鸿蒙面经

在这里插入图片描述

为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2043591.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32—RTC实时时钟

1.Unix时间戳 Unix时间戳定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数&#xff0c;不考虑闰秒 时间戳存储在一个秒计数器中&#xff0c;秒计数器为32位/64位的整形变量 世界上所有时区的秒计数器相同&#xff0c;不同时区通过添加偏移来得到当地时间 2.时间戳转…

函数(子程序)的常见、易混淆概念详解【对初学者有帮助】

C语⾔中的函数也被称做子程序&#xff0c;意思就是⼀个完成某项特定的任务的⼀小段代码。 C语⾔标准中提供了许多库函数&#xff0c;点击下面的链接可以查看c语言的库函数和头文件。 C/C官⽅的链接&#xff1a;https://zh.cppreference.com/w/c/header 目录 一、函数头与函…

VsCode配置Cph实现高效刷题教程

cph作用 : 自动创建文件自动获取题目案例自动测试样例自动配置模板 在vscode中安装cph插件 : 在扩展的搜素框中输入Competitive Programming Helper(cph)&#xff0c;点击下载即可 在浏览器中安装Competitive Companion 浏览器插件 这里推荐离线下载 : 网址 : Competit…

2024/8/15 不上电测伺服端子是否正常

拿3线220V举例&#xff0c;拿两种测量表举例&#xff0c;下图均为正常情况 L1和L2测量&#xff0c;L3不用管&#xff08;空的&#xff09;。 1.先测输入L1/2是否短路&#xff0c;输出UVW是短路为正常&#xff08;与变频器相反&#xff09; 2.正&#xff08;红&#xff09;—RS…

PL/SQL是什么软件 PL/SQL最新版本功能介绍

PL/SQL是什么软件&#xff1f;PL/SQL软件多指PL/SQL Developer&#xff0c;这是一款专业的PL/SQL开发工具&#xff0c;它可以帮助开发者编写、调试和优化PL/SQL代码&#xff0c;提高开发效率和质量。本文将介绍PL/SQL Developer 15最新版本的主要功能和特点。 一、PL/SQL是什么…

华为od统一考试B卷【比赛】python实现

def split_params(param_str): return list(map(int, param_str.split(,))) def main(): # 获取输入 target_str input().strip() # 输入验证&#xff0c;拆分并转换为整数 try: m, n split_params(target_str) except ValueError: print(-1) return # 检查 M 和 …

opencascade Adaptor3d_Curve源码学习

opencascade Adaptor3d_Curve 前言 用于几何算法工作的3D曲线的根类。 适配曲线是曲线提供的服务与使用该曲线的算法所需服务之间的接口。 提供了两个派生具体类&#xff1a; GeomAdaptor_Curve&#xff0c;用于Geom包中的曲线Adaptor3d_CurveOnSurface&#xff0c;用于Geom包…

时钟缓冲器的相关知识

时钟缓冲器是比较常用的器件&#xff0c;其主要功能作用有时钟信号复制&#xff0c;时钟信号格式转换&#xff0c;时钟信号电平转换等。我们下面简单了解下&#xff1a; 1.时钟信号复制 例如ICS553芯片&#xff0c;其将单路输入时钟信号复制4份进行输出&#xff0c;输出信号具…

CSS相关修改样式、伪类样式

一、css颜色 1.颜色表示法&#xff1a; 直接以单词来表示颜色&#xff0c;如red&#xff0c;green。 2.十六进制表示法&#xff1a;&#xff08;常用&#xff09; 以#开头的6位十六进制数&#xff0c;如#000000&#xff08;#000&#xff09;。 3.RGB三原色表示法&#xff…

Spark数据倾斜解决产生原因和解决方案

1、提高shuffle操作的并行度 在对RDD执行shuffle算子时&#xff0c;给shuffle算子传入一个参数&#xff0c;比如reduceByKey(1000)&#xff0c;该参数就设置了这个shuffle算子执行 时shuffle read task的数量&#xff0c;即Spark.sql.shuffle.partitions&#xff0c;该参数代表…

AI/机器学习(计算机视觉/NLP)方向面试复习5

目录 1. GNN graph neural network 2. 0-1背包问题 3. 0-1背包问题&#xff08;一维dp&#xff09; 4. 螺旋矩阵 按顺时针顺序返回所有数 5. fasttext与glove 1. GNN graph neural network &#xff08;1&#xff09;图的基本定义 GNN的Roadmap&#xff1a;其中用的最常见…

SD卡电路设计基础

一、定义 SD卡按尺寸分类可分为三类:标准 SD 卡、Mini SD 卡和 Micro SD 卡。其中Mini SD 卡比较少见&#xff0c;标准 SD 卡因为体积较大主要用在数码相机等对体积要求不严格的地方,我们最常用的是 Micro SD 卡&#xff0c;原名Trans-flash Card (TF 卡)。 Micro SD 作用:一…

★ C++基础篇 ★ 栈和队列

Ciallo&#xff5e;(∠・ω< )⌒☆ ~ 今天&#xff0c;我将继续和大家一起学习C基础篇第八章----栈和队列 ~ 目录 一 容器适配器 二 deque的简单介绍 2.1 deque的原理介绍 2.2 deque vector list 的优缺点 2.2.1 vector 2.2.2 list 2.2.3 deque 2.3 为什么选择deq…

ETL数据集成丨PostgreSQL数据迁移至Hive数据库

PostgreSQL数据迁移至Hive数据库 在现代企业数据架构中&#xff0c;将数据从关系型数据库如PostgreSQL迁移到分布式数据仓库系统如Hive&#xff0c;是一项至关重要的任务&#xff0c;旨在实现数据的高效存储、处理与分析。这一过程不仅涉及技术层面的操作转换&#xff0c;还深…

unity项目打包为webgl后应用于vue项目中(iframe模式)的数据交互

参考文章&#xff1a; 1.Unity打包WebGL: 导入Vue 2.unity文档-WebGL&#xff1a;与浏览器脚本交互 3.unity与vue交互(无第三方插件&#xff09; 目录 一、前期工作1.新建.jslib文件2.新建.cs脚本3. 新建一个Text对象和button按钮对象4.添加脚本空对象UIEvent5.导出unity为w…

SpringBoot-配置加载顺序

目录 前言 样例 内部配置加载顺序 ​ 样例 小结 前言 我之前写的配置文件&#xff0c;都是放在resources文件夹&#xff0c;根据当前目录下&#xff0c;优先级的高低&#xff0c;判断谁先被加载。但实际开发中&#xff0c;我们写的配置文件并不是&#xff0c;都放…

利用CICD管道和MLOps自动化微调、部署亚马逊云科技上的AI大语言模型

项目简介&#xff1a; 小李哥将继续每天介绍一个基于亚马逊云科技AWS云计算平台的全球前沿AI技术解决方案&#xff0c;帮助大家快速了解国际上最热门的云计算平台亚马逊云科技AWS AI最佳实践&#xff0c;并应用到自己的日常工作里。 本次介绍的是如何在亚马逊云科技利用CodeP…

DeepLearning.AI课程:从代码层面理解预训练大语言模型(Pretraining LLMs)

本文是学习 https://www.deeplearning.ai/short-courses/pretraining-llms/ 这门课的学习笔记。 What you’ll learn in this course In Pretraining LLMs you’ll explore the first step of training large language models using a technique called pretraining. You’ll …

如何从Mac 电脑恢复已删除的文件

您是否曾经不小心从Mac中删除了文件或文件夹&#xff0c;然后后来意识到您确实需要它&#xff1f;或者你有没有清空过你的垃圾桶&#xff0c;片刻后才意识到你不小心也从那里删除了一些重要文件&#xff1f;如果是&#xff0c;那么这篇博文就是为你准备的&#xff01; 今天&am…

书籍分享:【矩阵力量】豆瓣评分高达9.6,看完感叹《矩阵论》又白学了

书籍分享&#xff1a;【矩阵力量】豆瓣评分高达9.6&#xff0c;看完感叹《矩阵论》又白学了 《矩阵力量》简要介绍书籍下载链接 《矩阵力量》简要介绍 《矩阵力量》是姜伟生精心编写的线性代数的深度理解之作&#xff0c;作者将抽象的线性代数概念用通俗易懂的语言和大量生动形…