鸿蒙编译时 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/@[email protected]/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.transformed[0] 获取变化后的 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) 也可以安装相关依赖后使用.

八、相关链接

相关推荐
HarmonyOS_SDK2 分钟前
多格式文件在线预览,提升移动设备文件处理效率与体验
harmonyos
libo_202530 分钟前
HarmonyOS5 确定性执行:如何用ArkCompiler消除浮点数运算误差
harmonyos·arkts
Huang兄1 小时前
鸿蒙-flutter-环境搭建和第一个HelloWorld
flutter·harmonyos
Huang兄1 小时前
鸿蒙-flutter-使用PlatformView
flutter·harmonyos
勿念4361 小时前
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
harmonyos
勿念4362 小时前
用鸿蒙HarmonyOS5实现五子棋小游戏的过程
华为·harmonyos
二蛋和他的大花2 小时前
HarmonyOS运动开发:打造你的专属运动节拍器
华为·harmonyos
勿念4363 小时前
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
harmonyos
万少4 小时前
HarmonyOS Next 弹窗系列教程(3)
前端·harmonyos·客户端
Georgewu12 小时前
【HarmonyOS 5】鸿蒙CodeGenie AI辅助编程工具详解
华为·ai编程·harmonyos