Ember组件基础 #

一、组件概述 #

组件是Ember应用的核心构建块,用于创建可复用的UI单元。Ember 3.15+使用Glimmer组件,这是一种现代化的组件模型。

1.1 组件结构 #

一个完整的组件包含:

text
app/components/
├── user-card.js      # 组件逻辑
└── user-card.hbs     # 组件模板

1.2 生成组件 #

bash
# 生成组件
ember generate component user-card

# 生成模板only组件
ember generate component user-card --template-only

# 简写
ember g component user-card

二、Glimmer组件 #

2.1 基本组件 #

javascript
// app/components/user-card.js
import Component from '@glimmer/component';

export default class UserCardComponent extends Component {
}
handlebars
{{! app/components/user-card.hbs}}
<div class="user-card">
  <h3>{{@user.name}}</h3>
  <p>{{@user.email}}</p>
</div>

2.2 使用组件 #

handlebars
{{! Angle Bracket语法(推荐)}}
<UserCard @user={{@currentUser}} />

{{! 嵌套内容}}
<UserCard @user={{@currentUser}}>
  <p>额外信息</p>
</UserCard>

2.3 组件参数 #

handlebars
{{! 传递参数}}
<UserCard
  @user={{@currentUser}}
  @size="large"
  @showEmail={{true}}
  @onEdit={{this.handleEdit}}
/>

三、组件参数(Args) #

3.1 访问参数 #

javascript
// app/components/user-card.js
import Component from '@glimmer/component';

export default class UserCardComponent extends Component {
  get fullName() {
    const { firstName, lastName } = this.args.user;
    return `${firstName} ${lastName}`;
  }

  get isLarge() {
    return this.args.size === 'large';
  }
}
handlebars
{{! app/components/user-card.hbs}}
<div class="user-card {{if this.isLarge "large"}}">
  <h3>{{this.fullName}}</h3>
  {{#if @showEmail}}
    <p>{{@user.email}}</p>
  {{/if}}
</div>

3.2 参数默认值 #

javascript
// app/components/button.js
import Component from '@glimmer/component';

export default class ButtonComponent extends Component {
  get type() {
    return this.args.type || 'button';
  }

  get variant() {
    return this.args.variant || 'primary';
  }

  get size() {
    return this.args.size || 'medium';
  }
}
handlebars
{{! app/components/button.hbs}}
<button
  type={{this.type}}
  class="btn btn-{{this.variant}} btn-{{this.size}}"
  disabled={{@disabled}}
>
  {{yield}}
</button>
handlebars
{{! 使用}}
<Button>默认按钮</Button>
<Button @variant="danger" @size="large">删除</Button>
<Button @type="submit" @disabled={{true}}>提交</Button>

四、组件状态 #

4.1 追踪状态 #

使用 @tracked 装饰器管理组件内部状态:

javascript
// app/components/counter.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;

  @action
  increment() {
    this.count++;
  }

  @action
  decrement() {
    this.count--;
  }

  @action
  reset() {
    this.count = 0;
  }
}
handlebars
{{! app/components/counter.hbs}}
<div class="counter">
  <button type="button" {{on "click" this.decrement}}>-</button>
  <span>{{this.count}}</span>
  <button type="button" {{on "click" this.increment}}>+</button>
  <button type="button" {{on "click" this.reset}}>重置</button>
</div>

4.2 计算属性 #

javascript
// app/components/shopping-cart.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class ShoppingCartComponent extends Component {
  @tracked items = [];

  get total() {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  get itemCount() {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }

  get isEmpty() {
    return this.items.length === 0;
  }
}

五、组件动作 #

5.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;
  @tracked editTitle = '';

  @action
  startEdit() {
    this.isEditing = true;
    this.editTitle = this.args.todo.title;
  }

  @action
  cancelEdit() {
    this.isEditing = false;
    this.editTitle = '';
  }

  @action
  saveEdit() {
    if (this.args.onSave) {
      this.args.onSave(this.args.todo, this.editTitle);
    }
    this.isEditing = false;
  }

  @action
  updateTitle(event) {
    this.editTitle = event.target.value;
  }
}
handlebars
{{! app/components/todo-item.hbs}}
<div class="todo-item">
  {{#if this.isEditing}}
    <input
      value={{this.editTitle}}
      {{on "input" this.updateTitle}}
    />
    <button type="button" {{on "click" this.saveEdit}}>保存</button>
    <button type="button" {{on "click" this.cancelEdit}}>取消</button>
  {{else}}
    <span>{{@todo.title}}</span>
    <button type="button" {{on "click" this.startEdit}}>编辑</button>
  {{/if}}
</div>

5.2 传递动作 #

handlebars
{{! 父组件}}
<TodoItem
  @todo={{todo}}
  @onSave={{this.handleSave}}
  @onDelete={{this.handleDelete}}
/>
javascript
// 父组件控制器
import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class TodosController extends Controller {
  @action
  async handleSave(todo, newTitle) {
    todo.title = newTitle;
    await todo.save();
  }

  @action
  async handleDelete(todo) {
    await todo.destroyRecord();
  }
}

六、Yield产出 #

6.1 基本Yield #

handlebars
{{! app/components/card.hbs}}
<div class="card">
  <div class="card-header">
    {{#if @title}}
      <h3>{{@title}}</h3>
    {{/if}}
  </div>
  <div class="card-body">
    {{yield}}
  </div>
</div>
handlebars
{{! 使用}}
<Card @title="用户信息">
  <p>姓名:{{@user.name}}</p>
  <p>邮箱:{{@user.email}}</p>
</Card>

6.2 块参数 #

handlebars
{{! app/components/list.hbs}}
<ul class="list">
  {{#each @items as |item|}}
    <li>
      {{yield item @index}}
    </li>
  {{/each}}
</ul>
handlebars
{{! 使用}}
<List @items={{@users}} as |user index|>
  <span>{{index}}.</span>
  <span>{{user.name}}</span>
</List>

6.3 多个Yield块 #

handlebars
{{! app/components/modal.hbs}}
<div class="modal">
  <div class="modal-header">
    {{yield to="header"}}
  </div>
  <div class="modal-body">
    {{yield to="body"}}
  </div>
  <div class="modal-footer">
    {{yield to="footer"}}
  </div>
</div>
handlebars
{{! 使用}}
<Modal>
  <:header>
    <h2>确认删除</h2>
  </:header>
  
  <:body>
    <p>确定要删除这个项目吗?</p>
  </:body>
  
  <:footer>
    <button {{on "click" @onCancel}}>取消</button>
    <button {{on "click" @onConfirm}}>确认</button>
  </:footer>
</Modal>

七、模板Only组件 #

7.1 纯展示组件 #

对于没有状态的组件,可以使用模板only组件:

handlebars
{{! app/components/icon.hbs}}
<svg
  class="icon icon-{{@name}}"
  width={{or @size "24"}}
  height={{or @size "24"}}
  viewBox="0 0 24 24"
  fill={{or @color "currentColor"}}
>
  {{#if (eq @name "user")}}
    <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
  {{else if (eq @name "settings")}}
    <path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
  {{/if}}
</svg>
handlebars
{{! 使用}}
<Icon @name="user" @size="32" @color="blue" />
<Icon @name="settings" />

八、组件样式 #

8.1 CSS模块 #

css
/* app/components/user-card.css */
.user-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.user-card.large {
  padding: 24px;
}

.user-card h3 {
  margin: 0 0 8px;
}

.user-card p {
  margin: 0;
  color: #666;
}

8.2 动态类名 #

handlebars
<div class="user-card {{if @isActive "active"}} {{@size}}">
  {{! 内容}}
</div>

8.3 内联样式 #

handlebars
<div style="color: {{@color}}; font-size: {{@fontSize}}px">
  {{! 内容}}
</div>

九、组件组合 #

9.1 组件嵌套 #

handlebars
{{! app/components/user-profile.hbs}}
<div class="user-profile">
  <UserAvatar @user={{@user}} @size="large" />
  <UserInfo @user={{@user}} />
  <UserStats @user={{@user}} />
</div>

9.2 组件插槽模式 #

handlebars
{{! app/components/tabs.hbs}}
<div class="tabs">
  <div class="tabs-header">
    {{#each @tabs as |tab index|}}
      <button
        type="button"
        class="tab {{if (eq @activeTab index) "active"}}"
        {{on "click" (fn @onTabChange index)}}
      >
        {{tab.label}}
      </button>
    {{/each}}
  </div>
  <div class="tabs-content">
    {{yield}}
  </div>
</div>

十、组件最佳实践 #

10.1 单一职责 #

javascript
// 好的做法 - 单一职责
export default class UserAvatarComponent extends Component {
  // 只负责显示头像
}

export default class UserCardComponent extends Component {
  // 只负责显示用户卡片
}

// 避免 - 职责过多
export default class UserComponent extends Component {
  // 负责头像、信息、统计、编辑...
}

10.2 数据向下,动作向上 #

handlebars
{{! 数据通过参数向下传递}}
<UserCard @user={{@user}} />

{{! 动作通过回调向上传递}}
<UserCard @user={{@user}} @onEdit={{this.handleEdit}} />

10.3 组件命名 #

handlebars
{{! 好的命名 - 清晰表达用途}}
<UserAvatar />
<UserCard />
<UserList />
<SubmitButton />

{{! 避免 - 模糊的命名}}
<Box />
<Item />
<Component1 />

十一、总结 #

Ember组件核心概念:

概念 说明
Args 组件参数,只读
@tracked 追踪状态变化
@action 定义动作方法
{{yield}} 产出内容
块参数 向调用者传递数据

掌握组件是Ember开发的核心,通过组合组件可以构建复杂的应用界面。

最后更新:2026-03-28