请添加图片描述
请添加图片描述

第一章 项目架构总览

1.1 整体目录结构

一个规范的鸿蒙 Stage 模型项目采用模块化分层架构,本项目的核心代码集中在 entry/src/main/ets/ 目录:

entry/src/main/ets/
├── entryability/
│   └── EntryAbility.ets          # 应用入口,UIAbility 生命周期
├── pages/
│   ├── BindMenuDemo.ets          # bindMenu 弹出菜单演示(714 行)
│   ├── Index.ets                 # AI 推理大师游戏(584 行)
│   ├── RunnerPage.ets            # 单键跑酷游戏(611 行)
│   └── AIChatService.ets        # AI 流式对话服务
├── components/
│   └── CommonComponents.ets      # 通用组件库(Card / ProgressRing / AppHeader)
├── model/
│   ├── AppModel.ets              # 数据模型接口与枚举
│   ├── SampleData.ets            # 示例数据
│   └── SpacedRepetition.ets      # 间隔重复算法
└── module.json5                  # 模块配置

1.2 核心设计原则

本项目遵循以下设计原则:

  • 声明式 UI:所有界面通过 @Component + build() 方法声明式描述,状态变化自动驱动 UI 刷新
  • 组件化复用:通用 UI 元素封装为 @Component 导出组件,通过 @Prop / @BuilderParam 实现参数化
  • 关注点分离:数据层(model/)、业务逻辑层(pages/)、UI 复用层(components/)各自独立
  • 面向场景:每个页面独立演示一种核心布局技术,便于学习和复用

第二章 bindMenu 弹出菜单布局

2.1 核心概念

bindMenu 是鸿蒙 ArkTS 中最常用的菜单绑定 API。它允许开发者通过 @Builder 装饰器构建一个 Menu 组件树,然后使用链式方法 .bindMenu() 将这个菜单绑定到任意基础组件上。当用户点击或长按目标组件时,菜单会自动弹出。

核心 API 链如下:

@Builder 构建菜单 → Menu { MenuItem, MenuItemGroup, ... }
                  ↓
组件.bindMenu(@Builder引用)  →  用户点击/长按  →  菜单弹出

2.2 基础用法:按钮绑定下拉菜单

以下代码展示了一个最简单的 bindMenu 用法——点击按钮弹出排序选项菜单:

import { promptAction } from '@kit.ArkUI';

// 定义菜单数据接口
interface SortOption {
  label: string;
  icon: string;
  field: string;
}

@Entry
@Component
struct BindMenuDemo {
  @State currentSort: string = '默认排序';
  @State currentSortIcon: string = '📋';

  private sortOptions: SortOption[] = [
    { label: '默认排序', icon: '📋', field: 'default' },
    { label: '名称排序', icon: '🔤', field: 'name' },
    { label: '大小排序', icon: '📦', field: 'size' },
    { label: '日期排序', icon: '📅', field: 'date' },
    { label: '类型排序', icon: '🏷️', field: 'type' },
  ];

  // ★★★ 核心 @Builder:构建菜单内容 ★★★
  @Builder
  buildSortMenu() {
    Menu() {
      // 标题项(禁用,仅展示)
      MenuItem({ content: '选择排序方式', startIcon: '🔀' })
        .enabled(false)

      // 分隔线
      MenuItem({ content: '' })
        .enabled(false).height(1).backgroundColor('#EEEEEE')

      // 使用 ForEach 循环生成菜单项
      ForEach(this.sortOptions, (option: SortOption) => {
        MenuItem({
          content: option.label,
          startIcon: option.icon,
        })
          // 当前选中项高亮
          .fontColor(this.currentSortField === option.field ? '#C9A84C' : '#333333')
          .onClick(() => {
            this.currentSort = option.label;
            this.currentSortIcon = option.icon;
            promptAction.showToast({ message: `已切换「${option.label}`, duration: 1500 });
          })
      })
    }
  }

  build() {
    Column() {
      // ★★★ 核心:bindMenu 绑定 @Builder ★★★
      Button() {
        Row() {
          Text(this.currentSortIcon).fontSize(16)
          Text(this.currentSort).fontSize(14).fontColor(Color.White)
          Text('▾').fontSize(12).fontColor('rgba(255,255,255,0.6)')
        }
      }
      .bindMenu(() => this.buildSortMenu())  // ← 菜单绑定在此
    }
  }
}

2.3 核心组件详解

Menu 组件: 菜单容器,包裹所有菜单项。Menu 本身不负责显示位置——位置由 bindMenu 绑定的组件自动计算决定,通常向组件下方或组件附近弹出。一个 Menu 中可以混合放置 MenuItemMenuItemGroup

MenuItem 组件: 单个菜单项。通过 { content, startIcon } 对象构造器设置文字和图标。关键属性包括:

MenuItem 属性 类型 说明
content string 菜单项文字
startIcon string / Resource 起始图标(支持 emoji 字符串或 $r() 系统资源)
fontColor Color / string 文字颜色(可用于标注危险操作,如红色删除)
enabled boolean 是否可点击(false 时灰显禁用)
.onClick() 回调 点击菜单项时触发

MenuItemGroup 组件: 菜单项分组容器,支持设置分组标题 header

Menu() {
  MenuItemGroup({ header: '文件操作' }) {
    MenuItem({ content: '新建', startIcon: '📄' }).onClick(() => { /* ... */ })
    MenuItem({ content: '打开', startIcon: '📂' }).onClick(() => { /* ... */ })
    MenuItem({ content: '保存', startIcon: '💾' }).onClick(() => { /* ... */ })
  }
  MenuItemGroup({ header: '视图设置' }) {
    MenuItem({ content: '列表视图', startIcon: '📋' }).onClick(() => { /* ... */ })
    MenuItem({ content: '网格视图', startIcon: '🔲' }).onClick(() => { /* ... */ })
  }
}

MenuItemGroup 的分隔线是自动添加的,无需手动处理。

2.4 bindMenu 的多种绑定位置

bindMenu 不仅限于绑定到 Button,它可以绑定到任何基础组件:

// 绑定到 Text 组件
Text('长按我弹出菜单')
  .bindMenu(() => this.buildMenu())

// 绑定到 Row 容器
Row() {
  Text('操作区域').fontSize(16)
  Image($r('app.media.icon'))
}
.bindMenu(() => this.buildMenu())

// 绑定到整个 Column 区域(全局菜单)
Column() {
  Text('点击或长按空白区域')
  Text('此处绑定了全局菜单')
}
.width('100%').height(100)
.backgroundColor('#0F1A36')
.bindMenu(() => this.buildGlobalMenu())

这种灵活性使得 bindMenu 可以适应各种交互模式——从按钮点击下拉、行长按操作到全局空白区右键菜单。

2.5 完整的六种菜单演示

项目中 BindMenuDemo.ets 演示了六种不同的交互式菜单:

演示 菜单类型 技术亮点 @Builder 名称
排序切换 单选列表 ForEach 循环 + 选中高亮 buildSortMenu
分类筛选 双组列表 MenuItemGroup 双分组 + 数量徽标 buildFilterMenu
主题色设置 颜色选择 自定义颜色值,实时切换主题 buildThemeMenu
数量选择 数字选择 选中态图标动态切换 buildNumberMenu
批量操作 操作列表 危险操作红色标注 buildActionMenu
更多功能 功能列表 一键重置全部状态 buildMoreMenu

第三章 MenuItem 与 MenuItemGroup 深入解析

3.1 菜单项的构造方式

MenuItem 有两种构造方式:

方式一:对象构造器(推荐)

MenuItem({
  content: '菜单项文字',
  startIcon: '📄',                    // emoji 字符串
  // 或 startIcon: $r('sys.media.ohos_ic_public_edit')  // 系统资源
})

对象构造器更简洁,适合大多数场景。startIcon 支持 emoji 字符串和系统资源引用两种形式。

方式二:Builder 尾随闭包(自定义内容)

MenuItem() {
  Row() {
    Image($r('app.media.icon')).width(20).height(20)
    Text('自定义菜单项').fontSize(14)
    Badge({ count: 3, position: BadgePosition.RightTop }) {
      Text('带徽标')
    }
  }
  .padding(12)
}
.onClick(() => { /* ... */ })

尾随闭包方式允许在菜单项中嵌入任意自定义布局,实现更复杂的菜单样式。

3.2 启用与禁用状态

通过 enabled 属性控制菜单项是否可交互:

// 标题项——仅展示,不可点击
MenuItem({ content: '设置标题', startIcon: '⚙️' })
  .enabled(false)

// 普通操作项——可点击
MenuItem({ content: '编辑', startIcon: '✏️' })
  .enabled(true)   // 默认即为 true
  .onClick(() => { /* ... */ })

// 条件禁用
MenuItem({ content: '删除', startIcon: '🗑️' })
  .enabled(this.hasSelection)   // 根据条件动态控制

禁用的菜单项会自动应用灰色样式,且点击无响应。

3.3 视觉分隔与分组

鸿蒙提供了两种视觉分隔方式:

方式一:MenuItem 伪装分隔线

MenuItem({ content: '' }).enabled(false).height(1).backgroundColor('#EEEEEE')

这种方式在需要自定义分隔位置时很灵活。

方式二:MenuItemGroup 自动分组(推荐)

Menu() {
  MenuItemGroup({ header: '编辑操作' }) {
    MenuItem({ content: '复制', startIcon: '📋' })
    MenuItem({ content: '剪切', startIcon: '✂️' })
    MenuItem({ content: '粘贴', startIcon: '📌' })
  }
  // 两个 MenuItemGroup 之间自动出现分隔
  MenuItemGroup({ header: '视图操作' }) {
    MenuItem({ content: '全屏', startIcon: '🖥️' })
    MenuItem({ content: '缩放', startIcon: '🔍' })
  }
}

MenuItemGroup 不仅会自动添加分组间的分隔线,还会在每组上方显示 header 标题文字,是推荐的分组方式。


第四章 @Builder 装饰器深度解析

4.1 @Builder 的本质

@Builder 是 ArkTS 中用于构建 UI 片段的特殊装饰器。被它修饰的方法返回的不是数据,而是一个可执行的 UI 构建块。它本质上是一种 延迟渲染的模板函数——只有在被调用时才会执行内部的组件创建逻辑。

// @Builder 方法可以接受参数
@Builder
buildUserCard(name: string, avatar: string, role: string) {
  Row() {
    Image(avatar).width(40).height(40).borderRadius(20)
    Column() {
      Text(name).fontSize(16).fontWeight(FontWeight.Bold)
      Text(role).fontSize(12).fontColor('#8899AA')
    }.margin({ left: 12 })
  }
  .padding(12).backgroundColor('#FFFFFF').borderRadius(8)
}

// 在 build() 中调用
build() {
  Column() {
    this.buildUserCard('张三', 'avatar1.png', '管理员')
    this.buildUserCard('李四', 'avatar2.png', '编辑者')
  }
}

4.2 三种使用场景

场景一:组件内部复用 UI(@Builder + this. 调用)

在 struct 内部定义 @Builder 方法,通过 this.methodName() 调用,适用于拆分复杂的 build() 方法:

@Entry
@Component
struct MyPage {
  @Builder
  buildSectionTitle(title: string): void {
    Row() {
      Text(title).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333')
      Blank()
      Text('更多 >').fontSize(12).fontColor('#999')
    }.width('100%').margin({ bottom: 12 })
  }

  build() {
    Column() {
      this.buildSectionTitle('推荐内容')
      // ... 内容卡片 ...
      this.buildSectionTitle('热门排行')
      // ... 排行列表 ...
    }
  }
}

场景二:bindMenu / bindContentMenu 构建菜单(@Builder + 引用传递)

这是 bindMenu 的核心用法——将 @Builder 方法的引用传递给 bindMenu(),系统在需要时调用该方法动态构建菜单:

Button('操作')
  .bindMenu(() => this.buildMyMenu())  // 传递 @Builder 引用

场景三:@BuilderParam 插槽注入(@Builder + 跨组件传递)

通过 @BuilderParam 装饰器,父组件可以将一个 @Builder 注入到子组件中,实现类似 Vue slot / React children 的效果。

4.3 @BuilderParam 组件插槽

ArkTS 不同于传统前端框架——子组件不支持 children 插槽。代之以 @BuilderParam 装饰器:

// 子组件定义:Card 卡片容器
@Component
export struct Card {
  @Prop cardPadding: number = 16;
  @Prop cardColor: string = '#ffffff';
  @Prop cardRadius: number = 16;
  @BuilderParam content: () => void = this.defaultContent;  // 插槽

  @Builder
  defaultContent(): void {
    Text('默认内容').fontSize(14).fontColor('#888')
  }

  build() {
    Column() {
      this.content()       // 渲染注入的内容
    }
    .width('100%')
    .padding(this.cardPadding)
    .backgroundColor(this.cardColor)
    .borderRadius(this.cardRadius)
    .shadow({ radius: 4, color: '#1a000000', offsetX: 0, offsetY: 2 })
  }
}

// 父组件使用
@Entry
@Component
struct Parent {
  @Builder
  customContent(): void {
    Column() {
      Text('自定义标题').fontSize(18).fontWeight(FontWeight.Bold)
      Text('自定义描述内容').fontSize(14).fontColor('#666')
      Button('点击操作').onClick(() => { /* ... */ })
    }
  }

  build() {
    Column() {
      // 传入自定义 @Builder 作为插槽内容
      Card({ content: this.customContent, cardColor: '#F8F9FA' })
    }
  }
}

@BuilderParam 的工作原理与 React 的 render props 模式相同——父组件传递一个渲染函数,子组件在合适的位置调用它。这使得组件库可以设计出高度灵活的通用容器组件。


第五章 Column + layoutWeight 弹性布局

5.1 弹性布局原理

layoutWeight 是鸿蒙 ArkUI 中最重要的弹性布局手段,其工作方式类似于 CSS Flexbox 中的 flex-grow。核心公式如下:

子组件分配高度 = (父容器总高度 − 所有固定组件高度之和)
              × (该组件 layoutWeight ÷ 所有弹性组件 layoutWeight 之和)

关键前提:父容器必须设置 height('100%')。否则没有"剩余空间"可分配,layoutWeight 将无效。

5.2 跑酷游戏的全屏弹性布局

项目中的 RunnerPage.ets 使用 Column + layoutWeight 实现了游戏全屏自适应布局:

build() {
  Column() {
    // ── 固定顶部区(不参与弹性分配) ──
    Row() {
      Text('单键跑酷').fontSize(18).fontWeight(FontWeight.Bold)
      Blank()
      Text('得分: ' + this.score).fontSize(16)
    }
    .height(50)                          // 固定 50vp
    .padding({ left: 16, right: 16 })
    .backgroundColor('#2d5f8a')

    // ── 弹性区 A:游戏主场景(layoutWeight = 1.0) ──
    Canvas(this.ctx)
      .layoutWeight(1.0)                 // 占总弹性的 50%

    // ── 弹性区 B:状态信息(layoutWeight = 0.3) ──
    Row() {
      Text('最高分: ' + this.bestScore)
      Text('速度: ' + this.speedDisp)
    }
    .layoutWeight(0.3)                   // 占总弹性的 15%

    // ── 弹性区 C:跳跃按钮(layoutWeight = 0.7) ──
    Button('🦘 点击跳跃!')
      .layoutWeight(0.7)                 // 占总弹性的 35%
      .fontSize(20).fontColor(Color.White)
      .backgroundColor('#FF6B35')
      .onClick(() => { this.jump(); })
  }
  .width('100%')
  .height('100%')                        // ← 必须设置!
}

布局计算示例(假设屏幕高度 800vp):

  • 固定区总高度:50vp
  • 剩余空间:800 − 50 = 750vp
  • 弹性总权重:1.0 + 0.3 + 0.7 = 2.0
  • Canvas 高度:750 × (1.0 ÷ 2.0) = 375vp(占比 50%)
  • 状态区高度:750 × (0.3 ÷ 2.0) = 112.5vp(占比 15%)
  • 按钮区高度:750 × (0.7 ÷ 2.0) = 262.5vp(占比 35%)

无论屏幕尺寸是 6.1 英寸手机还是 10.4 英寸平板,各区域的比例始终保持不变。

5.3 layoutWeight 使用避坑指南

错误:父容器没设固定高度

// ❌ 错误写法:Column 高度由内容撑开,layoutWeight 不生效
Column() {
  Text('标题')
  ChildComponent().layoutWeight(1)    // 不生效!
}

// ✅ 正确写法:Column 必须有明确的高度
Column() {
  Text('标题')
  ChildComponent().layoutWeight(1)    // 生效!
}
.width('100%').height('100%')

最佳实践:固定高度在前,弹性组件在后

在 Column 中,先声明所有固定高度的子组件,再声明弹性组件。这有助于提高代码可读性,也便于计算各区域的比例。


第六章 Canvas 游戏渲染引擎

6.1 Canvas 的声明式用法

在 ArkTS 中使用 Canvas 需要三步:创建 CanvasRenderingContext2D 实例、声明 Canvas 组件、在 onReady 回调或定时器中执行绘制:

@Component
struct GameScene {
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D();

  build() {
    Canvas(this.ctx)
      .width('100%')
      .height('100%')
      .onReady(() => {
        this.drawScene();  // 初始绘制
      })
  }

  drawScene(): void {
    const ctx = this.ctx;
    // 清空画布
    ctx.clearRect(0, 0, this.canvasW, this.canvasH);
    // 绘制地面
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(0, this.groundY, this.canvasW, 4);
    // 绘制角色
    ctx.fillStyle = '#FF6600';
    ctx.fillRect(this.playerX, this.playerY, P_SIZE, P_SIZE);
  }
}

6.2 游戏循环与帧动画

RunnerPage.ets 使用 setInterval 实现固定帧率的游戏循环:

private timerId: number = -1;

// 启动游戏循环
startGameLoop(): void {
  if (this.timerId < 0) {
    this.timerId = setInterval(() => {
      this.gameLoop();     // 每 24ms 调用一次(≈ 42 FPS)
    }, 24);
  }
}

// 停止游戏循环
stopGameLoop(): void {
  if (this.timerId >= 0) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

// 每帧更新:物理 + 碰撞 + 渲染
private gameLoop(): void {
  this.updatePhysics();    // 物理更新:重力、跳跃
  this.updateObstacles();  // 障碍物移动
  this.checkCollision();   // 碰撞检测
  this.spawnObstacle();    // 生成新障碍物
  this.drawScene();        // 渲染画面
}

6.3 简易物理引擎

游戏物理系统仅包含重力和跳跃两个要素,但足够支撑完整的跑酷体验:

// 物理常量
const GRAVITY: number = 0.55;     // 重力加速度
const JUMP_VEL: number = -9.0;    // 跳跃初速度(负值=向上)
const BASE_SPEED: number = 2.5;   // 基础障碍物速度

private playerY: number = 0;      // 角色 Y 偏移
private playerVY: number = 0;     // 角色垂直速度
private isJumping: boolean = false;

// 物理更新(每帧调用)
updatePhysics(): void {
  if (this.gameState !== GameState.PLAYING) return;

  // 重力加速
  this.playerVY += GRAVITY;
  this.playerY += this.playerVY;

  // 地面碰撞检测
  const groundY = this.canvasH * P_GROUND;  // 地面位于 Canvas 的 78% 高度
  if (this.playerY >= groundY) {
    this.playerY = groundY;
    this.playerVY = 0;
    this.isJumping = false;
  }
}

// 跳跃动作
jump(): void {
  if (!this.isJumping) {
    this.playerVY = JUMP_VEL;   // 施加向上的初速度
    this.isJumping = true;
  }
}

物理逻辑的关键在于状态的一致性isJumping 防止空中二次跳跃;地面碰撞时将 playerY 钳位到地面高度并清零速度;playerVY 在每一帧都累加重力加速度,模拟抛物运动。

6.4 Canvas 性能优化要点

  • 避免创建新对象:障碍物对象池化,减少垃圾回收
  • 控制绘制频率:24ms(42 FPS)在流畅度和性能间取得平衡
  • 智能碰撞检测:仅检测障碍物在角色附近时的碰撞,减少计算量
  • 使用整数坐标:避免子像素渲染开销

第七章 SSE 流式网络请求与 AI 交互

7.1 SSE 协议简介

Server-Sent Events(SSE)是一种基于 HTTP 的服务器推送技术,AI 大模型聊天场景中广泛应用。与 WebSocket 不同,SSE 是单向的(服务器→客户端),但胜在实现简单、自动重连、与标准 HTTP 完全兼容。

SSE 典型数据流:

data: {"choices":[{"delta":{"content":"你好"}}]}

data: {"choices":[{"delta":{"content":",我是"}}]}

data: {"choices":[{"delta":{"content":"AI助手"}}]}

data: [DONE]

7.2 @kit.NetworkKit 流式请求实现

AIChatService.ets 使用 @kit.NetworkKithttp 模块实现 SSE 流式解析:

import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';

export interface AICallbacks {
  onData: (text: string) => void;    // 每次收到 token
  onDone: () => void;                // 流结束
  onError: (errMsg: string) => void; // 错误处理
}

let httpRequestTask: http.HttpRequest | null = null;

export function queryAI(callbacks: AICallbacks, messages: ChatMessage[]): void {
  const httpRequest = http.createHttp();
  httpRequestTask = httpRequest;

  let buffer = '';
  let isDone = false;

  // 监听数据到达
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
    const text = arrayBufferToString(data);
    buffer += text;

    // 按行拆解 SSE 协议
    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;
  });

  // 发起 POST 请求
  httpRequest.request(
    'https://api.example.com/v1/chat/completions',
    {
      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,
    },
    (err, resp) => {
      if (err) { callbacks.onError(`请求失败: ${err.message}`); return; }
      // 非流式回退处理...
    }
  );
}

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

7.3 SSE 协议解析

SSE 数据行以 data: 开头,后面跟随 JSON 字符串。[DONE] 标记流结束。项目中的解析函数如下:

function parseSSEDataLine(line: string): string | null {
  const jsonStr = line.slice(5).trim();  // 去掉 "data:" 前缀
  if (!jsonStr) return null;

  try {
    const parsed = JSON.parse(jsonStr);
    const choices = parsed.choices;
    if (choices && choices.length > 0) {
      const delta = choices[0].delta;
      if (delta && delta.content) return delta.content;
      // 兼容非流式格式
      const message = choices[0].message;
      if (message && message.content) return message.content;
    }
  } catch (_) {
    // JSON 解析失败,跳过此行
  }
  return null;
}

7.4 流式渲染与状态管理

Index.ets 页面中,流式响应用 onData 逐步累加,最后一次性解析:

let rawContent = '';

queryAI({
  onData: (text: string) => {
    rawContent += text;              // 逐步累加 token
  },
  onDone: () => {
    this.isLoading = false;
    try {
      const parsed = JSON.parse(rawContent) as CaseData;
      this.caseData = parsed;         // 状态更新 → UI 自动重绘
      this.revealedHints = new Array(parsed.hints.length).fill(false);
    } catch (e) {
      this.errorMsg = '数据解析失败,请重试';
    }
  },
  onError: (errMsg) => {
    this.errorMsg = '请求失败:' + errMsg;
    this.isLoading = false;
  }
}, chatHistory);

第八章 完整项目总结

8.1 核心技术要点回顾

通过本项目的学习,我们系统掌握了以下鸿蒙 ArkTS 布局关键技术:

技术领域 核心 API 应用文件
弹出菜单 bindMenu + @Builder + Menu / MenuItem / MenuItemGroup BindMenuDemo.ets
弹性布局 Column + layoutWeight + height('100%') RunnerPage.ets
Canvas 渲染 CanvasRenderingContext2D + setInterval 游戏循环 RunnerPage.ets
SSE 流请求 @kit.NetworkKit.http + on('dataReceive') AIChatService.ets
自定义组件 @Component + @Prop + @BuilderParam CommonComponents.ets
状态管理 @State / @Prop / @Link / @Watch 所有页面
Builder 模式 @Builder / @BuilderParam 插槽注入 CommonComponents.ets

8.2 常见错误与避坑

bindMenu 不弹出:

  • 确认 bindMenu 的 @Builder 方法没有被错误地调用(应该传递引用而非调用结果)
  • 正确:.bindMenu(() => this.buildMenu())
  • 错误:.bindMenu(this.buildMenu())

layoutWeight 无效:

  • 检查父容器是否设置了 height('100%')
  • 检查是否有其他父组件限制了高度

Canvas 不显示:

  • 确认 CanvasRenderingContext2D 是通过 new 创建的
  • 确认 Canvas 组件宽度和高度有明确值
  • 确认绘制操作发生在 onReady 之后

SSE 流式数据不全:

  • 检查缓冲区拼接逻辑:最后一行可能不完整,需留到下次处理
  • 确认 [DONE] 标记已正确处理

8.3 总结

鸿蒙 ArkTS 提供了一套完整的声明式 UI 框架,从弹窗菜单到游戏渲染,从数据模型到网络请求,都有一套清晰、一致的 API。与传统的命令式 UI 开发相比,ArkTS 的声明式范式让代码更简洁、更可预测、更易于维护。

本项目的每一页代码都经过 HarmonyOS NEXT 6.1.1(API 24)环境验证,可以在 DevEco Studio 中直接导入运行。读者可以根据需要,将各章中的代码片段直接复用到自己的项目中。

Logo

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

更多推荐