细说NEXT(5.0)中的module

module是什么?

​编辑

如上图的三级结构中红色圈住的部分,在DevEco Studio工程中创建一个module时,也即是创建了一个特殊的文件目录,在该目录下包含了特定的模板文件以及固定的子目录,开发者可以在固定的子目录下填充源代码文件、资源文件等,并完善创建时自动生成的配置文件里的内容。module是鸿蒙应用工程中的基本功能单元,它分为"Ability"、"Library"两个大类型。

和bundle的关系

Ability类型的module在构建后可以生成一个.hap文件,每个HarmonyOS应用中可以包含1个或多个.hap文件,这些.hap文件在打包时合在一起称为一个bundle。每个应用对应一个bundle,都有一个bundleName。在每台设备上,已安装的应用的bundleName是唯一的。

应用上架到应用市场前,需要把包含的所有的.hap文件打包为一个.app后缀的文件进行上架,这个.app文件(Application Package)里还包含了一个pack.info文件,该文件描述了安装包里的一些属性信息。但是,在云端分发时和端侧安装时都是以hap文件为单位进行的。

module的类型‌

1、Ability类型的module‌:

Ability类型的module编译构建后生成一个.hap(Harmony Ability Package)包,hap是一个应用在安装和运行时的核心部分。hap包是由代码、资源、第三方库、配置文件等打包生成的模块包,其主要分为两种类型:entry和feature,在创建module时IDE会要求手动选择创建哪种类型的。

a、‌entry类型的hap:

是应用的主模块,作为应用的入口,提供了应用的基础功能,比如入口的UIAbility、入口图标等。在同一个应用中,同一设备类型只支持拥有一个entry类型的module。

注意:定义的UIAbility实现类只能位于该module中,推荐只使用一个入口UIAbility,各个业务模块统一以各个page的形式进行设计。

b、‌feature类型的hap:

是应用的动态特性模块,即应用能力扩展模块,在该module里可以定义基于ExtensionAbility的派生类(对三方应用而言,这些是系统定义好的)实现的一些功能扩展,比如继承FormExtensionAbility实现的卡片功能,可以根据用户的需求和设备类型进行选择性开发和安装。

在工程中的配置文件里情况如下:

​编辑

应用中hap包的应用场景:

应用程序包可以只包含一个基础的entry包,也可以包含一个基础的entry包和多个扩展功能性的feature包。

a、单hap场景:如果只包含UIAbility组件,无需使用ExtensionAbility组件,优先采用单hap(即一个entry包)来实现应用开发。虽然一个hap中可以包含一个或多个UIAbility组件,为了避免不必要的资源加载,推荐采用"一个UIAbility+多个页面"的方式。

b、多hap场景:如果应用的功能比较复杂,需要使用ExtensionAbility组件,可以采用多hap(即一个entry包+多个feature包)来实现应用开发,每个hap中包含一个UIAbility组件或者一个ExtensionAbility组件。在这种场景下,可能会存在多个hap引用相同的库文件,导致重复打包的问题。

注意:

1>、hap不支持导出接口和ArkUI组件给其他模块使用。

2>、多hap场景下,App Pack包中同一设备类型的所有hap中必须有且只有一个Entry类型的hap,Feature类型的hap可以有一个或者多个,也可以没有。

3>、多hap场景下,同一应用中的所有hap的配置文件中的bundleName、versionCode、versionName、minCompatibleVersionCode、debug、minAPIVersion、targetAPIVersion、apiReleaseType必须相同,同一设备类型的所有hap对应的moduleName标签必须唯一。hap打包生成App Pack包时,会对上述参数配置进行校验。

4>、多hap场景下,同一应用的所有hap、hsp的签名证书要保持一致。上架应用市场是以App Pack的形式上架的,应用市场在分发时会将所有的hap从App Pack中拆分出来,同时对其中所有的hap进行重签名,这样保证了所有的hap的签名证书的一致性。在调试阶段,开发者通过命令行或DevEco Studio将hap安装到设备上时,要保证所有hap的签名证书一致,否则会出现安装失败的问题。

5>、UIAbility们和继承ExtensionAbility的派生类的扩展Ability们是处于不同的进程中的,所以会出现前面要求bundleName、versionCode、versionName等要一致的情况。

2、Library类型的module‌

服务于Ability类型的module,和其他技术栈里的library类似,用于实现模块化和共享一些通用代码或资源。

Library类型的module可以细分为Static和Shared两种类型:

a、‌Static Library‌:

静态共享库,编译后生成一个.har文件。har(Harmony Archive)相当于Android中的lib下的jar或者aar‌。

har文件的主要用途是实现应用的精细模块化和可通用代码的复用。当开发者需要在多个应用中使用相同的功能时,这里的'功能'可以是util(比如Logger、HttpUtil等),也可以是不同app间可通用的页面模块(比如人脸识别模块等),可以将这些功能分别封装到一个个har类型的module中,编译成一个个.har文件,然后在不同的项目中或module中引入该文件。这种做法不仅减少了重复开发,还提升了项目的维护效率。

使用场景:

可以作为二方库发布到OHPM私仓,供公司内部的其他应用使用。

也可以作为三方库发布到OHPM中心仓,供其他应用使用。

注意:

1>、har不支持在设备上单独安装/运行,它只能作为其他应用模块的依赖项被引用。

2>、har不支持在其配置文件中声明UIAbility组件与ExtensionAbility组件。

3>、har不支持在其配置文件中声明pages页面,但是可以包含pages页面的代码,并允许通过命名路由的方式进行跳转。一般不会在har里写page页面,可能会出现代码重复的情况。

4>、har不支持引用AppScope目录中的资源。在编译构建时,AppScope中的内容不会打包到har中,因此会导致har资源引用失败。

5>、har可以依赖其他har,但不支持循环依赖,也不支持依赖传递。

6>、har里要对外暴露的接口,需要在Index.ets导出文件中二次声明,如下所示:

​编辑

注意:在文件中使用了export default 声明的可导出class、interface、function、type、变量等在对外暴露时不需要在Index.ets文件里声明了。

b、‌Shared Library‌:

动态共享库,该类型的module在编译后生成一个.hsp(Harmony Shared Package)文件‌,需要跟随其宿主应用的APP包一起发布,与宿主应用同进程。

两种形态:

应用内的hsp:即工程内的hsp module,项目中自用,在编译过程中与应用包名(bundleName)强耦合,只能给某个特定的应用使用。

集成态的hsp:即在API 12及以上版本中,使用标准化的OHMUrl格式对一个hsp进行构建生成一个.tgz文件,可以在不同应用间实现共享。在构建、发布过程中,不与特定的应用包名耦合,使用时,工具链支持自动将集成态hsp的包名替换成宿主应用包名。

使用场景:

a、多个hap/hsp共用的代码和资源可以放在同一个hsp中,可以提高代码、资源的重用和可维护性,同时编译打包时也只保留一份hsp代码和资源,能够有效控制应用包的大小。

b、hsp在运行时按需加载,有助于提升应用性能。

c、同一个组织内部的多个应用之间,可以使用集成态hsp实现代码和资源的共享。

注意:

1>、hsp不支持独立发布,需要跟随其宿主应用的APP包一起发布,与宿主应用同进程,具有相同的包名和生命周期。

2>、hsp不支持在设备上单独安装/运行,需要与依赖该hsp的hap一起安装/运行。hsp的版本号必须与hap的版本号一致。

3>、hsp不支持在其配置文件中声明UIAbility组件与ExtensionAbility组件。

4>、hsp可以依赖其他har或hsp,但不支持循环依赖,也不支持依赖传递。

5>、对外暴露的接口,需要在入口文件index.ets中声明:

javascript 复制代码
// library/index.ets

export { MyTitleBar } from './src/main/ets/components/MyTitleBar';

在工程中的配置文件里情况如下:

​编辑

开发中划分module的经验总结

针对超大型项目(需要更复杂的设计)以外的一般项目:

1、采用1个entry类型的hap + n个hsp(各个业务模块、部分共享模块)+1个/n个har(可发布的模块),如果需要使用扩展服务,比如卡片,再+0个/n个feature类型的hap。

2、entry类型的module中,只有1个UIAbility,@entry修饰的入口page可以只设置1个,即Navigation的导航页,用于展示app的启动页。

3、使用动态路由的方式,在各个业务hsp的module中定义系统路由表,完成各个页面的切换。

4、通用自定义组件,通用系统组件,可以抽取出来,写在一个hsp中,以供业务hsp们使用。

使用组件导航实现不同module间的页面跳转

以组件导航来实现动态路由的方式实现各个页面之间的跳转是推荐使用的方式。

动态路由具体的优势

1、路由定义时除了跳转的URL以外,可以配置丰富的扩展信息,如横竖屏默认模式、是否需要鉴权等等,在路由跳转时统一处理。

2、可以给每个路由页面设置一个名字,然后按照名字进行跳转而不是通过文件路径。

3、通过路由加载页面时可以使用动态Import以按需加载,防止首个页面因加载大量代码导致了卡顿。

动态路由提供了系统路由表和自定义路由表两种方式。

系统路由表相对自定义路由表使用起来更简单,只需要添加对应页面的跳转配置项即可实现页面跳转。自定义路由表使用起来更复杂,但是可以根据应用的业务进行定制处理。支持自定义路由表和系统路由表混用。

系统路由表

从API version 12开始,Navigation支持使用系统路由表的方式进行动态路由。在各个业务模块(HSP/HAR)中需要独立配置route_map.json文件,在触发路由跳转时,应用只需要通过NavPathStack提供的路由方法,传入需要路由的目标页面的配置名字,然后系统会自动完成路由模块的动态加载、页面组件构建,并完成路由跳转,从而实现了开发层面的模块彻底解耦。

其主要步骤如下:

1、目标module中(也可以是自身module)开发页面组件。

typescript 复制代码
import Logger from '../common/utils/Logger';

// 用于配置文件中使用的配置函数
// 注意:export修饰的
@Builder
export function PageOneBuilder(name: string, param: Object) {
  PageOne()
}

const COLUMN_SPACE: number = 12;

@Component
export struct PageOne {
  pageInfos: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {   // 这里是子页的容器组件
      Column({ space: COLUMN_SPACE }) {
        Button($r('app.string.entry_index'), { stateEffect: true, type: ButtonType.Capsule })
          .width($r('app.string.button_width'))
          .height($r('app.string.button_height'))
          .onClick(() => {
            //Clear all pages in the stack.
            this.pageInfos.clear();
          })
        Button($r('app.string.entry_pageTwo'), { stateEffect: true, type: ButtonType.Capsule })
          .width($r('app.string.button_width'))
          .height($r('app.string.button_height'))
          .onClick(() => {
            //Push the NavDestination page information specified by name onto the stack, and pass the data as param.
            this.pageInfos.pushPathByName('pageTwo', null);
          })
      }
      .width($r('app.string.navDestination_width'))
      .height($r('app.string.navDestination_height'))
      .justifyContent(FlexAlign.End)
      .padding({
        bottom: $r('app.string.column_padding'),
        left: $r('app.string.column_padding'),
        right: $r('app.string.column_padding')
      })
    }
    .title('entry-pageOne')
    .onReady((context: NavDestinationContext) => {
      this.pageInfos = context.pathStack;       // 获取到来自导航页的navPathStack对象
      Logger.info("current page config info is " + JSON.stringify(context.getConfigInRouteMap()));
    })
  }
}

2、在目标module下的resources/base/profile中创建route_map.json文件,并添加配置信息。

​编辑

配置说明如下:

配置项 说明
name 供路由跳转时标记的页面名字。
pageSourceFile 跳转目标页在包内的路径,相对src目录的相对路径。
buildFunction 跳转目标页的入口函数名称,必须以@Builder修饰。
data 应用自定义字段。可以通过配置项读取接口getConfigInRouteMap获取。

3、在当前页通过pushPathByName等路由接口进行页面跳转。(注意:此时Navigation中可以不用配置navDestination属性)。

以下代码示例是从导航页跳转到上面子页的,也可以子页跳转到子页。

less 复制代码
@Entry
@Component
struct NavigationExample {
  pageInfos: NavPathStack = new NavPathStack();   // 在导航页初始化NavPathStack对象,并传入Navigation容器组件中

  build() {
    Navigation(this.pageInfos) {     // 这里是导航页的容器组件
      Column({ space: 12 }) {
        Button($r('app.string.entry_pageOne'), { stateEffect: true, type: ButtonType.Capsule })
          .width($r('app.string.button_width'))
          .height($r('app.string.button_height'))
          .onClick(() => {
            // 直接使用名字就可以进行跳转了
            this.pageInfos.pushPathByName('pageOne', null);
          })
      }
      .width($r('app.string.navDestination_width'))
      .height($r('app.string.navDestination_height'))
      .justifyContent(FlexAlign.End)
      .padding({
        bottom: $r('app.string.column_padding'),
        left: $r('app.string.column_padding'),
        right: $r('app.string.column_padding')
      })
    }
    .title($r('app.string.entry_index_title'))
  }
}

自定义路由表

开发者可以通过自定义路由表的方式来实现跨包动态路由。

typescript 复制代码
class DerivedNavPathStack extends NavPathStack {
  
  id: string = "__default__"

  setId(id: string) {
    this.id = id;
  }

  
  getInfo(): string {
    return "this page used Derived NavPathStack, id: " + this.id
  }

  // overwrite function of NavPathStack
  pushPath(info: NavPathInfo, animated?: boolean): void
  pushPath(info: NavPathInfo, options?: NavigationOptions): void
  pushPath(info: NavPathInfo, secArg?: boolean | NavigationOptions): void {
    console.log('[derive-test] reached DerivedNavPathStack's pushPath');
    if (typeof secArg === 'boolean') {
      super.pushPath(info, secArg);
    } else {
      super.pushPath(info, secArg);
    }
  }

  // overwrite and overload function of NavPathStack
  pop(animated?: boolean | undefined): NavPathInfo | undefined
  pop(result: Object, animated?: boolean | undefined): NavPathInfo | undefined
  pop(result?: Object, animated?: boolean | undefined): NavPathInfo | undefined {
    console.log('[derive-test] reached DerivedNavPathStack's pop');
    return super.pop(result, animated);
  }

  // other function of base class...
}

class param {
  info: string = "__default_param__";
  constructor(info: string) { this.info = info }
}

@Entry
@Component
struct Index {
  derivedStack: DerivedNavPathStack = new DerivedNavPathStack();

  aboutToAppear(): void {
    this.derivedStack.setId('origin stack');
  }

  @Builder
  pageMap(name: string) {
    PageOne()
  }

  build() {
    Navigation(this.derivedStack) {
      Button('to Page One').margin(20).onClick(() => {
        this.derivedStack.pushPath({
          name: 'pageOne',
          param: new param('push pageOne in homePage when stack size: ' + this.derivedStack.size())
        });
      })
    }.navDestination(this.pageMap)
    .title('Home Page')
  }
}

@Component
struct PageOne {
  derivedStack: DerivedNavPathStack = new DerivedNavPathStack();
  curStringifyParam: string = "NA";

  build() {
    NavDestination() {
      Column() {
        Text(this.derivedStack.getInfo())
          .margin(10)
          .fontSize(25)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Start)
        Text('current page param info:')
          .margin(10)
          .fontSize(25)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Start)
        Text(this.curStringifyParam)
          .margin(20)
          .fontSize(20)
          .textAlign(TextAlign.Start)
      }.backgroundColor(Color.Pink)
      Button('to Page One').margin(20).onClick(() => {
        this.derivedStack.pushPath({
          name: 'pageOne',
          param: new param('push pageOne in pageOne when stack size: ' + this.derivedStack.size())
        });
      })
    }.title('Page One')
    .onReady((context: NavDestinationContext) => {
      console.log('[derive-test] reached PageOne's onReady');
      // get derived stack from navdestinationContext
      this.derivedStack = context.pathStack as DerivedNavPathStack;
      console.log('[derive-test] -- got derivedStack: ' + this.derivedStack.id);
      this.curStringifyParam = JSON.stringify(context.pathInfo.param);
      console.log('[derive-test] -- got param: ' + this.curStringifyParam);
    })
  }
}

实现方案:

1、定义页面跳转的配置项。

使用资源文件进行定义,通过资源管理@ohos.resourceManager在运行时对资源文件解析。

在ets文件中配置路由加载配置项,一般包括路由页面名称(即pushPath等接口中页面的别名),文件所在模块名称(hsp/har的模块名),加载页面在模块内的路径(相对src目录的路径)。

2、加载目标跳转页面,通过动态import将跳转目标页面所在的模块在运行时加载, 在模块加载完成后,调用模块中的方法,通过import在模块的方法中加载模块中显示的目标页面,并返回页面加载完成后定义的Builder函数。

3、触发页面跳转,在Navigation的navDestination属性执行步骤2中加载的Builder函数,即可跳转到目标页面。

具体的路由跳转传参、返回以及带参数返回、转场动画等可以参照官方的说明文档。

使用router路由框架实现不同module间的页面跳转

注意:5.0开始已经不再推荐使用该方式了。

两种跳转模式

系统的router模块提供了两种跳转模式,分别是router.pushUrl和router.replaceUrl。这两种模式决定了目标页面是否会替换当前页。

1、router.pushUrl:目标页面不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back方法返回到当前页。

2、router.replaceUrl:目标页面会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

注意:

页面栈的最大容量为32个页面。如果超过了这个限制,可以调用router.clear方法清空历史页面栈,释放内存空间。

两种page实例的使用模式

Router模块提供了两种实例模式,分别是Standard和Single。这两种模式决定了目标url对应的目标page在页面栈中是否会存在多个实例。

1、Standard:多实例模式,也是默认情况下的跳转模式。目标页面会被添加到页面栈顶,无论栈中是否存在相同url的页面。

2、Single:单实例模式。如果目标页面的url已经存在于页面栈中,则会将离栈顶最近的同url页面移动到栈顶,展示该页面,而不再新建页面实例。如果目标页面的url在页面栈中不存在同url的页面,则按照默认的多实例模式进行跳转。

javascript 复制代码
import { router } from '@kit.ArkUI';

// 在Home页面中
function onJumpClick(): void {
  router.pushUrl({
    url: 'pages/Detail'   // 目标url
  }, router.RouterMode.Standard, (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  });
}


// 在Setting页面中
function onJumpClick(): void {
  router.pushUrl({
    url: 'pages/Theme'   // 目标url
  }, router.RouterMode.Single, (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  });
}

注意:在多实例模式下,router.RouterMode.Standard参数可以省略。

页面返回并传值

可以使用以下几种方式返回页面:

方式一:返回到上一个页面。

ini 复制代码
router.back();

这种方式会返回到上一个页面,即上一个页面在页面栈中的位置。但是,上一个页面必须存在于页面栈中才能够返回,否则该方法将无效。

方式二:返回到指定页面。

1>返回到同module里的普通页面。

css 复制代码
    router.back({
      url: 'pages/Home'
    });

2>返回到命名路由页面。

arduino 复制代码
    router.back({
      url: 'myPage'     // myPage为返回的命名路由页面别名
    });

这种方式可以返回到指定页面,需要指定目标页面的路径。目标页面必须存在于页面栈中才能够返回。

方式三:返回到指定页面,并传递自定义参数信息。

1>返回到普通页面。

php 复制代码
    router.back({
      url: 'pages/Home',
      params: {
        info: '来自Home页'
      }
    });

2>返回命名路由页面。

php 复制代码
    router.back({
      url: 'myPage', //myPage为返回的命名路由页面别名
      params: {
        info: '来自Home页'
      }
    });

这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义的参数信息。这些参数信息可以在目标页面中通过调用router.getParams方法进行获取和解析。

在目标页面中,在需要获取参数的位置调用router.getParams方法即可,例如在onPageShow生命周期回调中:

注意:直接使用router可能导致实例不明确的问题,建议使用getUIContext获取UIContext实例,并使用getRouter获取绑定实例的router。

typescript 复制代码
@Entry
@Component
struct Home {
  @State message: string = 'Hello World';

  onPageShow() {
    const params = this.getUIContext().getRouter().getParams() as Record<string, string>; // 获取传递过来的参数对象
    if (params) {
      const info: string = params.info as string; // 获取info属性的值
    }
  }
  ...
}

当使用router.back方法返回到指定页面时,原栈顶页面(包括)到指定页面(不包括)之间的所有页面都将从栈中弹出并销毁。

另外,如果使用router.back方法返回到原来的页面,原页面不会被重复创建,因此使用@State声明的变量不会重复声明,也不会触发页面的aboutToAppear生命周期回调。如果需要在原页面中使用返回页面传递的自定义参数,可以在需要的位置进行参数解析。例如,在onPageShow生命周期回调中进行参数解析。

页面返回时加拦截提示框

1>系统默认询问框

为了实现这个功能,可以使用页面路由Router模块提供的两个方法:router.showAlertBeforeBackPage和router.back来实现这个功能。

javascript 复制代码
import { router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
  // 调用router.showAlertBeforeBackPage()方法,设置返回询问框的信息
  try {
    router.showAlertBeforeBackPage({
      message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
    });
  } catch (err) {
    let message = (err as BusinessError).message
    let code = (err as BusinessError).code
    console.error(`Invoke showAlertBeforeBackPage failed, code is ${code}, message is ${message}`);
  }

  // 调用router.back()方法,返回上一个页面
  router.back();
}

当用户点击"返回"按钮时,会弹出确认对话框,询问用户是否确认返回。选择"取消"将停留在当前的目标页面;选择"确认"将触发router.back方法,并根据参数决定如何执行跳转。

2>自定义询问框

自定义询问框的方式,可以使用弹窗promptAction.showDialog或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。

javascript 复制代码
import { promptAction, router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

function onBackClick() {
  // 弹出自定义的询问框
  promptAction.showDialog({
    message: '您还没有完成支付,确定要返回吗?',
    buttons: [
      {
        text: '取消',
        color: '#FF0000'
      },
      {
        text: '确认',
        color: '#0099FF'
      }
    ]
  }).then((result:promptAction.ShowDialogSuccessResponse) => {
    if (result.index === 0) {
      // 用户点击了"取消"按钮
      console.info('User canceled the operation.');
    } else if (result.index === 1) {
      // 用户点击了"确认"按钮
      console.info('User confirmed the operation.');
      // 调用router.back()方法,返回上一个页面
      router.back();
    }
  }).catch((err:Error) => {
    let message = (err as BusinessError).message
    let code = (err as BusinessError).code
    console.error(`Invoke showDialog failed, code is ${code}, message is ${message}`);
  })
}

跳转到hap里的page并返回

即前面通用的跳转和返回。

跳转到hsp里的page并返回

typescript 复制代码
import { Log, add, MyTitleBar, ResManager, nativeMulti } from 'library';
import { BusinessError } from '@ohos.base';
import router from '@ohos.router';

const TAG = 'Index';

@Entry
@Component
struct Index {
  @State message: string = '';

  build() {
    Column() {
      List() {
        ListItem() {
          Text($r('app.string.click_to_menu'))
            .fontSize(18)
            .textAlign(TextAlign.Start)
            .width('100%')
            .fontWeight(500)
            .height('100%')
        }
        .id('clickToMenu')
        .borderRadius(24)
        .width('685px')
        .height('84px')
        .backgroundColor($r('sys.color.ohos_id_color_foreground_contrary'))
        .margin({ top: 10, bottom: 10 })
        .padding({ left: 12, right: 12, top: 4, bottom: 4 })
        .onClick(() => {
          router.pushUrl({
            url: '@bundle:com.samples.hspsample/library/ets/pages/Menu'
          }).then(() => {
            console.log('push page success');
          }).catch((err: BusinessError) => {
            console.error('pushUrl failed, code is' + err.code + ', message is' + err.message);
          })
        })
      }
      .alignListItem(ListItemAlign.Center)
    }
    .width('100%')
    .backgroundColor($r('app.color.page_background'))
    .height('100%')
  }
}

其中router.pushUrl方法的入参中url的内容为:'@bundle:com.samples.hspsample/library/ets/pages/Menu'

url内容的模板为:

'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'

页面路由返回:

如果当前处于HSP中的page中,需要返回到之前的页面时,可以使用router.back方法,但是返回的页面必须是当前页面跳转路径上的页面,即当前页面栈里的page。

php 复制代码
import router from '@ohos.router';

@Entry
@Component
struct Index3 {              // 路径为:`library/src/main/ets/pages/Back.ets
  @State message: string = 'HSP back page';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontFamily('HarmonyHeiTi')
          .fontWeight(FontWeight.Bold)
          .fontSize(32)
          .fontColor($r('app.color.text_color'))
          .margin({ top: '32px' })
          .width('624px')

        Button($r('app.string.back_to_HAP'))
          .id('backToHAP')
          .fontFamily('HarmonyHeiTi')
          .height(48)
          .width('624px')
          .margin({ top: 550 })
          .type(ButtonType.Capsule)
          .borderRadius($r('sys.float.ohos_id_corner_radius_button'))
          .backgroundColor($r('app.color.button_background'))
          .fontColor($r('sys.color.ohos_id_color_foreground_contrary'))
          .fontSize($r('sys.float.ohos_id_text_size_button1'))
          // 绑定点击事件
          .onClick(() => {
            router.back({             //  返回到HAP里的页面
              url: 'pages/Index'      // 路径为:`entry/src/main/ets/pages/Index.ets`
            })
          })

        Button($r('app.string.back_to_HSP'))
          .id('backToHSP')
          .fontFamily('HarmonyHeiTi')
          .height(48)
          .width('624px')
          .margin({ top: '4%' , bottom: '6%' })
          .type(ButtonType.Capsule)
          .borderRadius($r('sys.float.ohos_id_corner_radius_button'))
          .backgroundColor($r('app.color.button_background'))
          .fontColor($r('sys.color.ohos_id_color_foreground_contrary'))
          .fontSize($r('sys.float.ohos_id_text_size_button1'))
          // 绑定点击事件
          .onClick(() => {
            router.back({          //  返回到HSP里的页面
              url: '@bundle:com.samples.hspsample/library/ets/pages/Menu'      //路径为:`library/src/main/ets/pages/Menu.ets
            })
          })
      }
      .width('100%')
    }
    .backgroundColor($r('app.color.page_background'))
    .height('100%')
  }
}

页面返回router.back方法的入参中url的说明:

1、如果从HSP里的page返回HAP里的page,url的内容为:'pages/Index'

url内容的模板为:

'页面所在的文件名(不加.ets后缀)'

2、如果从HSP1里的page跳转到HSP2里的page后,需要返回到HSP1里的page时,url的内容为:

'@bundle:com.samples.hspsample/library/ets/pages/Menu'

url内容的模板为:

'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'

跳转到har里的page

har里也可以书写page代码,但是不能在配置文件中声明,且只能通过命名路由的方式进行跳转,可以使用router.pushNamedRoute来实现。

注意:这里没法路由返回。

在har module里定义命名page:

scss 复制代码
// library/src/main/ets/pages/Index.ets
// library为新建共享包自定义的名字

@Entry({ routeName: 'myPage' })     // 该page的命名路由为myPage
@Component
export struct MyComponent {
  build() {
    Row() {
      Column() {
        Text('Library Page')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

在跳转的页面中引入命名路由的页面:

typescript 复制代码
import { BusinessError } from '@kit.BasicServicesKit';
import '@ohos/library/src/main/ets/pages/Index';        // 引入共享包中的命名路由页面

@Entry
@Component
struct Index {
  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Text('Hello World')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
        .backgroundColor('#ccc')
        .onClick(() => { // 点击跳转到其他共享包中的页面
          try {
            this.getUIContext().getRouter().pushNamedRoute({
              name: 'myPage',
              params: {
                data1: 'message',
                data2: {
                  data3: [123, 456, 789]
                }
              }
            })
          } catch (err) {
            let message = (err as BusinessError).message
            let code = (err as BusinessError).code
            console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`);
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

注意:使用命名路由的方式跳转时,需要在当前应用包的oh-package.json5文件中配置依赖的har包。

less 复制代码
"dependencies": {
   "@ohos/library": "file:../library",
   ...
}

同一module构建后的多目标产物以及多个目标app产物

对于同一个module来说,在有些情况下,需要根据不同的产品形态构建成稍微有差异的不同的软件包,这就需要使用到配置target的功能了。同样,在不同的产品形态(应用)下,构建各个不同的module时,选择不同的module进行打包生成的app也是不同的,就需要配置product的功能。

target和product的概念:

1、工程内的每一个Entry/Feature模块,对应的构建产物为一个hap,hap是应用/服务可以独立运行在设备中的形态。由于在不同的业务场景中,同一个模块可能需要定制不同的功能或资源,因此引入target的概念。一个module可以定义多个target,每个target对应一个定制的hap,通过配置可以实现一个module构建出不同的hap。

2、一个HarmonyOS工程的构建产物为APP包,APP包用于应用/服务发布上架到应用市场。由于不同的业务场景,需要定制不同的应用包,因此引入product的概念。一个工程可以定义多个product,每个product对应一个定制化的应用包,通过配置可以实现一个工程构建出多个不同的应用包。

定制HAP的多目标构建产物

每一个Entry/Feature模块均支持定制不同的target,通过在模块中的build-profile.json5文件中实现差异化定制,当前支持HAP包名、设备类型(deviceType)、源码集(source)、资源(resource)、buildOption配置项(如C++依赖的.so、混淆配置、abi类型、cppFlags等)、分发规则(distributionFilter)的定制。

定制HAR的多目标构建产物

每一个HAR模块均支持定制不同的target,通过在模块中的build-profile.json5文件中实现差异化定制,当前支持设备类型(deviceType)、资源(resource)、buildOption配置项(如C++依赖的.so、混淆配置、abi类型、cppFlags等)、源码集(source)的定制。

注意:当前版本,在DevEco Studio中编译时,仅支持编译target为default的模块。若需指定其他target,需通过命令行来指定,并通过命令行来编译。

例如构建指定的自定义target:free的har,可参考执行以下命令:

hvigorw --mode module -p product=default -p module=library@free -p buildMode=debug assembleHar

配置APP的多目标构建产物

APP用于应用/服务上架发布,针对不同的应用场景,可以定制不同的product,每个product中支持对bundleName、bundleType、签名信息、icon和label以及包含的target进行定制。

注意:在定制product时,必须存在"default"的product,否则编译时会出现错误。

在工程目录下的build-profile.json5文件中的示例配置如下:

json 复制代码
{ 
  "app": { 
    "signingConfigs": [], //此处通过界面配置签名后会自动生成相应的签名配置,本文略 
    "products": [ 
      { 
        "name": "default", 
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)", 
        "runtimeOS": "HarmonyOS", 
        "bundleName": "com.example00.com"  
      }, 
      { 
        "name": "productA", 
        "signingConfig": "productA",
        "compatibleSdkVersion": "5.0.0(12)", 
        "runtimeOS": "HarmonyOS", 
        "bundleName": "com.example01.com"  
      }, 
      { 
        "name": "productB", 
        "signingConfig": "productB",  
        "compatibleSdkVersion": "5.0.0(12)", 
        "runtimeOS": "HarmonyOS", 
        "bundleName": "com.example02.com" 
      } 
    ], 
  "modules": [ 
    { 
      "name": "entry", 
      "srcPath": "./entry", 
      "targets": [ 
        { 
          "name": "default",  //将default target打包到default APP中 
          "applyToProducts": [ 
            "default" 
          ] 
        }, 
        { 
          "name": "free",  //将free target打包到productA APP中 
          "applyToProducts": [ 
            "productA" 
          ] 
        }, 
        { 
          "name": "pay",  //将pay target打包到productB APP中 
          "applyToProducts": [ 
            "productB" 
          ] 
        } 
      ] 
    } 
  ] 
}

target的优先级

1、align target

在编译构建时,align target是优先级最高的target。在工程中配置了align target后,如果一个模块中存在align target,那么无论使用哪种构建方式都将自动选择align target进行构建。align target的作用范围是针对整个工程,所以在工程中只能配置一个,支持命令行和配置文件两种方式。

命令行方式示例如下:

hvigorw -c properties.ohos.align.target=target1 assembleHap

在工程的hvigor目录下的hvigor-config.json5配置文件中添加ohos.align.target,示例如下:

arduino 复制代码
            "properties": {
              'ohos.align.target': 'target1'
            },

2、fallback target

当一个module中不存在指定的target时,会选用default进行构建,但如果不想用default进行构建,那么可以配置fallback target,当找不到指定target时,如果模块中存在fallback target,则使用fallback target进行构建。fallback target的作用范围是整个工程,可配置多个,配置多个时按数组中的顺序先命中的生效。

命令行方式示例如下:

hvigorw -c properties.ohos.fallback.target=target1,target2 assembleHap

在工程的hvigor目录下的hvigor-config.json5配置文件中添加ohos.fallback.target,示例如下:

arduino 复制代码
            "properties": {
              'ohos.fallback.target': ['target1', 'target2']
            }

3、target的优先级顺序

align target和fallback target的配置方式中命令行的优先级高于配置文件。

使用配置文件配置align target和fallback target时,仅支持DevEco Studio界面的Build菜单栏功能,不支持Run菜单栏功能,可通过hdc命令行工具进行推包运行、调试。

多个target的优先级顺序为:align target > 命令行指定模块target > 父级模块target > fallback target > default。

​编辑

举例说明:

比如工程中的module的依赖关系:entry->lib1->lib2,需要构建多个产品A、B、C时,工程中的target配置如下:

entry: A、B、default

lib1: B、C、default

lib2: A、C、default

指定align target为A,fallback target为C。那么构建hap时的编译命令为:

hvigorw --mode module -p module=entry -c properties.ohos.align.target=A -c properties.ohos.fallback.target=C assembleHap

编译的target选择就是:entry@A, lib1@C, lib2@A。

注意:以上所有说明仅针对非ohosTest模式。在ohosTest模式下,依赖的target固定为default,其他target均不生效。

相关推荐
HarmonyOS_SDK12 分钟前
【FAQ】HarmonyOS SDK 闭源开放能力 —Vision Kit
harmonyos
Forever_Hopeful2 小时前
华为 HarmonyOS NEXT 原生应用开发: 动画的基础使用(属性、显示、专场)动画
华为·harmonyos
青瓷看世界2 小时前
华为HarmonyOS打造开放、合规的广告生态 - 开屏广告
华为·harmonyos·广告投放
青瓷看世界5 小时前
华为HarmonyOS借助AR引擎帮助应用实现虚拟与现实交互的能力3-获取设备位姿
华为·ar·harmonyos·虚拟现实
青瓷看世界7 小时前
华为HarmonyOS借助AR引擎帮助应用实现虚拟与现实交互的能力4-检测环境中的平面
平面·ar·harmonyos·虚拟现实
SoraLuna7 小时前
「Mac畅玩鸿蒙与硬件27」UI互动应用篇4 - 猫与灯的互动应用
macos·ui·harmonyos
SoraLuna8 小时前
「Mac畅玩鸿蒙与硬件29」UI互动应用篇6 - 多选问卷小应用
macos·ui·harmonyos
青瓷看世界13 小时前
华为HarmonyOS打造开放、合规的广告生态 - 原生广告
华为·harmonyos·广告投放
lqj_本人17 小时前
Flutter&鸿蒙next中封装一个列表组件
flutter·华为·harmonyos