ArkTS原生 | 知识问答引擎 —— 鸿蒙Next声明式UI实战
ArkTS原生知识问答引擎摘要 本项目基于鸿蒙ArkTS原生能力开发了一个轻量级知识问答模块,主要功能包括: 分类标签云:采用Grid网格展示12个知识分类,支持点击筛选 问答列表:使用List组件展示卡片式问答条目,包含标题、摘要等详细信息 关键词搜索:实现实时搜索过滤,支持标题和摘要中的关键词高亮显示 组合筛选:支持分类筛选与关键词搜索的组合过滤 技术特点: 完全基于ArkTS原生组件开发,无
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采用单向数据流 + 装饰器驱动的响应式更新:
- 数据源定义:
CATEGORIES和QA_LIST作为顶层常量数据。 - 状态持有:
QApage通过@State持有categories、originList、filteredList、searchKeyword、selectedCategoryId等可变状态。 - 过滤逻辑:用户交互(搜索输入/分类点击)触发
applyFilter(),遍历originList生成新的filteredList。 - UI响应:
@State数据变更自动触发build()重新渲染,子组件通过@Prop接收数据。
2.2 组件职责划分
| 组件 | 职责 | 数据输入 |
|---|---|---|
QApage |
页面容器、状态管理、过滤逻辑、布局编排 | 全局常量数据 |
QACard |
单条问答卡片渲染,触发摘要高亮 | QAItem、keyword |
HighlightText |
文本关键词高亮渲染 | text、keyword |
这种"父组件管状态、子组件管渲染"的分工模式,符合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') 定义四列均分宽度,rowsGap 和 columnsGap 设置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;
}
算法特点:
- 分类优先过滤:先检查分类筛选条件,不匹配的直接
continue,避免后续无意义的字符串匹配。 - 大小写不敏感:搜索关键词和文本内容均统一转为小写再比较,提升搜索体验。
- 多字段搜索:标题、摘要、标签三个字段同时匹配,提高召回率。
- 实时响应:通过
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 循环等特定语法结构,不允许出现 let、const 等变量声明语句。
应对:将所有计算逻辑提取到独立的成员方法中,在 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文档确认每个属性的正确调用方式。一般来说,核心配置属性(如 value、placeholder、controller)优先放在构造函数中,样式属性使用链式调用:
// ✅ 正确:构造函数参数 + 链式样式
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样式和性能优化,覆盖了鸿蒙原生应用开发的核心环节。
通过这个项目,我们可以看到:
- ArkTS原生组件能力已经足够强大——仅凭 Grid、List、Search、Text 等基础组件,配合装饰器驱动和状态管理,就能构建出交互流畅、视觉优雅的知识问答页面。
- 声明式UI的开发范式带来了显著的效率提升——数据驱动视图、状态自动响应,让开发者可以专注于业务逻辑而非DOM操作。
- ArkTS语法约束虽然严苛,但并非限制,而是引导——强制将计算逻辑从build()中分离,客观上促进了组件代码的更清晰解耦和更易维护。
在未来的鸿蒙生态中,ArkTS不仅是开发应用的工具,更是构建鸿蒙原生体验的基石。掌握其声明式UI的编程范式和组件的组合艺术,是每一位鸿蒙开发者进阶的必经之路。
项目代码仓库:该页面的完整源码位于项目 entry/src/main/ets/pages/QApage.ets,可直接在 DevEco Studio 中打开编译运行。
技术栈:ArkTS + ArkUI (HarmonyOS API 11+)
关键词:HarmonyOS、ArkTS、ArkUI、知识问答、搜索高亮、Grid标签云、List列表、声明式UI
更多推荐



所有评论(0)