介紹
本示例將介紹如何使用裝飾器和插件,自動生成動態路由表,并通過動態路由跳轉到模塊中的頁面,以及如何使用動態import的方式加載模塊。
目前,已有三方庫HMRouter封裝了完整的動態路由功能,添加了生命周期回調、內置轉場動畫等功能,如有需要,可直接使用。
使用說明
- 自定義裝飾器
- 添加裝飾器和插件配置文件,編譯時自動生成動態路由表
- 配置動態路由,通過WrapBuilder接口,動態創建頁面并跳轉。
- 動態import變量表達式,需要DevEco Studio NEXT Developer Preview1 (4.1.3.500)版本IDE,配合hvigor 4.0.2版本使用。
- 支持自定義路由棧管理,相關內容請參考路由來源頁的相關說明
實現思路
動態路由的實現
- 初始化動態路由
public static routerInit(config: RouterConfig, context: Context) {
DynamicsRouter.config = config;
DynamicsRouter.appRouterStack.push(HOME_PAGE);
RouterLoader.load(config.mapPath, DynamicsRouter.routerMap, context);
}
- 獲取路由
private static getNavPathStack(): NavPathStack {
return DynamicsRouter.navPathStack;
}
- 通過builderName,注冊WrappedBuilder對象,用于動態創建頁面
private static registerBuilder(builderName: string, builder: WrappedBuilder<[object]>): void {
DynamicsRouter.builderMap.set(builderName, builder);
}
- 通過builderName,獲取注冊的WrappedBuilder對象
public static getBuilder(builderName: string): WrappedBuilder<[object]> {
let builder = DynamicsRouter.builderMap.get(builderName);
if (!builder) {
let msg = "not found builder";
console.info(msg + builderName);
}
return builder as WrappedBuilder<[object]>;
}
- 通過頁面棧跳轉到指定頁面
public static pushUri(name: string, param?: Object) {
if (!DynamicsRouter.routerMap.has(name)) {
return;
}
let routerInfo: AppRouterInfo = DynamicsRouter.routerMap.get(name)!;
if (!DynamicsRouter.builderMap.has(name)) {
import(`${DynamicsRouter.config.libPrefix}/${routerInfo.pageModule}`)
.then((module: ESObject) => {
module[routerInfo.registerFunction!](routerInfo);
DynamicsRouter.navPathStack.pushPath({ name: name, param: param });
})
.catch((error: BusinessError) => {
logger.error(`promise import module failed, error code:${error.code}, message:${error.message}`);
});
} else {
DynamicsRouter.navPathStack.pushPath({ name: name, param: param });
DynamicsRouter.pushRouterStack(routerInfo);
}
}
- 注冊動態路由跳轉的頁面信息
public static registerAppRouterPage(routerInfo: AppRouterInfo, wrapBuilder: WrappedBuilder<[object]>): void {
const builderName: string = routerInfo.name;
if (!DynamicsRouter.builderMap.has(builderName)) {
DynamicsRouter.registerBuilder(builderName, wrapBuilder);
}
}
動態路由的使用
- 在工程的hvigor/hvigor-config.json5中配置插件
{
...
"dependencies": {
...
"@app/ets-generator": "file:../plugin/AutoBuildRouter"
}
}
- 在工程的根目錄的build-profile.json5中添加動態路由模塊和需要加載的子模塊的依賴。
{
"app":{
...
}
"modules":{
...
{
"name": "eventpropagation",
"srcPath": "./feature/eventpropagation"
},
{
"name": "routermodule",
"srcPath": "./common/routermodule"
}
...
}
}
- 在主模塊中添加動態路由和需要加載的子模塊的依賴。
"dependencies": {
"@ohos/dynamicsrouter": "file:../../common/routermodule",
"@ohos/event-propagation": "file:../../feature/eventpropagation",
...
}
- 在主模塊中添加動態import變量表達式需要的參數,此處在packages中配置的模塊名必須和oh-package.json中配置的名稱相同。
...
"buildOption": {
"arkOptions": {
"runtimeOnly": {
"packages": [
"@ohos/event-propagation",
...
]
}
}
}
- 在主模塊EntryAbility的onCreate接口初始化動態路由。
...
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
DynamicsRouter.routerInit({
libPrefix: "@ohos", mapPath: "routerMap"
}, this.context);
...
}
...
- 在主模塊的WaterFlowData.ets中,將子模塊要加載的頁面,添加到列表中,詳細代碼請參考WaterFlowData.ets和SceneModuleInfo。
export const waterFlowData: SceneModuleInfo[] = [
...
new SceneModuleInfo($r('app.media.address_exchange'), '地址交換動畫', new RouterInfo("", ""), '動效', 2, "addressexchange/AddressExchangeView"),
...
}
- 在需要加載時將頁面放入路由棧,詳細代碼請參考FunctionalScenes.ets。
@Builder
methodPoints(listData: ListData) {
Column() {
...
.onClick(() => {
...
DynamicsRouter.pushUri(this.listData.appUri);
...
})
}
- 在子模塊中添加動態路由的依賴,詳細代碼可參考oh-package.json。
...
"dependencies": {
...
"@ohos/dynamicsrouter": "file:../../common/routermodule"
}
以上是需要在主模塊中添加的配置,如果已經添加過相關代碼,則可以直接略過,按照下面的步驟在子模塊中添加相關即可自動生成動態路由相關文件。
- 在子模塊的oh-package.json5中添加路由模塊依賴,可參考oh-package.json5
{
...
"dependencies": {
...
// 動態路由模塊,用于配置動態路由
"@ohos/dynamicsrouter": "file:../../common/routermodule"
}
}
- 在子模塊的hvigorfile.ts文件中添加插件配置,可參考hvigorfile.ts
...
import { PluginConfig, etsGeneratorPlugin } from '@app/ets-generator';
// 配置路由信息
const config: PluginConfig = {
// 需要掃描的文件的路徑,即配置自定義裝飾AppRouter的文件路徑
scanFiles: ["src/main/ets/view/AddressExchangeView"]
}
export default {
...
plugins: [etsGeneratorPlugin(config)] /* Custom plugin to extend the functionality of Hvigor. */
}
- 在需要跳轉的頁面的自定義組件上添加裝飾器,可參考AddressExchangeView.ets,如果需要通過路由傳遞參數,則需要設置hasParam為true,可參考NavigationParameterTransferView.ets。
// 自定義裝飾器,用于自動生成動態路由代碼及頁面的跳轉。命名規則:模塊名/自定義組件名
@AppRouter({ name: "addressexchange/AddressExchangeView" })
@Component
export struct AddressExchangeView {
...
}
自定義裝飾器入參支撐常量寫法
介紹
開發者在har包中使用原有的裝飾器+路由路徑實現動態路由之外,還可將路由路徑存入常量文件內,通過在裝飾器中輸入文件路徑和常量名,實現固定文件管理路由路徑常量。
使用說明
- 新增自定義裝飾器參數。
- 新增路由常量文件,在入口頁面路由裝飾器內傳入常量文件相對路徑和路由常量名。
- 修改動態路由插件內解析裝飾器方法,解析傳入的字符串,通過相對路徑實現在編譯時獲取對應常量文件,并根據常量名獲取對應路由路徑。
- 編譯修改后的路由插件,重新部署到工程內。
實現思路
- 新增自定義裝飾器參數,用于在頁面裝飾器內傳入文件路徑和路由常量名。自定義裝飾器AppRouter
// 裝飾器參數
export interface AppRouterParam {
// 跳轉的路由名
name?: string;
// 是否需要傳遞參數,需要的話設置為true,否則可不需要設置。
hasParam?: boolean;
// 新增路由參數
routeLocation?: string;
}
- 修改前,需向裝飾器的name參數中傳入 feature包名/入口文件名 字符串,示例如下(以feature包citysearch為例):
@AppRouter({ name: "citysearch/CitySearch" })
修改后,新增常量文件A。常量文件A的寫法如下:
// ../../A.ets
// ROUTE_LOCATION為路由常量,存入原有的路由(包名/入口文件名)
const ROUTE_LOCATION: string = 'citysearch/CitySearch';
將常量文件對于入口頁面的相對路徑和路由常量名以 相對路徑,常量名 的格式傳入裝飾器中。示例如下:
"../../A.ets,ROUTE_LOCATION"
在citysearch頁面的路由裝飾器中向新增的routeLocation參數傳入字符串。示例如下:
// 以(相對路徑,常量名)格式將字符串傳入新增路由參數routeLocation
@AppRouter({ routeLocation: "../../A.ets,ROUTE_LOCATION" })
- 修改前的路由參數寫在應用頁面里,不方便維護。本案例實現在固定文件內以常量形式保存路由路徑,方便統一管理和后續維護。
- 開發者可根據自身需要自定義傳參的字符串格式,然后在第3步修改解析字符串的方法即可。
- 修改工程中plugin/AutoBuildRouter插件,新增編譯器對新增路由參數的解析。
首先找到index.ts文件中解析裝飾器方法resolveDecoration
,在遍歷裝飾器中的所有參數時添加對路由參數routeLocation
的解析。 由于本案例使用字符串,字符串
的格式傳參,故選擇用split方法分隔字符串。開發者若使用自定義格式傳參,可根據分隔符自定義分隔方法。
import ts from "typescript";
// 解析裝飾器
resolveDecoration(node: ts.Node) {
// ...
// 遍歷裝飾器中的所有參數
properties.forEach((propertie) => {
if (propertie.kind === ts.SyntaxKind.PropertyAssignment) {
// 參數是否是自定義裝飾器中的變量名
if ((propertie.name as ts.Identifier).escapedText === "name") {
// ...
} else if ((propertie.name as ts.Identifier).escapedText === "hasParam") {
// ...
} else if ((propertie.name as ts.Identifier).escapedText === "routeLocation") {
//TODO:知識點: 新增routeLocation參數解析方法
// 解析參數內容
const routeLocationStr = (propertie.initializer as ts.StringLiteral).text;
// 分隔字符串
const routeLocationArray = routeLocationStr.split(",");
// 使用path.resolve方法將參數中相對路徑和當前入口文件絕對路徑組合,獲取常量文件的絕對路徑
const locationSrc = path.resolve(this.sourcePath, routeLocationArray[0]);
// 讀取文件,生成文件字符串
const locationCode = readFileSync(locationSrc, "utf-8");
// 解析文件,生成節點樹信息
const locationFile = ts.createSourceFile(locationSrc, locationCode, ts.ScriptTarget.ES2021, false);
// 遍歷節點信息
ts.forEachChild(locationFile, (node: ts.Node) => {
// 解析節點,通過node節點的kind屬性對應常量文件表達式的方法獲取常量名和值
if(node.kind === ts.SyntaxKind.VariableStatement) {
const locationDecorator = node as ts.VariableStatement;
const variableStatement = locationDecorator.declarationList as ts.VariableDeclarationList
// 遍歷文件中的所有常量
variableStatement.declarations.forEach((value,index) => {
const identifier = value.name as ts.Identifier
// 判斷循環中當前常量名是否等于傳參內常量名
if(identifier.escapedText === routeLocationArray[1]) {
const routeName = value.initializer as StringLiteral
// 將對應常量名的路由值傳出
this.analyzeResult.name = routeName.text
}
})
}
});
}
}
})
}
- 注:在遍歷節點信息時,可使用JSON.stringify方法打印節點樹,根據json對象的kind值對照ts.SyntaxKind枚舉值判斷節點屬性。
- 修改插件后,需將package.json內版本號提升,打包后替換到libs文件內。
{
"name": "autobuildrouter",
"version": "1.0.2",
// ...
}
修改hvigor-config.json5內插件路徑。
{
"modelVersion": "5.0.0",
"dependencies": {
// 修改插件版本號
"@app/ets-generator": "file:../libs/autobuildrouter-1.0.2.tgz",
"@app/ets-decoration-generator": "file:../libs/autobuilddecoration-1.0.2.tgz"
}
}
高性能知識點
本示例使用動態import的方式加載模塊,只有需要進入頁面時才加載對應的模塊,減少主頁面啟動時間和初始化效率,減少內存的占用。
工程結構&模塊類型
routermodule // har類型
|---annotation
|---|---AppRouter.ets // 自定義裝飾器
|---constants
| |---RouterInfo.ets // 路由信息類,用于配置動態路由跳轉頁面的名稱和模塊名(后續會刪除)
|---model
| |---AppRouterInfo.ets // 路由信息類
| |---RouterParam.ets // 路由參數
|---router
| |---DynamicsRouter.ets // 動態路由實現類
|---util
| |---RouterLoader.ets // 路由表加載類
FAQ
Q:動態路由用起來比較麻煩,為什么不直接使用系統提供的頁面路由,而是要重寫一套路由棧管理?
A:系統層面現在提供了兩種方式進行頁面跳轉,分別是頁面路由 (@ohos.router)和組件導航 (Navigation)。這兩種方式用起來都比較簡單,但是Router相較于Navigation缺少很多能力(具體可參考Router和Navigation能力對標),所以目前應用開發中推薦使用Navigation進行頁面跳轉。
而使用Navigation時存在一個問題,需要將跳轉的子頁面組件通過import的方式引入,即不論子頁面是否被跳轉,都會使子頁面引用的部分組件被初始化。例如頁面A使用Navigation跳轉到頁面B,頁面B中有用到Web組件加載一個H5頁面。那么當進入頁面A時,就會初始化Web組件相關的so庫。即使用戶只是在頁面A停留,并沒有進入頁面B,也會在進入頁面A時多出一部分初始化so庫的時間和內存。這是因為在頁面A中會直接import頁面B的自定義組件,導致so庫提前初始化。這樣就會導致主頁面啟動耗時延長,以及不必要的內存消耗。
由于動態路由使用了動態import實現,可以很好的避免這種情況的發生。只有在進入子頁面時,才會去初始化子頁面的相關組件,減少主頁面的啟動時間和內存占用,提升性能。而且由于使用了自定義路由棧,可以定制業務上的需求,更好的進行管理。
當主頁面中需要跳轉的子頁面較少時,使用Navigation更加方便。反之,則更推薦使用動態路由進行跳轉。
寫在最后
- 如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
- 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
- 關注小編,同時可以期待后續文章ing??,不定期分享原創知識。
- 想要獲取更多完整鴻蒙最新學習知識點,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu