赞
踩
场景描述
在应用开发中无论是出于工程组织效率还是开发体验的考虑,开发者都需要对项目进行模块间解耦,此时需要构建一套用于模块间组件跳转、数据通信的路由框架。
业界常见的实现方式是在编译期生成路由表。
1. 实现原理及流程
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"
- },
- //...
- }
插件实现流程
定义插件配置
- 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);
- }
语法树解析流程
核心代码:
- 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))
- }
将路由表和组件注册类写入文件
核心代码:
- 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,即可在编译时自动生成路由表配置并打包运行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。