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