哪些人适合看这篇笔记 ?
- 如果你想通过这篇笔记了解鸿蒙应用开发的全貌。
- 如果你在看鸿蒙的面试题,但是不知道从哪里开始看。
- 如果你只是想复习一下鸿蒙的知识。
- 如果你想简单了解一下鸿蒙应用开发的基础知识。
非常不适合看这篇笔记
- 试图通过该笔记彻底学会鸿蒙应用开发。
- 试图找到开发过程中遇到问题的具体解决方案。
- 几乎无任何基础。
相较于看官网文档的好处
官网文档的主要风格围绕 “指南” 和 “API” 进行陈述,这篇笔记主要目的是为你进一步梳理知识体系,强调 “骨架清晰” 且不废话,会着重强调某些实践过程中的坑。
指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/application-dev-guide-V5?catalogVersion=V5API参考
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/development-intro-api-V5?catalogVersion=V5在某些知识梯度比较大的地方,我会尽可能附上链接供你直接跳转到官网。
我会较少的出现代码和图片(因为这并不是直接教会你的文章),但我也会尽可能的使用它们来解释重点部分和难以理解的内容。
(好多代码演示部分都是直接从官网拿下来的,叠甲中 - - )
UI范式的基础
声明式的语法规则
如果熟悉ts、js等的开发人员更容易理解其基本用法。ArkTS 基本的组成部分有:
- 装饰器 (用来定义或增强组件行为)
- 自定义组件(允许用户来自定义组件,使得符合组件化思想,强调可复用性)
- UI描述 (描述一个UI 的基本结构,写在build 方法中的代码块, 其中主要包含了系统组件 和 自定义组件)
- 系统组件(Button,Row,Column等,系统提供的基本组件,帮助你构建容器或其他基础功能,开发者可以直接使用)
- 属性方法(通过链式调用的方式为组件配置多个属性,fontSize()、width()等)
- 事件方法(同样的也是通过链式调用的方式为组件设置事件的响应,比如 onClick 是为了响应点击事件)
细粒度的封装和复用UI或样式 (很重要)
- @Builder 和 BuilderParam,通过标记一个函数,来封装一些UI,使得UI可以细粒度的被封装在组件内部,最终完成复用。
-
@Extend 和 @Styles,拓展内置组件和样式的封装,前面说到了,无论是系统组件还是自定义组件。其样式都是通过链式方法调用设置的,区别于 Css 那种通过选择器绑定的方式设置样式,因此,样式的复用需要使用@Styles 来进行封装。
-
stateStyles,依据组件内部状态来应用不同样式。
自定义组件
其主要目的是为了可组合和可复用,其次通过状态的改变来驱动UI的重新渲染。
- 使用@Component 来定义一个自定义组件
- 编写build 函数的实现,在其内部进行UI描述(相当于编写html)
- 在其内部定义成员变量,函数都没问题,但要定义组件受控状态需要使用 @State (该装饰器属于状态管理V1,建议选择V2)
- 如果自定义组件作为了页面入口,需要增加@Entry
- 如果需要调试预览组件,需要增加@Preview
- 组件拥有生命周期,主要地: aboutToAppear (组件即将出现回调该函数,你可以在此初始化你的状态来进行预备),onDidBuild(组件build()函数执行结束回调该函数,这里只能做不影响UI 的一些事,比如埋点等)、aboutToDisappear 自定义组件在销毁之前执行。
另外在官网的自定义组件介绍中,强调了页面与组件的关系,还联系说明了页面的生命周期函数:
- onPageShow: 页面每次显示都会触发。
- onPageHide:页面每次隐藏都会触发。
- onBackPress:用户按下了返回按钮会触发。
这里其实还涉及一个知识点:如果你的自定义组件不是作为入口页面(没添加@Entry装饰器),但你又想要监听页面的生命周期(未增加@Entry没办法直接获得生命周期函数的执行),那么你就需要 通过 listener来实现监听。
自定义组件的拓展
自定义组件还可以有更加丰富的拓展,通过使用某些扩展装饰器来增强组件功能。
- @Builder 装饰器:自定义构建函数 (是在组件内部维持UI复用)
- @LocalBuilder 装饰器:维持组件父子关系,解决 函数内部的 this 指向问题。(实践下来用的频率不高)
- @BuilderParam 装饰器:在父组件中向子组件传递UI时,子组件使用该装饰器装饰 @Builder 函数,可获得对 UI 的引用,相当于子组件内的一个 插槽。父组件传递UI时,常见的可使用尾随闭包的方式传递(在初始化自定义组件时,组件后紧跟一个大括号“{}”形成尾随闭包场景。)
- @Styles 装饰器:预声明一些样式的函数调用,供自定义组件直接使用(参见细粒度的封装和复用UI或样式 )。
- @Require 装饰器:子组件可使用该装饰器来强调某一参数对于父组件来说是必须的。
- @Reusable 装饰器:被该装饰器标记的组件在组件卸载时,会将组件放入缓存,下次创建的时候会优先从缓存复用,加快性能(面试会问到性能优化,那你就可以拿这个说)。
渲染控制 (if else forEach 等)
这部分其实挺重要的,对于一些前端过度到鸿蒙的开发者他们会经常在这部分摸不着头脑,但总体属于一看就懂的部分。
if、else 条件渲染
最好理解的一部分了,你可以直接在build 函数中使用 if else,甚至是嵌套 if(之前写react 写多了,很羡慕这种写法,因为 jsx 不在函数里的话不能这么干,都是写表达式来达到同等效果)。
@Entry
@Component
struct MainView {
@State toggle: boolean = true;
build() {
Column() {
if (this.toggle) {
CounterView({ label: 'CounterView #positive' })
} else {
CounterView({ label: 'CounterView #negative' })
}
Button(`toggle ${this.toggle}`)
.onClick(() => {
this.toggle = !this.toggle;
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
ForEach 循环渲染
循环渲染某些UI的时候使用,但是这里开始有些和 if 不一样的地方了,你不能直接使用 for 了(至少大部分语言是 for),你得需要ForEach (注意了,这里 F 可是大写,别掉坑里),之所以不直接使用 ts 的 for 很大概率是因为鸿蒙底层对于 ForEach 进行了性能增强,优化了非首次渲染的一些情况。
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(50)
}
}
这里其实还有些大坑,就是比如数据源发生了变化,或者数据源数组里的某个元素的属性发生了变化,如何重新渲染、拖拽排序如何解决等等问题。
直接看官网吧,这里不再赘述:ForEach:循环渲染https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-rendering-control-foreach-V5
LazyForEach 懒加载
按需迭代数据,不是一次性加载的场景,另外它具备一定的性能优化,比如当组件超出可视范围了,框架会先回收它们,降低内存的占用。
使用限制
官网介绍了很多,我这里挑几点说一下:
- 只能在容器组件内使用。
- 一个容器组件里只能有一个 LazyForEach
- 每次迭代,只能创建一个子组件。
- 你可以把 LazyForEach 写到 条件渲染(if)里。
- 别试图重新赋值 dataSource,你得蛹 DataChangeListener 来进行更新。
- 如果你想节点复用,请加上 @Reusable 装饰器。
状态管理
V2版本的状态管理 , 其装饰器的命名上更加语义化, 深化了其具体作用, 但是其数量上有所增加,学习难度上也比V1版本更加陡峭; 但是最好是拥抱V2 的状态管理,,官方强调它是V1的增强版本, 为开发者提供了更多的功能和灵活性。
对于新手来说,官网也强调了建议直接使用V2版本的范式来进行开发。
如果你之前是已经使用过 V1 的开发者,官方也出了迁移指导来帮助你快速过度到V2版本,猛戳:
V1V2混用和迁移指导https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-v1-v2-migration-V5另外对于V2 版本的装饰器学习,官网围绕着大量V1 版本的混用进行了说明,比如相较于V1 的限制条件等的解释,其实很难全部记起来,所以最好别混搭,V2用多了最好直接忘记V1的特性。
@Local装饰器:组件内部状态
表示内部状态,无法从外部或者说父组件进行初始化。
观察Array 等复杂类型时,你只能观察到API调用带来的变化。
观察复杂类型时的变化https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-new-local-V5#%E8%A7%82%E5%AF%9F%E5%8F%98%E5%8C%96观察基本类型时(number、boolean、string、Object、class),几乎符合你的直觉。
使用限制
- 确保你是v2 版本的状态管理(@ComponentV2 装饰器装饰的组件,否则编译报错)
- 不允许外部(父组件)传入初始化
@ComponentV2
struct ChildComponent {
@Local message: string = "Hello World";
build() {
}
}
@ComponentV2
struct MyComponent {
build() {
ChildComponent({ message: "Hello" }) // 错误用法,编译时报错
}
}
@Local 装饰器:组件内部状态
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-new-local-V5
@Param: 组件外部输入
相较于@Local 更加复杂一些,但也具有一些交叉特性。
- 可以本地初始化,也可以从外部(父组件)初始化。
- 组件内部不能直接修改。
- 除了基本类型的变量,也可以接受函数作为值。
- 变化时会刷新关联的组件(由父组件传入初始化的,父组件一旦变化了值,所有引用的组件都会刷新)
- 与@Local 相比,它也可以直接观测 number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
- 在响应 Array 等内嵌类型时,也是仅能观察API调用带来的变化(上面@Local部分有它的详细官网解释)。
@Param 与 @Local 一样,对于一些类对象的观察,你仅能够观察到对象整体赋值的变化,对于类成员属性的观察你可能更需要 @ObservedV2 和 @Trace 装饰器,这里有一个例子。
class RawObject {
name: string;
constructor(name: string) {
this.name = name;
}
}
@ObservedV2
class ObservedObject {
@Trace name: string;
constructor(name: string) {
this.name = name;
}
}
@Entry
@ComponentV2
struct Index {
@Local rawObject: RawObject = new RawObject("rawObject");
@Local observedObject: ObservedObject = new ObservedObject("observedObject");
build() {
Column() {
Text(`${this.rawObject.name}`)
Text(`${this.observedObject.name}`)
Button("change object")
.onClick(() => {
// 对类对象整体的修改均能观察到
this.rawObject = new RawObject("new rawObject");
this.observedObject = new ObservedObject("new observedObject");
})
Button("change name")
.onClick(() => {
// @Local与@Param均不具备观察类对象属性的能力,因此对rawObject.name的修改无法观察到
this.rawObject.name = "new rawObject name";
// 由于ObservedObject的name属性被@Trace装饰,因此对observedObject.name的修改能被观察到
this.observedObject.name = "new observedObject name";
})
Child({
rawObject: this.rawObject,
observedObject: this.observedObject
})
}
}
}
@ComponentV2
struct Child {
@Require @Param rawObject: RawObject;
@Require @Param observedObject: ObservedObject;
build() {
Column() {
Text(`${this.rawObject.name}`)
Text(`${this.observedObject.name}`)
}
}
}
这里有关于@Param更多解释的官网链接:
@Param:组件外部输入https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-new-param-V5
@Once:初始化同步一次
目的是增强@Param 的仅一次初始化的能力。
- 搭配@Param 使用,单独使用不可以。
- 不影响@Param 的观测能力,只是为了防止被二次初始化。
- 编写代码的顺序无关紧要,和@Param 一起使用时,它俩你随便排列。
- 它和@Param 搭配时,变量就可以在本地修改了。
没太多限制条件,一个代码演示帮你避开所有的坑:
@ComponentV2
struct MyComponent {
@Param @Once onceParam: string = "onceParam"; // 正确用法
@Once onceStr: string = "Once"; // 错误用法,@Once无法单独使用
@Local @Once onceLocal: string = "onceLocal"; // 错误用法,@Once不能与@Local一起使用
}
@Component
struct Index {
@Once @Param onceParam: string = "onceParam"; // 错误用法
}
@Event装饰器:规范组件输出(要求父组件更新@Param变量)
子组件某些事件发生了,要求父组件重新更新传入的@Param 变量,这时候你就得用它了。
换句话说:你想更新@Param 装饰的值,但是组件自身肯定是不行的(为啥不行? 回去看下@Param),你就得在组件自身里声明@Event 标记的变量(实际值是个函数),由父组件传入,当你想要更改@Param 的值,调用一下@Event 标记的函数,父组件为该函数的调用做出响应(比如更改你想更改的@Param 的值)。!!废话有点多了这里!!
它的具体特征有:
- 装饰的函数完全你自己决定, 可以有参数和返回值。
- 别拿来装饰非函数类型。
- 没初始化的话默认为空函数。
- 外部(父组件)没初始化时,使用本地默认的函数处理。
一个代码示例向你完全解释错误的用法:
@ComponentV2
struct Index {
@Event changeFactory: ()=>void = ()=>{}; //正确用法
@Event message: string = "abcd"; // 错误用法,装饰非方法类型变量,@Event无作用
}
@Component
struct Index {
@Event changeFactory: ()=>void = ()=>{}; // 错误用法,编译时报错
}
一个代码示例向你解释常见的用法:
@Entry
@ComponentV2
struct Index {
@Local title: string = "Title One";
@Local fontColor: Color = Color.Red;
build() {
Column() {
Child({
title: this.title,
fontColor: this.fontColor,
changeFactory: (type: number) => {
if (type == 1) {
this.title = "Title One";
this.fontColor = Color.Red;
} else if (type == 2) {
this.title = "Title Two";
this.fontColor = Color.Green;
}
}
})
}
}
}
@ComponentV2
struct Child {
@Param title: string = '';
@Param fontColor: Color = Color.Black;
@Event changeFactory: (x: number) => void = (x: number) => {};
build() {
Column() {
Text(`${this.title}`)
.fontColor(this.fontColor)
Button("change to Title Two")
.onClick(() => {
this.changeFactory(2);
})
Button("change to Title One")
.onClick(() => {
this.changeFactory(1);
})
}
}
}
正在疯狂的往下写- - -
别走呀,有用的话点个赞吧