HarmonyOS 组件复用 指南

HarmonyOS 组件复用 指南

什么是组件复用?

🎯 简单理解

想象一下,你在玩积木。当你不再需要某个积木时,你不会把它扔掉,而是放到一个盒子里。下次需要相同类型的积木时,你直接从盒子里拿出来用就行了。

组件复用就是这个道理:

  • 组件 = 界面上的一个部分(比如列表中的一项)
  • 复用 = 重复使用,而不是重新创建
  • 缓存池 = 存放"积木"的盒子

🔍 技术定义

组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。


为什么需要组件复用?

🚀 性能优势

没有组件复用时:

复制代码
用户滑动列表 → 创建新组件 → 显示内容 → 滑出屏幕 → 销毁组件
                ↑                              ↓
            耗时耗内存                        浪费资源

有组件复用时:

复制代码
用户滑动列表 → 从缓存取组件 → 更新内容 → 显示 → 滑出屏幕 → 放入缓存
                ↑                                    ↓
            快速高效                              循环利用

📊 实际效果

  • ✅ 减少内存回收频率
  • ✅ 降低 CPU 计算开销
  • ✅ 提升滑动流畅度
  • ✅ 改善用户体验

🎮 典型应用场景

  • 📱 长列表滑动(如朋友圈、商品列表)
  • 🔄 界面切换频繁的场景
  • 📊 数据展示类应用
  • 🎯 任何需要频繁创建/销毁组件的场景

组件复用的基本原理

🔄 复用流程图解

📝 三个关键步骤

  1. 标记阶段 :给组件打上 @Reusable 标签
  2. 回收阶段:组件滑出屏幕时,放入缓存池
  3. 复用阶段:需要新组件时,从缓存池取出并更新数据

🎯 关键概念解释

概念 简单理解 技术含义
@Reusable 给组件贴上"可重复使用"的标签 装饰器,标记组件可复用
reuseId 给不同类型的组件分类存放 复用标识,区分缓存池
aboutToReuse() 组件"重新上岗"时的准备工作 生命周期回调,处理数据更新
缓存池 存放待复用组件的"仓库" CachedRecycleNodes 集合

入门实践:基础列表复用

🎯 场景一:相同结构的列表项

这是最简单的复用场景,列表中每一项都长得一样,只是内容不同。

🛠️ 实现步骤

第 1 步:创建可复用组件

typescript 复制代码
@Reusable  // 👈 关键:标记组件可复用
@Component
struct ItemView {
  @State title: string = ''
  @State content: string = ''

  // 👈 关键:实现复用回调
  aboutToReuse(params: Record<string, Object>): void {
    // 组件从缓存池取出时,更新数据
    this.title = params.title as string
    this.content = params.content as string
  }

  build() {
    Column() {
      Text(this.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      Text(this.content)
        .fontSize(14)
        .fontColor(Color.Gray)
    }
    .padding(10)
  }
}

第 2 步:在列表中使用

typescript 复制代码
@Component
struct ContentPage {
  @State dataList: Array<any> = [
    { title: '标题1', content: '内容1' },
    { title: '标题2', content: '内容2' },
    // ... 更多数据
  ]

  build() {
    List() {
      LazyForEach(this.dataSource, (item: any) => {
        ListItem() {
          ItemView({ title: item.title, content: item.content })
        }
        .reuseId('item_view')  // 👈 关键:设置复用ID
      })
    }
  }
}
💡 新手提示
  1. @Reusable 是必须的,没有它就没有复用效果
  2. aboutToReuse() 是数据更新的地方,不要忘记实现
  3. reuseId 用来区分不同类型的组件,相同类型用相同 ID

🎯 场景二:不同结构的列表项

当列表中有多种不同类型的条目时,比如有些是纯文本,有些带图片,有些是视频。

🛠️ 实现步骤

第 1 步:创建不同类型的组件

typescript 复制代码
// 文本类型组件
@Reusable
@Component
struct TextItemView {
  @State title: string = ''
  @State content: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string
    this.content = params.content as string
  }

  build() {
    Column() {
      Text(this.title).fontSize(16)
      Text(this.content).fontSize(14)
    }
  }
}

// 图片类型组件
@Reusable
@Component
struct ImageItemView {
  @State title: string = ''
  @State imageUrl: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string
    this.imageUrl = params.imageUrl as string
  }

  build() {
    Column() {
      Text(this.title).fontSize(16)
      Image(this.imageUrl)
        .width(100)
        .height(100)
    }
  }
}

第 2 步:根据数据类型选择组件

typescript 复制代码
@Component
struct ContentPage {
  @State dataList: Array<any> = [
    { type: 'text', title: '纯文本', content: '这是内容' },
    { type: 'image', title: '带图片', imageUrl: 'path/to/image' },
    // ... 更多数据
  ]

  build() {
    List() {
      LazyForEach(this.dataSource, (item: any) => {
        ListItem() {
          if (item.type === 'text') {
            TextItemView({ title: item.title, content: item.content })
              .reuseId('text_item')  // 👈 不同类型用不同ID
          } else if (item.type === 'image') {
            ImageItemView({ title: item.title, imageUrl: item.imageUrl })
              .reuseId('image_item')  // 👈 不同类型用不同ID
          }
        }
      })
    }
  }
}
💡 新手提示
  1. 不同类型的组件需要不同的 reuseId
  2. 每种类型都有自己的缓存池
  3. 数据结构要考虑类型区分

🎯 场景三:组件内部可拆分复用

有时候组件内部的某些部分可以单独复用,这样可以更精细地控制复用。

🛠️ 实现步骤

第 1 步:拆分为可复用的子组件

typescript 复制代码
// 标题组件
@Reusable
@Component
struct TitleComponent {
  @State title: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string
  }

  build() {
    Text(this.title)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
  }
}

// 单图组件
@Reusable
@Component
struct SingleImageComponent {
  @State imageUrl: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    this.imageUrl = params.imageUrl as string
  }

  build() {
    Image(this.imageUrl)
      .width(100)
      .height(100)
  }
}

// 多图组件
@Reusable
@Component
struct MultiImageComponent {
  @State imageList: Array<string> = []

  aboutToReuse(params: Record<string, Object>): void {
    this.imageList = params.imageList as Array<string>
  }

  build() {
    Row() {
      ForEach(this.imageList, (url: string) => {
        Image(url)
          .width(60)
          .height(60)
          .margin(2)
      })
    }
  }
}

// 底部时间组件
@Reusable
@Component
struct TimeComponent {
  @State time: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    this.time = params.time as string
  }

  build() {
    Text(this.time)
      .fontSize(12)
      .fontColor(Color.Gray)
  }
}

第 2 步:用 @Builder 组合不同类型

typescript 复制代码
@Component
struct ContentPage {

  // 文本类型布局
  @Builder textItemBuilder(item: any) {
    Column() {
      TitleComponent({ title: item.title })
        .reuseId('title_component')
      TimeComponent({ time: item.time })
        .reuseId('time_component')
    }
  }

  // 单图类型布局
  @Builder singleImageItemBuilder(item: any) {
    Column() {
      TitleComponent({ title: item.title })
        .reuseId('title_component')
      SingleImageComponent({ imageUrl: item.imageUrl })
        .reuseId('single_image_component')
      TimeComponent({ time: item.time })
        .reuseId('time_component')
    }
  }

  // 多图类型布局
  @Builder multiImageItemBuilder(item: any) {
    Column() {
      TitleComponent({ title: item.title })
        .reuseId('title_component')
      MultiImageComponent({ imageList: item.imageList })
        .reuseId('multi_image_component')
      TimeComponent({ time: item.time })
        .reuseId('time_component')
    }
  }

  build() {
    List() {
      LazyForEach(this.dataSource, (item: any) => {
        ListItem() {
          if (item.type === 'text') {
            this.textItemBuilder(item)
          } else if (item.type === 'single_image') {
            this.singleImageItemBuilder(item)
          } else if (item.type === 'multi_image') {
            this.multiImageItemBuilder(item)
          }
        }
      })
    }
  }
}
💡 新手提示
  1. 为什么用 @Builder 而不是直接嵌套组件?

    • 直接嵌套会分割缓存池,导致复用失效
    • @Builder 可以保持所有组件在同一个缓存池
  2. 细粒度复用的好处

    • 标题组件可以在所有类型间复用
    • 时间组件也可以在所有类型间复用
    • 提高了复用效率

进阶实践:复杂场景应用

🎯 场景:多个列表间的组件复用

当应用有多个页面,每个页面都有列表,我们希望不同页面的列表项也能互相复用。

🤔 问题分析

普通的组件复用只能在同一个父组件内生效。但是不同页面的列表是不同的父组件,所以需要创建一个全局的复用缓存池。

🛠️ 解决方案

第 1 步:创建全局复用池

typescript 复制代码
// 单例模式的全局复用池
class NodePool {
  private static instance: NodePool;
  private cachedNodes: Map<string, Array<NodeItem>> = new Map();

  static getInstance(): NodePool {
    if (!NodePool.instance) {
      NodePool.instance = new NodePool();
    }
    return NodePool.instance;
  }

  // 获取复用节点
  getNode(
    type: string,
    builder: WrappedBuilder<[Object]>,
    data: Object
  ): NodeItem {
    let cachedArray = this.cachedNodes.get(type);
    if (cachedArray && cachedArray.length > 0) {
      // 从缓存中取出
      let nodeItem = cachedArray.pop()!;
      // 检查节点是否有效
      if (nodeItem.getNodePtr() !== null) {
        nodeItem.updateData(data);
        return nodeItem;
      }
    }

    // 创建新节点
    let nodeItem = new NodeItem(type, builder);
    nodeItem.updateData(data);
    return nodeItem;
  }

  // 回收节点
  recycleNode(nodeItem: NodeItem) {
    let type = nodeItem.getType();
    if (!this.cachedNodes.has(type)) {
      this.cachedNodes.set(type, []);
    }

    // 重置节点状态
    nodeItem.resetState();
    this.cachedNodes.get(type)!.push(nodeItem);
  }
}

第 2 步:创建节点包装类

typescript 复制代码
class NodeItem extends NodeController {
  private type: string;
  private builder: WrappedBuilder<[Object]>;
  private data: Object = {};
  private node: BuilderNode<[Object]> | null = null;

  constructor(type: string, builder: WrappedBuilder<[Object]>) {
    super();
    this.type = type;
    this.builder = builder;
  }

  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.node === null) {
      this.node = new BuilderNode(uiContext, { builder: this.builder });
    }

    // 更新数据
    this.node.update(this.data);
    return this.node.getFrameNode();
  }

  updateData(data: Object) {
    this.data = data;
  }

  getType(): string {
    return this.type;
  }

  resetState() {
    // 重置状态,避免复用时显示异常
    this.data = {};
  }
}

第 3 步:创建复用组件包装器

typescript 复制代码
@Component
struct ReusableItemWrapper {
  @Prop type: string
  @Prop data: Object
  @Prop builder: WrappedBuilder<[Object]>

  private nodeItem: NodeItem | null = null

  aboutToAppear() {
    // 从全局复用池获取节点
    this.nodeItem = NodePool.getInstance().getNode(this.type, this.builder, this.data)
  }

  aboutToDisappear() {
    // 回收到全局复用池
    if (this.nodeItem) {
      NodePool.getInstance().recycleNode(this.nodeItem)
    }
  }

  build() {
    NodeContainer(this.nodeItem)
      .height(100)
  }
}

第 4 步:在列表中使用

typescript 复制代码
// 定义列表项视图
@Builder
function listItemBuilder(data: Object) {
  let item = data as ItemData
  Column() {
    Text(item.title).fontSize(16)
    Text(item.content).fontSize(14)
  }
  .padding(10)
}

@Component
struct NewsPage {
  @State newsList: Array<ItemData> = []

  build() {
    List() {
      LazyForEach(this.dataSource, (item: ItemData) => {
        ListItem() {
          ReusableItemWrapper({
            type: 'news_item',
            data: item,
            builder: wrapBuilder(listItemBuilder)
          })
        }
      })
    }
  }
}

@Component
struct HotPage {
  @State hotList: Array<ItemData> = []

  build() {
    List() {
      LazyForEach(this.dataSource, (item: ItemData) => {
        ListItem() {
          ReusableItemWrapper({
            type: 'news_item',  // 👈 相同类型可以复用
            data: item,
            builder: wrapBuilder(listItemBuilder)
          })
        }
      })
    }
  }
}
💡 新手提示
  1. 全局复用池比较复杂,建议先掌握基础复用
  2. 可以使用现成的三方库:nodepool
  3. 注意控制缓存池大小,避免内存占用过多

🚀 性能优化:使用 onIdle() 预创建组件

🤔 问题

首次进入页面时,由于缓存池为空,所有组件都需要重新创建,可能导致卡顿。

💡 解决方案

利用每帧的空闲时间,提前创建一些组件放入缓存池。

typescript 复制代码
class IdleCallback extends FrameCallback {
  private preCreateData: Array<any>
  private currentIndex: number = 0

  constructor(data: Array<any>) {
    super()
    this.preCreateData = data
  }

  onIdle(idleTimeInNano: number): void {
    // 假设每个组件预创建耗时 1ms = 1000000ns
    const singleComponentTime = 1000000
    let remainingTime = idleTimeInNano

    while (remainingTime > singleComponentTime && this.currentIndex < this.preCreateData.length) {
      // 预创建组件
      let data = this.preCreateData[this.currentIndex]
      NodePool.getInstance().preBuild(data.type, data.builder)

      this.currentIndex++
      remainingTime -= singleComponentTime
    }

    // 如果还有未创建的组件,继续下一帧
    if (this.currentIndex < this.preCreateData.length) {
      this.postFrameCallback(this)
    }
  }
}

// 使用方式
@Entry
@Component
struct MainPage {
  aboutToAppear() {
    // 页面加载时开始预创建
    let preCreateData = [
      { type: 'news_item', builder: wrapBuilder(listItemBuilder) },
      { type: 'hot_item', builder: wrapBuilder(listItemBuilder) },
      // ... 更多类型
    ]

    let callback = new IdleCallback(preCreateData)
    this.getUIContext().postFrameCallback(callback)
  }
}
💡 新手提示
  1. 预创建要适量:创建太多会占用内存
  2. 根据实际需求调整:统计常用的组件类型
  3. 监控性能:确保预创建本身不影响性能

高级技巧:性能优化

🎯 技巧一:使用 attributeUpdater 精确刷新

🤔 问题

默认情况下,更新组件数据会导致整个组件重新渲染,即使只改变了一个属性。

💡 解决方案

使用 attributeUpdater 只更新需要改变的属性。

❌ 不推荐的做法

typescript 复制代码
@Reusable
@Component
struct ItemView {
  @State title: string = ''
  @State fontColor: Color = Color.Black

  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string
    this.fontColor = params.fontColor as Color  // 👈 导致整个组件刷新
  }

  build() {
    Text(this.title)
      .fontColor(this.fontColor)
      .fontSize(16)
  }
}

✅ 推荐的做法

typescript 复制代码
@Reusable
@Component
struct ItemView {
  @State title: string = ''
  @State fontColor: Color = Color.Black
  private textUpdater: AttributeUpdater<TextAttribute> = new AttributeUpdater()

  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string

    // 只更新颜色属性,不触发整个组件刷新
    this.textUpdater.fontColor(params.fontColor as Color)
  }

  build() {
    Text(this.title)
      .attributeUpdater(this.textUpdater)  // 👈 绑定属性更新器
      .fontSize(16)
  }
}
🤔 问题

@Prop 会进行深拷贝,增加创建时间和内存消耗。

💡 解决方案

使用 @Link 或 @ObjectLink 共享数据引用。

❌ 不推荐的做法

typescript 复制代码
@Reusable
@Component
struct ItemView {
  @Prop item: ItemData  // 👈 会进行深拷贝

  aboutToReuse(params: Record<string, Object>): void {
    this.item = params.item as ItemData
  }
}

✅ 推荐的做法

typescript 复制代码
@Observed
class ItemData {
  title: string = ''
  content: string = ''
}

@Reusable
@Component
struct ItemView {
  @ObjectLink item: ItemData  // 👈 共享引用,自动同步

  aboutToReuse(params: Record<string, Object>): void {
    // 👈 不需要重新赋值,数据会自动同步
  }
}

🎯 技巧三:合理使用 reuseId 区分组件

🤔 问题

组件内部使用 if/else 切换布局时,可能导致组件结构变化,影响复用效果。

💡 解决方案

为不同的布局分支设置不同的 reuseId。

❌ 不推荐的做法

typescript 复制代码
@Reusable
@Component
struct ItemView {
  @State hasImage: boolean = false

  build() {
    Column() {
      Text('标题')
      if (this.hasImage) {
        Flex() {  // 👈 结构变化时,可能需要重新创建
          Image('image.png')
        }
      }
    }
  }
}

✅ 推荐的做法

typescript 复制代码
@Reusable
@Component
struct ItemView {
  @State hasImage: boolean = false

  build() {
    Column() {
      Text('标题')
      if (this.hasImage) {
        Flex() {
          Image('image.png')
        }
        .reuseId('with_image')  // 👈 不同布局用不同ID
      } else {
        Flex()
          .reuseId('without_image')  // 👈 不同布局用不同ID
      }
    }
  }
}

🎯 技巧四:避免函数作为组件参数

🤔 问题

函数作为参数时,每次复用都会重新执行,造成性能损耗。

💡 解决方案

提前计算结果,通过状态变量传递。

❌ 不推荐的做法

typescript 复制代码
@Reusable
@Component
struct ItemView {
  @Prop sum: number = 0

  aboutToReuse(params: Record<string, Object>): void {
    // 👈 每次复用都会执行这个耗时函数
    this.sum = (params.calculator as Function)()
  }
}

// 使用时
ItemView({ sum: this.countAndReturn() })  // 👈 耗时函数

✅ 推荐的做法

typescript 复制代码
@Component
struct ParentView {
  @State calculatedSum: number = 0

  aboutToAppear() {
    // 👈 只在初始化时计算一次
    this.calculatedSum = this.countAndReturn()
  }

  build() {
    List() {
      LazyForEach(this.dataSource, (item: any) => {
        ListItem() {
          ItemView({ sum: this.calculatedSum })  // 👈 直接传递结果
        }
      })
    }
  }
}

常见问题解答

❓ 如何检查组件复用是否生效?

方法一:使用 Code Linter 工具
bash 复制代码
# 在 DevEco Studio 中
1. 打开 Tools > Code Linter
2. 关注 @performance/hp-arkui-use-reusable-component 规则
3. 查看代码检查结果
方法二:使用 Profiler 工具
bash 复制代码
# 在 DevEco Studio 中
1. 打开 Profiler 工具
2. 抓取 Trace 数据
3. 搜索组件名称
4. 查看是否有 "BuildRecycle" 字段
方法三:添加日志调试
typescript 复制代码
@Reusable
@Component
struct ItemView {
  @State title: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    console.log('组件复用成功:', this.title, '->', params.title)  // 👈 添加日志
    this.title = params.title as string
  }

  aboutToAppear() {
    console.log('创建新组件:', this.title)  // 👈 添加日志
  }
}

❓ 复用不生效的常见原因

1. 忘记添加 @Reusable 装饰器
typescript 复制代码
// ❌ 错误
@Component
struct ItemView {
  // ...
}

// ✅ 正确
@Reusable
@Component
struct ItemView {
  // ...
}
2. 没有实现 aboutToReuse 方法
typescript 复制代码
// ❌ 错误
@Reusable
@Component
struct ItemView {
  // 没有 aboutToReuse 方法
}

// ✅ 正确
@Reusable
@Component
struct ItemView {
  aboutToReuse(params: Record<string, Object>): void {
    // 实现数据更新逻辑
  }
}
3. 组件不在同一父组件下
typescript 复制代码
// ❌ 错误:不同的父组件
@Component
struct PageA {
  build() {
    List() {
      // ItemView 在 PageA 下
    }
  }
}

@Component
struct PageB {
  build() {
    List() {
      // ItemView 在 PageB 下,无法复用 PageA 的
    }
  }
}

// ✅ 正确:在同一父组件下
@Component
struct ParentPage {
  build() {
    Swiper() {
      PageA()
      PageB()
      // 两个页面在同一父组件下
    }
  }
}

❓ 性能优化建议

1. 控制缓存池大小
typescript 复制代码
class NodePool {
  private maxCacheSize: number = 20; // 👈 限制缓存数量

  recycleNode(nodeItem: NodeItem) {
    let cachedArray = this.cachedNodes.get(type);
    if (cachedArray && cachedArray.length >= this.maxCacheSize) {
      return; // 👈 超过限制就不缓存
    }
    // ... 正常缓存逻辑
  }
}
2. 监控内存使用
typescript 复制代码
@Component
struct MainPage {
  aboutToAppear() {
    // 定期清理缓存
    setInterval(() => {
      NodePool.getInstance().clearCache()
    }, 300000)  // 5分钟清理一次
  }
}
3. 合理选择复用粒度
  • 粗粒度复用 :整个列表项作为一个复用单位
    • 优点:简单易用
    • 缺点:复用率可能不高
  • 细粒度复用 :将列表项拆分成多个小组件
    • 优点:复用率高,性能更好
    • 缺点:代码复杂度增加

❓ 最佳实践总结

✅ 应该做的事情
  1. 优先使用基础复用:从简单场景开始
  2. 合理设置 reuseId:相同类型用相同 ID
  3. 及时更新数据:在 aboutToReuse 中处理
  4. 控制缓存大小:避免内存泄漏
  5. 性能监控:定期检查复用效果
❌ 不应该做的事情
  1. 嵌套 @Reusable 组件:会导致缓存池分割
  2. 频繁改变组件结构:影响复用效率
  3. 忽略内存管理:可能导致内存泄漏
  4. 过度优化:简单场景不需要复杂的复用逻辑

🎯 实战练习

练习 1:基础列表复用

创建一个简单的联系人列表,实现基础的组件复用。

需求

  • 显示联系人姓名和电话
  • 支持滑动浏览
  • 实现组件复用优化

提示

  • 使用 @Reusable 装饰器
  • 实现 aboutToReuse 方法
  • 设置合适的 reuseId

练习 2:多类型列表复用

创建一个新闻列表,包含文本、图片、视频三种类型。

需求

  • 支持多种类型的新闻条目
  • 不同类型有不同的布局
  • 实现类型间的复用优化

提示

  • 为不同类型创建不同组件
  • 使用不同的 reuseId 区分
  • 根据数据类型选择合适的组件

练习 3:全局复用池

实现一个多页面应用,不同页面的列表项可以互相复用。

需求

  • 多个页面都有相似的列表
  • 页面切换时能复用组件
  • 实现全局复用池管理

提示

  • 使用 NodePool 管理全局缓存
  • 实现 NodeController 包装组件
  • 合理控制缓存大小

📖 参考资料

相关推荐
万少1 小时前
2025中了 聊一聊程序员为什么都要做自己的产品
前端·harmonyos
网络小白不怕黑2 小时前
华为设备 QoS 流分类与流标记深度解析及实验脚本
网络·华为
网络小白不怕黑2 小时前
华为交换机堆叠与集群技术深度解析附带脚本
网络·华为
幽蓝计划14 小时前
HarmonyOS NEXT仓颉开发语言实战案例:动态广场
华为·harmonyos
万少20 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
幽蓝计划20 小时前
HarmonyOS NEXT仓颉开发语言实战案例:电影App
华为·harmonyos
HMS Core1 天前
HarmonyOS免密认证方案 助力应用登录安全升级
安全·华为·harmonyos
生如夏花℡1 天前
HarmonyOS学习记录3
学习·ubuntu·harmonyos
伍哥的传说1 天前
鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
前端·华为·前端框架·harmonyos·鸿蒙
funnycoffee1231 天前
Huawei 6730 Switch software upgrade example版本升级
java·前端·华为