当前位置:   article > 正文

基于自定义注解和代码生成实现路由框架_arkts routermap

arkts routermap

场景描述

应用开发中无论是出于工程组织效率还是开发体验的考虑,开发者都需要对项目进行模块间解耦,此时需要构建一套用于模块间组件跳转、数据通信的路由框架。

业界常见的实现方式是在编译期生成路由表

1. 实现原理及流程

  • 在编译期通过扫描并解析ets文件中的自定义注解来生成路由表和组件注册类
  • Har中的rawfile文件在Hap编译时会打包在Hap中,通过这一机制来实现路由表的合并
  • 自定义组件通过wrapBuilder封装来实现动态获取
  • 通过NavDestination的Builder机制来获取wrapBuilder封装后的自定义组件

2. 使用ArkTS自定义装饰器来代替注解的定义

由于TS语言特性,当前只能使用自定义装饰器

使用@AppRouter装饰器来定义路由信息

  1. // 定义空的装饰器
  2. export function AppRouter(param:AppRouterParam) {
  3.   return Object;
  4. }
  5.  
  6. export interface AppRouterParam{
  7.   uri:string;
  8. }

自定义组件增加路由定义

  1. @AppRouter({ uri: "app://login" })
  2. @Component
  3. export struct LoginView {
  4.   build(){
  5.     //...
  6.   }
  7. }

3. 实现动态路由模块

定义路由表(该文件为自动生成的路由表)

  1. {
  2.   "routerMap": [
  3.     {
  4.       "name": "app://login",   /* uri定义  */
  5.       "pageModule": "loginModule"/* 模块名  */
  6.       "pageSourceFile": "src/main/ets/generated/RouterBuilder.ets"/* Builder文件  */
  7.       "registerFunction": "LoginViewRegister"  /* 组件注册函数  */
  8.     }
  9.   ]
  10. }

应用启动时,在EntryAbility.onCreate中加载路由表

  1. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  2.   DynamicRouter.init({
  3.     libPrefix: "@app", mapPath: "routerMap"
  4.   }, this.context);
  5. }
  6. export class DynamicRouter {
  7.   // 路由初始化配置
  8.   static config: RouterConfig;
  9.   // 路由表
  10.   static routerMap: Map<string, RouterInfo> = new Map();
  11.   // 管理需要动态导入的模块,key是模块名,value是WrappedBuilder对象,动态调用创建页面的接口
  12.   static builderMap: Map<string, WrappedBuilder<Object[]>> = new Map();
  13.   // 路由栈
  14.   static navPathStack: NavPathStack = new NavPathStack();
  15.   // 通过数组实现自定义栈的管理
  16.   static routerStack: Array<RouterInfo> = new Array();
  17.   static referrer: string[] = [];
  18.  
  19.   public static init(config: RouterConfig, context: Context) {
  20.     DynamicRouter.config = config;
  21.     DynamicRouter.routerStack.push(HOME_PAGE)
  22.     RouterLoader.load(config.mapPath, DynamicRouter.routerMap, context)
  23.   }
  24.   //...
  25. }

路由表存放在src/main/resources/rawfile目录中,通过ResourceManager进行读取

  1. export namespace RouterLoader {
  2.  
  3.   export function load(dir: string, routerMap: Map<string, RouterInfo>, context: Context) {
  4.     const rm: resourceManager.ResourceManager = context.resourceManager;
  5.     try {
  6.       rm.getRawFileList(dir)
  7.         .then((value: Array<string>) => {
  8.           let decoder: util.TextDecoder = util.TextDecoder.create('utf-8', {
  9.             fatal: false, ignoreBOM: true
  10.           })
  11.           value.forEach(fileName => {
  12.             let fileBytes: Uint8Array = rm.getRawFileContentSync(`${dir}/${fileName}`)
  13.             let retStr = decoder.decodeWithStream(fileBytes)
  14.             let routerMapModel: RouterMapModel = JSON.parse(retStr) as RouterMapModel
  15.             loadRouterMap(routerMapModel, routerMap)
  16.           })
  17.         })
  18.         .catch((error: BusinessError) => {
  19.           //...
  20.         });
  21.     } catch (error) {
  22.       //...
  23.     }
  24.   }
  25. }

根据URI跳转页面时,通过动态import并执行路由表中定义的registerFunction方法来实现动态注册组件

  1. Button("跳转")
  2.   .onClick(()=>{
  3.     DynamicRouter.pushUri("app://settings")
  4.   })
  5. export class DynamicRouter {
  6.   //...
  7.   public static pushUri(uri: string, param?: Object, onPop?: (data: PopInfo) => void): void {
  8.     if (!DynamicRouter.routerMap.has(uri)) {
  9.       return;
  10.     }
  11.     let routerInfo: RouterInfo = DynamicRouter.routerMap.get(uri)!;
  12.     if (!DynamicRouter.builderMap.has(uri)) {
  13.       // 动态加载模块
  14.       import(`${DynamicRouter.config.libPrefix}/${routerInfo.pageModule}`)
  15.         .then((module: ESObject) => {
  16.           module[routerInfo.registerFunction!](routerInfo)   // 进行组件注册,实际执行了下文中的LoginViewRegister方法
  17.           DynamicRouter.navPathStack.pushDestination({ name: uri, onPop: onPop, param: param });
  18.           DynamicRouter.pushRouterStack(routerInfo);
  19.         })
  20.         .catch((error: BusinessError) => {
  21.           console.error(`promise import module failed, error code: ${error.code}, message: ${error.message}.`);
  22.         });
  23.     } else {
  24.       DynamicRouter.navPathStack.pushDestination({ name: uri, onPop: onPop, param: param });
  25.       DynamicRouter.pushRouterStack(routerInfo);
  26.     }
  27.   }
  28. }

组件注册实际执行的方法为LoginViewRegister(该文件为自动生成的模版代码)

  1. // auto-generated RouterBuilder.ets
  2. import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
  3. import { LoginView } from '../components/LoginView'
  4.  
  5. @Builder
  6. function LoginViewBuilder() {
  7.   LoginView()
  8. }
  9.  
  10. export function LoginViewRegister(routerInfo: RouterInfo) {
  11.   DynamicRouter.registerRouterPage(routerInfo, wrapBuilder(LoginViewBuilder))
  12. }

通过wrapBuilder将自定义组件保存在组件表

  1. export class DynamicRouter {
  2.   //...
  3.   // 通过URI注册builder
  4.   public static registerRouterPage(routerInfo: RouterInfo, wrapBuilder: WrappedBuilder<Object[]>): void {
  5.      const builderName: string = routerInfo.name;
  6.      if (!DynamicRouter.builderMap.has(builderName)) {
  7.        DynamicRouter.registerBuilder(builderName, wrapBuilder);
  8.      }
  9.    }
  10.  
  11.    private static registerBuilder(builderName: string, builder: WrappedBuilder<Object[]>): void {
  12.      DynamicRouter.builderMap.set(builderName, builder);
  13.    }
  14.  
  15.   // 通过URI获取builder
  16.   public static getBuilder(builderName: string): WrappedBuilder<Object[]> {
  17.     const builder = DynamicRouter.builderMap.get(builderName);
  18.     return builder as WrappedBuilder<Object[]>;
  19.   }
  20. }

首页Navigation通过组件表获取自定义组件Builder

  1. @Entry
  2. @Component
  3. struct Index {
  4.   build() {
  5.     Navigation(DynamicRouter.getNavPathStack()) {
  6.       //...
  7.     }
  8.     .navDestination(this.PageMap)
  9.     .hideTitleBar(true)
  10.   }
  11.  
  12.   @Builder
  13.   PageMap(name: string, param?: ESObject) {
  14.     NavDestination() {
  15.       DynamicRouter.getBuilder(name).builder(param);
  16.     }
  17.   }
  18.  
  19. }

4. 实现路由表生成插件

新建插件目录etsPlugin,建议创建在HarmonyOS工程目录之外

  1. mkdir etsPlugin
  2. cd etsPlugin

创建npm项目

npm init

安装依赖

  1. npm i --save-dev @types/node @ohos/hvigor @ohos/hvigor-ohos-plugin
  2. npm i typescript handlebars

初始化typescript配置

./node_modules/.bin/tsc --init

修改tsconfig.json

  1. {
  2.   "compilerOptions": {
  3.     "target": "es2021",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
  4.     "module": "commonjs",                                /* Specify what module code is generated. */
  5.     "strict": true,                                      /* Enable all strict type-checking options. */
  6.     "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
  7.     "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
  8.     "skipLibCheck": true,                                 /* Skip type checking all .d.ts files. */
  9.     "sourceMap": true,
  10.     "outDir": "./lib",
  11.   },
  12.   "include": [".eslintrc.js", "src/**/*"],
  13.   "exclude": ["node_modules", "lib/**/*"],
  14. }

创建插件文件src/index.ts

  1. export function etsGeneratorPlugin(pluginConfig: PluginConfig): HvigorPlugin {
  2.   return {
  3.     pluginId: PLUGIN_ID,
  4.     apply(node: HvigorNode) {
  5.       pluginConfig.moduleName = node.getNodeName();
  6.       pluginConfig.modulePath = node.getNodePath();
  7.       pluginExec(pluginConfig);
  8.     },
  9.   };
  10. }

修改package.json

  1. {
  2.   //...
  3.   "main": "lib/index.js",
  4.   "scripts": {
  5.     "test": "echo \"Error: no test specified\" && exit 1",
  6.     "dev": "tsc && node lib/index.js",
  7.     "build": "tsc"
  8.   },
  9.   //...
  10. }

插件实现流程

  • 通过扫描自定义组件的ets文件,解析语法树,拿到注解里定义的路由信息
  • 生成路由表、组件注册类,同时更新Index.ets

定义插件配置

  1. const config: PluginConfig = {
  2.   builderFileName: "RouterBuilder.ets"// 生成的组件注册类文件名
  3.   builderDir: "src/main/ets/generated"// 代码生成路径
  4.   routerMapDir: "src/main/resources/rawfile/routerMap"// 路由表生成路径
  5.   scanDir: "src/main/ets/components"// 自定义组件扫描路径
  6.   annotation: "AppRouter"// 路由注解
  7.   viewKeyword: "struct"// 自定义组件关键字
  8.   builderTpl: "viewBuilder.tpl"// 组件注册类模版文件
  9. };

插件核心代码:

  1. function pluginExec(config: PluginConfig) {
  2.   // 读取指定自定义组件目录下的文件
  3.   const scanPath = `${config.modulePath}/${config.scanDir}`;
  4.   const files: string[] = readdirSync(scanPath);
  5.   files.forEach((fileName) => {
  6.     // 对每个文件进行解析
  7.     const sourcePath = `${scanPath}/${fileName}`;
  8.     const importPath = path
  9.       .relative(`${config.modulePath}/${config.builderDir}`, sourcePath)
  10.       .replaceAll("\\", "/")
  11.       .replaceAll(".ets", "");
  12.  
  13.     // 执行语法树解析器
  14.     const analyzer = new EtsAnalyzer(config, sourcePath);
  15.     analyzer.start();
  16.  
  17.     // 保存解析结果
  18.     console.log(JSON.stringify(analyzer.analyzeResult));
  19.     console.log(importPath);
  20.     templateModel.viewList.push({
  21.       viewName: analyzer.analyzeResult.viewName,
  22.       importPath: importPath,
  23.     });
  24.     routerMap.routerMap.push({
  25.       name: analyzer.analyzeResult.uri,
  26.       pageModule: config.moduleName,
  27.       pageSourceFile: `${config.builderDir}/${config.builderFileName}`,
  28.       registerFunction: `${analyzer.analyzeResult.viewName}Register`,
  29.     });
  30.   });
  31.  
  32.   // 生成组件注册类
  33.   generateBuilder(templateModel, config);
  34.   // 生成路由表
  35.   generateRouterMap(routerMap, config);
  36.   // 更新Index文件
  37.   generateIndex(config);
  38. }

语法树解析流程

  • 遍历语法树节点,找到自定义注解@AppRouter
  • 读取URI的值
  • 通过识别struct关键字来读取自定义组件类名
  • 其他节点可以忽略

核心代码:

  1. export class EtsAnalyzer {
  2.   sourcePath: string;
  3.   pluginConfig: PluginConfig;
  4.   analyzeResult: AnalyzeResult = new AnalyzeResult();
  5.   keywordPos: number = 0;
  6.  
  7.   constructor(pluginConfig: PluginConfig, sourcePath: string) {
  8.     this.pluginConfig = pluginConfig;
  9.     this.sourcePath = sourcePath;
  10.   }
  11.  
  12.   start() {
  13.     const sourceCode = readFileSync(this.sourcePath, "utf-8");
  14.     // 创建ts语法解析器
  15.     const sourceFile = ts.createSourceFile(
  16.       this.sourcePath,
  17.       sourceCode,
  18.       ts.ScriptTarget.ES2021,
  19.       false
  20.     );
  21.     // 遍历语法节点
  22.     ts.forEachChild(sourceFile, (node: ts.Node) => {
  23.       this.resolveNode(node);
  24.     });
  25.   }
  26.  
  27.   // 根据节点类型进行解析
  28.   resolveNode(node: ts.Node): NodeInfo | undefined {
  29.     switch (node.kind) {
  30.       case ts.SyntaxKind.ImportDeclaration: {
  31.         this.resolveImportDeclaration(node);
  32.         break;
  33.       }
  34.       case ts.SyntaxKind.MissingDeclaration: {
  35.         this.resolveMissDeclaration(node);
  36.         break;
  37.       }
  38.       case ts.SyntaxKind.Decorator: {
  39.         this.resolveDecorator(node);
  40.         break;
  41.       }
  42.       case ts.SyntaxKind.CallExpression: {
  43.         this.resolveCallExpression(node);
  44.         break;
  45.       }
  46.       case ts.SyntaxKind.ExpressionStatement: {
  47.         this.resolveExpression(node);
  48.         break;
  49.       }
  50.       case ts.SyntaxKind.Identifier: {
  51.         return this.resolveIdentifier(node);
  52.       }
  53.       case ts.SyntaxKind.StringLiteral: {
  54.         return this.resolveStringLiteral(node);
  55.       }
  56.       case ts.SyntaxKind.PropertyAssignment: {
  57.         return this.resolvePropertyAssignment(node);
  58.       }
  59.     }
  60.   }
  61.  
  62.   resolveImportDeclaration(node: ts.Node) {
  63.     let ImportDeclaration = node as ts.ImportDeclaration;
  64.   }
  65.  
  66.   resolveMissDeclaration(node: ts.Node) {
  67.     node.forEachChild((cnode) => {
  68.       this.resolveNode(cnode);
  69.     });
  70.   }
  71.  
  72.   resolveDecorator(node: ts.Node) {
  73.     let decorator = node as ts.Decorator;
  74.     this.resolveNode(decorator.expression);
  75.   }
  76.  
  77.   resolveIdentifier(node: ts.Node): NodeInfo {
  78.     let identifier = node as ts.Identifier;
  79.     let info = new NodeInfo();
  80.     info.value = identifier.escapedText.toString();
  81.     return info;
  82.   }
  83.  
  84.   resolveCallExpression(node: ts.Node) {
  85.     let args = node as ts.CallExpression;
  86.     let identifier = this.resolveNode(args.expression);
  87.     this.parseRouterConfig(args.arguments, identifier);
  88.   }
  89.  
  90.   resolveExpression(node: ts.Node) {
  91.     let args = node as ts.ExpressionStatement;
  92.     let identifier = this.resolveNode(args.expression);
  93.     if (identifier?.value === this.pluginConfig.viewKeyword) {
  94.       this.keywordPos = args.end;
  95.     }
  96.     if (this.keywordPos === args.pos) {
  97.       this.analyzeResult.viewName = identifier?.value;
  98.     }
  99.   }
  100.  
  101.   resolveStringLiteral(node: ts.Node): NodeInfo {
  102.     let stringLiteral = node as ts.StringLiteral;
  103.     let info = new NodeInfo();
  104.     info.value = stringLiteral.text;
  105.     return info;
  106.   }
  107.  
  108.   resolvePropertyAssignment(node: ts.Node): NodeInfo {
  109.     let propertyAssignment = node as ts.PropertyAssignment;
  110.     let propertyName = this.resolveNode(propertyAssignment.name)?.value;
  111.     let propertyValue = this.resolveNode(propertyAssignment.initializer)?.value;
  112.     let info = new NodeInfo();
  113.     info.value = { key: propertyName, value: propertyValue };
  114.     return info;
  115.   }
  116.  
  117. }

使用模版引擎生成组件注册类

使用Handlebars生成组件注册类

  1. const template = Handlebars.compile(tpl);
  2. const output = template({ viewList: templateModel.viewList });

模版文件viewBuilder.tpl示例:

  1. // auto-generated RouterBuilder.ets
  2. import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
  3. {{#each viewList}}
  4. import { {{viewName}} } from '{{importPath}}'
  5. {{/each}}
  6.  
  7. {{#each viewList}}
  8. @Builder
  9. function {{viewName}}Builder() {
  10.   {{viewName}}()
  11. }
  12.  
  13. export function {{viewName}}Register(routerInfo: RouterInfo) {
  14.   DynamicRouter.registerRouterPage(routerInfo, wrapBuilder({{viewName}}Builder))
  15. }
  16.  
  17. {{/each}}

生成的RouterBuilder.ets代码示例:

  1. // auto-generated RouterBuilder.ets
  2. import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
  3. import { LoginView } from '../components/LoginView'
  4.  
  5. @Builder
  6. function LoginViewBuilder() {
  7.   LoginView()
  8. }
  9.  
  10. export function LoginViewRegister(routerInfo: RouterInfo) {
  11.   DynamicRouter.registerRouterPage(routerInfo, wrapBuilder(LoginViewBuilder))
  12. }

将路由表和组件注册类写入文件

  • 路由表保存在rawfile目录
  • 组件注册类保存在ets代码目录
  • 更新模块导出文件Index.ets

核心代码:

  1. function generateBuilder(templateModel: TemplateModel, config: PluginConfig) {
  2.   console.log(JSON.stringify(templateModel));
  3.   const builderPath = path.resolve(__dirname, `../${config.builderTpl}`);
  4.   const tpl = readFileSync(builderPath, { encoding: "utf8" });
  5.   const template = Handlebars.compile(tpl);
  6.   const output = template({ viewList: templateModel.viewList });
  7.   console.log(output);
  8.   const routerBuilderDir = `${config.modulePath}/${config.builderDir}`;
  9.   if (!existsSync(routerBuilderDir)) {
  10.     mkdirSync(routerBuilderDir, { recursive: true });
  11.   }
  12.   writeFileSync(`${routerBuilderDir}/${config.builderFileName}`, output, {
  13.     encoding: "utf8",
  14.   });
  15. }
  16.  
  17. function generateRouterMap(routerMap: RouterMap, config: PluginConfig) {
  18.   const jsonOutput = JSON.stringify(routerMap, null, 2);
  19.   console.log(jsonOutput);
  20.   const routerMapDir = `${config.modulePath}/${config.routerMapDir}`;
  21.   if (!existsSync(routerMapDir)) {
  22.     mkdirSync(routerMapDir, { recursive: true });
  23.   }
  24.   writeFileSync(`${routerMapDir}/${config.moduleName}.json`, jsonOutput, {
  25.     encoding: "utf8",
  26.   });
  27. }
  28.  
  29. function generateIndex(config: PluginConfig) {
  30.   const indexPath = `${config.modulePath}/Index.ets`;
  31.   const indexContent = readFileSync(indexPath, { encoding: "utf8" });
  32.   const indexArr = indexContent
  33.     .split("\n")
  34.     .filter((value) => !value.includes(config.builderDir!));
  35.   indexArr.push(
  36.     `export * from './${config.builderDir}/${config.builderFileName?.replace(
  37.       ".ets",
  38.       ""
  39.     )}'`
  40.   );
  41.   writeFileSync(indexPath, indexArr.join("\n"), {
  42.     encoding: "utf8",
  43.   });
  44. }

5. 在应用中使用

修改项目的hvigor/hvigor-config.json文件,导入路由表插件

  1. {
  2.   "hvigorVersion": "4.2.0",
  3.   "dependencies": {
  4.     "@ohos/hvigor-ohos-plugin": "4.2.0",
  5.     "@app/ets-generator" : "file:../../etsPlugin"   // 插件目录的本地相对路径,或者使用npm仓版本号
  6.   },
  7.   //...
  8. }

修改loginModule模块的hvigorfile.ts文件(loginModule/hvigorfile.ts),加载插件

  1. import { harTasks } from '@ohos/hvigor-ohos-plugin';
  2. import {PluginConfig,etsGeneratorPlugin} from '@app/ets-generator'
  3.  
  4. const config: PluginConfig = {
  5.     builderFileName: "RouterBuilder.ets",
  6.     builderDir: "src/main/ets/generated",
  7.     routerMapDir: "src/main/resources/rawfile/routerMap",
  8.     scanDir: "src/main/ets/components",
  9.     annotation: "AppRouter",
  10.     viewKeyword: "struct",
  11.     builderTpl: "viewBuilder.tpl",
  12. }
  13.  
  14. export default {
  15.     system: harTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
  16.     plugins:[etsGeneratorPlugin(config)]         /* Custom plugin to extend the functionality of Hvigor. */
  17. }

在loginModule模块的oh-package.json5中引入动态路由模块依赖

  1. {
  2.   "name": "loginmodule",
  3.   "version": "1.0.0",
  4.   "description": "Please describe the basic information.",
  5.   "main": "Index.ets",
  6.   "author": "",
  7.   "license": "Apache-2.0",
  8.   "dependencies": {
  9.     "@app/dynamicRouter": "file:../routerModule"
  10.   }
  11. }

在loginModule模块的自定义组件中使用@AppRouter定义路由信息

  1. @AppRouter({ uri: "app://login" })
  2. @Component
  3. export struct LoginView {
  4.   build(){
  5.     //...
  6.   }
  7. }

在entry中的oh-package.json5中引入依赖

  1. {
  2.   "name": "entry",
  3.   "version": "1.0.0",
  4.   "description": "Please describe the basic information.",
  5.   "main": "",
  6.   "author": "",
  7.   "license": "",
  8.   "dependencies": {
  9.     "@app/loginModule": "file:../loginModule",
  10.     "@app/commonModule": "file:../commonModule",
  11.     "@app/dynamicRouter": "file:../routerModule"
  12.   }
  13. }

在entry中的build-profile.json5中配置动态import

  1. {
  2.   "apiType": "stageMode",
  3.   "buildOption": {
  4.     "arkOptions": {
  5.       "runtimeOnly": {
  6.         "packages": [
  7.           "@app/loginModule",  // 仅用于使用变量动态import其他模块名场景,静态import或常量动态import无需配置。
  8.           "@app/commonModule"  // 仅用于使用变量动态import其他模块名场景,静态import或常量动态import无需配置。
  9.         ]
  10.       }
  11.     }
  12.   },
  13.   //...
  14. }

在entry中的EntryAbility.onCreate中初始化路由组件

  1. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  2.   DynamicRouter.init({
  3.     libPrefix: "@app", mapPath: "routerMap"
  4.   }, this.context);
  5. }

组件内使用pushUri进行跳转

  1. Button("立即登录", { buttonStyle: ButtonStyleMode.TEXTUAL })
  2.   .onClick(() => {
  3.     DynamicRouter.pushUri("app://login")
  4.   })
  5.   .id("button")

在entry模块执行Run/Debug,即可在编译时自动生成路由表配置并打包运行。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/985584
推荐阅读
  

闽ICP备14008679号