ArkTS原生 | 知识问答引擎 —— 鸿蒙Next声明式UI实战

在这里插入图片描述

一、项目背景与概述

在鸿蒙生态快速发展的当下,ArkTS作为鸿蒙原生应用开发的首选语言,凭借其声明式UI框架、强类型系统以及深度的系统能力集成,正逐步被广大开发者接受和采用。知识问答引擎项目正是在这一背景下诞生的——它是一套完全基于ArkTS原生能力构建的轻量级知识问答功能模块,不依赖任何第三方UI库或框架,纯粹利用鸿蒙ArkUI的Grid、List、Search等原生组件,实现了包含分类标签云、关键词搜索高亮、多维度过滤等功能的知识问答页面。

本文将从工程实践的角度,逐层拆解该模块的架构设计、组件实现、状态管理以及ArkTS语法约束下的应对策略,旨在为鸿蒙开发者提供一个可参考、可复用的实战范例。

1.1 功能需求概览

  • 分类标签云:以Grid网格形式展示12个知识分类,每个分类包含图标、名称和条目数,支持点击选中/取消选中进行过滤。
  • 问答条目列表:以卡片式List展示问答条目,包含标题、摘要、标签、浏览数、点赞数、日期等信息。
  • 关键词搜索:输入关键词后实时过滤,并在标题和摘要中对匹配文字进行高亮标记。
  • 组合过滤:搜索关键词与分类过滤可叠加使用,"清除过滤"一键重置所有筛选条件。

1.2 技术选型决策

选择纯ArkTS原生实现而非引入第三方库,主要基于以下考量:

  • 零依赖:避免npm包版本冲突和鸿蒙兼容性问题。
  • 编译优化:ArkTS编译器对原生组件有深度优化,性能更优。
  • 包体积:无外部依赖意味着更小的hap包体积。
  • API一致性:随鸿蒙版本升级同步更新,无需等待第三方库适配。

二、系统架构设计

整个模块采用组件树层级架构,从数据层到视图层共分为三个层次。

┌─────────────────────────────────────────┐
│               QApage (页面容器)           │
│  ┌─────────────────────────────────────┐ │
│  │         Search (搜索组件)            │ │
│  ├─────────────────────────────────────┤ │
│  │   Grid (分类标签云, 4列x3行)         │ │
│  │   ├── GridItem × 12               │ │
│  └─────────────────────────────────────┘ │
│  ┌─────────────────────────────────────┐ │
│  │  List (问答条目列表)                 │ │
│  │   ├── ListItem → QACard            │ │
│  │   │   ├── HighlightText (标题)     │ │
│  │   │   └── 摘要 + 标签 + 统计       │ │
│  └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

2.1 数据流方向

ArkTS采用单向数据流 + 装饰器驱动的响应式更新:

  1. 数据源定义CATEGORIESQA_LIST 作为顶层常量数据。
  2. 状态持有QApage 通过 @State 持有 categoriesoriginListfilteredListsearchKeywordselectedCategoryId 等可变状态。
  3. 过滤逻辑:用户交互(搜索输入/分类点击)触发 applyFilter(),遍历 originList 生成新的 filteredList
  4. UI响应@State 数据变更自动触发 build() 重新渲染,子组件通过 @Prop 接收数据。

2.2 组件职责划分

组件 职责 数据输入
QApage 页面容器、状态管理、过滤逻辑、布局编排 全局常量数据
QACard 单条问答卡片渲染,触发摘要高亮 QAItemkeyword
HighlightText 文本关键词高亮渲染 textkeyword

这种"父组件管状态、子组件管渲染"的分工模式,符合ArkTS推荐的最佳实践。

三、数据模型设计

3.1 分类数据模型

interface Category {
  id: number;
  name: string;
  count: number;     // 该分类下问题数
  icon: string;      // 图标 emoji
  isSelected: boolean;
}

Category 接口包含了展示标签云所需的全部信息。isSelected 字段用于控制网格项的高亮状态样式切换。模拟数据覆盖了HarmonyOS开发的12个核心领域:

分类 条目数 覆盖主题
HarmonyOS 24 分布式、Ability调度
ArkTS 18 装饰器、状态管理
UI组件 32 Grid、List、弹窗
网络编程 15 HTTP、WebSocket
数据管理 21 分布式数据对象
多媒体 10 相机、XComponent
传感器 8 设备能力
安全 12 HUKS加密
性能优化 14 SmartPerf、启动优化
测试 9 单元测试
动画 16 转场、自定义弹窗
AI能力 7 语音识别

3.2 问答条目数据模型

interface QAItem {
  id: number;
  title: string;
  summary: string;
  categoryId: number;
  tags: string[];
  viewCount: number;
  likeCount: number;
  date: string;
}

每个问答条目通过 categoryId 与分类关联,构成了典型的一对多关系。14条模拟问答数据覆盖了各分类的真实技术问题,标题采用中文问句形式,增强了内容的可读性和真实感。

3.3 数据深拷贝策略

@State categories: Category[] = JSON.parse(JSON.stringify(CATEGORIES));
@State originList: QAItem[] = JSON.parse(JSON.stringify(QA_LIST));

使用 JSON.parse(JSON.stringify(...)) 进行深拷贝,确保 @State 持有的数据不与顶层常量共享引用,避免意外的副作用。

四、核心组件实现详解

4.1 HighlightText —— 关键词高亮引擎

HighlightText 是本次实现中最精妙的小型组件,它承担着在文本中定位关键词并包裹高亮样式的任务。在Web开发中这通常由 dangerouslySetInnerHTML 或正则替换完成,但在ArkTS的 Text 组件中,必须利用 Text() 及其子组件的组合来实现。

4.1.1 设计思路

高亮文本的渲染策略可概括为:用关键词分割原字符串,然后按"普通文本 → 关键词 → 普通文本"的顺序依次渲染 Text 片段。其中关键词部分使用红色字体 + 红色背景,实现视觉上的高亮效果。

4.1.2 关键实现
@Builder
KeywordHighlightText(parts: string[], keyword: string) {
  ForEach(parts, (part: string, index: number) => {
    if (index === 0) {
      Text(part)                       // 第一个片段总是普通文本
    } else {
      Text(keyword)                    // 高亮关键词
        .fontColor('#ff6b6b')
        .backgroundColor('#ffd4d4')
      Text(part)                       // 关键词后的普通文本
    }
  })
}

这里有一个精妙的设计点:string.split(keyword) 的返回值中,第一个元素(index === 0)总是普通文本,之后每次遇到关键词时,索引加1对应关键词后的普通文本片段。因此 ForEach 中,index === 0 时只渲染普通文本,index > 0 时先渲染关键词高亮,再渲染普通文本。

4.1.3 ArkTS语法约束的应对

最初的设计思路是在 build() 中直接写 let parts = this.text.split(this.keyword),但ArkTS编译器严格禁止在 build()@Builder 方法中出现 let 声明。解决方案是将计算逻辑提取到普通方法中:

getParts(): string[] {
  if (this.keyword.length === 0) return [this.text];
  return this.text.split(this.keyword);
}

然后在 build() 中调用 this.KeywordHighlightText(this.getParts(), this.keyword) — 将计算结果作为参数传递给 @Builder 方法。这种"计算在方法、渲染在build"的模式是ArkTS开发中的核心技巧。

4.2 QACard —— 问答卡片组件

QACard 负责渲染单条问答条目的卡片式布局,包含标题、摘要、标签和统计信息四个区域。

4.2.1 标题区

标题区直接复用 HighlightText 组件:

HighlightText({ text: this.item.title, keyword: this.keyword })
  .margin({ bottom: 8 });

由于 HighlightText 内已处理了关键词不存在时的兜底逻辑(直接渲染纯文本),父组件无需额外判断。

4.2.2 摘要区

摘要区的实现比标题更为复杂,因为它需要处理更长的文本和可能的关键词匹配。QACard提供了三个辅助方法来为build()提供数据:

  • getSummaryParts():按关键词分割摘要文本,返回字符串数组。
  • getHasKeyword():判断摘要中是否包含关键词,决定是否启用高亮渲染。
  • getPartsCount():计算分割后的片段数(为后续扩展预留)。

build() 中根据 getHasKeyword() 的结果决定渲染方式:

if (this.getHasKeyword()) {
  Text() {
    this.SummaryHighlight(this.getSummaryParts(), this.keyword);
  }
  .maxLines(2)
  .textOverflow({ overflow: TextOverflow.Ellipsis });
} else {
  Text(this.item.summary)
    .maxLines(2)
    .textOverflow({ overflow: TextOverflow.Ellipsis });
}

值得注意的设计细节:使用了 .maxLines(2) + .textOverflow(TextOverflow.Ellipsis) 来限制摘要最多显示两行,超出部分以省略号截断。这是移动端卡片布局的标准交互模式。

4.2.3 标签与统计区

底部的标签和统计信息占据一行,使用弹性布局 Row() + Blank() 实现左右分布:

Row() {
  // 左侧标签
  ForEach(this.item.tags, (tag: string) => { ... })
  Blank()                     // 弹性填充
  // 右侧统计
  Row() { Text('👁️') ... }   // 浏览数
  Row() { Text('👍') ... }    // 点赞数
  Text(this.item.date)        // 日期
}

每个标签使用 backgroundColor('#e8f0fe') 配合 borderRadius(4) 形成蓝色药丸样式,与主内容的白色背景形成层次对比。统计信息前的 emoji 图标代替了纯文字标注,使界面更加轻量化。

4.2.4 卡片样式

卡片的整体风格采用圆角白色卡片 + 轻微阴影:

.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
  radius: 6,
  offsetX: 0,
  offsetY: 2,
  color: 'rgba(0, 0, 0, 0.06)',
})

.shadow() 的属性值经过精心调校:radius: 6 产生柔和弥散效果,offsetY: 2 营造轻微的悬浮感,color 使用低透明度黑色而非纯灰色,在深色模式下表现更自然。

4.3 QApage —— 页面主组件

QApage 是整个模块的入口和大脑,负责状态管理、事件处理和布局编排。它注册为 @Entry 组件,作为页面路由的目标。

4.3.1 状态定义
@State categories: Category[] = ...;
@State originList: QAItem[] = ...;    // 原始完整数据
@State filteredList: QAItem[] = ...;  // 过滤后数据
@State searchKeyword: string = '';
@State selectedCategoryId: number = -1;

采用双列表模式originList 作为不可变数据源,filteredList 作为UI直接消费的数据。每次过滤操作都重新遍历 originList 生成新的 filteredList,而非在原数据上逐次缩小范围,避免了二次过滤时状态不一致的问题。

4.3.2 搜索组件适配
Search({
  value: this.searchKeyword,
  placeholder: '搜索问题、关键词...',
  controller: this.searchInputController
})

ArkTS原生 Search 组件提供搜索输入框功能。在实际开发中需要注意:

  • placeholder 属性必须作为构造函数参数传入,不支持链式调用 .placeholder()
  • fontSize 等文本样式属性在某些API版本中不支持链式调用,需移除。
  • onCancel 回调在某些API版本中不存在,需改用其他方案(如清空搜索时触发 onChange)。
  • 通过 .onChange() 回调实时监听输入变化并触发过滤。
4.3.3 Grid分类标签云

分类标签云使用 4列 × 3行 的 Grid 布局展示12个分类:

Grid() {
  ForEach(this.categories, (cat: Category) => {
    GridItem() {
      Column() {
        Text(cat.icon)    // emoji图标
        Text(cat.name)    // 分类名称
        Text(`${cat.count}`) // 条目数
      }
      ...
      .onClick(() => { this.selectCategory(cat); })
    }
  })
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.height(120)

columnsTemplate('1fr 1fr 1fr 1fr') 定义四列均分宽度,rowsGapcolumnsGap 设置8vp的间隔。每个 GridItem 通过点击事件调用 selectCategory() 实现选中/取消选中切换。

分类项的样式变化通过状态驱动:

.backgroundColor(cat.isSelected ? '#007aff' : '#f2f2f7')
.fontColor(cat.isSelected ? Color.White : '#333333')

选中时变为蓝色主题(蓝色背景 + 白色文字),未选中时为浅灰色背景。

4.3.4 问答条目列表

采用 List + ForEach + ListItem + QACard 的组合渲染问答列表:

List({ space: 0 }) {
  ForEach(this.filteredList, (item: QAItem) => {
    ListItem() {
      QACard({ item: item, keyword: this.searchKeyword });
    }
  })
}
.layoutWeight(1)

.layoutWeight(1) 使 List 占据页面的剩余空间,确保列表内容不足时底部不会留白。

五、过滤与搜索逻辑

5.1 核心过滤算法

applyFilter() 方法实现了组合过滤的核心逻辑,支持关键词搜索与分类过滤的同时作用:

applyFilter(): void {
  let keyword: string = this.searchKeyword.trim().toLowerCase();
  let catId: number = this.selectedCategoryId;

  let result: QAItem[] = [];
  for (let i: number = 0; i < this.originList.length; i++) {
    let item: QAItem = this.originList[i];
    // 优先按分类过滤
    if (catId !== -1 && item.categoryId !== catId) continue;
    // 若有搜索关键词,在标题/摘要/标签中匹配
    if (keyword.length > 0) {
      let inTitle = item.title.toLowerCase().includes(keyword);
      let inSummary = item.summary.toLowerCase().includes(keyword);
      // 标签匹配检测...
      if (inTitle || inSummary || inTags) result.push(item);
    } else {
      result.push(item);
    }
  }
  this.filteredList = result;
}

算法特点:

  1. 分类优先过滤:先检查分类筛选条件,不匹配的直接 continue,避免后续无意义的字符串匹配。
  2. 大小写不敏感:搜索关键词和文本内容均统一转为小写再比较,提升搜索体验。
  3. 多字段搜索:标题、摘要、标签三个字段同时匹配,提高召回率。
  4. 实时响应:通过 Search.onChange 实时触发,每次输入变化都重新过滤。

5.2 分类选择逻辑

selectCategory() 方法实现了单选式分类选择:

selectCategory(cat: Category): void {
  if (this.selectedCategoryId === cat.id) {
    this.selectedCategoryId = -1;     // 已选中的再次点击取消
  } else {
    this.selectedCategoryId = cat.id;  // 切换为新分类
  }
  // 同步更新所有分类的 isSelected 状态
  for (let i = 0; i < this.categories.length; i++) {
    this.categories[i].isSelected = (
      this.categories[i].id === this.selectedCategoryId
    );
  }
  this.applyFilter();
}

交互细节:再次点击已选中的分类会取消选中(回到"全部"状态),通过 selectedCategoryId = -1 实现。

5.3 清除过滤

当页面处于过滤状态时(有搜索关键词或选中了分类),右上角会出现"清除过滤"链接,一键重置所有条件:

clearFilter(): void {
  this.searchKeyword = '';
  this.selectedCategoryId = -1;
  this.searchInputController.caretPosition(0);  // 光标归位
  // 重置所有分类选中状态
  for (let i = 0; i < this.categories.length; i++) {
    this.categories[i].isSelected = false;
  }
  this.applyFilter();
}

六、ArkTS语法约束与工程实践

在开发过程中,ArkTS语法约束带来了一些独特的挑战,这里总结了几条在实践中验证有效的应对策略。

6.1 build()中的变量声明限制

问题:ArkTS编译器规定 build() 方法内只能包含组件声明、@Builder 调用、if/else 条件、ForEach 循环等特定语法结构,不允许出现 letconst 等变量声明语句。

应对:将所有计算逻辑提取到独立的成员方法中,在 build() 中仅调用方法获取返回值:

// ❌ 错误:build() 中声明变量
build() {
  let parts = this.text.split(this.keyword); // 编译错误
}

// ✅ 正确:提取到方法中
getParts(): string[] {
  return this.text.split(this.keyword);
}
build() {
  this.KeywordHighlightText(this.getParts(), this.keyword);
}

6.2 组件属性的链式调用限制

问题:某些系统组件(如 Search)的属性在特定API版本中不支持链式 .xxx() 调用,部分属性必须通过构造函数参数传入。

应对:查阅鸿蒙API文档确认每个属性的正确调用方式。一般来说,核心配置属性(如 valueplaceholdercontroller)优先放在构造函数中,样式属性使用链式调用:

// ✅ 正确:构造函数参数 + 链式样式
Search({
  value: this.searchKeyword,
  placeholder: '搜索...',
  controller: this.searchInputController
})
.backgroundColor('#f2f2f7')
.borderRadius(20);

6.3 ForEach的使用规范

问题:在 @Builder 中使用 ForEach 时,回调函数的参数必须明确标注类型。

应对:始终为 ForEach 的回调参数添加显式类型标注:

ForEach(this.categories, (cat: Category, index: number) => { ... })

6.4 数据不可变性

问题@State 装饰的数组,当修改其中某个元素的属性时,ArkTS 可能无法检测到深层变化。

应对:在修改分类的 isSelected 状态时,通过索引直接赋值而非重新创建数组,因为ArkTS的 @State 对数组元素的属性变化有深度观测能力:

for (let i = 0; i < this.categories.length; i++) {
  this.categories[i].isSelected = (
    this.categories[i].id === this.selectedCategoryId
  );
}

七、UI样式设计分析

7.1 色彩系统

页面采用简约的双色主题:

用途 色值 使用场景
主色调 #007aff 分类选中态、链接、标签文字
高亮色 #ff6b6b / #ffd4d4 关键词搜索高亮(红字红底)
文字主色 #1a1a2e 标题、主内容
文字辅色 #666666 摘要
文字浅色 #999999 / #cccccc 统计信息、日期
背景 #f5f5f5 页面底色
卡片色 #ffffff 问答卡片背景

7.2 布局与间距

页面采用20vp的左右边距统一对齐(列表中内层卡片使用16vp内边距),各区块间通过12~8vp的间距分隔,形成清晰的视觉层次。

7.3 圆角与阴影

  • 搜索框:borderRadius(20) 全圆角
  • 分类项:borderRadius(12) 中等圆角
  • 问答卡片:borderRadius(12) + 阴影
  • 标签:borderRadius(4) 小圆角

圆角体系从大到小形成了"搜索框 > 分类项/卡片 > 标签"的层级递减,符合视觉权重。

八、性能优化思考

8.1 避免不必要的渲染

当前实现中,每次搜索输入都会触发 applyFilter() 并更新 filteredList,这会触发整个 List 重新渲染。对于14条数据的规模,性能完全在可接受范围内。但当数据量扩展到成百上千条时,可以考虑:

  • 使用 LazyForEach 替代 ForEach,实现虚拟滚动。
  • 引入防抖(debounce)机制,搜索输入结束后再进行过滤。
  • 使用 @Monitor@Watch 精确控制状态更新的触发条件。

8.2 数据源的选择

采用 originList 作为不可变数据源、每次重新遍历过滤的策略,虽然在时间复杂度上是 O(n),但保证了过滤逻辑的正确性和可预测性。对于中小规模数据集(千条以内),这正是推荐的做法。

九、扩展与展望

9.1 可扩展功能

  • 网络数据源:将模拟数据替换为HTTP请求,接入RESTful API或GraphQL。
  • 分页加载:在 List 底部添加 .onReachEnd() 回调,实现滚动加载更多。
  • 问答详情页:点击卡片跳转到详情页,展示完整问答内容。
  • 收藏功能:添加收藏状态,支持收藏夹管理。
  • 富文本渲染:问答内容支持代码块、表格等Markdown格式渲染。

9.2 组件抽象优化

当前的 HighlightText 组件只支持单关键词高亮,可扩展为支持多关键词高亮版本:

@Prop keywords: string[] = [];

通过遍历关键词数组,对文本进行递归分割和渲染。

十、总结

本文从工程实战的角度,完整呈现了一个基于 ArkTS 原生能力的知识问答页面的开发全过程。从数据模型设计、组件层级架构、关键词高亮算法,到ArkTS语法约束下的应对策略,再到UI样式和性能优化,覆盖了鸿蒙原生应用开发的核心环节。

通过这个项目,我们可以看到:

  1. ArkTS原生组件能力已经足够强大——仅凭 Grid、List、Search、Text 等基础组件,配合装饰器驱动和状态管理,就能构建出交互流畅、视觉优雅的知识问答页面。
  2. 声明式UI的开发范式带来了显著的效率提升——数据驱动视图、状态自动响应,让开发者可以专注于业务逻辑而非DOM操作。
  3. ArkTS语法约束虽然严苛,但并非限制,而是引导——强制将计算逻辑从build()中分离,客观上促进了组件代码的更清晰解耦和更易维护。

在未来的鸿蒙生态中,ArkTS不仅是开发应用的工具,更是构建鸿蒙原生体验的基石。掌握其声明式UI的编程范式和组件的组合艺术,是每一位鸿蒙开发者进阶的必经之路。


项目代码仓库:该页面的完整源码位于项目 entry/src/main/ets/pages/QApage.ets,可直接在 DevEco Studio 中打开编译运行。

技术栈:ArkTS + ArkUI (HarmonyOS API 11+)

关键词:HarmonyOS、ArkTS、ArkUI、知识问答、搜索高亮、Grid标签云、List列表、声明式UI

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐