一、详细知识点
1. 类型系统
ArkTS 开发要尽量避免"随便传对象"。页面、接口、缓存和组件都应该有明确类型。
ts
export interface NewsArticle {
id: string
title: string
summary: string
content: string
category: string
publishTime: string
readMinutes: number
favorite: boolean
}
工程要求:
- API DTO 与页面模型分离。
- 可空字段必须显式处理。
- 不让页面直接依赖后端原始字段。
2. 接口、类与枚举
接口适合表达数据结构;类适合表达默认值和行为;枚举或字面量联合类型适合表达有限状态。
ts
export type PageName = 'home' | 'detail' | 'favorites' | 'settings'
export class UserSetting {
darkMode: boolean = false
fontScale: number = 1
}
3. 模块化
ts
import { NewsArticle } from '../model/NewsArticle'
import { NewsService } from '../services/NewsService'
规范:
model只放类型和轻量模型。services放业务动作。pages不直接写复杂数据转换。components尽量可复用,依靠入参和回调。
4. 异步与错误边界
ts
private async reload(): Promise<void> {
this.loading = true
this.errorMessage = ''
try {
this.articles = await NewsService.queryArticles()
} catch (error) {
this.errorMessage = '加载失败,请重试'
} finally {
this.loading = false
}
}
工程要求:
- 所有远程、文件、权限和系统能力调用都必须有失败路径。
- 错误要包含业务上下文,但不能泄露隐私。
- 页面要有加载中、空状态、失败状态和重试入口。
5. 工程编码规范
| 规则 | 原因 | 示例 |
|---|---|---|
| 类型明确 | 降低运行时错误 | let articles: NewsArticle[] = [] |
| 函数单一职责 | 便于测试 | toggleFavorite(id) |
| 页面薄、服务厚 | 降低 UI 耦合 | Page 调用 Service |
| 状态可追踪 | 避免刷新混乱 | 页面状态集中定义 |
| 错误可恢复 | 用户体验完整 | 失败提示 + 重试 |
二、本章 demo
Demo 1:数据模型
见 demo/harmony-news-demo/entry/src/main/ets/model/NewsArticle.ets:
ts
export interface NewsArticle {
id: string
title: string
summary: string
content: string
category: string
publishTime: string
readMinutes: number
favorite: boolean
}
Demo 2:服务层模拟异步请求
见 services/NewsService.ets:
ts
static async queryArticles(): Promise<NewsArticle[]> {
await delay(250)
return MOCK_ARTICLES.map((article) => ({ ...article }))
}
Demo 3:浏览器预览同源逻辑
demo/web-preview/app.js 使用同样的文章模型、收藏状态和设置状态,保证没有 DevEco 命令行时也能验证业务流程。
三、面试题与详细答案
1. ArkTS 项目为什么要重视类型?
类型是鸿蒙工程可维护性的基础。页面、组件、服务和缓存之间如果没有明确类型,字段变更会很难定位,运行时错误也会增多。明确类型还能让 IDE 自动补全、编译检查和重构更可靠。
追问:接口返回字段和页面模型是否应该完全一致?不一定。生产项目建议 DTO 与页面模型分离,中间用转换函数处理默认值、空值和字段命名。
2. 页面层为什么不建议直接处理复杂业务?
页面层主要负责展示和交互。如果网络请求、缓存、数据转换、权限处理都写在页面里,会导致页面难测试、难复用、难排错。更好的方式是 Page 调用 Service,Service 调用 Store 或 Remote API。
3. async/await 的错误应该在哪里处理?
Service 层负责记录技术错误和转换业务错误;Page 层负责展示用户可理解的状态。不能只在底层 catch 后返回空数据,否则页面无法区分"真的为空"和"加载失败"。
4. 如何判断一个组件是否拆得合理?
看三个指标:是否能复用、是否降低父页面复杂度、是否有清晰输入输出。比如 NewsCard 接收 article 和事件回调,自己不关心数据从哪里来,这就是合理拆分。
四、五倍扩展知识点矩阵
1. ArkTS 能力地图
| 能力 | 基础要求 | 工程化要求 | 工程要求 |
|---|---|---|---|
| 基础类型 | 会用 string/number/boolean | 能处理联合类型和数组 | 能设计稳定领域模型 |
| 接口 | 会定义字段 | 区分 DTO 和 ViewModel | 能做版本兼容转换 |
| 类 | 会写默认值 | 封装行为 | 避免把类变成全局状态容器 |
| 模块 | 会 import/export | 按目录拆分 | 控制依赖方向 |
| 异步 | 会 async/await | 处理 loading/error | 设计取消、重试、超时 |
| 集合 | 会数组 map/filter/find | 不直接修改被追踪对象 | 控制大数据性能 |
| 空值 | 会判断 undefined | 路由参数校验 | 系统性设计边界 |
| 异常 | 会 try/catch | 区分技术错误和业务错误 | 错误可观测、可恢复 |
| 泛型 | 理解通用结构 | 封装 Result/Repository | 保持类型安全和可读性 |
| 代码规范 | 命名清晰 | 分层清晰 | 可测试、可审查、可演进 |
2. 类型设计扩展
ts
export interface ApiResponse<T> {
code: number
message: string
data?: T
}
export interface NewsDto {
article_id: string
headline: string
digest?: string
}
export interface NewsArticle {
id: string
title: string
summary: string
}
export function toArticle(dto: NewsDto): NewsArticle {
return {
id: dto.article_id,
title: dto.headline,
summary: dto.digest ?? '暂无摘要'
}
}
为什么要做转换:后端字段可能用下划线,页面模型可能用驼峰;后端字段可能为空,页面需要默认值;后端返回可能多字段,页面只需要一部分。转换层能保护 UI 不被接口变化直接冲击。
3. Result 模式
ts
export type Result<T> =
| { success: true; data: T }
| { success: false; message: string; retryable: boolean }
async function loadNews(): Promise<Result<NewsArticle[]>> {
try {
const data = await NewsService.queryArticles()
return { success: true, data }
} catch (error) {
return { success: false, message: '新闻加载失败', retryable: true }
}
}
Result 模式的价值是让调用方明确处理成功和失败,而不是把异常藏在空数组里。可上线代码不把"没有数据"和"加载失败"混为一谈。
4. 异步并发与顺序
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 必须按顺序 | await step1(); await step2() |
保证依赖顺序 |
| 可并发 | Promise.all |
缩短等待时间 |
| 任一成功即可 | Promise.race 或业务封装 |
做降级 |
| 可重试请求 | 包装 retry 函数 | 避免页面重复写逻辑 |
| 页面退出 | 记录请求版本 | 避免旧请求覆盖新状态 |
页面常见问题:用户快速切换页面,旧请求回来后覆盖新页面状态。解决方式是维护请求序号,只接受最后一次请求结果。
5. 模块依赖规则
text
pages -> components
pages -> services
services -> model
services -> storage/network
components -> model
model -> no dependency
不要让 model 反向依赖 pages,不要让 components 直接调用网络服务,不要让 services 引用具体 UI 组件。依赖方向越稳定,项目越容易维护。
五、ArkTS demo 扩展任务
| 任务 | 文件 | 目标 | 验证 |
|---|---|---|---|
增加 NewsCategory 类型 |
model |
限制分类取值 | 错误分类编译失败 |
| 增加 DTO 转换 | services |
模拟接口转换 | 页面字段不变 |
| 增加 Result 返回 | NewsService |
区分失败和空列表 | 页面显示错误 |
| 增加请求版本号 | Index.ets |
防旧请求覆盖 | 快速刷新不乱 |
| 增加搜索函数 | NewsService |
练习 filter | 搜索标题成功 |
| 增加排序函数 | NewsService |
练习不可变数组 | 阅读时长排序 |
| 增加设置模型校验 | SettingsStore |
限制字体范围 | 不能超出区间 |
| 增加收藏统计 | FavoriteStore |
练习派生数据 | 显示收藏数量 |
| 增加错误日志上下文 | services |
便于排查 | 日志含方法名 |
| 增加单测伪代码 | docs |
建立测试意识 | 每个函数有输入输出 |
扩展示例:请求版本号
ts
private requestVersion: number = 0
private async reload(): Promise<void> {
const currentVersion = ++this.requestVersion
this.loading = true
const articles = await NewsService.queryArticles()
if (currentVersion !== this.requestVersion) {
return
}
this.articles = articles
this.loading = false
}
这个写法解决"连续点击刷新,后返回的旧请求覆盖新请求"的问题。生产项目还会配合取消请求、超时和重试。
六、常见编码错误
| 错误 | 后果 | 修复 |
|---|---|---|
| 任意对象到处传 | 字段变更难定位 | 定义接口和转换函数 |
| 页面直接写 mock 数据 | 页面不可复用 | 移到 Service |
| catch 后返回空数组 | 无法区分失败和无数据 | 使用 Result 或错误状态 |
| 深层修改对象 | UI 可能不刷新 | 生成新对象或新数组 |
| 命名过短 | 可读性差 | 使用业务语义命名 |
| Service 引用页面 | 依赖倒置 | Service 只处理业务 |
| Store 无限增长 | 内存风险 | 做容量和清理策略 |
| 日志输出敏感信息 | 安全风险 | 脱敏和分级 |
| 异步结果不校验 | 状态错乱 | 请求版本或取消机制 |
| 类型全写 any | 编译保护失效 | 用接口、泛型、联合类型 |
七、扩展面试题
5. DTO、Entity、ViewModel 有什么区别?
DTO 面向接口传输,字段由后端或协议决定;Entity 或领域模型面向业务含义;ViewModel 面向页面展示。三者分离可以降低接口变化对 UI 的影响。小 demo 可以合并,但生产项目建议至少区分接口返回和页面模型。
6. 为什么不建议滥用全局单例 Store?
单例 Store 简单,但容易带来隐式依赖、测试困难、生命周期不清和内存长期占用。适合保存轻量、明确、跨页面共享的数据;复杂业务要结合模块边界、持久化和清理策略。
7. 如何设计一个可测试的 Service?
Service 应该输入明确、输出明确,不直接依赖 UI,不直接弹窗,不读取隐式页面状态。网络和存储可以通过接口抽象或可替换函数注入。这样单元测试可以用 mock 数据验证成功、失败和边界。
8. try/catch 为什么不能随便吞错误?
吞错误会让上层无法判断失败原因,用户也看不到正确状态。正确方式是记录必要上下文,把底层错误转换成业务可理解的错误,再由页面展示重试、空状态或降级。
9. 什么时候需要泛型?
当多个结构只有数据类型不同而处理逻辑一致时,例如 ApiResponse<T>、Result<T>、PageData<T>。泛型可以减少重复,但不要为了炫技过度抽象,影响团队理解。
10. 如何避免 ArkTS 工程越写越乱?
先定目录职责,再定依赖方向,再定命名和错误处理规则。每新增一个功能都要问:类型放哪里、服务放哪里、状态属于谁、失败怎么展示、是否需要测试。长期坚持这些问题,工程复杂度才不会失控。
八、ArkTS 知识点详解库
1. 类型是架构边界
类型不仅是编译工具提示,它定义了模块之间的契约。NewsArticle 一旦成为页面模型,组件、服务和测试都会依赖它。随意改字段会影响所有调用方,因此字段命名、可空性和默认值都要谨慎。
2. 可空字段必须有业务解释
字段可空不是简单加 ?。要说明为什么可空:接口可能缺失、权限未授权、用户未填写,还是缓存版本旧。不同原因对应不同 UI 和错误处理。
3. 接口转换是防腐层
接口字段属于外部系统,页面模型属于本应用。中间转换层可以防止后端字段命名、默认值、枚举变化直接影响 UI。可上线项目会把转换函数单独测试。
4. 不可变更新更适合声明式 UI
在声明式 UI 中,生成新数组和新对象通常比深层原地修改更安全。它让状态变化更明确,也更容易触发刷新和调试。
5. 异步函数要有完整状态
一次异步加载至少对应四种状态:未开始、加载中、成功、失败。很多页面只写成功状态,导致失败时用户看到空白。
6. 错误要分类
网络错误、权限错误、数据格式错误、业务错误、未知错误应分开处理。分类后才能决定是否重试、是否提示用户、是否上报日志。
7. 业务函数不要返回魔法值
例如用空字符串表示失败、用 -1 表示不存在,会让调用方猜语义。更好的方式是返回 undefined、Result<T> 或明确错误对象。
8. 命名要表达业务
handleClick、changeData、doSomething 不能表达意图。toggleFavorite、queryArticles、updateFontScale 能直接说明业务行为。
9. 模块越底层越不能依赖 UI
model 和 services 应该不知道页面怎么展示。底层依赖 UI 会导致业务逻辑无法复用,也难以测试。
10. Store 需要生命周期策略
内存 Store 简单,但应用重启会丢失;持久化 Store 稳定,但要处理版本迁移、损坏数据和清理。选择 Store 要基于业务数据生命周期。
11. 泛型要服务真实重复
只有当多个函数结构相同、类型不同,才需要泛型。过度泛型会降低可读性,让普通业务开发者难以维护。
12. 代码审查要看边界
ArkTS 代码审查不只看语法,还要看类型是否清晰、异常是否处理、状态是否可追踪、模块依赖是否合理、日志是否安全。
九、ArkTS 场景化实战库
| 场景 | 练习点 | 实现方向 | 验收标准 |
|---|---|---|---|
| 新闻搜索 | 字符串过滤 | filter 标题和摘要 |
输入后列表变化 |
| 分类过滤 | 联合类型 | 限制分类值 | 分类不乱写 |
| 阅读排序 | 数组复制排序 | 不改原数组 | 排序可恢复 |
| 收藏切换 | Store | 增删 id | 计数正确 |
| 设置校验 | 数值边界 | 限制 0.85-1.25 | 超界无效 |
| 接口转换 | DTO Mapper | 下划线转驼峰 | 页面字段稳定 |
| 错误显示 | Result | 成功失败分支 | 失败可重试 |
| 请求防抖 | 异步控制 | 延迟执行 | 连续输入不卡 |
| 请求去重 | 缓存 Promise | 同请求复用 | 不重复加载 |
| 旧请求丢弃 | 版本号 | 只接受最新 | 快速刷新不乱 |
| 日志脱敏 | 字符串处理 | 隐藏敏感值 | 日志安全 |
| 单元测试 | 纯函数 | 输入输出断言 | 可自动验证 |
十、扩展面试题库
11. ArkTS 中为什么要避免大量 any?
any 会绕过类型检查,让编译器失去保护能力。短期写起来快,长期会让字段错误、空值错误和接口变化更晚暴露。生产项目应使用接口、联合类型、泛型或明确转换函数。
12. 如何处理接口字段新增和删除?
新增字段通常不会影响旧页面,但删除或改名会破坏转换层。应把接口 DTO 和页面模型分开,转换函数中处理默认值和兼容逻辑,并给关键转换补测试。
13. 为什么 Service 不应该直接弹 UI 提示?
Service 是业务层,不应该知道页面展示方式。直接弹提示会让 Service 难测试、难复用。正确方式是返回业务结果,由 Page 决定展示 Toast、空状态、弹窗还是重试。
14. 如何设计错误对象?
错误对象至少包含用户文案、技术原因、是否可重试和日志上下文。用户文案给页面展示,技术原因给日志,是否可重试决定按钮,日志上下文帮助定位。
15. 什么时候应该抽公共工具函数?
当同一逻辑出现三次以上,或者逻辑本身有明确业务含义且需要测试时,可以抽。不要把只有一行且语义不稳定的代码过早抽象。
十一、ArkTS 语言体系补全
| 主题 | 必须掌握 | 项目中怎么用 | 常见错误 |
|---|---|---|---|
| 基础类型 | string、number、boolean、array、object | 定义页面状态和模型 | 用 any 绕过检查 |
| 字面量类型 | 固定状态集合 | `type PageName = 'home' | 'detail'` |
| interface | 数据契约 | DTO、ViewModel、配置对象 | 字段可空性不清 |
| class | 默认值和行为 | UserSetting、错误模型 |
把 class 当全局变量 |
| enum/联合类型 | 有限状态 | 分类、页面名、错误码 | 状态值散落 |
| 泛型 | 通用响应结构 | Result<T>、PageData<T> |
抽象过度 |
| 模块 | import/export | model/service/component 分层 | 循环依赖 |
| 异步 | Promise、async/await | 网络、文件、权限 | 不处理失败 |
| 并发 | Promise.all、请求版本 | 并行加载、丢弃旧请求 | 旧结果覆盖新状态 |
| 异常 | try/catch、Result | 错误映射 | catch 后静默 |
| 集合 | map/filter/find/reduce | 列表、搜索、统计 | 原地修改导致状态不清 |
| 函数式转换 | DTO -> Model | 接口防腐层 | 页面直接吃接口字段 |
十二、ArkTS 工程模板
1. 统一结果模型
ts
export type AppErrorCode =
| 'NETWORK_TIMEOUT'
| 'PERMISSION_DENIED'
| 'DATA_EMPTY'
| 'DATA_INVALID'
| 'UNKNOWN'
export interface AppError {
code: AppErrorCode
message: string
retryable: boolean
cause?: string
}
export type AppResult<T> =
| { ok: true; data: T }
| { ok: false; error: AppError }
这样页面能明确知道:是否成功、失败原因、是否可重试、应该显示什么文案。
2. DTO 转换模板
ts
interface NewsDto {
id?: string
title?: string
digest?: string
content?: string
tag?: string
}
function normalizeNews(dto: NewsDto): NewsArticle {
return {
id: dto.id ?? `local-${Date.now()}`,
title: dto.title ?? '未命名内容',
summary: dto.digest ?? '暂无摘要',
content: dto.content ?? '',
category: dto.tag ?? 'General',
publishTime: new Date().toISOString().slice(0, 10),
readMinutes: Math.max(1, Math.ceil((dto.content?.length ?? 100) / 300)),
favorite: false
}
}
3. 请求去重模板
ts
class RequestCache<T> {
private pending?: Promise<T>
run(task: () => Promise<T>): Promise<T> {
if (!this.pending) {
this.pending = task().finally(() => {
this.pending = undefined
})
}
return this.pending
}
}
用于首页重复刷新、配置重复拉取、用户信息重复请求。
4. 状态机模板
ts
type LoadState<T> =
| { type: 'idle' }
| { type: 'loading'; keepOldData: boolean }
| { type: 'success'; data: T }
| { type: 'empty'; message: string }
| { type: 'error'; message: string; retryable: boolean }
页面不要只用 loading: boolean 和 errorMessage: string 凑合。复杂页面用状态机更清晰。
十三、编码规范补强
| 规则 | 原因 | 检查方式 |
|---|---|---|
| 页面不直接写接口 DTO | 防止接口变化冲击 UI | 看 Page 是否 import DTO |
| Service 不引用 UI 组件 | 保持业务层可测试 | 看 services 是否 import pages/components |
| Store 不无限保存数据 | 防止内存和隐私问题 | 看是否有清理策略 |
| 日志不包含敏感字段 | 防止隐私泄露 | 搜索 token、phone、location |
| 错误不静默 | 用户可恢复 | catch 是否返回明确结果 |
| 异步有并发策略 | 防止状态乱序 | 是否有请求版本/去重/取消 |
| 数组更新生成新对象 | 响应式更稳定 | 是否大量原地修改 |
| 命名带业务含义 | 提升可读性 | 禁止 doIt、data1 |
十四、ArkTS 练习套件
| 练习 | 输入 | 输出 | 要求 |
|---|---|---|---|
| DTO 转换 | 不完整新闻对象 | 完整 NewsArticle |
默认值合理 |
| 搜索过滤 | keyword + list | 过滤列表 | 大小写和空值处理 |
| 分类过滤 | category + list | 分类列表 | 未知分类返回空 |
| 收藏切换 | id + favorites | 新 favorites | 不原地污染 |
| 错误映射 | unknown error | AppError |
文案可展示 |
| 请求版本 | 连续请求 | 最新结果 | 旧请求不覆盖 |
| 分页合并 | old + page | merged | 去重 |
| 设置校验 | scale | safe scale | 范围控制 |
| 缓存解析 | JSON string | model/null | 损坏数据不崩 |
| 统计派生 | list | count/map | 不重复计算 |
十五、补充面试题
16. ArkTS 中如何设计页面状态?
页面状态要能表达完整 UI:未加载、加载中、成功、空数据、失败、提交中。简单页面可用多个 @State,复杂页面建议使用状态机结构,避免 boolean 组合出不可能状态。
17. 为什么 DTO 转换函数值得单独测试?
DTO 是外部输入,最容易出现字段缺失、类型变化和版本兼容问题。转换函数如果稳定,页面模型就稳定。测试转换函数能提前发现接口变更影响。
18. 如何防止旧异步请求覆盖新状态?
可以维护请求版本号、使用取消机制或请求去重。每次发起请求记录当前版本,结果回来时只接受最新版本,旧结果直接丢弃。
19. ArkTS 模块拆分的核心原则是什么?
按职责和依赖方向拆分。模型不依赖服务,服务不依赖 UI,组件不直接操作系统能力。依赖方向稳定,项目才能长期维护。
20. 什么时候使用状态机而不是多个布尔值?
当页面有多种互斥状态时使用状态机。例如 loading、empty、error、success 不能随意组合。状态机能让 UI 分支更清楚,也便于测试。