【鸿蒙实战开发】基于自定义注解和代码生成实现路由框架

101 篇文章 0 订阅
101 篇文章 0 订阅

场景描述

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

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

1. 实现原理及流程

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

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

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

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

// 定义空的装饰器
export function AppRouter(param:AppRouterParam) {
  return Object;
}

export interface AppRouterParam{
  uri:string;
}

自定义组件增加路由定义

@AppRouter({ uri: "app://login" })
@Component
export struct LoginView {
  build(){
    //...
  }
}

3. 实现动态路由模块

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

{
  "routerMap": [
    {
      "name": "app://login",   /* uri定义  */
      "pageModule": "loginModule",  /* 模块名  */
      "pageSourceFile": "src/main/ets/generated/RouterBuilder.ets",  /* Builder文件  */
      "registerFunction": "LoginViewRegister"  /* 组件注册函数  */
    }
  ]
}

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

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

export class DynamicRouter {
  // 路由初始化配置
  static config: RouterConfig;
  // 路由表
  static routerMap: Map<string, RouterInfo> = new Map();
  // 管理需要动态导入的模块,key是模块名,value是WrappedBuilder对象,动态调用创建页面的接口
  static builderMap: Map<string, WrappedBuilder<Object[]>> = new Map();
  // 路由栈
  static navPathStack: NavPathStack = new NavPathStack();
  // 通过数组实现自定义栈的管理
  static routerStack: Array<RouterInfo> = new Array();
  static referrer: string[] = [];

  public static init(config: RouterConfig, context: Context) {
    DynamicRouter.config = config;
    DynamicRouter.routerStack.push(HOME_PAGE)
    RouterLoader.load(config.mapPath, DynamicRouter.routerMap, context)
  }
  //...
}

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

export namespace RouterLoader {

  export function load(dir: string, routerMap: Map<string, RouterInfo>, context: Context) {
    const rm: resourceManager.ResourceManager = context.resourceManager;
    try {
      rm.getRawFileList(dir)
        .then((value: Array<string>) => {
          let decoder: util.TextDecoder = util.TextDecoder.create('utf-8', {
            fatal: false, ignoreBOM: true
          })
          value.forEach(fileName => {
            let fileBytes: Uint8Array = rm.getRawFileContentSync(`${dir}/${fileName}`)
            let retStr = decoder.decodeWithStream(fileBytes)
            let routerMapModel: RouterMapModel = JSON.parse(retStr) as RouterMapModel
            loadRouterMap(routerMapModel, routerMap)
          })
        })
        .catch((error: BusinessError) => {
          //...
        });
    } catch (error) {
      //...
    }
  }
}

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

Button("跳转")
  .onClick(()=>{
    DynamicRouter.pushUri("app://settings")
  })

export class DynamicRouter {
  //...
  public static pushUri(uri: string, param?: Object, onPop?: (data: PopInfo) => void): void {
    if (!DynamicRouter.routerMap.has(uri)) {
      return;
    }
    let routerInfo: RouterInfo = DynamicRouter.routerMap.get(uri)!;
    if (!DynamicRouter.builderMap.has(uri)) {
      // 动态加载模块
      import(`${DynamicRouter.config.libPrefix}/${routerInfo.pageModule}`)
        .then((module: ESObject) => {
          module[routerInfo.registerFunction!](routerInfo)   // 进行组件注册,实际执行了下文中的LoginViewRegister方法
          DynamicRouter.navPathStack.pushDestination({ name: uri, onPop: onPop, param: param });
          DynamicRouter.pushRouterStack(routerInfo);
        })
        .catch((error: BusinessError) => {
          console.error(`promise import module failed, error code: ${error.code}, message: ${error.message}.`);
        });
    } else {
      DynamicRouter.navPathStack.pushDestination({ name: uri, onPop: onPop, param: param });
      DynamicRouter.pushRouterStack(routerInfo);
    }
  }
}

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

// auto-generated RouterBuilder.ets
import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
import { LoginView } from '../components/LoginView'

@Builder
function LoginViewBuilder() {
  LoginView()
}

export function LoginViewRegister(routerInfo: RouterInfo) {
  DynamicRouter.registerRouterPage(routerInfo, wrapBuilder(LoginViewBuilder))
}

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

export class DynamicRouter {
  //...
  // 通过URI注册builder
  public static registerRouterPage(routerInfo: RouterInfo, wrapBuilder: WrappedBuilder<Object[]>): void {
     const builderName: string = routerInfo.name;
     if (!DynamicRouter.builderMap.has(builderName)) {
       DynamicRouter.registerBuilder(builderName, wrapBuilder);
     }
   }

   private static registerBuilder(builderName: string, builder: WrappedBuilder<Object[]>): void {
     DynamicRouter.builderMap.set(builderName, builder);
   }

  // 通过URI获取builder
  public static getBuilder(builderName: string): WrappedBuilder<Object[]> {
    const builder = DynamicRouter.builderMap.get(builderName);
    return builder as WrappedBuilder<Object[]>;
  }
}

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

@Entry
@Component
struct Index {
  build() {
    Navigation(DynamicRouter.getNavPathStack()) {
      //...
    }
    .navDestination(this.PageMap)
    .hideTitleBar(true)
  }

  @Builder
  PageMap(name: string, param?: ESObject) {
    NavDestination() {
      DynamicRouter.getBuilder(name).builder(param);
    }
  }

}

4. 实现路由表生成插件

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

mkdir etsPlugin
cd etsPlugin

创建npm项目

npm init

安装依赖

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

初始化typescript配置

./node_modules/.bin/tsc --init

修改tsconfig.json

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

创建插件文件src/index.ts

export function etsGeneratorPlugin(pluginConfig: PluginConfig): HvigorPlugin {
  return {
    pluginId: PLUGIN_ID,
    apply(node: HvigorNode) {
      pluginConfig.moduleName = node.getNodeName();
      pluginConfig.modulePath = node.getNodePath();
      pluginExec(pluginConfig);
    },
  };
}

修改package.json

{
  //...
  "main": "lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "tsc && node lib/index.js",
    "build": "tsc"
  },
  //...
}

插件实现流程

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

定义插件配置

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

插件核心代码:

function pluginExec(config: PluginConfig) {
  // 读取指定自定义组件目录下的文件
  const scanPath = `${config.modulePath}/${config.scanDir}`;
  const files: string[] = readdirSync(scanPath);
  files.forEach((fileName) => {
    // 对每个文件进行解析
    const sourcePath = `${scanPath}/${fileName}`;
    const importPath = path
      .relative(`${config.modulePath}/${config.builderDir}`, sourcePath)
      .replaceAll("\\", "/")
      .replaceAll(".ets", "");

    // 执行语法树解析器
    const analyzer = new EtsAnalyzer(config, sourcePath);
    analyzer.start();

    // 保存解析结果
    console.log(JSON.stringify(analyzer.analyzeResult));
    console.log(importPath);
    templateModel.viewList.push({
      viewName: analyzer.analyzeResult.viewName,
      importPath: importPath,
    });
    routerMap.routerMap.push({
      name: analyzer.analyzeResult.uri,
      pageModule: config.moduleName,
      pageSourceFile: `${config.builderDir}/${config.builderFileName}`,
      registerFunction: `${analyzer.analyzeResult.viewName}Register`,
    });
  });

  // 生成组件注册类
  generateBuilder(templateModel, config);
  // 生成路由表
  generateRouterMap(routerMap, config);
  // 更新Index文件
  generateIndex(config);
}

语法树解析流程

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

核心代码:

export class EtsAnalyzer {
  sourcePath: string;
  pluginConfig: PluginConfig;
  analyzeResult: AnalyzeResult = new AnalyzeResult();
  keywordPos: number = 0;

  constructor(pluginConfig: PluginConfig, sourcePath: string) {
    this.pluginConfig = pluginConfig;
    this.sourcePath = sourcePath;
  }

  start() {
    const sourceCode = readFileSync(this.sourcePath, "utf-8");
    // 创建ts语法解析器
    const sourceFile = ts.createSourceFile(
      this.sourcePath,
      sourceCode,
      ts.ScriptTarget.ES2021,
      false
    );
    // 遍历语法节点
    ts.forEachChild(sourceFile, (node: ts.Node) => {
      this.resolveNode(node);
    });
  }

  // 根据节点类型进行解析
  resolveNode(node: ts.Node): NodeInfo | undefined {
    switch (node.kind) {
      case ts.SyntaxKind.ImportDeclaration: {
        this.resolveImportDeclaration(node);
        break;
      }
      case ts.SyntaxKind.MissingDeclaration: {
        this.resolveMissDeclaration(node);
        break;
      }
      case ts.SyntaxKind.Decorator: {
        this.resolveDecorator(node);
        break;
      }
      case ts.SyntaxKind.CallExpression: {
        this.resolveCallExpression(node);
        break;
      }
      case ts.SyntaxKind.ExpressionStatement: {
        this.resolveExpression(node);
        break;
      }
      case ts.SyntaxKind.Identifier: {
        return this.resolveIdentifier(node);
      }
      case ts.SyntaxKind.StringLiteral: {
        return this.resolveStringLiteral(node);
      }
      case ts.SyntaxKind.PropertyAssignment: {
        return this.resolvePropertyAssignment(node);
      }
    }
  }

  resolveImportDeclaration(node: ts.Node) {
    let ImportDeclaration = node as ts.ImportDeclaration;
  }

  resolveMissDeclaration(node: ts.Node) {
    node.forEachChild((cnode) => {
      this.resolveNode(cnode);
    });
  }

  resolveDecorator(node: ts.Node) {
    let decorator = node as ts.Decorator;
    this.resolveNode(decorator.expression);
  }

  resolveIdentifier(node: ts.Node): NodeInfo {
    let identifier = node as ts.Identifier;
    let info = new NodeInfo();
    info.value = identifier.escapedText.toString();
    return info;
  }

  resolveCallExpression(node: ts.Node) {
    let args = node as ts.CallExpression;
    let identifier = this.resolveNode(args.expression);
    this.parseRouterConfig(args.arguments, identifier);
  }

  resolveExpression(node: ts.Node) {
    let args = node as ts.ExpressionStatement;
    let identifier = this.resolveNode(args.expression);
    if (identifier?.value === this.pluginConfig.viewKeyword) {
      this.keywordPos = args.end;
    }
    if (this.keywordPos === args.pos) {
      this.analyzeResult.viewName = identifier?.value;
    }
  }

  resolveStringLiteral(node: ts.Node): NodeInfo {
    let stringLiteral = node as ts.StringLiteral;
    let info = new NodeInfo();
    info.value = stringLiteral.text;
    return info;
  }

  resolvePropertyAssignment(node: ts.Node): NodeInfo {
    let propertyAssignment = node as ts.PropertyAssignment;
    let propertyName = this.resolveNode(propertyAssignment.name)?.value;
    let propertyValue = this.resolveNode(propertyAssignment.initializer)?.value;
    let info = new NodeInfo();
    info.value = { key: propertyName, value: propertyValue };
    return info;
  }

}

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

使用Handlebars生成组件注册类

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

模版文件viewBuilder.tpl示例:

// auto-generated RouterBuilder.ets
import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
{{#each viewList}}
import { {{viewName}} } from '{{importPath}}'
{{/each}}

{{#each viewList}}
@Builder
function {{viewName}}Builder() {
  {{viewName}}()
}

export function {{viewName}}Register(routerInfo: RouterInfo) {
  DynamicRouter.registerRouterPage(routerInfo, wrapBuilder({{viewName}}Builder))
}

{{/each}}

生成的RouterBuilder.ets代码示例:

// auto-generated RouterBuilder.ets
import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
import { LoginView } from '../components/LoginView'

@Builder
function LoginViewBuilder() {
  LoginView()
}

export function LoginViewRegister(routerInfo: RouterInfo) {
  DynamicRouter.registerRouterPage(routerInfo, wrapBuilder(LoginViewBuilder))
}

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

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

核心代码:

function generateBuilder(templateModel: TemplateModel, config: PluginConfig) {
  console.log(JSON.stringify(templateModel));
  const builderPath = path.resolve(__dirname, `../${config.builderTpl}`);
  const tpl = readFileSync(builderPath, { encoding: "utf8" });
  const template = Handlebars.compile(tpl);
  const output = template({ viewList: templateModel.viewList });
  console.log(output);
  const routerBuilderDir = `${config.modulePath}/${config.builderDir}`;
  if (!existsSync(routerBuilderDir)) {
    mkdirSync(routerBuilderDir, { recursive: true });
  }
  writeFileSync(`${routerBuilderDir}/${config.builderFileName}`, output, {
    encoding: "utf8",
  });
}

function generateRouterMap(routerMap: RouterMap, config: PluginConfig) {
  const jsonOutput = JSON.stringify(routerMap, null, 2);
  console.log(jsonOutput);
  const routerMapDir = `${config.modulePath}/${config.routerMapDir}`;
  if (!existsSync(routerMapDir)) {
    mkdirSync(routerMapDir, { recursive: true });
  }
  writeFileSync(`${routerMapDir}/${config.moduleName}.json`, jsonOutput, {
    encoding: "utf8",
  });
}

function generateIndex(config: PluginConfig) {
  const indexPath = `${config.modulePath}/Index.ets`;
  const indexContent = readFileSync(indexPath, { encoding: "utf8" });
  const indexArr = indexContent
    .split("\n")
    .filter((value) => !value.includes(config.builderDir!));
  indexArr.push(
    `export * from './${config.builderDir}/${config.builderFileName?.replace(
      ".ets",
      ""
    )}'`
  );
  writeFileSync(indexPath, indexArr.join("\n"), {
    encoding: "utf8",
  });
}

5. 在应用中使用

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

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

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

import { harTasks } from '@ohos/hvigor-ohos-plugin';
import {PluginConfig,etsGeneratorPlugin} from '@app/ets-generator'

const config: PluginConfig = {
    builderFileName: "RouterBuilder.ets",
    builderDir: "src/main/ets/generated",
    routerMapDir: "src/main/resources/rawfile/routerMap",
    scanDir: "src/main/ets/components",
    annotation: "AppRouter",
    viewKeyword: "struct",
    builderTpl: "viewBuilder.tpl",
}

export default {
    system: harTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[etsGeneratorPlugin(config)]         /* Custom plugin to extend the functionality of Hvigor. */
}

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

{
  "name": "loginmodule",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "Index.ets",
  "author": "",
  "license": "Apache-2.0",
  "dependencies": {
    "@app/dynamicRouter": "file:../routerModule"
  }
}

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

@AppRouter({ uri: "app://login" })
@Component
export struct LoginView {
  build(){
    //...
  }
}

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

{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "@app/loginModule": "file:../loginModule",
    "@app/commonModule": "file:../commonModule",
    "@app/dynamicRouter": "file:../routerModule"
  }
}

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

{
  "apiType": "stageMode",
  "buildOption": {
    "arkOptions": {
      "runtimeOnly": {
        "packages": [
          "@app/loginModule",  // 仅用于使用变量动态import其他模块名场景,静态import或常量动态import无需配置。
          "@app/commonModule"  // 仅用于使用变量动态import其他模块名场景,静态import或常量动态import无需配置。
        ]
      }
    }
  },
  //...
}

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

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

组件内使用pushUri进行跳转

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

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

鸿蒙全栈开发全新学习指南

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以要有一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值