第1.4篇:@Link、@Prop 与 @StorageLink------组件间数据通信
难度 :⭐⭐ 进阶
前置知识 :1.3 @State 与状态管理
涉及源文件 :
CreationComponents.ets、BreakpointSystem.ets、Index.ets、RecognitionWaitingPage.ets、PhotoRecognitionPage.ets、FreeDoodlePage.ets

在上一篇中,我们学习了用 @State 管理组件内部状态。但在真实应用中,数据很少只在一个组件内闭环------它需要在父子组件之间 、兄弟组件之间 、甚至跨页面之间 共享和同步。HarmonyOS ArkUI 为这些场景提供了三种装饰器:@Prop、@Link 和 @StorageLink。
本文将通过「画伴梦工厂」项目的真实代码,带你掌握它们的核心区别与使用场景。
1. 三种装饰器概览
| 装饰器 | 数据流方向 | 适用场景 | 同步机制 |
|---|---|---|---|
@Prop |
父 → 子(单向) | 子组件接收父组件传入的只读/可覆写初始值 | 父变则子变,子变不影响父 |
@Link |
父 ↔ 子(双向) | 父子组件共享同一状态,任一方修改都同步 | 数据引用共享,实时同步 |
@StorageLink |
任意组件 ↔ AppStorage(跨组件/跨页面) | 多页面共享全局状态,如主题、断点、用户信息 | 通过 AppStorage 代理同步 |
一句话总结:
- @Prop:父组件"告诉"子组件一个值,子组件可以自己改着玩,但不影响父组件。
- @Link:父子组件"共享"一个值,谁改都会通知对方。
- @StorageLink:任何组件都能读写一个全局值,适合跨页面状态。
2. @Link 双向绑定:父子组件状态同步
在「画伴梦工厂」中,PhotoRecognitionPage(父组件)和 PhotoRecognitionComponent(子组件)之间通过 @Link 实现了紧密的状态同步。
2.1 子组件声明 @Link
在 CreationComponents.ets 中,子组件用 @Link 声明需要从父组件获取的共享状态:
typescript
@Component
export struct PhotoRecognitionComponent {
@Link generationProgress: number; // 生成进度
@Link noticeText: string; // 通知文本
// 组件内部通过 @State 管理自身状态
@State private hasPhoto: boolean = false;
@State private recognizing: boolean = false;
@State private activeStep: number = 0;
@State private capturedImageUri: string = '';
// 在方法中可以直接修改 @Link 变量
private capturePhoto(uri: string = '', sourceLabel: string = '拍照图片') {
// ...
this.generationProgress = 35; // ← 修改同步到父组件
this.noticeText = '已采集画作,可以直接生成动画';
}
}
同理,FreeDoodleComponent 声明了更多的 @Link 变量:
typescript
@Component
export struct FreeDoodleComponent {
@Link selectedTool: number;
@Link selectedColor: number;
@Link brushSize: number;
@Link generationProgress: number;
@Link noticeText: string;
@Link isDrawingOnCanvas: boolean;
// ...
}
2.2 父组件通过 $ 语法传递引用
父组件 PhotoRecognitionPage.ets 在自己的 @State 中定义状态,然后通过 $ 前缀将引用传给子组件:
typescript
@Entry
@Component
struct PhotoRecognitionPage {
@State private generationProgress: number = 0;
@State private noticeText: string = '';
build() {
Scroll() {
Column() {
// $ 符号将 @State 变量转为 @Link 引用
PhotoRecognitionComponent({
generationProgress: $generationProgress,
noticeText: $noticeText
})
}
}
}
}
FreeDoodlePage 的用法类似:
typescript
FreeDoodleComponent({
selectedTool: $selectedTool,
selectedColor: $selectedColor,
brushSize: $brushSize,
generationProgress: $generationProgress,
noticeText: $noticeText,
isDrawingOnCanvas: $isDrawingOnCanvas
})
关键点 :
$variableName是 ArkUI 中获取@State变量引用的语法,传递给子组件的@Link后,双方操作的是同一个数据源。
2.3 实际效果
当用户在 PhotoRecognitionComponent 中点击「生成动画」时:
- 子组件修改
this.generationProgress = 100 - 父组件
PhotoRecognitionPage中的generationProgress同步更新 - 父组件 UI 中的
Progress组件自动刷新
typescript
// PhotoRecognitionPage build 中的进度条
Progress({ value: this.generationProgress, total: 100, type: ProgressType.Linear })
这就是 @Link 双向绑定的威力------子组件操作状态,父组件的 UI 立即响应。
3. @Prop 单向传值:父传子,子可改写但不影响父
@Prop 与 @Link 的核心区别在于同步方向:
- 父组件更新 →
@Prop变量同步更新 ✅ - 子组件修改
@Prop变量 → 不同步回父组件 ❌
虽然「画伴梦工厂」项目中未直接使用 @Prop,但我们可以通过 @Builder 的参数传递模式来理解单向传值的概念(详见 1.5 篇)。
typescript
// 这相当于单向传值模式------父组件传参进来,子 UI 片段使用
@Builder
private Pill(text: string, active: boolean = false) {
Text(text)
.fontSize(12)
.fontColor(active ? '#FFFFFF' : '#6F7590')
.backgroundColor(active ? this.brandPurple : '#F0F1F8')
.borderRadius(14)
}
何时用 @Prop? 当子组件只需要"读"父组件的值,或者子组件想有自己的副本但不影响父组件时。比如一个仅展示用的信息卡片。
4. @StorageLink 应用级状态:跨组件、跨页面共享
如果说 @Link 解决的是父子组件通信,那 @StorageLink 解决的是跨页面/跨组件 的全局状态共享。它通过 ArkUI 的 AppStorage 实现。
4.1 注册断点系统
在 Index.ets 中,通过 @StorageLink 声明一个应用级状态变量:
typescript
@Entry
@Component
struct Index {
@StorageLink('mainBreakpoint') currentBreakpoint: string = 'md';
// ↑ 键名 ↑ 默认值
// ...
private readonly breakpointSystem: BreakpointSystem = new BreakpointSystem('mainBreakpoint');
}
4.2 BreakpointSystem 写入 AppStorage
BreakpointSystem.ets 监听媒体查询变化,当断点改变时通过 AppStorage.set() 更新全局状态:
typescript
export class BreakpointSystem {
private currentBreakpoint: string = 'md';
private readonly breakpointId: string;
constructor(breakpointId: string) {
this.breakpointId = breakpointId;
}
public register(uiContext: UIContext): void {
this.breakpoints.forEach((breakpoint, index) => {
let condition: string = '';
if (index === this.breakpoints.length - 1) {
condition = `(${breakpoint.size}vp<=width)`;
} else {
condition = `(${breakpoint.size}vp<=width<${this.breakpoints[index + 1].size}vp)`;
}
breakpoint.mediaQueryListener = uiContext.getMediaQuery().matchMediaSync(condition);
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(breakpoint.name);
}
});
});
}
private updateCurrentBreakpoint(breakpoint: string): void {
if (this.currentBreakpoint !== breakpoint) {
this.currentBreakpoint = breakpoint;
// 写入 AppStorage,所有 @StorageLink('mainBreakpoint') 自动更新
AppStorage.set<string>(this.breakpointId, this.currentBreakpoint);
}
}
}
4.3 Index.ets 响应断点变化
当 AppStorage 中的 'mainBreakpoint' 发生变化时,Index.ets 中的 currentBreakpoint 自动更新,驱动 UI 响应式布局:
typescript
// 根据 currentBreakpoint 返回不同的间距值
private pageEdge(): number {
return new BreakPointType<number>({ sm: 12, md: 24, lg: 36, xl: 56 })
.getValue(this.currentBreakpoint);
}
// 根据断点返回不同的面板宽度
private panelWidth(): string {
return new BreakPointType<string>({ sm: '90%', md: '86%', lg: '74%', xl: '64%' })
.getValue(this.currentBreakpoint);
}
// 底部栏宽度也响应断点
private bottomBarWidth(): string {
return new BreakPointType<string>({ sm: '86%', md: '72%', lg: '58%', xl: '48%' })
.getValue(this.currentBreakpoint);
}
// Hero 区域高度
private heroHeight(): number {
return new BreakPointType<number>({ sm: 420, md: 430, lg: 460, xl: 480 })
.getValue(this.currentBreakpoint);
}
4.4 数据流全景
设备窗口变化
↓
BreakpointSystem.register() 监听媒体查询
↓
断点触发 → updateCurrentBreakpoint()
↓
AppStorage.set('mainBreakpoint', 'lg')
↓
Index.ets @StorageLink('mainBreakpoint') 自动更新
↓
pageEdge() / panelWidth() / heroHeight() 重新计算
↓
UI 响应式刷新
这种模式下,任何页面只要用 @StorageLink('mainBreakpoint') 声明变量,就能自动感知断点变化,实现真正的响应式设计。
4.5 @StorageLink 的其他用途
在 Index.ets 的 aboutToAppear 中,还展示了 AppStorage.get 和 AppStorage.setOrCreate 的用法:
typescript
aboutToAppear() {
// 从 AppStorage 读取跨页面传递的参数
const launchTab = AppStorage.get<string>('launchTab');
if (launchTab === 'works') {
const launchWorkIndex = AppStorage.get<number>('launchWorkIndex');
if (typeof launchWorkIndex === 'number') {
this.openWorkDetail(launchWorkIndex);
}
AppStorage.setOrCreate<string>('launchTab', '');
AppStorage.setOrCreate<number>('launchWorkIndex', -1);
}
}
这种方式适合页面间传递启动参数,比 Router 传参更灵活------即使页面还未创建,数据也不会丢失。
5. 页面间数据传递:Router + getParams()
除了 @StorageLink,页面间还有一种常用的传参方式:Router 参数传递。
5.1 发起方:pushUrl 携带参数
在 CreationComponents.ets 中,组件通过 getUIContext().getRouter().pushUrl() 跳转并传参:
typescript
private navigateToGeneration() {
this.getUIContext().getRouter().pushUrl({
url: 'pages/RecognitionWaitingPage',
params: {
source: '图片生成',
workSource: 'photo',
prompt: this.buildVideoPrompt(),
imageUri: this.capturedImageUri,
coverUri: this.capturedImageUri
}
});
}
FreeDoodleComponent 也是同样的模式:
typescript
this.getUIContext().getRouter().pushUrl({
url: 'pages/RecognitionWaitingPage',
params: {
source: '自由涂鸦',
workSource: 'doodle',
prompt: '把这张儿童涂鸦变成温暖的短动画,保留粗笔触和明亮色块',
imageUri: imageUri,
coverUri: imageUri
}
});
5.2 接收方:aboutToAppear + getParams()
在 RecognitionWaitingPage.ets 中,通过 aboutToAppear 生命周期方法接收参数:
typescript
interface WaitingParams {
source?: string;
workSource?: string;
prompt?: string;
imageUri?: string;
coverUri?: string;
recognitionResult?: string;
}
@Entry
@Component
struct RecognitionWaitingPage {
@State private source: string = '画作识别';
@State private workSource: string = 'photo';
@State private prompt: string = '';
@State private imageUri: string = '';
@State private coverUri: string = '';
aboutToAppear() {
const params = this.getUIContext().getRouter().getParams() as WaitingParams;
if (params && params.source) {
this.source = params.source;
}
if (params && params.workSource) {
if (params.workSource === 'doodle') {
this.workSource = 'doodle';
} else if (params.workSource === 'ai-chat') {
this.workSource = 'ai-chat';
} else {
this.workSource = 'photo';
}
}
if (params && params.prompt) {
this.prompt = params.prompt;
}
if (params && params.imageUri) {
this.imageUri = params.imageUri;
}
if (params && params.coverUri) {
this.coverUri = params.coverUri;
}
this.startGeneration();
}
}
5.3 Router 与 @StorageLink 的选择
| 传递方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Router + params | 页面跳转时的上下文数据 | 职责清晰,一次性传递 | 仅跳转时可用,返回时需重新传 |
| @StorageLink / AppStorage | 全局状态、跨页面共享 | 随时读写,全局生效 | 需要管理键名,避免冲突 |
| @Link | 父子组件实时同步 | 强类型,编译期检查 | 仅限父子关系 |
6. 回调模式:通过参数传递函数实现反向通信
@Builder 除了构建 UI,还可以通过参数传递回调函数 实现子组件到父组件的反向通信。虽然这不是严格意义上的 @Prop/@Link,但在 ArkUI 中是一种常见的通信模式。
在 Index.ets 中,NavIcon 通过 onClick 回调父组件的方法:
typescript
@Builder
private NavIcon(index: number) {
Column() {
Text(NAV_ICONS[index])
.fontSize(20)
.fontColor(this.isPrimaryTabActive(index) ? this.brandPurple : '#8E94A6')
Text(NAV_ITEMS[index])
.fontSize(12)
.fontColor(this.isPrimaryTabActive(index) ? this.brandPurple : '#747A92')
}
.onClick(() => {
this.openPrimaryTab(index); // ← 调用父组件的方法
})
}
而 FeatureCard 通过 modeIndex 参数和 onClick 回调调用 openCreationFlow:
typescript
@Builder
private FeatureCard(title: string, desc: string, icon: string, color: string, modeIndex: number) {
// ...
.onClick(() => {
this.openCreationFlow(modeIndex); // 回调父组件方法
})
}
这种模式本质上是一种事件冒泡------子 UI 片段通过回调函数通知父组件执行逻辑,父组件持有状态的修改权。
7. 对比总结
| 特性 | @Prop | @Link | @StorageLink |
|---|---|---|---|
| 数据流 | 单向(父→子) | 双向(父↔子) | 全局双向 |
| 使用场景 | 子组件展示父数据 | 父子实时同步状态 | 跨页面/跨组件 |
| 声明方式 | @Prop variable: type |
@Link variable: type |
@StorageLink('key') variable: type |
| 传递方式 | 直接传值 variable={value} |
$ 引用 variable: $stateVar |
通过 AppStorage 自动同步 |
| 子组件修改 | 可修改,不影响父 | 修改后同步到父 | 修改后同步到所有使用者 |
| 性能特征 | 浅比较,仅值变化时更新 | 引用绑定,实时同步 | AppStorage 代理,跨页面无延迟 |
| 项目中示例 | (@Builder 参数模拟) |
PhotoRecognitionComponent |
BreakpointSystem + currentBreakpoint |
8. 小结
本文从三个维度梳理了 ArkUI 的数据通信体系:
- @Link :父子组件双向绑定,用
$语法传递引用,适合紧密耦合的组件对。 - @Prop:父子组件单向传值,适合子组件只读展示或独立维护副本。
- @StorageLink + AppStorage:应用级全局状态,通过键值对实现任意组件的状态共享,是实现响应式布局的基石。
此外,我们还学习了 Router getParams() 的页面间传参方式,以及 回调函数模式 在 @Builder 中的反向通信用法。
在实际开发中,数据通信方案的选择应遵循"最小化共享范围"原则:能用局部 @State 解决的不用 @Link,能用 @Link 解决的不用 @StorageLink,避免过度设计导致的状态管理混乱。
下一篇预告:第1.5篇《@Builder 与 @BuilderParam------复用 UI 的利器》。当你的页面中有大量重复的 UI 片段时,@Builder 是如何让代码变得简洁而优雅的?我们将在下一篇中深入探讨。