Ember组件通信 #
一、通信概述 #
Ember组件通信遵循"数据向下,动作向上"的原则:
text
┌─────────────────────────────────────────────────────────┐
│ 父组件 │
│ (数据源) │
├─────────────────────────────────────────────────────────┤
│ │ ▲ │
│ │ 数据向下 │ 动作向上 │
│ ▼ │ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 子组件A │ │ 子组件B │ │
│ └───────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
二、父传子通信 #
2.1 通过参数传递 #
javascript
// app/components/user-card.js
import Component from '@glimmer/component';
export default class UserCardComponent extends Component {
get displayName() {
return this.args.user?.name || '匿名用户';
}
}
handlebars
{{! app/components/user-card.hbs}}
<div class="user-card">
<h3>{{this.displayName}}</h3>
<p>{{@user.email}}</p>
</div>
handlebars
{{! 父组件使用}}
<UserCard @user={{@currentUser}} />
2.2 传递不同类型数据 #
handlebars
{{! 字符串}}
<Alert @message="操作成功" />
{{! 数字}}
<Progress @percent={{80}} />
{{! 布尔值}}
<Modal @visible={{true}} />
{{! 对象}}
<UserProfile @user={{this.user}} />
{{! 数组}}
<List @items={{this.items}} />
{{! 函数}}
<Button @onClick={{this.handleClick}} />
三、子传父通信 #
3.1 通过回调函数 #
javascript
// app/components/todo-item.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TodoItemComponent extends Component {
@tracked isEditing = false;
@action
deleteItem() {
if (this.args.onDelete) {
this.args.onDelete(this.args.todo);
}
}
@action
toggleComplete() {
if (this.args.onToggle) {
this.args.onToggle(this.args.todo);
}
}
}
handlebars
{{! app/components/todo-item.hbs}}
<div class="todo-item">
<input
type="checkbox"
checked={{@todo.completed}}
{{on "change" this.toggleComplete}}
/>
<span>{{@todo.title}}</span>
<button type="button" {{on "click" this.deleteItem}}>删除</button>
</div>
handlebars
{{! 父组件}}
<TodoItem
@todo={{todo}}
@onDelete={{this.deleteTodo}}
@onToggle={{this.toggleTodo}}
/>
3.2 使用fn助手 #
handlebars
{{! 使用fn绑定参数}}
<button {{on "click" (fn @onDelete @todo)}}>
删除
</button>
{{! 等价于}}
<button {{on "click" (fn this.deleteItem)}}>
删除
</button>
javascript
@action
deleteItem() {
this.args.onDelete?.(this.args.todo);
}
四、Yield通信 #
4.1 向调用者传递数据 #
javascript
// app/components/tabs.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TabsComponent extends Component {
@tracked activeIndex = 0;
@action
setActive(index) {
this.activeIndex = index;
}
}
handlebars
{{! app/components/tabs.hbs}}
<div class="tabs">
<div class="tabs-header">
{{#each @tabs as |tab index|}}
<button
type="button"
class="tab {{if (eq this.activeIndex index) "active"}}"
{{on "click" (fn this.setActive index)}}
>
{{tab.label}}
</button>
{{/each}}
</div>
<div class="tabs-content">
{{yield this.activeIndex this.setActive}}
</div>
</div>
handlebars
{{! 使用组件}}
<Tabs @tabs={{this.tabList}} as |activeIndex setActive|>
<p>当前激活: {{activeIndex}}</p>
<button {{on "click" (fn setActive 0)}}>切换到第一个</button>
</Tabs>
4.2 上下文组件 #
handlebars
{{! app/components/form.js}}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class FormComponent extends Component {
@tracked values = {};
getFieldValue(name) {
return this.values[name] ?? '';
}
setFieldValue(name, value) {
this.values = { ...this.values, [name]: value };
}
}
handlebars
{{! app/components/form.hbs}}
<form ...attributes>
{{yield
(hash
field=(component "form-field" form=this)
values=this.values
submit=(fn this.handleSubmit)
)
}}
</form>
handlebars
{{! 使用}}
<Form as |form|>
<form.field @name="email" @label="邮箱" @type="email" />
<form.field @name="password" @label="密码" @type="password" />
<button {{on "click" form.submit}}>提交</button>
</Form>
五、兄弟组件通信 #
5.1 通过共同父组件 #
javascript
// app/controllers/posts.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class PostsController extends Controller {
@tracked selectedPostId = null;
get selectedPost() {
return this.model.find((post) => post.id === this.selectedPostId);
}
@action
selectPost(post) {
this.selectedPostId = post.id;
}
}
handlebars
{{! app/templates/posts.hbs}}
<div class="posts-layout">
<PostList @posts={{@model}} @selectedId={{this.selectedPostId}} @onSelect={{this.selectPost}} />
<PostDetail @post={{this.selectedPost}} />
</div>
5.2 通过服务 #
javascript
// app/services/selection.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class SelectionService extends Service {
@tracked selectedId = null;
select(id) {
this.selectedId = id;
}
clear() {
this.selectedId = null;
}
}
javascript
// app/components/post-list.js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class PostListComponent extends Component {
@service selection;
@action
selectPost(post) {
this.selection.select(post.id);
}
}
javascript
// app/components/post-detail.js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class PostDetailComponent extends Component {
@service selection;
get selectedPost() {
return this.args.posts.find((post) => post.id === this.selection.selectedId);
}
}
六、跨层级通信 #
6.1 使用服务 #
javascript
// app/services/theme.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class ThemeService extends Service {
@tracked current = 'light';
toggle() {
this.current = this.current === 'light' ? 'dark' : 'light';
}
}
javascript
// 任意组件中使用
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class ThemeToggleComponent extends Component {
@service theme;
}
handlebars
<button {{on "click" this.theme.toggle}}>
切换主题 (当前: {{this.theme.current}})
</button>
6.2 Context Provider模式 #
javascript
// app/components/theme-provider.js
import Component from '@glimmer/component';
export default class ThemeProviderComponent extends Component {
}
handlebars
{{! app/components/theme-provider.hbs}}
{{yield (hash theme=@theme toggle=@onToggle)}}
handlebars
{{! 使用}}
<ThemeProvider @theme={{this.theme}} @onToggle={{this.toggleTheme}} as |ctx|>
<Header>
<ThemeToggle @theme={{ctx.theme}} @onToggle={{ctx.toggle}} />
</Header>
<Main>
<Content @theme={{ctx.theme}} />
</Main>
</ThemeProvider>
七、事件总线 #
7.1 创建事件服务 #
javascript
// app/services/events.js
import Service from '@ember/service';
import Evented from '@ember/object/evented';
export default class EventsService extends Service.extend(Evented) {
emit(event, data) {
this.trigger(event, data);
}
onEvent(event, callback) {
this.on(event, callback);
}
offEvent(event, callback) {
this.off(event, callback);
}
}
7.2 使用事件服务 #
javascript
// 发送事件
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class PublisherComponent extends Component {
@service events;
@action
publish() {
this.events.emit('data-updated', { id: 123 });
}
}
javascript
// 接收事件
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class SubscriberComponent extends Component {
@service events;
constructor(owner, args) {
super(owner, args);
this.events.onEvent('data-updated', this.handleUpdate);
}
willDestroy() {
this.events.offEvent('data-updated', this.handleUpdate);
super.willDestroy(...arguments);
}
@action
handleUpdate(data) {
console.log('收到更新:', data);
}
}
八、最佳实践 #
8.1 保持单向数据流 #
handlebars
{{! 好的做法}}
<Input
@value={{this.searchTerm}}
@onChange={{this.updateSearchTerm}}
/>
{{! 避免 - 双向绑定}}
<Input @value={{this.searchTerm}} />
8.2 命名约定 #
handlebars
{{! 回调函数使用 on 前缀}}
<Button @onClick={{this.handleClick}} />
<Modal @onClose={{this.closeModal}} />
<Input @onChange={{this.handleChange}} />
{{! 布尔属性使用 is/has 前缀}}
<Alert @isVisible={{true}} />
<Card @hasShadow={{true}} />
8.3 适度使用服务 #
javascript
// 好的做法 - 全局状态使用服务
@service session;
@service theme;
@service notifications;
// 避免 - 局部状态使用服务
// 如果状态只在少数组件间共享,考虑状态提升
8.4 文档化接口 #
typescript
/**
* UserCard 组件
*
* @param user - 用户对象
* @param size - 卡片大小
* @param onEdit - 编辑回调
* @param onDelete - 删除回调
*/
interface UserCardSignature {
Args: {
user: User;
size?: 'small' | 'medium' | 'large';
onEdit?: (user: User) => void;
onDelete?: (user: User) => void;
};
}
九、总结 #
Ember组件通信方式:
| 方式 | 场景 | 示例 |
|---|---|---|
| 参数传递 | 父传子 | @user={{user}} |
| 回调函数 | 子传父 | @onClick={{handleClick}} |
| Yield | 双向通信 | {{yield data}} |
| 服务 | 跨组件/全局 | @service session |
| 事件总线 | 解耦通信 | events.emit() |
遵循"数据向下,动作向上"原则,可以让组件通信更加清晰可维护。
最后更新:2026-03-28