1# 创建自定义组件 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiyujia926; @huyisuo--> 5<!--Designer: @zhangboren--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。进行UI界面开发时,不仅要组合使用系统组件,还需考虑代码的可复用性、业务逻辑与UI的分离,以及后续版本的演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。 10 11自定义组件具有以下特点: 12 13- 可组合:允许开发者组合使用系统组件及其属性和方法。 14 15- 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。 16 17- 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。 18 19## 自定义组件的基本用法 20 21以下示例展示了自定义组件的基本用法。 22 23```ts 24@Component 25struct HelloComponent { 26 @State message: string = 'Hello, World!'; 27 28 build() { 29 // HelloComponent自定义组件组合系统组件Row和Text 30 Row() { 31 Text(this.message) 32 .onClick(() => { 33 // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!' 34 this.message = 'Hello, ArkUI!'; 35 }) 36 } 37 } 38} 39``` 40> **注意:** 41> 42> 如果在其他文件中引用自定义组件,需要使用`export`关键字导出组件,并在使用的页面`import`该自定义组件。 43 44可以在其他自定义组件的`build()`函数中多次创建`HelloComponent`,以实现自定义组件的重用。 45 46```ts 47@Entry 48@Component 49struct ParentComponent { 50 build() { 51 Column() { 52 Text('ArkUI message') 53 HelloComponent({ message: 'Hello World!' }); 54 Divider() 55 HelloComponent({ message: '你好,世界!' }); 56 } 57 } 58} 59``` 60 61要完全理解上面的示例,需要了解自定义组件的以下概念定义,本文将在后面的小节中介绍: 62 63- [自定义组件的基本结构](#自定义组件的基本结构) 64 65- [成员函数/变量](#成员函数变量) 66 67- [自定义组件的参数规定](#自定义组件的参数规定) 68 69- [build()函数](#build函数) 70 71- [自定义组件通用样式](#自定义组件通用样式) 72 73 74## 自定义组件的基本结构 75 76### struct 77 78自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。 79 80 > **说明:** 81 > 82 > 自定义组件名、类名、函数名不得与系统组件名重复。 83 84### \@Component 85 86\@Component装饰器仅装饰struct关键字声明的数据结构。被装饰的struct具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个\@Component装饰。\@Component可以接受一个可选的boolean类型参数。 87 88 > **说明:** 89 > 90 > 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 91 > 92 > 从API version 11开始,\@Component可以接受一个可选的boolean类型参数。 93 > 94 > 从API version 11开始,该装饰器支持在原子化服务中使用。 95 96 ```ts 97 @Component 98 struct MyComponent { 99 } 100 ``` 101 102 #### freezeWhenInactive<sup>11+</sup> 103 [组件冻结](arkts-custom-components-freeze.md)选项。 104 105 | 名称 | 类型 | 必填 | 说明 | 106 | ------ | ------ | ---- | ------------------------------------------------------------ | 107 | freezeWhenInactive | boolean | 否 | 是否开启组件冻结。默认值false。true:开启组件冻结,false:不开启组件冻结。 | 108 109 ```ts 110 @Component({ freezeWhenInactive: true }) 111 struct MyComponent { 112 } 113 ``` 114 115### build()函数 116 117build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。 118 119 ```ts 120 @Component 121 struct MyComponent { 122 build() { 123 } 124 } 125 ``` 126 127### \@Entry 128 129\@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,仅允许存在一个由@Entry装饰的自定义组件作为页面的入口。\@Entry可以接受一个可选的[LocalStorage](arkts-localstorage.md)参数。 130 131 > **说明:** 132 > 133 > 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 134 > 135 > 从API version 10开始,\@Entry可以接受一个可选的[LocalStorage](arkts-localstorage.md)参数或者一个可选的EntryOptions<sup>10+</sup>参数。 136 > 137 > 从API version 11开始,该装饰器支持在原子化服务中使用。 138 139 ```ts 140 @Entry 141 @Component 142 struct MyComponent { 143 } 144 ``` 145 146#### EntryOptions<sup>10+</sup> 147 148 命名路由跳转选项。 149 150 | 名称 | 类型 | 必填 | 说明 | 151 | ------ | ------ | ---- | ------------------------------------------------------------ | 152 | routeName | string | 否 | 表示作为命名路由页面的名字。 | 153 | storage | [LocalStorage](arkts-localstorage.md) | 否 | 页面级的UI状态存储。当未传入时,框架会创建一个新的LocalStorage实例作为默认值。 | 154 | useSharedStorage<sup>12+</sup> | boolean | 否 | 是否使用[LocalContent](../../reference/apis-arkui/arkts-apis-window-WindowStage.md#loadcontent9)传入的LocalStorage实例对象。默认值false。true:使用共享的[LocalStorage](arkts-localstorage.md)实例对象。false:不使用共享的[LocalStorage](arkts-localstorage.md)实例对象。 | 155 156 > **说明:** 157 > 158 > 当useSharedStorage设置为true且storage已赋值时,useSharedStorage的值优先级更高。 159 160 ```ts 161 @Entry({ routeName : 'myPage' }) 162 @Component 163 struct MyComponent { 164 } 165 ``` 166 167### \@Reusable 168 169\@Reusable装饰的自定义组件具备可复用能力。详细请参考:[\@Reusable装饰器:组件复用](./arkts-reusable.md#使用场景)。 170 171 > **说明:** 172 > 173 > 从API version 10开始,该装饰器支持在ArkTS卡片中使用。 174 175 ```ts 176 @Reusable 177 @Component 178 struct MyComponent { 179 } 180 ``` 181 182## 成员函数/变量 183 184自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束: 185 186- 自定义组件的成员函数仅能从组件内部访问,且不建议声明为静态函数。 187 188自定义组件可以包含成员变量,成员变量具有以下约束: 189 190- 自定义组件的成员变量仅能从组件内部访问,且不建议声明为静态变量。 191 192- 自定义组件的成员变量本地初始化有些是可选的,有些是必选的。具体是否需要本地初始化,是否需要从父组件通过参数传递初始化子组件的成员变量,请参考[状态管理](arkts-state-management-overview.md)。 193 194 195## 自定义组件的参数规定 196 197以下示例展示了如何在build方法里创建自定义组件,并在创建自定义组件的过程中,根据装饰器的规则来初始化自定义组件的参数。 198 199```ts 200@Component 201struct MyComponent { 202 private countDownFrom: number = 0; 203 private color: Color = Color.Blue; 204 205 build() { 206 } 207} 208 209@Entry 210@Component 211struct ParentComponent { 212 private someColor: Color = Color.Pink; 213 214 build() { 215 Column() { 216 // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10,将成员变量color初始化为this.someColor 217 MyComponent({ countDownFrom: 10, color: this.someColor }) 218 } 219 } 220} 221``` 222 223以下示例代码将父组件中的函数传递给子组件,并在子组件中调用。 224 225```ts 226@Entry 227@Component 228struct Parent { 229 @State cnt: number = 0 230 submit: () => void = () => { 231 this.cnt++; 232 } 233 234 build() { 235 Column() { 236 Text(`${this.cnt}`) 237 Son({ submitArrow: this.submit }) 238 } 239 } 240} 241 242@Component 243struct Son { 244 submitArrow?: () => void 245 246 build() { 247 Row() { 248 Button('add') 249 .width(80) 250 .onClick(() => { 251 if (this.submitArrow) { 252 this.submitArrow() 253 } 254 }) 255 } 256 .height(56) 257 } 258} 259``` 260 261## build()函数 262 263所有在build()函数中声明的语句统称为UI描述,UI描述需要遵循以下规则: 264 265- \@Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。 266 \@Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。 267 268 ```ts 269 @Entry 270 @Component 271 struct MyComponent { 272 build() { 273 // 根节点唯一且必要,必须为容器组件 274 Row() { 275 ChildComponent() 276 } 277 } 278 } 279 280 @Component 281 struct ChildComponent { 282 build() { 283 // 根节点唯一且必要,可为非容器组件 284 Image('test.jpg') 285 } 286 } 287 ``` 288 289- 不允许声明本地变量,反例如下。 290 291 ```ts 292 build() { 293 // 反例:不允许声明本地变量 294 let num: number = 1; 295 } 296 ``` 297 298- 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用,反例如下。 299 300 ```ts 301 build() { 302 // 反例:不允许console.info 303 console.info('print debug log'); 304 } 305 ``` 306 307- 不允许创建本地的作用域,反例如下。 308 309 ```ts 310 build() { 311 // 反例:不允许本地作用域 312 { 313 // ... 314 } 315 } 316 ``` 317 318- 不允许调用没有用\@Builder装饰的方法,允许系统组件的参数是TS方法的返回值。 319 320 ```ts 321 @Component 322 struct ParentComponent { 323 doSomeCalculations() { 324 } 325 326 calcTextValue(): string { 327 return 'Hello World'; 328 } 329 330 @Builder doSomeRender() { 331 Text(`Hello World`) 332 } 333 334 build() { 335 Column() { 336 // 反例:不能调用没有用@Builder装饰的方法 337 this.doSomeCalculations(); 338 // 正例:可以调用 339 this.doSomeRender(); 340 // 正例:参数可以为调用TS方法的返回值 341 Text(this.calcTextValue()) 342 } 343 } 344 } 345 ``` 346 347- 不允许使用switch语法,当需要使用条件判断时,请使用[if](./arkts-rendering-control-ifelse.md)。示例如下。 348 349 ```ts 350 build() { 351 Column() { 352 // 反例:不允许使用switch语法 353 switch (expression) { 354 case 1: 355 Text('...') 356 break; 357 case 2: 358 Image('...') 359 break; 360 default: 361 Text('...') 362 break; 363 } 364 // 正例:使用if 365 if(expression == 1) { 366 Text('...') 367 } else if(expression == 2) { 368 Image('...') 369 } else { 370 Text('...') 371 } 372 } 373 } 374 ``` 375 376- 不允许使用表达式,请使用if组件,示例如下。 377 378 ```ts 379 build() { 380 Column() { 381 // 反例:不允许使用表达式 382 (this.aVar > 10) ? Text('...') : Image('...') 383 384 // 正例:使用if判断 385 if(this.aVar > 10) { 386 Text('...') 387 } else { 388 Image('...') 389 } 390 } 391 } 392 ``` 393 394- 不允许直接改变状态变量,反例如下。详细分析见[\@State常见问题:不允许在build里改状态变量](./arkts-state.md#不允许在build里改状态变量)。 395 396 ```ts 397 @Component 398 struct MyComponent { 399 @State textColor: Color = Color.Yellow; 400 @State columnColor: Color = Color.Green; 401 @State count: number = 1; 402 build() { 403 Column() { 404 // 应避免直接在Text组件内改变count的值 405 Text(`${this.count++}`) 406 .width(50) 407 .height(50) 408 .fontColor(this.textColor) 409 .onClick(() => { 410 this.columnColor = Color.Red; 411 }) 412 Button("change textColor").onClick(() =>{ 413 this.textColor = Color.Pink; 414 }) 415 } 416 .backgroundColor(this.columnColor) 417 } 418 } 419 ``` 420 421 在ArkUI状态管理中,状态驱动UI更新。 422 423  424 425 所以,不能在自定义组件的build()或\@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险。Text('${this.count++}')在全量更新或最小化更新会产生不同的影响: 426 427 - 全量更新(API8及以前版本): ArkUI可能会陷入一个无限的重渲染的循环里,因为Text组件的每一次渲染都会改变应用的状态,就会再引起下一轮渲染的开启。 当 this.columnColor 更改时,都会执行整个build构建函数,因此,Text(`${this.count++}`)绑定的文本也会更改,每次重新渲染Text(`${this.count++}`),又会使this.count状态变量更新,导致新一轮的build执行,从而陷入无限循环。 428 - 最小化更新(API9及以上版本):当this.columnColor更新时,仅Column组件更新,Text组件不会更新。只有当this.textColor更改时,会去更新整个Text组件,其所有属性函数都会执行,所以会看到Text(`${this.count++}`)自增。因为目前UI以组件为单位进行更新,如果组件上某一个属性发生改变,会更新整个的组件。所以整体的更新链路是:this.textColor = Color.Pink ->Text组件整个更新->this.count++ ->Text组件整个更新。值得注意的是,这种写法在初次渲染时会导致Text组件渲染两次,影响性能。 429 430 build函数中更改应用状态的行为可能比上面的示例更加隐蔽,例如: 431 432 - 在\@Builder,\@Extend或\@Styles方法内改变状态变量 。 433 434 - 在计算参数时调用函数中改变应用状态变量,例如 Text('${this.calcLabel()}')。 435 436 - 对当前数组做出修改,sort()改变了数组this.arr,随后的filter方法会返回一个新的数组。 437 438 ```ts 439 // 反例 440 @State arr : Array<...> = [ ... ]; 441 ForEach(this.arr.sort().filter(...), 442 item => { 443 // ... 444 }) 445 // 正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr 446 ForEach(this.arr.filter(...).sort(), 447 item => { 448 // ... 449 }) 450 ``` 451 452## 自定义组件通用样式 453 454自定义组件通过“.”链式调用设置通用样式。 455 456```ts 457@Component 458struct ChildComponent { 459 build() { 460 Button(`Hello World`) 461 } 462} 463 464@Entry 465@Component 466struct MyComponent { 467 build() { 468 Row() { 469 ChildComponent() 470 .width(200) 471 .height(300) 472 .backgroundColor(Color.Red) 473 } 474 } 475} 476``` 477 478> **说明:** 479> 480> ArkUI给自定义组件设置样式时,相当于给ChildComponent套了一个不可见的容器组件,这些样式是设置在容器组件上,而非直接设置给ChildComponent的Button组件。渲染结果显示,背景颜色红色并没有直接设置到Button上,而是设置在Button所在的不可见容器组件上。 481<!--no_check-->