HarmonyOS 鸿蒙Next 5 使用forench 展示动态 tab 本地页面的web组件的问题

HarmonyOS 鸿蒙Next 5 使用forench 展示动态 tab 本地页面的web组件的问题 鸿蒙5 使用forench 展示动态 tab 本地页面的web组件。多个tab页 删除第一个或中间的页面 ,会重绘后面的所有页面。 key值时产生的唯一的时间戳字符串。


更多关于HarmonyOS 鸿蒙Next 5 使用forench 展示动态 tab 本地页面的web组件的问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

6 回复

尊敬的开发者,您好!时间戳的获取方式和结果是否对应list的序号,能否提供完整复现复现的代码。以下是复现代码,并没发现所述问题:

import { webview } from "@kit.ArkWeb";

class WebKey{
  id:number=0;
  uri:string='';
  img:Resource=$r('app.media.foreground')
  constructor(id:number,uri:string,img:Resource) {
    this.id=id;
    this.uri=uri;
    this.img=img;
  }
}

@Entry
@Component
export struct TabForeachWeb {
  @State tabindex: number = 0;
  tabsController: TabsController = new TabsController();
  controller: webview.WebviewController = new webview.WebviewController();
  @State WebKeys:WebKey[]=[new WebKey(0,'www.huawei.com',$r('app.media.background'))
  ,new WebKey(1,'www.baidu.com',$r('app.media.foreground'))
  ,new WebKey(2,'https://developer.huawei.com',$r('app.media.startIcon'))]
  build() {
    Tabs({ barPosition: BarPosition.End, index: $$this.tabindex, controller: this.tabsController }) {
      ForEach(this.WebKeys,(webkey:WebKey,index:number)=>{
        TabContent() {
          Column(){
            Text(webkey.id+webkey.uri).width('50%')
            Image(webkey.img).width('50%')
            Web({ src: webkey.uri, controller: this.controller }).width('50%')
          }.height('10%').width('50%').alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
          .onClick(()=>{
            let webkeys:WebKey[]=[]
            this.WebKeys.forEach((webkey2:WebKey)=>{
              if(webkey2.id!==webkey.id){
                webkeys.push(webkey2)
              }
            })
            this.WebKeys = webkeys;
            this.getUIContext().getPromptAction().showToast({message:this.WebKeys.toString()})
          })
        }
      },(webkey:WebKey,index:number)=>JSON.stringify(webkey.id+'/'+index))
    }
  }
}

更多关于HarmonyOS 鸿蒙Next 5 使用forench 展示动态 tab 本地页面的web组件的问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


问题已解决,forench 不适合浏览器teb页 重绘时必须的,官网有示例处理我的问题,谢谢回答。

请问下,该问题如何解决的?官网示例在哪里?,

【问题现象】 在使用Tabs组件提供的页签进行内容视图切换时,需要使用增加或删除页签的能力,Tabs组件本身并没有提供相关能力,应该如何实现?

【背景知识】

  • Tabs是通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。TabContent仅在Tabs中使用,对应一个切换页签的内容视图。
  • Tab页签切换后会触发onChange事件,changeIndex可控制Tabs切换到指定页签。

【解决方案】

  1. 整体布局分为两部分:页签部分和页面视图部分,页签部分通过@Builder自定义封装一个组件,页面视图则用Tabs自定义组件:

页签部分:

Row({ space: 7 }) {
  Scroll() {
    Row() {
      ForEach(this.tabArray, (item: number, index: number) => {
        this.Tab(`页签 ${item}`, item, index);
      })
    }
    .justifyContent(FlexAlign.Start)
  }
  .align(Alignment.Start)
  .scrollable(ScrollDirection.Horizontal)
  .scrollBar(BarState.Off)
  .width('90%')
  .backgroundColor('#ffb7b7b7')

  Image($r('app.media.startIcon')).onClick(() => {
    if (this.tabArray.length === 0) {
      this.tabArray = [0];
      this.focusIndex = 0;
    } else {
      this.tabArray.push(this.tabArray[this.tabArray.length - 1] + 1);
      this.focusIndex = this.tabArray.length - 1;
      let add = this.focusIndex;
      if (add == 1) {
        this.test = true;
      }
      setTimeout(() => {
        this.controller.changeIndex(add);
      }, 100);
    }
  }).width(20).height(20)
}
.width('100%')
.backgroundColor('#ffb7b7b7')

页面视图:

Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
  ForEach(this.tabArray, (item: number, index: number) => {
    TabContent() {
      Text(`我是页面 ${item}${index} 的内容`)
        .height(300)
        .width('100%')
        .fontSize(30)
    }
  })
}
  1. 实现页签和页面视图的联动。主要是通过TabsController的changeIndex来实现对应的视图跳转,但需注意由于之后会有增删数组元素的操作,所以此处传入的index值是数组元素的索引值:
this.controller.changeIndex(pre - 1);
  1. 被选中页签背景颜色变化。当用户点击页签时,给变量focusIndex赋值,在背景色属性里与当前数组元素进行比较,同时也需要在Tabs的onChange方法里进行控制:
.backgroundColor(tabIndex === this.focusIndex ? '#ffffffff' : '#ffb7b7b7')
.onClick(() => {
  this.test = true;
  this.focusIndex = tabIndex;
  this.controller.changeIndex(tabIndex);
  console.info(`foo ${tabItem}`);
})
  1. 增添数组元素实现增加页签的效果。增添数组元素使用push方法,但由于此demo原始定义的数组是连续的自然数,后续增删数组会打乱原有顺序,所以此处处理为先判断最后一个元素的值再加1,当把所有数组元素删除时,需要再次添加数组元素,同时也需要控制选中样式,所以此基础上加一个判断,让重新生成的页签选中样式在第一个元素上:
if (this.tabArray.length === 0) {
  this.tabArray = [0];
  this.focusIndex = 0;
} else {
  this.tabArray.push(this.tabArray[this.tabArray.length - 1] + 1);
  this.focusIndex = this.tabArray.length - 1;
  let add = this.focusIndex;
  if (add == 1) {
    this.test = true;
  }
  setTimeout(() => {
    this.controller.changeIndex(add);
  }, 100);
}
  1. 删除数组元素实现删减页签的效果:
let pre = tabIndex;
if (this.focusIndex === pre && pre === this.tabArray.length - 1) {
  this.tabArray.splice(tabIndex, 1);
  this.focusIndex = pre - 1;
  this.controller.changeIndex(pre - 1);
  this.test = true;
  setTimeout(() => {
    this.test = false;
  }, 400);
} // 最后一个元素并且当前选中情况
else if (this.focusIndex === pre) {
  this.tabArray.splice(pre, 1);
} // 非最后一个元素且当前选中情况
else if (this.focusIndex > pre) {
  this.focusIndex = this.focusIndex - 1;
  this.tabArray.splice(pre, 1);
  this.controller.changeIndex(this.focusIndex);
} // 非当前选中且比选中的小
else {
  this.tabArray.splice(pre, 1);
}
  1. 完整代码如下:
@Entry
@Component
struct Drag {
  @State tabArray: Array<number> = [0, 1];
  @State focusIndex: number = 0;
  private controller: TabsController = new TabsController();
  @State test: boolean = false;

  // 单独的页签
  [@Builder](/user/Builder)
  Tab(tabName: string, tabItem: number, tabIndex: number) {
    Row({ space: 20 }) {
      Text(tabName).fontSize(18)
      Image($r('app.media.startIcon')).width(20).height(20) // 替换自己icon
        .onClick(() => {
          let pre = tabIndex;
          if (this.focusIndex === pre && pre === this.tabArray.length - 1) {
            this.tabArray.splice(tabIndex, 1);
            this.focusIndex = pre - 1;
            this.controller.changeIndex(pre - 1);
            this.test = true;
            setTimeout(() => {
              this.test = false;
            }, 400);
          } // 最后一个元素并且当前选中情况
          else if (this.focusIndex === pre) {
            this.tabArray.splice(pre, 1);
          } // 非最后一个元素且当前选中情况
          else if (this.focusIndex > pre) {
            this.focusIndex = this.focusIndex - 1;
            this.tabArray.splice(pre, 1);
            this.controller.changeIndex(this.focusIndex);
          } // 非当前选中且比选中的小
          else {
            this.tabArray.splice(pre, 1);
          }
        })
    }
    .justifyContent(FlexAlign.Center)
    .constraintSize({ minWidth: 35 })
    .width(120)
    .height(30)
    .borderRadius({ topLeft: 10, topRight: 10 })
    .backgroundColor(tabIndex === this.focusIndex ? '#ffffffff' : '#ffb7b7b7')
    .onClick(() => {
      this.test = true;
      this.focusIndex = tabIndex;
      this.controller.changeIndex(tabIndex);
      console.info(`foo ${tabItem}`);
    })

  }

  build() {
    Column() {
      Column() {
        Row({ space: 7 }) {
          Scroll() {
            Row() {
              ForEach(this.tabArray, (item: number, index: number) => {
                this.Tab(`页签 ${item}`, item, index);
              })
            }
            .justifyContent(FlexAlign.Start)
          }
          .align(Alignment.Start)
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('90%')
          .backgroundColor('#ffb7b7b7')

          Image($r('app.media.startIcon')).onClick(() => {
            if (this.tabArray.length === 0) {
              this.tabArray = [0];
              this.focusIndex = 0;
            } else {
              this.tabArray.push(this.tabArray[this.tabArray.length - 1] + 1);
              this.focusIndex = this.tabArray.length - 1;
              let add = this.focusIndex;
              if (add == 1) {
                this.test = true;
              }
              setTimeout(() => {
                this.controller.changeIndex(add);
              }, 100);
            }
          }).width(20).height(20)
        }
        .width('100%')
        .backgroundColor('#ffb7b7b7')

        Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
          ForEach(this.tabArray, (item: number, index: number) => {
            TabContent() {
              Text(`我是页面 ${item}${index} 的内容`)
                .height(300)
                .width('100%')
                .fontSize(30)
            }
          })
        }
        .onChange((index: number) => {
          if (index !== 0 && this.tabArray.length > 2) {
            this.focusIndex = index;
            console.info(`change focusIndex ${this.focusIndex}`);
          } else if (this.tabArray.length == 1) {
            this.focusIndex = index;
          } else if (this.tabArray.length == 2 && this.focusIndex == 1) {
            if (this.test) {
              this.focusIndex = 1;
            } else {
              this.focusIndex = 0;
            }
          } else if (!this.test) {
            console.info(`foo ${index}`);
            this.focusIndex = index;
          }
        })
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
    }
    .height('100%')
  }
}

在鸿蒙Next 5中,使用ForEach展示动态Tab时,每个Tab页可内嵌Web组件。需在WebController中管理页面加载,并通过@State@Link装饰器绑定Tab数据源,确保Web视图与Tab状态同步。注意在aboutToAppear生命周期中初始化Web组件配置,并利用onPageEnd事件处理页面加载完成后的逻辑。

你遇到的问题是在使用 ForEach 渲染动态 Tab 页中的 Web 组件时,删除非末尾的 Tab 会导致后续所有 Web 组件被重建(重绘)。这是因为你使用了时间戳作为 key,导致框架无法正确识别和复用组件。

问题核心:key 值的不稳定性

你为每个 Tab 项生成的 key 是“唯一的时间戳字符串”。这意味着每次渲染(包括因数组变化而触发的渲染)时,每个项的 key 都是全新的、与上一次渲染无关的值。

当删除数组中间的一个元素时,ForEach 会进行差异比较(Diffing)。理想情况下,它应该识别出:“key 为 B 的项被删除了,key 为 C 和 D 的项应该保留并向前移动”。但由于你每次都为所有项生成全新的 key(例如从时间戳 t1, t2, t3, t4 变成了全新的 t5, t6, t7),框架的比较结果会变成:“keyt1 的项不见了,出现了三个全新的项 t5, t6, t7”。因此,框架会认为所有项都是新的,从而销毁旧的 Web 组件实例并创建新的,这就是你看到的“重绘后面所有页面”。

解决方案:使用稳定且唯一的 key

key 的作用是帮助 ArkUI 框架识别数组项的“身份”,在数组变化时高效地更新、移动或删除对应的组件,而不是销毁和重建。对于 Web 组件这类重资源组件,重建成本很高。

你需要为每个 Tab 页数据赋予一个稳定不变的唯一标识作为 key。这个标识应该在 Tab 页的整个生命周期内保持不变。

修改建议:

假设你的 Tab 页数据是一个对象数组 tabList,每个对象代表一个 Tab。

  1. 为数据模型添加唯一标识字段:在创建 Tab 页数据时,为其分配一个固定的 id(例如使用 UUID 或递增的数字,只要确保唯一且稳定即可)。

    // 示例:每个 Tab 项的数据结构
    class TabItem {
      id: string; // 稳定唯一的标识,在Tab创建时生成,之后不再改变
      title: string;
      url: string; // 或本地页面路径
      // ... 其他属性
    }
    
  2. ForEach 中使用这个稳定的 id 作为 key

    ForEach(this.tabList, (item: TabItem) => {
      TabContent() {
        // 你的Web组件或其他内容
        Web({ src: item.url, /* ... */ })
      }
    }, (item: TabItem) => item.id) // 关键:使用稳定的 id 作为 key
    

修改后的行为:

当你删除中间一个 Tab(假设 idB)时,ForEach 的比较过程变为:

  • 旧数组 key 序列:[A, B, C, D]
  • 新数组 key 序列:[A, C, D]
  • 框架能准确识别出:B 被移除,CD 被向前移动。
  • 因此,只会销毁 idB 的 Tab 对应的 Web 组件,而 idCD 的 Web 组件会被保留并移动到新的位置,不会触发重建

总结: 请立即将 key 的生成策略从“每次渲染都变的时间戳”改为“每个数据项固有且稳定的唯一标识符”。这是解决动态列表项(尤其是包含复杂组件如 WebView)非必要重建的标准做法。

回到顶部