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