鸿蒙5.0实战案例:基于自定义注解和代码生成实现路由框架
- 互联网
- 2025-08-21 19:30:02

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录) ✏️ 鸿蒙(HarmonyOS)北向开发知识点记录~ ✏️ 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~ ✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景? ✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~ ✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸? ✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选? ✏️ 记录一场鸿蒙开发岗位面试经历~ ✏️ 持续更新中……
场景描述
在应用开发中无论是出于工程组织效率还是开发体验的考虑,开发者都需要对项目进行模块间解耦,此时需要构建一套用于模块间组件跳转、数据通信的路由框架。
业界常见的实现方式是在编译期生成路由表。
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 pile(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 pile(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,即可在编译时自动生成路由表配置并打包运行。
鸿蒙5.0实战案例:基于自定义注解和代码生成实现路由框架由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“鸿蒙5.0实战案例:基于自定义注解和代码生成实现路由框架”
上一篇
.Net面试宝典【刷题系列】
下一篇
鸿蒙-canvas-画时钟