引言

由于公司开始支持鸿蒙生态,小编也开启了鸿蒙纯血NEXT版本的应用开发。

本篇主要整理总结实战项目中遇到的问题及重点知识内容。适合使用ArkTS进行鸿蒙NEXT及以上版本开发的初学者、进阶学习者。

内容分为以下几个维度,欢迎大家一起交流学习。

ArkTS 语言

使用ArkTs语言进行开发,了解如下两点即可

1. 数据类型

ArkTS支持的数据类型如下:

Object、string、number、boolean、enum 、Array、union、void、aliases、Record、Map 等。

下面列举一些个例的特性:

// string
undefined == name?:string

// Array
let name: string[] = ['rex','bob']

// union | 联合类型
type Animal = Cat | Dog
let animal : Animal = new Cat()
animal = new Dog()

// aliases | 匿名类型
type Predicate <T> = (x: T) => Boolean;
type NullableObject = Object | null;

// Record<K,V> | Record 与 Map 的区别是 Record 不需要 new,Map 需要 new
let map: Record<string, number> = {
         
         
  'John': 25,
  'Mary': 21,
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
2. 语法特点

ArkTS的语法与其他前端语言大同小异,下面列举一些个例的特性:

// Rest参数
fun sum(...numbers: number[])
sum()
sum(1,2,3)
 
// 定义回调
type trigFunc = (x:number) => number
function do_action(f: trigFunc)

// 闭包
function f(): () => number {
         
         
  let count = 0;
  return (): number => {
         
          count++; return count; }
}
let z = f();
z(); // 返回:1
z(); // 返回:2

// 可选参数
function hello(name?: string) // name == undefined
function multiply(n: number, coeff: number = 2)
nultiply(1)

// 字段不复值时默认值是 undefined

// 构造函数需要使用关键字
constructor(n: string, a: number)

// 模块导出
export let Origin = new Point(0, 0);
export function Distance(p1: Point, p2: Point): number{
         
          *** }

// 模块导入
import * as Utils from './utils' // Utils.X
import {
         
          X, Y } from './utils'
X // 表示来自utils的X
Y // 表示来自utils的Y

// 声明
readonly height : number = 1 //只读 
let hi: string = 'hello' //变量
const hello: string = 'hello' //常量

// 空安全
let x: number | null = null

// 接口 interface
interface W {
         
         
  bundleName: string
  action: string
  entities: string[]
}
let wantInfo: W = {
         
         
  bundleName: 'com.huawei.hmos.browser',
  action: 'ohos.want.action.viewData',
  entities: ['entity.system.browsable']
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

PS:需要注意的是,字符串里插入变量要使用`符号而不是单引号或双引号,如下:

Text(`插入变量到字符串中的值为${
           
           this.number}`)
  • 1.

HarmonyOS 的项目结构

关于 HarmonyOS 的项目包结构,我们暂时不需要做到 全盘掌握,了解如下几个知识点即可:

1. 项目类型

创建项目分为两种类型,分别是 app(应用)、atomicService( 元服务)

HarmonyOS :去繁化简,实战整理的开发者清单【NEXT版本】_HarmonyOS

这两种类型的项目创建后的包结构是一样的,代码编辑也一致。重点差异如下:

  • 元服务类似于微信小程序,用户可直接在手机的负一层页面搜索使用,无需下载
  • 元服务的开发api是应用的开发api的子集,即有些api在开发应用时可用,在开发元服务时不可用。
2. 模块类型

创建模块分为三种类型,分别是 hap、har、hsp

HarmonyOS :去繁化简,实战整理的开发者清单【NEXT版本】_HarmonyOS_02

  • HAP:应用安装和运行的基本单元,分为两种类型:entry和feature
  • HAR:静态共享包 (可以理解为封装的 Library,里面可以存放代码和资源,提供给 hap 或者 har 模块进行引用)
  • HSP:动态共享包(一般应用不常用),和静态包类似,用于封装公共 Library。

静态包和动态包差异点:

  • HAR中的代码和资源跟随使用方编译,如果有多个使用方,它们的编译产物中会存在多份相同拷贝;而HSP中的代码和资源可以独立编译,运行时在一个进程中代码也只会存在一份。
  • 动态包可用于分包模式,例如元服务对每个hap包有大小 2M 的限制,通过分包,preload 的方式满足发布条件。 分包链接->
3. 重点文件
  • app.json5 :bundleType 标识 元服务(atomicService)或 应用(app)
  • module.json5 :stage模型模块配置文件,type 标识模块类型(entry、hap、har、share)
  • oh-package.json5 :配置三方包声明文件的入口及包名

最后展示一张项目的包结构截图:

HarmonyOS :去繁化简,实战整理的开发者清单【NEXT版本】_HarmonyOS_03

UI 开发中的实用技巧

1. 单位
  • fp: 字体像素单位,随系统字体大小设置变化
  • vp: 密度像素单位
//使用示例
Text('示例').fontSize('16fp').widht('100vp')
  • 1.
  • 2.
2. @Extend

定义扩展组件样式

@Extend(Column)
function ColumnStyle() {
         
         
  .width('100%')
  .borderRadius(24)
  .backgroundColor(Color.White)
  .padding({
         
          left: 12, right: 12, bottom: 4, top: 4 })
}

Column(){
         
         }.ColumnStyle()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
3. @Styles

定义组件重用样式

@Styles fancy() {
         
         
  .width(200)
  .height(this.heightValue)
  .backgroundColor(Color.Yellow)
  .onClick(() => {
         
         
  this.heightValue = 200
  })
}

Text().fancy()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
4. 自定义组件 @Builder 和 @BuilderParam
@Component
struct Child {
         
         
  @Builder FunABuilder0() {
         
         }
  @BuilderParam aBuilder0: () => void = this.FunABuilder0;

  build() {
         
         
    Column() {
         
         
      this.aBuilder0()
    }
  }
}

@Entry
@Component
struct Parent {
         
         
  @Builder componentBuilder() {
         
         
    Text(`Parent builder `)
  }

  build() {
         
         
    Column() {
         
         
      Child({
         
          aBuilder0: this.componentBuilder })
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
5. 生命周期
  • 自定义组件:@Component装饰的UI单元,可以调用组件的生命周期。
  • 页面:可以由一个或者多个自定义组件组成,只有被@Entry装饰的组件才可以调用页面的生命周期。
    •  onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。
    •  onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景,仅@Entry装饰的自定义组件生效。
    •  onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。
    •  aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
    •  aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

HarmoneyOS 的状态管理

1. 组件的状态管理

建议看一遍官网的示例

2. 应用的状态管理

PS:重点提一下 LocalStorage 和 AppStorage 的区别,LocalStorage 是页面级存储、页面内共享,而 AppStorage 是可以跨 Ability 共享的。(即适用于多hap项目)

应用开发中常用方法

1. Toast 的使用
promptAction.showToast({
         
         
  message: $r('app.string.success_message'),
  duration: 2000
})
  • 1.
  • 2.
  • 3.
  • 4.
2. Preferences 的使用
import dataPreferences from '@ohos.data.preferences';

//声明
let dataPreferencesManager: dataPreferences.Preferences = {
         
         } as dataPreferences.Preferences;

//获取实例
let options: dataPreferences.Options = {
         
          name: 'myStore', dataGroupId:'myId' };
dataPreferences.getPreferences(this.context, options, (err: BusinessError, val: dataPreferences.Preferences) => {
         
         
    if (err) {
         
         
        return;
    }
    preferences = val;
})

//put
dataPreferencesManager.push(key,value)
dataPreferencesManager.flush()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
3. Context
/// 实现全局Context
// 在 EntryAbility 的 create 方法中绑定上下文
AppStorage.setOrCreate('context', this.context);

//获取全局Context
AppStorage.get('context')


/// UI页面获取局部Context
const CONTEXT: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
4. JSON 与 Object 的转换
/// JSON to Object
/// 方案一:
class A {
         
         
  v: number = 0
  s: string = ''
  fromJson (str: string) {
         
         
    let tmpStr: Record<string, Object> = JSON.parse(str);
    if (tmpStr.add != undefined) {
         
         
      this.v = tmpStr.v as number;
      this.s = tmpStr.s as string;
    }
  }
}
/// 方案二:
let json: string = '{"name":"rex","age":"10"}'
class Student {
         
         
  name: string;
  age: number;
  constructor(name: string, age: number) {
         
         
    this.name = name;
    this.age = age;
  }
}
let student = JSON.parse(json) as Student;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
/// Object to Json(String)
JSON.stringify(any)
  • 1.
  • 2.
5. Http 的使用

使用官方提供的 HTTP模块

// 引入包名
import http from '@ohos.net.http';
import {
         
          BusinessError } from '@ohos.base';

// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
         
         
  console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
  // 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
  "EXAMPLE_URL",
  {
         
         
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: [{
         
         
      'Content-Type': 'application/json'
    }],
    // 当使用POST请求时此字段用于传递内容
    extraData: "data to send",
    expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
    usingCache: true, // 可选,默认为true
    priority: 1, // 可选,默认为1
    connectTimeout: 60000, // 可选,默认为60000ms
    readTimeout: 60000, // 可选,默认为60000ms
    usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
    usingProxy: false, //可选,默认不使用网络代理,自API 10开始支持该属性
  }, (err: BusinessError, data: http.HttpResponse) => {
         
         
    if (!err) {
         
         
      // data.result为HTTP响应内容,可根据业务需要进行解析
      console.info('Result:' + JSON.stringify(data.result));
      console.info('code:' + JSON.stringify(data.responseCode));
      // data.header为HTTP响应头,可根据业务需要进行解析
      console.info('header:' + JSON.stringify(data.header));
      console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
      // 当该请求使用完毕时,调用destroy方法主动销毁
      httpRequest.destroy();
    } else {
         
         
      console.error('error:' + JSON.stringify(err));
      // 取消订阅HTTP响应头事件
      httpRequest.off('headersReceive');
      // 当该请求使用完毕时,调用destroy方法主动销毁
      httpRequest.destroy();
    }
  }
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

PS:后期小编会封装一个简化代码的静态库提供使用

6. 路由的选择与使用

路由提供两种选择方案:

组件 适用场景 特点 转场动画效果对比
Router 模块间与模块内页面切换 通过每个页面的url实现模块间解耦 页面平推转场效果
NavPathStack 模块内页面切换 通过组件级路由统一路由管理 向右折叠转场效果
  • Router的使用
router.pushUrl({
         
         
    url: 'pages/routerpage2',
    params: new routerParams('message' ,[123,456,789])
})

/// url 对应 resource - profile - mainpages.json
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • NavPathStack的使用

小编使用的方案是 NavPathStack ,下面来重点说说使用方式。

NavPathStack 是配合 Navigation 一起使用的,
Navigation导航组件做统一的页面跳转管理,它提供了一系列属性方法来设置页面的标题栏、工具栏以及菜单栏的各种展示样式。

//第一步,需要在应用的根页面自行维护 navStack 实例,并传递给根节点 Navigation
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()

Navigation(this.pageInfos) {
         
         
  Column() {
         
         }
}
.title('NavIndex')
.navDestination(this.PageMap)


// 统一管理维护路由跳转
@Builder
  PageMap(name: string) {
         
         
    if (name === 'pageOne') {
         
         
      PageOneTmp()
    } else if (name === 'pageTwo') {
         
         
      pageTwoTmp()
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
// 第二步,注意,目标跳转页面节点需要使用 NavDestination 进行包裹
// 目标跳转页面,例如 PageOneTmp
@Component
export struct PageOneTmp {
         
         
 build(){
         
         
    NavDestination(){
         
         
       Column(){
         
         
       
       }
    }
 }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
/// 如何跳转
this.pageInfos.pushPathByName('pageOne', null)

  • 1.
  • 2.
  • 3.

附注

Demo 示例已上传:

GitHub: https://github.com/liyufengrex/HarmonyAtomicService

GitCode: https://gitcode.com/liyufengrex/HarmonyAtomicService

(基于API11开发,支持NEXT及以上版本运行)已上传可供参考,包含如下内容:

  • 静态库+动态包+多模块设计
  • 状态管理
  • 统一路由管理(router+navPathStack)
  • 网络请求、Loading 等工具库封装
  • 自定义组件、自定义弹窗(解耦)
  • EventBus 事件通知
  • 扩展修饰器,实现 节流、防抖、权限申请
  • 动态路由 (navPathStack + 动态import + WrappedBuilder)
  • UI动态节点操作 (BuilderNode + NodeController)
  • 折叠屏适配示例
  • 组件工厂示例
  • 组件动态属性设置示例
  • 云函数、云数据库使用示例
  • 华为账号服务示例(快速登陆、快速验证手机号)