02 ArkTS 语言与工程规范

一、详细知识点

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 表示不存在,会让调用方猜语义。更好的方式是返回 undefinedResult<T> 或明确错误对象。

8. 命名要表达业务

handleClickchangeDatadoSomething 不能表达意图。toggleFavoritequeryArticlesupdateFontScale 能直接说明业务行为。

9. 模块越底层越不能依赖 UI

modelservices 应该不知道页面怎么展示。底层依赖 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: booleanerrorMessage: string 凑合。复杂页面用状态机更清晰。

十三、编码规范补强

规则 原因 检查方式
页面不直接写接口 DTO 防止接口变化冲击 UI 看 Page 是否 import DTO
Service 不引用 UI 组件 保持业务层可测试 看 services 是否 import pages/components
Store 不无限保存数据 防止内存和隐私问题 看是否有清理策略
日志不包含敏感字段 防止隐私泄露 搜索 token、phone、location
错误不静默 用户可恢复 catch 是否返回明确结果
异步有并发策略 防止状态乱序 是否有请求版本/去重/取消
数组更新生成新对象 响应式更稳定 是否大量原地修改
命名带业务含义 提升可读性 禁止 doItdata1

十四、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 分支更清楚,也便于测试。

相关推荐
楚国的小隐士1 小时前
在AI时代,如何从0接手一个项目?
java·ai·大模型·编程·ai编程·自闭症·自闭症谱系障碍·神经多样性
YJlio1 小时前
7.4.5 Windows 11 企业网络连接与网络重置实战:远程访问、本地策略与故障恢复
前端·chrome·windows·python·edge·机器人·django
yaki_ya1 小时前
yaki-C语言:从概念基础到内存解析---数组(array)完全指南
java·c语言·算法
刃神太酷啦1 小时前
扒透 STL 底层!map/set 如何封装红黑树?迭代器逻辑 + 键值限制全手撕----《Hello C++ Wrold!》(23)--(C/C++)
java·c语言·javascript·数据结构·c++·算法·leetcode
亚历克斯神1 小时前
Java 25 模式匹配增强:让代码更简洁优雅
java·spring·微服务
Slow菜鸟1 小时前
Codex CLI 教程(五)| Skills 安装指南:面向 Java 全栈工程师打造个人 ECC(V1版)
大数据·前端·人工智能
星辰徐哥1 小时前
Rust异步测试与调试的实践指南
android·java·rust
星河耀银海1 小时前
C++ 运算符重载:自定义类型的运算扩展
android·java·c++
Lee川1 小时前
打字机是怎么炼成的:Chat 流式输出深度解析
前端·后端·面试