HarmonyOS NEXT开发进阶(四):@Builder 装饰器实现UI结构复用

一、前言

当页面有多个相同的UI结构时,若每个都单独声明,同样会有大量重复的代码。为避免重复代码,可以将相同的UI结构提炼为一个自定义组件,完成UI结构的复用。

除此之外,ArkTS还提供了一种更轻量的UI结构复用机制@Builder方法,开发者可以将重复使用的UI元素抽象成一个@Builder方法,该方法可在build()方法中调用多次,以完成UI结构的复用。

二、自定义构建函数

ArkUI提供了一种更轻量的UI元素复用机制@Builder@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。

为简化描述,全文将@Builder装饰的函数也称为“自定义构建函数”。

2.1 自定义组件内自定义构建函数

组件内声明,使用时需要用this

定义语法:

@Builder MyBuilderFunction(){}

使用方法:

this.MyBuilderFunction()

@Builder 自定义组件内自定义构建函数使用注意事项如下:

  1. 允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
  2. 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
  3. 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。

2.2 全局自定义构建函数

全局声明需要加function关键字,使用时直接写函数名称。

定义语法:

@Builder function MyGlobalBuilderFunction(){}

使用方法:

MyGlobalBuilderFunction()

@Builder 全局自定义构建函数使用注意事项如下:

  1. 全局的自定义构建函数可以被整个应用获取,不允许使用thisbind方法。
  2. 如果不涉及组件状态变化,建议使用全局的自定义构建方法。

2.3 参数传递规则

自定义构建函数的参数传递有按值传递按引用传递两种,均需遵守以下规则:

  • 参数的类型必须与参数声明的类型一致,不允许undefinednull和返回undefinednull的表达式。
  • 在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用@Link
  • @Builder内UI语法遵循UI语法规则。
  • 只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。
2.3.1 按值传递参数
@Builder 
function overBuilder(paramA1: string) {
  Row() {
    Text(`UseStateVarByValue: ${paramA1} `)
  }
}
@Entry
@Component
struct Parent {
  @State label: string = 'Hello';
  build() {
    Column() {
      overBuilder(this.label)
    }
  }
}
2.3.2 按引用传递参数

按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供$$作为按引用传递参数的范式。

使用语法如下:

@Builder( $$ : { paramA1: string, paramB1 : string } );

应用示例如下:

@Builder function ABuilder($$:{par: string}){
  Row(){
    Text( 'abs:' + $$.par)
  }
}
@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        ABuilder({par:this.message})
        Button('Click Me')
          .onClick(() => {
            this.message = "哈哈"
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

或者按照如下方式实现:

@Builder
function ListModel(params:{title:string,count:number}){
   Row(){
     Text(`${params.title}`)
       .height(40)
       .textAlign(TextAlign.Center)
       .fontColor(Color.White)

     Text(`投票人数:${params.count}`)
       .fontColor(Color.White)
   }
   .width("100%")
   .height(100)
   .backgroundColor(Color.Brown)
    .justifyContent(FlexAlign.SpaceAround)
}

@Entry
@Component
struct Index {
  @State CountMusic:number=10
  build() {
      Column({space:10}) {
        ListModel({title:"音乐",count:this.CountMusic})
        ListModel({title:"美术",count:30})
        ListModel({title:"舞蹈",count:12})

        Button("增加音乐支持人数").onClick(()=>{
          this.CountMusic++;
        })
      }.width('100%')
      .height('100%')
  }
}

2.4 @Builder方法和自定义组件的区别

  • @Builder方法和自定义组件虽然都可以实现UI复用的效果,但是两者还是有着本质区别,其中最为显著的一个区别就是自定义组件可以定义自己的状态变量,而@Builder方法则不能。

  • 若复用的UI结构没有状态,推荐使用@Builder方法,否则使用自定义组件。

  • 在使用 @Builder 复用逻辑时,可以支持传递参数,从而实现更灵活的UI渲染。

  • 参数可以是状态数据,但建议使用对象的方式进行传递(直接传递,无法实现视图更新)。

  • 可以使用 Component 来抽象组件,而 @Builder 则可以实现轻量级的UI复用。

2.5 @BuilderParam

@BuilderParam用于装饰自定义组件(struct)中的属性,其装饰的属性可作为一个UI结构的占位符,待创建该组件时,可通过参数为其传入具体的内容。(其作用类似于Vue框架中的slot)。

注意⚠️:@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。

应用示例如下:


// ● 标题文字通过属性传入
// ● 内容结构“尾随闭包”传入
@Entry
@Component
struct Index {

  build() {
    Column({ space: 10 }) {
      ListModel({title:"音乐"}){
        Text("音乐的内容")
      }
      ListModel({title:"美术"}){
        Text("美术的内容")
      }
      ListModel({title:"舞蹈"})
    }.width('100%').height('100%')
  }
}

@Component
struct ListModel {
  title: string
  // ListContent 名字随便起,默认接收一个自定义构建函数
  @BuilderParam ListContent: () => void = this.defaultContent; 

  @Builder defaultContent () {
    Text('默认展示的内容')
  }

  build() {
    Column() {
      Text(this.title)
        .width('100%')
        .height(50)
        .textAlign(TextAlign.Center)
      this.ListContent();
    }.width('100%').height(200).backgroundColor(Color.Brown)
  }
}

总结:

  • 当子组件使用一个 @BuilderParam 时,使用组件时可以在尾随的大括号 {} 中插入UI结构。
  • 当子组件使用多个 @BuilderParam 时,可以在使用组件时传入参数,例如 Comp({ xxx: this.builderFn })
  • 不能既传递默认插槽也传递具名插槽,会报错。
  • 子组件本身可以提供一个默认的 @Builder 函数,作为 @BuilderParam 的备用函数,用于作为备用内容。

三、应用示例

下面应用List组件实现图文文章列表布局以及自定义模型类的使用。

ArkTS 中定义模型类与在纯 TypeScript 项目中定义类没有本质区别。但是,在 UI 开发中,你可能不会直接在组件模板中使用模型类的实例。相反,你可能会将这些实例绑定到组件的响应式状态变量上,并在模板中使用这些状态变量。这样做的好处是,当状态变量发生变化时,UI 可以自动更新。

示例代码如下:

// 定义模型类
class Item {
  id: number;
  title: string;
  img: ResourceStr;
  author: string;
  date: string;

  constructor(id: number, title: string, img: ResourceStr, author: string, date: string) {
    this.id = id
    this.title = title
    this.img = img
    this.author = author
    this.date = date
  }
}

// 全局自定义构建函数
@Builder function newsItem(item:Item){
  Row() {
    Column() {
      Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(`${item.author} ${item.date}`)
        .fontSize(14)
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Start)
    .height(68)
    .justifyContent(FlexAlign.SpaceBetween)

    Image(item.img)
      .width(100)
      .height(68)
      .margin({ left: 8 })
      .borderRadius(2)
      .objectFit(ImageFit.Cover)
  }
  .width('100%')
  .height(80)
}

@Entry
@Component
struct NewsBuilder {
  @State articleList: Item[] = [
    new Item(1, "嫦娥六号探测器进入环月轨道飞行", "https://p5.img.cctvpic.com/photoworkspace/contentimg/2024/05/08/2024050820144382684.jpg", "央视网", "2014-05-08"),
    new Item(2, "“五一”假期游处处火爆 有哪些文旅新潮流?", "https://cms-emer-res.cctvnews.cctv.com/image/1005/upload/17292d188bc64cef882f1cd07d3d2acc.jpeg", "央视新闻客户端", "2024-05-08"),
    new Item(3, "杭州:全面取消住房限购,购房即可申请落户", $r("app.media.2"), "界面新闻", "2024-05-09"),
    new Item(4, "时隔五年,合肥或将再次引进“国宝”大熊猫", "https://pics4.baidu.com/feed/d50735fae6cd7b899b1d7bef7ec7b8aad8330e5d.jpeg@f_auto?token=e70fa976dceb8e88433c493f7e86b88c", "九派新闻", "2024-05-08"),
    new Item(5, "发布擦边广告再被罚,椰树集团徘徊在“土味”和“低俗”之间?", "https://pics1.baidu.com/feed/e824b899a9014c083cd8d85d067bf9057af4f471.jpeg@f_auto?token=c6770b5d0d99a68721fc88e7a337d01c", "北京商报", "2024-05-08"),
    new Item(6, "特斯拉在华推进全自动驾驶 智能网联车产业链迎新机遇", $r("app.media.2"), "每日经济新闻", "2024-05-09"),
  ]

  build() {
    Column() {
      List() {
        ForEach(this.articleList, (item: Item) => {
          ListItem() {
          // 按值传递参数
            newsItem(item)
          }
          .padding({ top: 5, bottom: 5 })
        })
      }
      .divider({ strokeWidth: 1, color: "#eeeeee" })
      .backgroundColor(0xffffff)
      .borderRadius(10)
      .padding(10)
    }.padding(10)
    .backgroundColor(0xeeeeee)
    .width('100%')
    .height('100%')
  }
}

效果图如下:
在这里插入图片描述

四、拓展阅读

<think>嗯,用户的问题挺多的,涉及到JavaScript和TypeScript的异步编程、生命周期机制、装饰器差异,还有鸿蒙开发的相关知识。首先,我需要理清楚每个问题的核心点,然后逐一解答,确保覆盖全面又不遗漏重点。 首先,用户问到了Promise和async/await的区别。这部分需要解释它们的基本概念,以及各自的优缺点。比如,Promise是ES6引入的,用于处理异步操作,而async/await是ES8的语法糖,让异步代码看起来更像同步的。要提到错误处理的方式不同,async/await用try/catch,而Promise用catch方法。还要注意用户提到的生命周期问题,比如在鸿蒙的aboutToDisappear里使用async/await可能导致的问题,引用中的内容需要结合进来。 接下来是生命周期对比,包括组件、页面、应用。用户可能想了解不同层次的生命周期钩子及其用途。根据引用[2]和[3],鸿蒙中@Entry装饰的组件才有页面级别的生命周期方法,比如onPageShow和onPageHide。而组件级别的有aboutToAppear和aboutToDisappear。需要对比这些钩子的触发时机和用途,比如页面显示、隐藏,组件挂载卸载等。 Promise的使用场景,这里要列举常见的情况,比如网络请求、文件操作、定时任务等。用户可能想知道在什么情况下更适合使用Promise而不是回调函数,或者async/await。需要强调Promise链式调用的优势,以及如何处理多个异步任务并行或串行。 @Builder和@Component的区别,这部分需要参考鸿蒙的文档。@Component用于定义可复用的组件,而@Builder是用于构建UI布局的函数式描述。要说明它们的用途不同,比如@Component有生命周期,而@Builder更灵活,适合组合多个组件。 @Entry修饰符的作用,根据引用[2]和[3],@Entry标记的是入口组件,只有它才能触发页面级别的生命周期方法。需要强调它是应用的根节点,负责初始渲染和生命周期管理。 this指向问题及替代方案,这里要解释在JavaScript中函数作用域下this的变化,尤其是在回调函数或类方法中的问题。解决方案包括箭头函数、bind方法,或者使用类属性语法。TypeScript中的处理可能更严格,需要确保正确的上下文绑定。 Await async的底层实现,这部分可能涉及生成器和Promise的转换。需要说明async函数返回Promise,await会暂停执行直到Promise解决,底层通过生成器函数实现协程的效果。 Interface与type的不同,在TypeScript中两者都可以定义类型,但interface支持声明合并,而type可以定义联合类型、元组等。要举例说明适用场景,比如扩展性需求用interface,复杂类型用type。 泛型的概念,需要解释泛型在函数、类、接口中的应用,提高代码复用性和类型安全。比如泛型函数可以处理多种类型而不失去类型检查。 鸿蒙组件间通信方式,可能包括Props传递、状态管理、事件机制、全局变量或Context。需要结合引用中的内容,比如@Link和@State装饰器的使用,以及父子组件如何通信。 Builder与BuildParams的关系,可能需要说明BuildParams是用于动态生成UI结构的参数,而@Builder函数提供灵活的布局方式,两者结合使用可以实现动态UI构建。 在思考过程中,需要注意引用用户提供的资料,尤其是鸿蒙相关的生命周期和组件结构。比如在生命周期部分,引用[3]详细说明了各个钩子的用途,需要准确引用。同时,关于Promise在生命周期中的使用问题,引用[1]提到在aboutToDisappear中避免异步操作,这点要特别提醒用户。 可能用户的实际需求是进行鸿蒙应用开发,遇到了异步编程和生命周期管理的问题,需要综合JavaScript/TypeScript的知识和鸿蒙框架的特性来解答。因此,回答时要将两者结合起来,特别是在装饰器差异、组件通信等方面,突出鸿蒙特有的实现方式。 需要确保每个问题都得到清晰、简洁的回答,同时保持结构层次分明。可能用户是开发者,需要实用、直接的答案,所以示例代码和具体使用场景会很有帮助。例如,在解释@Builder时,给出代码示例说明其用法,与@Component对比。 最后,生成的相关问题要覆盖用户可能进一步询问的方向,比如性能优化、实际应用案例、底层原理等,帮助用户深化理解。</think>### JavaScript/TypeScript与鸿蒙开发关键知识点解析 --- #### 1. **Promise与async/await的区别** - **Promise**: 是ES6引入的异步编程解决方案,通过链式调用`.then()`和`.catch()`处理异步操作。例如: ```javascript fetch(url).then(res => res.json()).catch(err => console.log(err)); ``` - **async/await**: ES8语法糖,基于Promise实现,使异步代码更同步化。例如: ```javascript async function loadData() { try { const res = await fetch(url); } catch (err) { console.log(err); } } ``` **区别**: - 错误处理:`async/await`用`try/catch`,Promise用`.catch()`; - 可读性:`async/await`更清晰,避免多层嵌套; - 生命周期风险:在鸿蒙的`aboutToDisappear`中避免使用`async/await`,否则组件可能因异步操作延迟释放[^1]。 --- #### 2. **生命周期对比(组件、页面、应用)** - **组件级**(如`@Component`): - `aboutToAppear`:组件实例创建后、`build()`前调用,可修改状态变量[^3]; - `aboutToDisappear`:组件销毁前调用,禁止修改状态变量; - **页面级**(如`@Entry`修饰的组件): - `onPageShow`/`onPageHide`:页面显示/隐藏时触发(如路由切换、应用前后台切换)[^3]; - `onBackPress`:拦截返回按钮事件,返回`true`表示自定义处理; - **应用级**: - 鸿蒙中通过`AppLifecycle`管理(如应用启动、退出),需全局配置。 --- #### 3. **Promise使用场景** - **典型场景**: 1. 网络请求(如`fetch`); 2. 文件读写(如`fs.promises.readFile`); 3. 定时任务(如`setTimeout`封装为Promise); 4. 并行/串行任务(`Promise.all`/`Promise.race`)。 --- #### 4. **@Builder与@Component区别** | 特性 | @Component | @Builder | |--------------------|---------------------------------------|---------------------------------------| | **用途** | 定义可复用UI组件,含生命周期 | 构建UI布局的函数,无生命周期 | | **状态管理** | 支持`@State`、`@Link`等响应式变量 | 仅用于组合现有组件,无状态管理 | | **示例** | ```@Component struct MyButton { ... }``` | ```@Builder MyLayout() { Column() { ... } }``` | --- #### 5. **@Entry修饰符的作用** - **功能**:标记入口组件(根节点),触发页面级生命周期方法(如`onPageShow`)[^2]; - **限制**:一个页面仅允许一个`@Entry`组件; - **示例**: ```typescript @Entry @Component struct Index { ... } ``` --- #### 6. **this指向问题及替代方案** - **问题**:JavaScript中函数作用域的`this`可能丢失(如回调函数中); - **解决方案**: - 箭头函数:`() => {}`保留外层`this`; - `bind()`:显式绑定上下文(如`this.handleClick.bind(this)`); - TypeScript类属性语法:`handleClick = () => { ... }`。 --- #### 7. **async/await底层实现** - **原理**:通过生成器(Generator)和Promise实现协程: ```javascript // async函数转换为生成器 function _async(fn) { return function () { const gen = fn.apply(this, arguments); return new Promise((resolve, reject) => { function step(key, arg) { try { const { value, done } = gen[key](arg); if (done) resolve(value); else Promise.resolve(value).then(val => step("next", val)); } catch (err) { reject(err); } } step("next"); }); }; } ``` --- #### 8. **Interface与type的不同** | 特性 | Interface | Type Alias | |--------------------|--------------------------------------|-------------------------------------| | **扩展性** | 支持声明合并(多次定义自动合并) | 不可合并 | | **灵活性** | 仅描述对象结构 | 可定义联合、元组、字面量等类型 | | **示例** | `interface User { name: string; }` | `type ID = string | number;` | --- #### 9. **泛型的概念** - **用途**:创建可复用的类型/函数,支持多种类型而不丢失类型检查; - **示例**: ```typescript function identity<T>(arg: T): T { return arg; } type List<T> = T[]; ``` --- #### 10. **鸿蒙组件间通信方式** 1. **Props传递**:父组件通过属性向子组件传值; 2. **事件机制**:子组件通过`@Emit`触发父组件回调; 3. **状态管理**:使用`@State`(组件内状态)、`@Link`(父子组件双向绑定)[^4]; 4. **全局变量/Context**:跨层级组件共享数据。 --- #### 11. **Builder与BuildParams的关系** - **@Builder**:用于定义可复用UI片段,支持参数化; - **BuildParams**:动态生成UI结构的参数对象,与`@Builder`结合实现灵活布局: ```typescript @Builder function DynamicContent(params: BuildParams) { if (params.flag) { Text('Hello'); } else { Button('Click'); } } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

No Silver Bullet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值