在这里插入图片描述
在这里插入图片描述

一、引言

1.1 项目背景

随着 HarmonyOS NEXT 6.1.1(SDK API 24)的正式发布,鸿蒙生态迎来了完全剥离 Android 代码的纯血鸿蒙时代。在这个全新的操作系统平台上,ArkTS 作为鸿蒙原生的声明式 UI 开发语言,承载着构建下一代鸿蒙应用的重任。本文将以一个完整的实战项目为线索,带你从零到一理解 HarmonyOS NEXT 应用开发的核心技术。

本项目包含两个核心页面:

  • 宇宙知识 AI 问答助手(Index.ets):一个采用 SSE 流式技术、以深邃星空为主题设计的智能聊天应用,用户可以询问宇宙相关的科学问题,AI 以流式方式实时回复。
  • Column 布局完全演示(ColumnStartDemo.ets):一个通过 6 个独立区块系统展示 Column 容器六大 justifyContent 排列策略的教学示例。

1.2 技术栈概览

技术领域 具体内容
开发语言 ArkTS(HarmonyOS NEXT 声明式 UI)
SDK 版本 HarmonyOS NEXT 6.1.1(API 24)
开发工具 DevEco Studio
网络通信 @kit.NetworkKit(原生 HTTP API)
数据流 SSE(Server-Sent Events)流式传输
UI 框架 ArkUI(@ohos.arkui)
构建工具 hvigor

1.3 本文目标

通过逐行解读完整的项目代码,帮助你掌握以下核心技能:

  • ArkTS 基础语法与声明式 UI 编程范式
  • Column 容器及其 alignItems 和 justifyContent 属性的完整用法
  • SSE 流式响应的原生实现方案
  • HarmonyOS NEXT 网络请求的最佳实践
  • @Builder 装饰器实现组件复用
  • @State 状态管理机制
  • 宇宙主题 UI 设计技巧

二、项目结构全景

在开始编码之前,让我们先了解整个项目的目录结构和各文件的职责。

2.1 目录结构

Demo1/
├── AppScope/                          # 应用级配置
│   ├── app.json5                      # 应用元信息(bundleName、版本等)
│   └── resources/base/
│       ├── element/string.json        # 字符串资源
│       └── media/layered_image.json   # 应用图标定义
├── entry/                             # 主模块
│   ├── src/main/ets/
│   │   ├── entryability/
│   │   │   └── EntryAbility.ets       # Ability 生命周期管理
│   │   ├── entrybackupability/
│   │   │   └── EntryBackupAbility.ets # 备份扩展 Ability
│   │   └── pages/
│   │       ├── Index.ets              # 🌟 宇宙知识 AI 问答助手
│   │       ├── ColumnStartDemo.ets    # 🌟 Column 布局演示
│   │       └── AIChatService.ets      # 🌟 AI 服务层(SSE + HTTP)
│   ├── src/main/resources/            # 资源文件
│   ├── build-profile.json5            # 模块构建配置
│   └── oh-package.json5               # 模块包依赖
├── build-profile.json5                # 顶层构建配置(SDK 版本声明)
├── oh-package.json5                   # 顶层包依赖
└── hvigor/                            # 构建系统配置

2.2 关键文件职责

文件 职责 核心技术点
Index.ets AI 聊天 UI 主页面 Column 布局、@Builder、@State、SSE 流式渲染
AIChatService.ets AI API 调用服务层 http 网络请求、SSE 解析、非流式回退
ColumnStartDemo.ets Column 布局教学演示 6 种 justifyContent 对比
EntryAbility.ets 应用入口生命周期 windowStage.loadContent

2.3 构建配置解析

顶层 build-profile.json5 声明了 SDK 版本和目标平台信息:

{
  app: {
    products: [
      {
        name: "default",
        signingConfig: "default",
        targetSdkVersion: "6.1.1(24)",    // 目标 SDK 版本
        compatibleSdkVersion: "6.1.1(24)", // 兼容 SDK 版本
        runtimeOS: "HarmonyOS",
      }
    ]
  },
  modules: [
    {
      name: "entry",
      srcPath: "./entry",
      targets: [
        { name: "default", applyToProducts: ["default"] }
      ]
    }
  ]
}

这里的 targetSdkVersion: "6.1.1(24)" 表明应用基于 HarmonyOS NEXT 6.1.1,对应的 API 版本为 24。这是当前最新的稳定版本,提供了完整的 ArkUI API 支持。


三、核心布局:Column 容器深度解析

在本项目中,Column 容器贯穿始终——无论是 AI 聊天的消息列表、欢迎页面的示例问题按钮,还是 ColumnStartDemo 中的六个演示区块,都离不开 Column 布局。因此,我们首先深入理解 Column 容器的核心机制。

3.1 主轴与交叉轴

Column 是 ArkTS 中最基础的垂直排列容器。理解它的布局模型需要先掌握两个关键概念:

  • 主轴(Main Axis):垂直方向(Y 轴),子组件从上到下依次排列。主轴的对齐属性是 justifyContent
  • 交叉轴(Cross Axis):水平方向(X 轴),与主轴垂直。交叉轴的对齐属性是 alignItems

如果你有 CSS Flexbox 的使用经验,可以这样类比:

ArkTS CSS Flexbox 等价
Column display: flex; flex-direction: column
alignItems(HorizontalAlign.Start) align-items: flex-start
justifyContent(FlexAlign.Center) justify-content: center

3.2 HorizontalAlign 枚举详解

在 Column 容器上调用 .alignItems() 时,参数类型为 HorizontalAlign

枚举值 效果 CSS 类比
HorizontalAlign.Start 子组件水平靠左对齐 align-items: flex-start
HorizontalAlign.Center 子组件水平居中对齐 align-items: center
HorizontalAlign.End 子组件水平靠右对齐 align-items: flex-end

注意事项alignItems 的效果只有在子组件宽度小于容器宽度时才可见。如果子组件宽度填满了容器(例如设置了 width('100%')),无论设置 Start 还是 Center,视觉上都没有区别。

3.3 FlexAlign 枚举详解

主轴方向的对齐策略由 FlexAlign 枚举控制,共有六个取值:

枚举值 效果 CSS 类比
FlexAlign.Start 从顶部紧密排列(默认) justify-content: flex-start
FlexAlign.Center 垂直居中排列 justify-content: center
FlexAlign.End 从底部向上排列 justify-content: flex-end
FlexAlign.SpaceBetween 两端对齐,子组件等距 justify-content: space-between
FlexAlign.SpaceAround 每个子组件两侧等距 justify-content: space-around
FlexAlign.SpaceEvenly 所有间距(含两端)相等 justify-content: space-evenly

3.4 Column 的基本语法

Column() {
  // 子组件列表按顺序放入
  Text('标题')
    .fontSize(20)
    .margin({ bottom: 12 })
  
  Text('内容')
    .fontSize(14)
  
  Button('确认')
    .width('100%')
    .margin({ top: 24 })
}
// 链式方法调用来设置布局属性
.alignItems(HorizontalAlign.Start) // 交叉轴水平靠左
.justifyContent(FlexAlign.Start)    // 主轴顶部紧密排列
.width('100%')
.height('100%')
.backgroundColor('#FFF5F5F5')

四、宇宙知识 AI 问答助手:完整实现

这是本项目的主页面,一个以「深邃星空」为设计主题的 AI 聊天应用。用户输入关于宇宙的问题,AI 以 SSE 流式方式逐 token 实时回复。

4.1 整体布局结构

build() {
  Column() {
    // ─── 顶部标题栏 ───
    this.buildHeader();
    
    // ─── 聊天消息区(占满剩余空间) ───
    Column() {
      this.buildChatArea()
    }
    .layoutWeight(1);  // ★ 弹性占满剩余空间
    
    // ─── 底部输入区 ───
    this.buildInputArea();
  }
  .width('100%')
  .height('100%')
  .backgroundColor(this.COLOR_BG) // 深邃星空背景
}

这里使用了经典的「上-中-下」三段式结构:

  • Column 外层:纵向排列三个区块
  • layoutWeight(1):让中间消息区自动占满剩余空间,这是 ArkTS 中实现自适应高度的关键
  • 背景色#FF0A0E27(接近黑色的深空蓝)

4.2 顶部标题栏:@Builder 构建函数

@Builder
buildHeader() {
  Column() {
    Row() {
      // 左侧闪烁星星装饰
      Circle().width(6).height(6).fill('#FFFFFFFF').opacity(0.6).margin({ right: 4 })
      
      Text('🌌 宇宙知识 AI 问答')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.COLOR_GLOW)
        .letterSpacing(2)
      
      Circle().width(6).height(6).fill('#FF60A5FA').opacity(0.2).margin({ left: 4 })
    }
    .alignItems(VerticalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .width('100%')
    
    Text('探索星辰大海,一问便知')
      .fontSize(12)
      .fontColor(this.COLOR_TEXT_DIM)
      .letterSpacing(4)
    
    Divider()
      .width('60%')
      .height(1)
      .color(this.COLOR_DIVIDER)
  }
  .width('100%')
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 12, bottom: 4 })
  .linearGradient({
    direction: GradientDirection.Bottom,
    colors: [['#FF0A0E27', 0], ['rgba(59, 130, 246, 0.03)', 1]]
  })
}

设计亮点分析

  • 标题左右星星:两个半透明的小圆点(Circle)对称分布在标题两侧,模拟星星的闪烁效果,增强宇宙主题氛围
  • 发光文字COLOR_GLOW = '#FF60A5FA' 是一种明亮的星空蓝,配合 letterSpacing(2) 让标题文字有呼吸感
  • 渐变分割线:使用 linearGradient 在标题栏底部产生从透明到微弱的蓝色渐变,模拟星空与太空的过渡
  • @Builder 装饰器:将标题栏封装为独立的构建函数,使 build() 方法清晰简洁

4.3 聊天消息区:Scroll + Column + ForEach

@Builder
buildChatArea() {
  Scroll() {
    Column() {
      // 欢迎消息(没有聊天记录时显示)
      if (this.messageList.length === 0) {
        this.buildWelcomeMessage();
      }
      
      // 对话气泡列表
      ForEach(this.messageList, (msg: ChatMessage) => {
        if (msg.role === 'user') {
          this.buildUserBubble(msg.content);
        } else {
          this.buildAIBubble(msg.content);
        }
      }, (msg: ChatMessage) => msg.role + '#' + msg.content)
      
      // 流式 AI 回复(实时显示)
      if (this.isLoading && this.streamingContent) {
        this.buildAIBubble(this.streamingContent + ' ▍');
      }
      
      // 加载中占位
      if (this.isLoading && !this.streamingContent) {
        this.buildLoadingIndicator();
      }
      
      // 错误提示
      if (this.errorMsg) {
        this.buildErrorBubble(this.errorMsg);
      }
      
      Blank().height(12)
    }
    .width('100%')
    .padding(12)
  }
  .width('100%')
  .scrollBar(BarState.Auto)
}

架构精髓

  1. Scroll + Column:Scroll 包裹 Column 实现可滚动的聊天记录列表,这是 ArkTS 中列表页面的标准模式
  2. ForEach 动态渲染:遍历 messageList 数组,根据 role 字段区分用户消息和 AI 回复,分别渲染不同风格的聊天气泡
  3. 流式内容实时渲染:当 isLoading 为 true 且 streamingContent 不为空时,额外渲染一个带光标符号(▍)的 AI 气泡,实现逐字输出的效果
  4. 状态驱动的条件渲染:通过 if 条件判断,在合适的时机显示欢迎页、加载提示或错误信息

ForEach 的第三个参数是 key 生成函数

(msg: ChatMessage) => msg.role + '#' + msg.content

这个 key 的作用是帮助 ArkUI 框架追踪列表中每个元素的身份。当列表数据更新时,框架通过 key 来判断哪些元素是新增的、哪些是已有的、哪些被移除了,从而实现高效的差异化渲染。如果 key 缺失或重复,可能导致列表渲染异常或性能下降。

重要:这里使用 role + '#' + content 作为 key 有一个潜在问题——如果两条消息内容完全相同,key 会重复。在实际生产项目中,建议为每条消息分配一个唯一的 id(如时间戳或 UUID),以确保 key 的唯一性。

4.4 欢迎页面:示例问题轮播

private sampleQuestions: string[] = [
  '黑洞是如何形成的?它的内部是什么?',
  '宇宙大爆炸之前存在什么?',
  '人类什么时候能移民火星?',
  '暗物质和暗能量是什么?为什么我们看不见它们?',
  '光速为什么是宇宙速度极限?',
  '宇宙中到底有没有外星生命?',
  '太阳系中哪颗行星最奇特?',
  '恒星的生命周期是怎样的?',
  '虫洞真的可能存在吗?',
  '地球在宇宙中的位置有多特殊?',
];

欢迎页面的核心是「示例问题按钮列表」:

Column() {
  ForEach(this.sampleQuestions, (q: string, idx?: number) => {
    Button(q)
      .fontSize(13)
      .fontColor(this.COLOR_TEXT)
      .backgroundColor(this.COLOR_CARD)
      .border({ width: 1, color: this.COLOR_BORDER })
      .borderRadius(20)
      .height(42)
      .width('92%')
      .margin({ bottom: 8 })
      .onClick(() => {
        this.inputText = q;  // 点击将问题填入输入框
      })
  }, (_q: string, i?: number) => (i ?? 0).toString())
}
.alignItems(HorizontalAlign.Start)  // ★ 按钮左对齐
.width('100%')

这里有一个交互设计细节值得注意:onClick 事件只是将问题文本填入输入框(this.inputText = q),而不是直接发送。这样让用户有机会在发送前修改问题,比直接发送更友好。同时,底部还有一个「换一批问题」按钮,使用「斐波那契式洗牌法」轮换示例问题:

rotateSamples(): void {
  const arr: string[] = this.sampleQuestions;
  for (let i: number = 0; i < arr.length; i++) {
    const j: number = (i * 13 + 7) % arr.length;
    const tmp: string = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }
  this.sampleQuestions = [...arr];  // 触发 UI 更新
}

注意最后一行 this.sampleQuestions = [...arr],这里使用了扩展运算符创建了一个新数组。如果不这样做(直接 this.sampleQuestions = arr),由于数组引用没有变化,ArkUI 框架无法检测到数据变化,UI 不会刷新。这是 ArkTS 状态管理中需要特别注意的不可变性原则——修改数组或对象时,必须创建新的引用。

4.5 聊天气泡组件:左右对齐布局

本项目中最精彩的部分是用户消息和 AI 消息气泡的左右对齐布局。它巧妙运用了 Row + Blank 的组合来实现:

用户气泡(右对齐)
@Builder
buildUserBubble(content: string) {
  Column() {
    Row() {
      Blank()  // ★ 空白弹性占位,将气泡推到右侧
      
      Text(content)
        .fontSize(15)
        .fontColor(Color.White)
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(this.COLOR_PRIMARY)  // 宇宙蓝
        .borderRadius({
          topLeft: 18,
          topRight: 4,      // ★ 右上角小圆角,模拟聊天气泡的"尾巴"
          bottomLeft: 18,
          bottomRight: 18
        })
        .maxLines(20)
        .lineHeight(22)
    }
    .width('100%')
    .margin({ top: 6 })
  }
}
AI 气泡(左对齐)
@Builder
buildAIBubble(content: string) {
  Column() {
    Row() {
      Text(content)
        .fontSize(15)
        .fontColor(this.COLOR_TEXT)
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(this.COLOR_CARD)  // 深色卡片
        .border({ width: 1, color: this.COLOR_BORDER })
        .borderRadius({
          topLeft: 4,       // ★ 左上角小圆角
          topRight: 18,
          bottomLeft: 18,
          bottomRight: 18
        })
        .lineHeight(22)
      
      Blank()  // ★ 空白弹性占位,气泡在左侧
    }
    .width('100%')
    .margin({ top: 6 })
  }
}

核心原理

  • 用户气泡的 Row 中,Blank() 在左侧 → 将右方的 Text 推到右侧 → 实现右对齐
  • AI 气泡的 Row 中,Blank() 在右侧 → 左侧的 Text 自然靠左 → 实现左对齐
  • Blank() 是 ArkTS 中的弹性空白组件,它会自动占据剩余空间

气泡样式设计

  • 用户气泡(宇宙蓝 #FF3B82F6):右上角小圆角(topRight: 4),模拟聊天气泡的「尾巴」指向右侧
  • AI 气泡(深色卡片 #FF141B3D):左上角小圆角(topLeft: 4),模拟聊天气泡的「尾巴」指向左侧
  • 边框 + 阴影:AI 气泡有微弱的边框(#FF1E2756),增加层次感

4.6 底部输入区:交互完整闭环

@Builder
buildInputArea() {
  Column() {
    Row() {
      TextInput({
        placeholder: '输入关于宇宙的问题...',
        text: this.inputText
      })
        .layoutWeight(1)  // ★ 自适应宽度
        .height(44)
        .fontSize(15)
        .fontColor(this.COLOR_TEXT)
        .placeholderColor(this.COLOR_TEXT_DIM)
        .backgroundColor(this.COLOR_CARD)
        .borderRadius(22)  // ★ 大圆角输入框
        .padding({ left: 16 })
        .enabled(!this.isLoading)  // 加载中禁用
        .onChange((val: string) => { this.inputText = val; })
        .onSubmit(() => { this.handleSend(); })
      
      Blank().width(8)
      
      Button() {
        Text('🚀 发射')
          .fontSize(13)
          .fontColor(Color.White)
      }
      .width(72)
      .height(44)
      .backgroundColor(
        this.isLoading 
          ? 'rgba(59, 130, 246, 0.3)'   // 加载中:半透明
          : this.COLOR_PRIMARY           // 正常:宇宙蓝
      )
      .borderRadius(22)
      .enabled(!this.isLoading && this.inputText.trim().length > 0)
      .onClick(() => { this.handleSend(); })
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 8, bottom: 8 })
    
    // 取消按钮(加载时显示)
    if (this.isLoading) {
      Button('⏹ 停止回复')
        .onClick(() => { this.handleCancel(); })
    }
  }
  .width('100%')
}

交互细节

  1. 输入框禁用:AI 回复期间,enabled(!this.isLoading) 让输入框不可用,防止用户多次提交
  2. 按钮视觉反馈:加载时发送按钮变为半透明,同时出现「停止回复」按钮
  3. 发送条件enabled(!this.isLoading && this.inputText.trim().length > 0) 确保加载中或空输入时按钮不可点击
  4. 键盘提交onSubmit 监听键盘回车事件,与点击发送按钮执行相同的 handleSend 逻辑

4.7 状态管理与消息发送逻辑

// 状态变量
@State messageList: ChatMessage[] = [];    // 聊天历史
@State inputText: string = '';             // 输入框内容
@State isLoading: boolean = false;         // AI 回复中
@State streamingContent: string = '';      // 流式累积内容
@State errorMsg: string = '';              // 错误信息

handleSend(): void {
  const msg: string = this.inputText.trim();
  if (!msg) return;
  
  this.inputText = '';  // 清空输入
  
  const userMsg: ChatMessage = { role: 'user', content: msg };
  this.messageList = [...this.messageList, userMsg];  // ★ 展开新数组
  
  this.streamingContent = '';
  this.errorMsg = '';
  this.isLoading = true;
  
  queryAI({
    onData: (text: string) => {
      this.streamingContent += text;  // ★ 流式累积
    },
    onDone: () => {
      if (this.streamingContent) {
        const aiMsg: ChatMessage = { 
          role: 'assistant', 
          content: this.streamingContent 
        };
        this.messageList = [...this.messageList, aiMsg];
      }
      this.streamingContent = '';
      this.isLoading = false;
    },
    onError: (errMsg: string) => {
      this.errorMsg = errMsg;
      this.isLoading = false;
    },
  }, this.messageList);
}

handleCancel(): void {
  cancelAI();
  this.isLoading = false;
  if (this.streamingContent) {
    const aiMsg: ChatMessage = { 
      role: 'assistant', 
      content: this.streamingContent 
    };
    this.messageList = [...this.messageList, aiMsg];
  }
  this.streamingContent = '';
}

@State 状态管理原则

ArkTS 的 @State 装饰器标记的变量,当其值发生变化时,会自动触发与之绑定的组件重新渲染。但需要注意:

  1. 数组更新必须创建新引用this.messageList = [...this.messageList, newItem] 而不是 this.messageList.push(newItem)
  2. 字符串赋值直接触发this.streamingContent += text 直接修改字符串值即可触发更新
  3. 布尔值切换this.isLoading = true/false 直接赋值

五、SSE 流式 AI 服务:AIChatService 完整解析

这是整个项目中最具技术含量的部分——在 HarmonyOS NEXT 中使用原生 HTTP API 实现 SSE(Server-Sent Events)流式响应。

5.1 接口定义

export interface ChatMessage {
  role: string;      // 'user' | 'assistant' | 'system'
  content: string;   // 消息文本内容
}

export interface ChatCompletionRequest {
  model: string;                    // AI 模型名称
  messages: ChatMessage[];          // 消息历史
  stream: boolean;                  // 是否开启流式
  max_tokens: number;               // 最大生成 token 数
  temperature: number;              // 生成温度(0-2)
  top_p: number;                    // 核采样参数
  frequency_penalty: number;        // 频率惩罚
  thinking_budget: number;          // 思考预算
}

export interface AICallbacks {
  onData: (text: string) => void;    // 每次收到 token
  onDone: () => void;                // 流式完成
  onError: (errMsg: string) => void; // 发生错误
}

5.2 核心请求逻辑

export function queryAI(
  callbacks: AICallbacks,
  messages: ChatMessage[],
): void {
  // 取消上一次未完成的请求
  if (httpRequestTask) {
    try { httpRequestTask.destroy(); } catch (_) { }
    httpRequestTask = null;
  }

  const httpRequest = http.createHttp();
  httpRequestTask = httpRequest;

  // 构建请求体(合并系统提示词 + 聊天历史)
  const fullMessages: ChatMessage[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    ...messages,
  ];
  const requestBody: ChatCompletionRequest = {
    model: 'deepseek-ai/DeepSeek-V3',
    messages: fullMessages,
    stream: true,
    max_tokens: 2048,
    temperature: 0.6,
    top_p: 0.95,
    frequency_penalty: 0,
    thinking_budget: 2048,
  };

  // 发起 POST 请求
  httpRequest.request(
    API_URL,
    {
      method: http.RequestMethod.POST,
      header: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
      },
      extraData: JSON.stringify(requestBody),
      connectTimeout: 30000,
      readTimeout: 120000,
    },
    // 回调处理...
  );
}

关键配置说明

  • Accept: text/event-stream:告知服务器我们期望 SSE 流式响应,这是 SSE 协议的标准头
  • readTimeout: 120000:读取超时设置为 120 秒,因为流式响应可能持续较长时间
  • connectTimeout: 30000:连接超时 30 秒
  • 模型选择:使用 deepseek-ai/DeepSeek-V3 模型,这是一个高性能的开源大语言模型

5.3 SSE 流式事件监听

HarmonyOS NEXT 的 http 模块提供了便捷的事件监听机制:

let isDone: boolean = false;
let receivedAnyData: boolean = false;
let buffer = '';

// 监听数据到达事件(流式场景)
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
  const text = arrayBufferToString(data);
  buffer += text;
  receivedAnyData = true;

  const lines = buffer.split('\n');
  buffer = lines.pop() ?? '';  // 最后一行可能不完整

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed.startsWith('data:')) continue;

    if (trimmed === 'data:[DONE]') {
      if (!isDone) { isDone = true; callbacks.onDone(); }
      continue;
    }

    const content = parseSSEDataLine(trimmed);
    if (content) {
      callbacks.onData(content);
    }
  }
});

// 数据接收完毕
httpRequest.on('dataEnd', () => {
  if (!isDone) {
    isDone = true;
    callbacks.onDone();
  }
  httpRequestTask = null;
});

这里的 dataReceive 事件是 SSE 流式实现的核心。当服务端发送数据时,该事件会被触发,传入 ArrayBuffer 数据。我们需要将其按行切分,提取 data: 开头的行,然后解析其中的 JSON 数据提取内容。

缓冲区机制

const lines = buffer.split('\n');
buffer = lines.pop() ?? '';  // 保留最后一个不完整的行

这个设计非常巧妙:由于 SSE 数据是以 \n 分隔的,但网络传输可能在一个事件中只收到半行数据。每次处理完后,将最后一个可能不完整的行放回缓冲区,等待下一次 dataReceive 事件到来时再追加处理。

5.4 SSE 数据解析

function parseSSEDataLine(line: string): string | null {
  const jsonStr = line.slice(5).trim();  // 去掉 "data:" 前缀
  if (!jsonStr) return null;
  
  try {
    const parsed = JSON.parse(jsonStr) as Record<string, Object>;
    const choices = parsed.choices as Object[];
    if (choices && choices.length > 0) {
      const choice = choices[0] as Record<string, Object>;
      // 兼容 delta 和 message 两种格式
      const delta = choice.delta as Record<string, Object>;
      if (delta) {
        return delta.content as string;
      }
      const message = choice.message as Record<string, Object>;
      if (message) {
        return message.content as string;
      }
    }
  } catch (_) { /* JSON 解析失败,跳过 */ }
  
  return null;
}

格式兼容:这段代码同时兼容了两种响应格式:

  • 流式格式:每次返回一个包含 delta.content 的增量 token
  • 非流式格式:一次性返回包含 message.content 的完整响应

5.5 非流式回退机制

由于不同版本的 HarmonyOS 对 SSE 的支持程度不同,有些情况下 dataReceive 事件可能不会触发。为此,代码实现了一个重要的回退机制:

// 在 request 回调中
if (!receivedAnyData && resp.result) {
  const bodyStr = typeof resp.result === 'string'
    ? resp.result
    : arrayBufferToString(resp.result as ArrayBuffer);

  // 先尝试 SSE 格式解析
  const sseContent = parseFullSSEBody(bodyStr);
  if (sseContent) {
    callbacks.onData(sseContent);
  } else {
    // 再尝试非流式 JSON 格式
    const jsonContent = parseNonStreamingBody(bodyStr);
    if (jsonContent) {
      callbacks.onData(jsonContent);
    } else {
      const preview = bodyStr.length > 200 
        ? bodyStr.substring(0, 200) + '...' 
        : bodyStr;
      callbacks.onError(`无法解析响应: ${preview}`);
    }
  }
  
  if (!isDone) { isDone = true; callbacks.onDone(); }
}

三层解析策略

  1. 完整 SSE 体解析:尝试将整个响应体按 data: 行逐行解析,适用于 dataReceive 未触发但服务器确实返回了 SSE 格式的场景
  2. 非流式 JSON 解析:如果 SSE 解析失败,尝试直接将整个响应体当作标准 JSON 解析
  3. 错误提示:如果以上两种方式都失败,截取响应体前 200 字符作为错误信息

5.6 请求取消

export function cancelAI(): void {
  if (httpRequestTask) {
    try {
      httpRequestTask.destroy();
    } catch (_) { }
    httpRequestTask = null;
  }
}

通过保存 httpRequest 的引用,可以在用户点击「停止回复」时调用 destroy() 方法中止正在进行的网络请求。这个机制在用户体验上至关重要——当 AI 正在生成长篇回复时,用户有权利随时中止。

5.7 系统提示词设计

const SYSTEM_PROMPT: string =
  '你是一位专业的天体物理学家和宇宙知识科普专家...\n\n' +
  '以下是你要遵循的原则:\n' +
  '1. 科学准确:所有回答基于当前主流科学理论...\n' +
  '2. 通俗易懂:用类比和形象化的语言解释复杂概念...\n' +
  '3. 系统全面:回答时先给出核心结论,再展开详细解释...\n' +
  '4. 激发兴趣:在回答中适当融入有趣的宇宙事实或冷知识...\n' +
  '5. 知识范围包括但不限于:恒星与星系、行星科学、黑洞与相对论...';

系统提示词(System Prompt)是 AI 对话中指导模型行为的核心指令。好的系统提示词需要:

  • 明确角色定位:确定 AI 以什么身份回答问题
  • 规定回答风格:通俗易懂、科学准确、激发兴趣
  • 限定知识范围:明确什么话题可以回答,什么话题不要涉及
  • 提供回答框架:先结论、再展开、后补充

六、Column 布局演示:ColumnStartDemo 完全解读

这个页面是一个专门为学习 ArkTS Column 布局而设计的教学示例,通过 6 个独立的演示区块,系统展示 justifyContent 的六种取值及其效果。

6.1 页面整体架构

build() {
  Scroll() {
    Column() {
      // 页面标题区
      Column() {
        Text('Column + alignItems(HorizontalAlign.Start)')
        Text('鸿蒙原生 ArkTS 垂直排列布局演示')
        Text('Column 容器 · 子组件顶部对齐 · 左对齐排列')
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      
      // 布局说明卡片
      // ...
      
      // 6 个演示区块
      this.buildDemoBlock('❶ 默认排列:FlexAlign.Start', ...)
      this.buildDemoBlock('❷ 垂直居中:FlexAlign.Center', ...)
      this.buildDemoBlock('❸ 底部排列:FlexAlign.End', ...)
      this.buildDemoBlock('❹ 两端对齐:FlexAlign.SpaceBetween', ...)
      this.buildDemoBlock('❺ 周围等距:FlexAlign.SpaceAround', ...)
      this.buildDemoBlock('❻ 均匀分布:FlexAlign.SpaceEvenly', ...)
      
      Blank().height(30)
    }
    .alignItems(HorizontalAlign.Center)  // 卡片在页面中居中对齐
    .width('100%')
  }
  .backgroundColor('#FFF2F3F5')
  .width('100%')
  .height('100%')
}

注意这里的.alignItems(HorizontalAlign.Center)——主 Column 使用了 Center 对齐,让所有卡片在水平方向上居中显示,使页面整体观感更整洁。这展示了 Column 不同层级的对齐方式可以灵活组合。

6.2 核心演示区块构建器

@Builder
private buildDemoBlock(
  title: string,
  subtitle: string,
  justifyContentValue: FlexAlign,
  items: string[],
  bgColor?: ResourceColor
) {
  Column() {
    // 区块标题
    Text(title).fontSize(17).fontWeight(FontWeight.Medium)
    Text(subtitle).fontSize(12).fontColor('#FFAAAAAA')
    Divider().width('100%').height(1).color('#FFF0F0F0')
    
    // ★★★ 关键:子 Column 容器 ★★★
    Column() {
      ForEach(items, (item: string, idx?: number) => {
        this.renderRowItem(item, idx ?? 0)
      })
    }
    .alignItems(HorizontalAlign.Start)     // ★ 水平靠左对齐
    .justifyContent(justifyContentValue)    // ★ 垂直排列策略
    .width('100%')
    .height(this.getBlockHeight(justifyContentValue))
    .backgroundColor(bgColor ?? '#FFF9F9F9')
    .borderRadius(8)
    .padding(12)
  }
  .width('100%')
  .backgroundColor(Color.White)
  .borderRadius(14)
  .shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.04)', offsetX: 0, offsetY: 2 })
  .padding(16)
  .margin({ bottom: 14 })
}

6.3 高度控制策略

private getBlockHeight(align: FlexAlign): string {
  switch (align) {
    case FlexAlign.SpaceBetween:
    case FlexAlign.SpaceAround:
    case FlexAlign.SpaceEvenly:
      return '200vp';   // 分布模式需要固定高度
    default:
      return 'auto';    // Start/Center/End 自适应
  }
}

这是一个至关重要的细节。SpaceBetweenSpaceAroundSpaceEvenly 这三种分布模式的工作原理是将剩余空间均匀分配给子组件之间的间距。如果容器高度是 auto(自适应),子组件会紧密排列,根本没有剩余空间,因此这三种对齐方式就失去了效果。必须设置一个明确的高度值(如 '200vp'),让子组件的高度总和小于容器高度,才能产生可见的间距。

6.4 枚举到字符串的映射

这里有一个 ArkTS 语法的重要约束需要特别注意:

// ❌ 编译错误:ArkTS 不支持计算属性名
const map: Record<number, string> = {
  [FlexAlign.Start]: '顶部紧密排列',
};

// ✅ 正确做法:使用 switch 语句
private justifyLabel(align: FlexAlign): string {
  switch (align) {
    case FlexAlign.Start:
      return 'FlexAlign.Start — 顶部紧密排列';
    case FlexAlign.Center:
      return 'FlexAlign.Center — 垂直居中';
    case FlexAlign.End:
      return 'FlexAlign.End — 底部排列';
    case FlexAlign.SpaceBetween:
      return 'FlexAlign.SpaceBetween — 两端对齐';
    case FlexAlign.SpaceAround:
      return 'FlexAlign.SpaceAround — 周围等距';
    case FlexAlign.SpaceEvenly:
      return 'FlexAlign.SpaceEvenly — 均匀分布';
    default:
      return '未知';
  }
}

ArkTS 是 TypeScript 的一个严格子集,它通过限制某些 JavaScript 动态特性来获得更好的编译期类型安全和运行时性能。[expression] 计算属性名语法在 ArkTS 中不被支持,必须使用 switch 语句或显式定义的接口来实现枚举到字符串的映射。

6.5 单行子组件构建

@Builder
private renderRowItem(text: string, index: number) {
  Row() {
    Circle()
      .width(8)
      .height(8)
      .fill(this.getIconColor(index))
      .margin({ right: 10 })
    Text(text)
      .fontSize(15)
      .fontColor('#FF444444')
  }
  .alignItems(VerticalAlign.Center)
  .height(40)
}

这里使用了 Row 容器的 .alignItems(VerticalAlign.Center),它让圆形图标和文本在垂直方向(Row 的交叉轴)上居中对齐,这与 Column 的 alignItems(HorizontalAlign.Start) 形成对称关系。

private getIconColor(index: number): ResourceColor {
  const colors: ResourceColor[] = [
    '#FF007AFF', '#FF34C759', '#FFFF9500',
    '#FFFF3B30', '#FF5856D6', '#FF00C7BE'
  ];
  return colors[index % colors.length];
}

通过索引取模,为每个文本行分配不同的圆点颜色,增加了视觉层次感,方便区分不同条目。


七、EntryAbility:应用入口生命周期

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.getApplicationContext()
        .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    } catch (err) {
      hilog.error(0x0000, 'testTag', 
        'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag',
          'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  }
}

关键要点

  1. extends UIAbility:所有 Ability 必须继承 UIAbility 基类
  2. onWindowStageCreate:窗口创建时加载页面,通过 loadContent 指定首页路由
  3. setColorMode(COLOR_MODE_NOT_SET):跟随系统颜色模式(亮色/暗色)
  4. hilog:使用 HarmonyOS 的日志系统,%{public}s 表示可公开的字符串参数

onWindowStageCreate 中将 loadContent 的参数改为 'pages/ColumnStartDemo' 即可切换到布局演示页面。


八、ArkTS 开发常见问题与避坑指南

8.1 状态管理:不可变性原则

// ❌ 错误:数组引用未变化,UI 不会更新
this.messageList.push(newMsg);

// ✅ 正确:创建新数组,触发 UI 刷新
this.messageList = [...this.messageList, newMsg];

ArkTS 的 @State 通过引用比较来检测数据变化。当修改数组或对象时,必须创建一个新的引用,否则框架无法感知数据变更。

8.2 枚举类型无需导入

// ❌ 错误:编译会报 "Module '@kit.ArkUI' has no exported member 'FlexAlign'"
import { FlexAlign } from '@kit.ArkUI';

// ✅ 正确:FlexAlign 和 HorizontalAlign 是全局内置枚举,直接使用
Column().alignItems(HorizontalAlign.Start)

8.3 禁止计算属性名

// ❌ 编译错误
const labelMap = {
  [FlexAlign.Start]: 'Start',
};

// ✅ 正确
switch (align) {
  case FlexAlign.Start: return 'Start';
  // ...
}

8.4 对象字面量必须声明类型

// ❌ 错误:匿名对象没有类型声明
const obj = { name: 'test' };

// ✅ 正确:显式声明类型
interface MyObj { name: string; }
const obj: MyObj = { name: 'test' };

8.5 Space 系列对齐必须固定高度

// ❌ SpaceBetween 不会生效
Column() { /* ... */ }
  .justifyContent(FlexAlign.SpaceBetween)
  .height('auto')  // ★ 没有固定高度

// ✅ SpaceBetween 生效
Column() { /* ... */ }
  .justifyContent(FlexAlign.SpaceBetween)
  .height('200vp')  // ★ 固定高度

8.6 ForEach 必须提供唯一的 key

// ❌ 可能有重复 key 的风险
ForEach(this.list, (item) => { /* ... */ }, item => item.name)

// ✅ 使用唯一标识
ForEach(this.list, (item) => { /* ... */ }, item => item.id)

九、项目扩展与优化方向

9.1 功能扩展建议

  • 语音输入:集成 @kit.VoiceKit 实现语音转文字提问
  • 消息持久化:使用 @kit.ArkDataPreferencesRelationalStore 保存聊天记录
  • 主题切换:支持多种配色方案(如「极光绿」「星云紫」「银河金」)
  • 图片识别:集成相机能力,拍摄星空照片后让 AI 解说天体

9.2 性能优化

  • 虚拟列表:当聊天记录超过 50 条时,使用 LazyForEach 替代 ForEach 实现虚拟滚动
  • 图片缓存:如果 AI 回复包含图片,使用 Image 组件的缓存策略
  • 请求节流:防止用户快速连续点击发送按钮

9.3 包管理

本项目目前依赖极简,后续可以引入更多鸿蒙 Kit:

// oh-package.json5
{
  "dependencies": {
    "@kit.ArkData": "6.1.1-24",    // 数据持久化
    "@kit.VoiceKit": "6.1.1-24",   // 语音能力
    "@kit.AvSessionKit": "6.1.1-24" // 音频播报 AI 回复
  }
}

十、总结

本文通过一个完整的 HarmonyOS NEXT 实战项目,系统地讲解了以下核心知识点:

10.1 已掌握的能力

  • ArkTS 声明式 UI 开发:使用 @Component@Entry@State@Builder 等装饰器构建组件
  • Column 布局体系:理解主轴/交叉轴、alignItemsjustifyContent 的完整使用
  • SSE 流式通信:在 HarmonyOS 原生网络模块中实现流式数据接收与解析
  • 聊天 UI 设计:从气泡布局到输入区交互的完整实现
  • 状态管理:数组不可变性、条件渲染、流式内容更新

10.2 核心代码量统计

文件 代码行数 核心功能
Index.ets 585 行 AI 聊天 UI
AIChatService.ets 329 行 SSE 网络服务
ColumnStartDemo.ets 421 行 布局演示

10.3 寄语

HarmonyOS NEXT 代表了国产操作系统的未来方向,ArkTS 作为其原生开发语言,虽然在语法上有一些限制(如禁止计算属性名、对象字面量需显式类型等),但这些限制换来了更稳定的运行时性能和更安全的类型检查。随着鸿蒙生态的不断成熟,相信 ArkTS 将会成为越来越多开发者的首选。

宇宙浩瀚,代码无界。希望本文能帮助你在鸿蒙原生开发的道路上迈出坚实的一步。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐