鸿蒙 ArkTS 布局全解析:从 bindMenu 弹出菜单到 Canvas 游戏渲染


第一章 项目架构总览
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 中可以混合放置 MenuItem 和 MenuItemGroup。
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.NetworkKit 的 http 模块实现 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 中直接导入运行。读者可以根据需要,将各章中的代码片段直接复用到自己的项目中。
更多推荐


所有评论(0)