鸿蒙OS开发实例:【埋点探究】

背景

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

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

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

准备

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

声明周期

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

UIAbility

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

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

状态 Create Foreground Background Destroy
API接口 onCreate onForeground() onBackground() onDestroy()

被@Entry修饰的自定义组件

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

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

被 @Entry 修饰

总共有三个生命周期接口

  1. onPageShow\],页面每次显示时触发一次

  2. onBackPress\],当用户点击返回按钮时触发

  3. aboutToAppear\],组件即将出现时回调该接口

预研小结

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

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

探究

1)注解/装饰器方案

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

TypeScript装饰器\]可以了解一下:[《鸿蒙NEXT星河版开发学习文档》](https://docs.qq.com/doc/DUlNzQU5CV1JmUGlG "《鸿蒙NEXT星河版开发学习文档》") #### 准备代码 ##### 定义一个统计方法 ```cpp 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)); } } ``` ##### 布局测试页面 ```cpp ...... //引入自定义方法装饰器文件 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最后运行,即 "方法内-" 开头的日志 ![](https://file.jishuzhan.net/article/1774961937252093953/eeee7982c2be092507ef5e360608f14a.webp) ![](https://file.jishuzhan.net/article/1774961937252093953/7819fb1bf92af7295f6ea9aadc031ce3.webp) #### 结论 **自定义装饰器没法满足统一埋点需求的** ### 2)TypeScript AST #### 结论 **这种方案暂时没有尝试成功** #### 相关链接 ### 3) 脚本硬插入代码 这个方案比较原始,属于最笨的方法。 * 适用编译场景: 打包机编译 * 原因:编译前会直接修改源文件 大概流程如下 ![](https://file.jishuzhan.net/article/1774961937252093953/81aff860b2ed8dc0d5c7a12472be9006.webp) #### 最终效果 ![](https://file.jishuzhan.net/article/1774961937252093953/fabd781a1e9dbc81495ebf654bc84aab.webp) ![2xw.png](https://file.jishuzhan.net/article/1774961937252093953/c1e1e6bdf416b07f0cb8f2bcbfd67f13.webp) #### 尝试 ##### 创建埋点文件 1. 在项目项目根目录下创建一个"Project"的文件夹 2. Project文件夹下创建埋点文件 ```cpp 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 文件中提示文件不能修改,暂时不用去关心它 ##### 脚本代码 ```cpp 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 } ```

相关推荐
坚果的博客3 小时前
坚果派已适配的鸿蒙版flutter库【持续更新】
flutter·华为·开源·harmonyos
tianchang3 小时前
TS入门教程
前端·typescript
NapleC4 小时前
HarmonyOS:Navigation实现导航之页面设置和路由操作
华为·harmonyos·navigation
HMSCore4 小时前
HarmonyOS SDK助力鸿蒙版今日水印相机,真实地址防护再升级
harmonyos
胖方Hale10 小时前
04. Typescript 数组类型
前端·typescript
风中飘爻10 小时前
鸿蒙生态:鸿蒙生态校园行心得
华为·harmonyos
Otaku_尻男10 小时前
HarmonyOS 自定义RenderNode 绘图实战
android·面试·harmonyos
胖方Hale10 小时前
01. Typescript 基础数据类型
前端·typescript
Kjjia10 小时前
考试过程中校园网突然发力,答案没能保存...我炸了
前端·typescript