鸿蒙编译时 AOP实践之路

一、 AspectPro Plugin是什么?

aspect-pro-plugin是一款轻量级的鸿蒙编译时AOP 插件。

  • 让你的应用3分钟支持鸿蒙编译时aop能力。
  • 支持ets、ts、js 语法解析。
  • 支持自定义配置规则 (参考aspectProPluginConfig.txt)。
  • 支持replace自动导包。
  • 丰富插桩demo示例 (函数耗时、函数替换、隐私函数调用检测、装饰器函数...)。

二、为什么要继续开发 AspectPro Plugin?

24年8月我们随着AspectPro 一起开源了aspect-pro-plugin1.0 版本, 此版本在稳定性、兼容性、效率 以及能力上都有很大优化空间, 因此我们继续开发了V2版本。

##### PK ##### AspectProV1 ##### AspectProV2
易用性 中,需要开发plugin ,无需开发plugin
稳定性 低,基于正则 ,基于语义解析
兼容性 ,基于公开api 中,基于非公开api
效率 低,基于源码需还原 ,基于编译中间结果,无需还原
精准度 中,基于文件和正则 ,基于文件和语义
AST**** 不支持,基于源代码 支持,基于sourcefile

还没看过AspectPro的同学,可以先补下背景知识...

Github链接:github.com/HuolalaTech...

文章链接:juejin.cn/post/740326...

三、AspectPro Plugin前言篇

在方案实践前,我们充分调研了鸿蒙AOP 当前的的能力,进而梳理出影响方案选型的关键信息。

关键点

  • 鸿蒙暂未对外开放ArkTs对应的AST工具, 因此ArkTs部分语法无法直接解析。
  • 修改CompileArkTs的build产物, 无法生效于方舟字节文件。
  • Hvigor 开放的 api 有限,无法通过公开直接拿到ets_loader 处理后的内容。
  • 基于transformLib直接修改方舟字节码,上手难度较大且暂无相关demo。

基于上述调研结论且根据已知的ArkTs兼容Ts 生态。

我们确定了方案实践思路:

思路

方案还存在一个难点需要攻坚:

攻坚点

  • Hvigor 自定义Plugin中如何获取 "符合TS语法规范的 ETS文件? "

四、AspectPro Plugin实践篇

整个方案实践围绕着一个攻坚点展开, 经历了多次探索,一度深陷其中,为了大家少走弯路,我们先看结论....

4.1 那些我们实践过的方案

##### 方案 ##### 方案原理 ##### 是否推荐 ##### 备注
强转ETS 1.代码经常2次变动,基于代码diff & 相似度对比, 回滚中间的一次变动。 不推荐稳定性差 难精准处理代码结构变动大:
修改编译工具ets_loader 1.ets_loader负责编译ArkTs源码2.可插入自定义的transform 不推荐修改编译工具影响范围大 实现transform:升级IDE后失效:
自定义compile plugin 1.使用OhosModuleContextImpl非公开api在compile阶段插入自定义逻辑 (loadCompilePlugin)2.在compile plugin中 获取ets_loader中this.share对象,进而获取 当前推荐基于非公开api,影响范围小 此方案是和华为官方沟通后得出。需要说明:目前hvigor官方未对外提供 ast 解析能力,此方案非官方的
transformLLib ~~~~ 修改方舟字节码 -~~~~暂无尝试~~~~- ~~推荐~~~~公开~~~~api~~~~~~ -~~~~期待大家尝试~~~~-

基于当前现状,我们决定采用自定义compile plugin 方案,

同时为了降低使用成本我们对compile plugin进一步封装,开放了 "aspect-pro-plugin": "2.0.0"

基于此plugin 只需3分钟即可让你的应用支持Aop能力。

...想快速看效果的,可以直接看第六节 : "AspectPro Plugin 使用篇"

4.2 Compile Plugin 方案实现

首先声明:

  • 目前hvigor官方未对外提供ast解析能力,此方案虽然是和官方同学多次沟通后实现的,虽不是官方方案.但已是我们不断尝试后当前可落地的最佳实践。

方案原理:

利用Hvigor非公开api 在ets_loader -> ark compile 编译成abc 中间, 插入sourcefile的 compile plugin 。

方案实践步骤:

  • step 1: 定义你的Hvigor Plugin 并在entry/hvigorfile.ts中应用
javascript 复制代码
// 开发阶段 - 建议依赖local plugin(鸿蒙的一个module) 效率更高 
 import { aspectProPluginV2 } from '../local_plugin/src/main/AspectProPluginV2';
 export default {
   system: hapTasks,
   plugins: [aspectProPluginV2('../local_plugin/src/main/compile/AspectCompilePlugin.ts')]
 }
  • step 2: AspectProPluginV2中通过OhosHapContext.loadCompilePlugin( AspectCompilePlugin)
javascript 复制代码
/**
 * @param yourPluginAbsPath 自定义编译插件文件相对路径(相对于HllHvigorPlugin)
 * (本示例文件结构则传:'../local_plugin/src/main/compile/AspectCompilePlugin.ts')
 */
export  function  aspectProPluginV2(yourPluginAbsPath:string): HvigorPlugin {
  return {
    pluginId: 'aspectProPluginV2',
    async apply(node: HvigorNode): Promise<void> {
      hvigor.nodesEvaluated(async () => {
        const hapContext = node.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosHapContext;
        hapContext?.loadCompilePlugin(yourPluginAbsPath);
      });
    }
  };
}
  • step 3: 在AspectCompilePlugin中通过ets_loader中的this.share获取到ets转化为ts的sourcefile
ini 复制代码
const sourceFiles = this.share.getSourceFiles();
  • step 4: 获取ets_loader中的ts对象,按你的aop需求 利用ts compiler aip 修改sourcefile
csharp 复制代码
 //@ts-ignore
let ts = require(path.join(this.share.projectConfig.etsLoaderPath, 
    'node_modules', 'typescript'));
    
// 按需依次处理:此处演示多个AOP插桩逻辑 (可简单理解为:Android apply plugin)
updateSourcefile = SlowMethodAopImp.doTransform(ts, ModuleSourceFile.source, modulePath)
updateSourcefile = ReplaceMethodAopImp.doTransform(ts, updateSourcefile, modulePath)
// ...
  • step 5: 将修改后的sourcefile赋值给this.share.sourcefile, 系统编译工具执行编译生成产物
ini 复制代码
 ModuleSourceFile.source = updateSourcefile;

以上是方案实现的核心流程,完整实现方案源码我们也开源,有兴趣的可以看第八节 : "相关链接"

五、AspectPro Plugin展示篇

基于我们这套方案可快速实现 Android gradle plugin + ASM 相关功能,比如:

场景一:函数执行耗时统计

伪代码:

javascript 复制代码
/**
 * AOP插入代码统计方法执行耗时
 * step一:
 *  函数开头插入 const startTime = Date.now();
 * step 二:
 *  函数结束插入 Logger.w(tag, `${methodId} took ${(Date.now() - startTime)} ms`)
 */
export  class SlowMethodTransform {
  static doTransform(ts) {
    return (context) => {
      return (sourceFile) => {
        const visit = (node) => {
          if (!MethodTransformApi.supportsFunctions(node, ts)) {
            return ts.visitEachChild(node, visit, context);
          }
          if (MethodTransformApi.isSimpleOrEmptyMethod(node, ts)) {
            return ts.visitEachChild(node, visit, context);
          }
         let updatedNode = this.generateUpdatedMethod(node, ts, sourceFile);
          return ts.visitEachChild(updatedNode, visit, context);
        };
        let updatedSourceFile = ts.visitNode(sourceFile, visit);
        return ImportTransformApi.addImportStatement(updatedSourceFile, ts, importName, importPath);
      };
    };
  }

效果:

场景二:函数调用替换

伪代码:

typescript 复制代码
/**
 * AOP Transform : 方法调用替换
 * step 一:
 *  遍历找到函数调用, 比如router.pushUrl
 * step 二:
 *  将目标函数替换, 比如将router.pushUrl替换为 this.getUIContext().getRouter().pushUrl
 */
export  class  AspectProTransform {  static  doTransform ( ts, allReplaceRules: ReplaceRule[] ) {  return  ( context ) => {  return  ( sourceFile ) => {  const importsToAdd = new  Set <string>();  const  visit = ( node ) => {  if (!node) {  return node; }  let updatedNode = node;  for ( let rule of allReplaceRules) {  if (!rule) {  continue ; }  if ( this . isFunctionCallMatch (node, rule. pattern , ts)) { updatedNode = this . replaceFunctionCall (node, rule. pattern , rule. replacement , ts);  if (rule. imports ) {  for ( let imp of rule. imports ) { importsToAdd. add (imp); } } } }  return ts. visitEachChild (updatedNode, visit, context); };   let updatedSourceFile = ts. visitNode (sourceFile, visit);  for ( let imp of importsToAdd) {  if (!imp || typeof imp !== 'string' || !imp. includes ( SPLIT_FLAG ) || imp. split ( SPLIT_FLAG ). length !== 2 ) {  return updatedSourceFile; }   const [importName, importPath] = imp. split ( SPLIT_FLAG );  updatedSourceFile =  ImportTransformApi . addImportStatement (updatedSourceFile, ts, importName, importPath); }   return updatedSourceFile; }; }; } 

效果:

场景三:特定函数修改('装饰器')

伪代码:

javascript 复制代码
/**
 * AOP Transform : 修改"特定装饰器修饰"的方法
 * step 一:
 *  遍历找到函数调用,找到"特定装饰器修饰"
 * step 二:
 *  修改函数 (本例子仅:增加日志)
 */
export  class DescriptorMethodTransform {
  static doTransform(ts) {
    return (context) => {
      return (sourceFile) => {
        const visit = (node) => this.myVisitNode(ts, node, context);

        let updatedSourceFile = ts.visitNode(sourceFile, visit);
        return ImportTransformApi.addImportStatement(updatedSourceFile, ts, importName, importPath);
      };
    };
  }

  static myVisitNode(ts, node, context) {
    if (ts.isClassDeclaration(node) && this.hasDecorator(node, MY_CLASS_DESCRIPTOR, ts)) {
      // 遍历类的方法,查找是否有AUTO_CATCH_DESCRIPTOR装饰器的方法
      const newMembers =
        ts.factory.createNodeArray(node.members.map(member => this.aopWhenMethodDeclarationMatch(ts, member)));
      return ts.factory.updateClassDeclaration(
        node,
        node.modifiers,
        node.name,
        node.typeParameters,
        node.heritageClauses,
        newMembers
      );
    }
    return ts.visitEachChild(node, (childNode) => this.myVisitNode(ts, childNode, context), context);
  }

  static aopWhenMethodDeclarationMatch(ts, member) {
    if (ts.isMethodDeclaration(member)) {
      if (this.hasDecorator(member, AUTO_CATCH_DESCRIPTOR, ts)) {
        return  this.addConsoleDebugStatement(ts, member);
      }
    }
    return member;
  }

  static hasDecorator(node, decoratorName, ts) {
    return node.modifiers && node.modifiers.some(modifier => {
      let expression = modifier.expression;
      if (ts.isCallExpression(modifier.expression)) {
        expression = modifier.expression.expression;
      }
      return ts.isIdentifier(expression) && expression.text === decoratorName;
    });
  }  }

效果:

场景四:隐私api监控

伪代码:

typescript 复制代码
/**
 * AOP Transform : 查找隐私Api调用
 * step 一:
 *  遍历找到隐私函数调用, 比如hilog.fatal (此处仅演示 - 因为demo依赖的Logger中调用了此aip)
 * step 二:
 *  打印出函数堆栈信息
 */
export  class PrivacyMethodTransform {
  static doTransform(ts, allReplaceRules: ReplaceRule[]) {
    return (context) => {
      return (sourceFile) => {
        const visit = (node, curClassName, curMethodName) => {
          if (!node) {
            return node;
          }

          if (ts.isClassDeclaration(node) && node.name && node.name.getText) {
            curClassName = node.name.getText();
          } else  if (ts.isMethodDeclaration(node) && node.name && node.name.getText) {
            curMethodName = node.name.getText();
          }

          for (let rule of allReplaceRules) {
            if (!rule) {
              continue;
            }
            if (this.isFunctionCallMatch(node, rule.pattern, ts)) {
              const { line } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart());

              const className = curClassName || 'UnknownClass';
              const methodName = curMethodName || 'UnknownMethod';
              console.log(`PrivacyMethodTransform-> [Found privacy API <${rule.pattern}>call in ${className}.${methodName} at line:${line + 1}]`);
            }
          }

          return ts.visitEachChild(node, (childNode) => visit(childNode, curClassName, curMethodName), context);
        };
        return ts.visitNode(sourceFile, (node) => visit(node, undefined, undefined));
        ;
      };
    };
  }

  static isFunctionCallMatch(node, pattern: string, ts): boolean {
    if (!node || !pattern || typeof pattern !== 'string' || !pattern.includes(SPLIT_FLAG)) {
      return  false;
    }
    const patternParts = pattern.split(SPLIT_FLAG);
    if (patternParts.length < 2) {
      return  false;
    }
    try {
      if (ts.isCallExpression(node)) {
        if (!node.expression) {
          return  false;
        }

        const expr = node.expression;
        if (ts.isPropertyAccessExpression(expr) && expr.expression
          && ts.isIdentifier(expr.expression) && expr.name
          && ts.isIdentifier(expr.name)) {
          const isMatch = expr.expression.getText() === patternParts[0] && expr.name.getText() === patternParts[1];
          return isMatch;
        }
      }
    } catch (e) {
      return  false;
    }
    return  false;
  }
}

效果:

相关配置说明

ruby 复制代码
# 配置规则说明 - plugin 按行读取配置文件 (默认读取hvigor-file同级目录 所有.js 、.ts 、 .ets文件)
# -hook path | file : 配置需要hook 处理的文件目录 | 文件
# -keep path | file : 配置需要额外 keep 的目录 | 文件  (非必需, 当-hook 的文件目录中有一些特殊文件,不需要处理时,配合使用)
# -target pattern replacement [import xxx';import xxx] : 配置替换的原始函数和对目标函数 [import aaa;import bbb] 同时需要新导入的依赖

# 比如:
#-hook ./src/main/ets/test/TestClass1.ts
-hook ./src/main/ets/pages/Index.ets
#-hook ./src/main/ets/entryability/EntryAbility.ets
-keep ./src/main/ets/hook/
#-target router:pushUrl this.getUIContext().getRouter():pushUrl
-target router:pushUrl this.getUIContext().getRouter():pushUrl [-import Logger:@huolala/logger/src/main/com.wp/Logger]

# 支持三方库代码替换 (需要注意三方库的路径)
#-hook ../oh_modules/.ohpm/@huolala+logger@1.0.0/oh_modules/@huolala/logger/src/main/com.wp/Logger.ts
#-target Logger:i Logger:e

六、AspectPro Plugin 使用篇

只需三步让你的鸿蒙应用支持 Aop能力

第一步:

javascript 复制代码
1.添加并使用aspect-pro-plugin插件
  1.1 在工程hvigor-package.json文件中添加
"dependencies": {
    "aspect-pro-plugin": "2.0.0"
  }
  
  1.2 在entry或其他模块的 hvigorfile.ts文件中使用
import { aspectProPluginV2 } from 'aspect-pro-plugin';
export default {
  system: hapTasks, 
  plugins: [aspectProPluginV2(require.resolve('aspect-pro-plugin'))]
}

第二步:

bash 复制代码
2.在工程目录下创建aop/aopConfig.json文件, 配置你的aop实现类的绝对路径
{
  "aopConfigs": [
    {
      "name": "YourSlowMethodAop",
      "path": "/Users/xxx/HarmonyOs/openSource/AspectPro/entry/src/main/yourAop/YourSlowMethodAop.ts"
    },
    {
      "name": "YourEmptyAop",
      "path": "./src/main/yourAop/YourSlowMethodAop.ts"
    }
  ]
}

第三步:(如果之前未使用过Ts Compile Api - 可以先参考源码快速尝鲜,再看第7节 扩展篇,一步步实现)

typescript 复制代码
3.在你的YourSlowMethodAop.ts这个实现 doTransform()方法实现你的aop插桩逻辑并导出
export class YourSlowMethodAop {
  /**
   * 3.1 此方法必须按下述格式实现
   * @param ts         鸿蒙ets_loader中的ts对象
   * @param sourcefile 鸿蒙ets_loader处理后的 ts sourcefile对象
   * @param modulePath 当前工程路径
   * @returns          sourcefile
   */
  static doTransform(ts, sourcefile, modulePath: string) {
    try {
      // TOOD 实现你的插桩逻辑
      if (sourcefile.fileName.includes("EntryAbility.ets")) {
        console.log(`YourSlowMethodAop -> doTransform() ----> 开始处理目标文件:${sourcefile.fileName}`);
        let result = ts.transform(sourcefile, [YourSlowMethodTransform.doTransform(ts)]);
        return result.transformed[0];
      }
    } catch (e) {
      console.log(`YourSlowMethodAop -> doTransform()  exp:${e} ,sourcefile:${sourcefile.fileName}`);
    }
    return sourcefile;
  }
}

// 3.2 必须导出, plugin 内部使用require方式import
//@ts-ignore
module.exports = YourSlowMethodAop;

是不是很简单:

至此,你的应用就有了 AOP能力...

七、Transform 扩展篇

考虑到部分同学可能对Ts Compile Api 比较陌生, 因此接下来一步步介绍使用步骤,方便大家快速上手

Step 1 : transform流程介绍

Transform流程: AST(Sourcefile) -> Ts Compile Api <visit、update、transform> -> updateSourceFile

AopTransform步骤 关键 API 作用
1.创建 Program ts.createProgram() 管理编译上下文和依赖
2.获取 AST program.getSourceFile() 获取文件对应的 AST
3.遍历 AST ts.visitEachChild() 递归访问每个节点
4.修改节点 ts.factory.updateXxx() 修改节点(增、删、改)
5.执行变换 ts.transform() 源码 应用变换生成新 AST
6.获取新AST transformResult.transformed0 获取变化后的 AST

Step 2 : transform难点分析

上述流程只有第4点 是依赖具体Aop逻辑 "动态"变化的, 其他都是标准操作, 因此关键是掌握修改节点。

咱们都熟悉 TS 语法,所以可以很熟练的进行日常开发: 编写源码(ts) -> 编译器经过<扫描器、Token流、解析器> 生成 -> AST(Sourcefile)

而直接修改AST这种"逆向编码 "的语法并不熟悉且不同场景Aop逻辑又是 "动态"变化的 ,因为上手难

解决方案:

Step 3 : YourSlowMethodTransform.doTransform实现

scala 复制代码
// 1.ts.transform
export function transform<T extends Node>(source: T | T[], 
transformers: TransformerFactory<T>[], 
compilerOptions?: CompilerOptions): TransformationResult<T> {}

// 2.TransformationResult
export interface TransformationResult<T extends Node>

// 3.TransformerFactory
export type TransformerFactory<T extends Node> = (context: TransformationContext) => Transformer<T>;
export type Transformer<T extends Node> = (node: T) => T;

3.1 看了上面源码大家应该可以理解, ****doTransform函数返回一个 TransformerFactory类型的函数即可

typescript 复制代码
static doTransform(ts, sourcefile, modulePath: string) {
  return (context: ts.TransformationContext) => {
    return (sourceFile: ts.SourceFile) => {
      // 实际转换逻辑
      return sourceFile;
    };
  };

3.2 实际转换逻辑分2步 遍历和修改 , 可参考TS源码中的 customTransforms.ts

  • 递归遍历每个node节点
  • 判断节点类型SyntaxKind,如果是你的目标类型,则进行下一步修改操作
javascript 复制代码
const before: ts.TransformerFactory<ts.SourceFile> = context => {
        return file => ts.visitEachChild(file, visit, context);
        function visit(node: ts.Node): ts.VisitResult<ts.Node> {
            switch (node.kind) {
                case ts.SyntaxKind.FunctionDeclaration:
                    return visitFunction(node as ts.FunctionDeclaration);
                default:
                    return ts.visitEachChild(node, visit, context);
            }
        }
        function visitFunction(node: ts.FunctionDeclaration) {
            ts. addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, "@before", true);
            return node;
        }
    };

3.3 具体怎么修改了? 比如ts.addSyntheticLeadingComment 我怎么知道调哪些api实现修改?

利用 TypeScript ****AST Viewer 生成需要插入代码的AST语句

3.4 首先:以最简单的代码插入 & 修改举例

scss 复制代码
onForeground(): void {
    Logger.d(TAG, "1.onForeground->a()  method invoked");
}
  1. 我们先实现插入const startTime = Date.now();
ini 复制代码
function visit(node) {
  if (ts.isMethodDeclaration(node) && node.name && node.name.text === 'onForeground') {
    // 1.直接copy TypeScript AST Viewer 生成的代码
    const timeStampStatement = insertTimeStamp(ts, "startTime");
    // 2.将生成的代码插入到函数体中
    const newStatements = ts.factory.createNodeArray([timeStampStatement, ...node.body!.statements]);
    const newBody = ts.factory.updateBlock(node.body, newStatements);
    // 3.最后调用ts.updateXXX 更新即可
    return ts.factory.updateMethodDeclaration(
      node, node.decorators, node.modifiers, node.asteriskToken,
      node.name, node.questionToken, node.typeParameters, node.parameters, node.type, newBody
    );
  }
  return ts.visitEachChild(node, visit, context);
}
  1. 进一步 :先实现方法修改Logger.d修改为Logger.w
ini 复制代码
function visit(node) {
  if (ts.isMethodDeclaration(node) && node.name && node.name.text === 'onForeground') {
    // 1.直接copy TypeScript AST Viewer 生成的代码
    const timeStampStatement = insertTimeStamp(ts, "startTime");
    // 2.修改 Logger.d 为 Logger.w
    const updateStatements = updateLoggerD2W(node, ts);
    // 3.将生成的代码插入到函数体中
    const newStatements = ts.factory.createNodeArray([timeStampStatement, ...updateStatements]);
    const newBody = ts.factory.updateBlock(node.body, newStatements);
    // 4.最后调用ts.updateXXX 更新即可
    return ts.factory.updateMethodDeclaration(
      node, node.decorators, node.modifiers, node.asteriskToken,
      node.name, node.questionToken, node.typeParameters, node.parameters, node.type, newBody
    );
  }
  return ts.visitEachChild(node, visit, context);
}
function updateLoggerD2W(node, ts) {
  return node.body.statements.map(statement => {
    if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
      const callExpr = statement.expression;
      if (ts.isPropertyAccessExpression(callExpr.expression) && callExpr.expression.name.text === 'd' &&
        callExpr.expression.expression.getText() === 'Logger') {
        const newPropertyAccess =
          ts.factory.updatePropertyAccessExpression(callExpr.expression, callExpr.expression.expression,
            ts.factory.createIdentifier('w'));
        const newCallExpr =
          ts.factory.updateCallExpression(callExpr, newPropertyAccess, callExpr.typeArguments, callExpr.arguments);
        return ts.factory.updateExpressionStatement(statement, newCallExpr);
      }
    }
    return statement;
  });
}
  1. 再进一步 :把函数内容全替换成自定义的 AppStorage.setOrCreate('IsForeground', true);
markdown 复制代码
1.  还是去 TypeScript AST Viewer中 copy 要替换代码的AST语句
2.  定位到函数后,直接用新的AST statement 替换之前的, 然后向外依次updateBolck、updateMethod、update node.
3.  ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/25da7366759840559e98a1cb21e07e18~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1750141461&x-signature=Jg8TdQyLCOBkKoO8wyj6RljJun0%3D)
dart 复制代码
function visit(node) {
  if (ts.isMethodDeclaration(node) && node.name && node.name.text === 'onForeground') {
    // 1.直接copy TypeScript AST Viewer 生成的代码
    const newStatements = replaceAllStatements(ts);
    // 2.更新函数体
    const newBody = ts.factory.createBlock([newStatements], true);
    // 3.更新node
    return ts.factory.updateMethodDeclaration(
      node, node.decorators, node.modifiers, node.asteriskToken,
      node.name, node.questionToken, node.typeParameters, node.parameters, node.type, newBody
    );
  }
  return ts.visitEachChild(node, visit, context);
}

function replaceAllStatements(ts) {
  return ts.factory.createExpressionStatement(
    ts.factory.createCallExpression(
      ts.factory.createPropertyAccessExpression(
        ts.factory.createIdentifier('AppStorage'),
        ts.factory.createIdentifier('setOrCreate')
      ),
      undefined,
      [
        ts.factory.createStringLiteral('IsForeground'),
        ts.factory.createTrue()
      ]
    )
  );
}

OK,经过上述步骤相信大家都可以开始一步步实现自己的Aop,我们简单总结下步骤:

  1. 利用 TypeScript ****AST Viewer 查看源码结构 & 生成目标源码对应的AST代码块.
  2. 在自定义的Visit函数中遍历 node ,根据第一步源码结构找到目标node.
  3. Copy 第一步生成的 目标源码对应的AST代码块.
  4. 如果是****更新操作: 调ts.factory.updateXXX, 从内到外依次更新 ,最后返回更新后的 node .
  5. 如果是****替换操作: 调ts.factory.createXXX, 从内到外依次更新 ,最后返回替换后的 node .

... 如果你熟悉其他的ts ast 库(比如 ts-morph) 也可以安装相关依赖后使用.

八、相关链接

相关推荐
想你依然心痛36 分钟前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“药界智脑“——PC端AI智能体沉浸式药物研发与分子模拟工作台
人工智能·华为·ar·harmonyos·智能体
G_dou_1 小时前
Flutter +OpenHarmony 实战:clock 时钟应用
flutter·harmonyos
G_dou_2 小时前
Flutter+OpenHarmony 实战:weather 天气查询应用
flutter·harmonyos
yuegu7772 小时前
HarmonyOS应用<节气通>开发第1篇:启动页开发——留下第一印象的2秒
harmonyos
川石课堂软件测试2 小时前
零基础小白如何学习自动化测试
python·功能测试·学习·测试工具·jmeter·压力测试·harmonyos
Swift社区3 小时前
OpenHarmony鸿蒙PC平台移植 gifsicle:CC++ 三方库适配实践(Lycium tpc_c_cplusplus)
c语言·c++·harmonyos
川石课堂软件测试3 小时前
作为一名测试工程师如何学习Kubernetes(k8s)技能
学习·测试工具·容器·职场和发展·kubernetes·测试用例·harmonyos
yuegu7773 小时前
HarmonyOS应用<节气通>开发第4篇:TabBar导航实现
华为·harmonyos
阿钱真强道4 小时前
25 鸿蒙LiteOS GPIO轮询模式实战教程:电平读取与上升沿检测
嵌入式·harmonyos·liteos·开源鸿蒙·瑞芯微·rk2206