第一个Ember应用 #

一、项目概述 #

我们将创建一个简单的待办事项(Todo)应用,通过这个项目学习Ember.js的核心概念:

  • 创建和显示待办事项
  • 标记完成状态
  • 删除待办事项
  • 数据过滤

二、创建项目 #

2.1 初始化项目 #

bash
# 创建新项目
ember new todo-app

# 进入项目目录
cd todo-app

# 启动开发服务器
ember serve

访问 http://localhost:4200,你将看到Ember的欢迎页面。

2.2 项目结构 #

text
todo-app/
├── app/
│   ├── app.js
│   ├── index.html
│   ├── router.js
│   ├── routes/
│   │   └── application.js
│   ├── templates/
│   │   └── application.hbs
│   └── styles/
│       └── app.css
├── config/
│   └── environment.js
├── tests/
├── package.json
└── ember-cli-build.js

三、创建路由 #

3.1 配置路由 #

修改 app/router.js

javascript
import EmberRouter from '@ember/routing/router';
import config from 'todo-app/config/environment';

export default class Router extends EmberRouter {
  location = config.locationType;
  rootURL = config.rootURL;
}

Router.map(function () {
  this.route('todos', { path: '/' });
  this.route('about');
});

3.2 生成路由文件 #

bash
# 生成todos路由
ember generate route todos

# 生成about路由
ember generate route about

3.3 路由文件 #

app/routes/todos.js

javascript
import Route from '@ember/routing/route';

export default class TodosRoute extends Route {
  model() {
    return [
      { title: '学习Ember.js', completed: false, id: 1 },
      { title: '创建第一个应用', completed: true, id: 2 },
      { title: '部署到生产环境', completed: false, id: 3 },
    ];
  }
}

四、创建模板 #

4.1 应用模板 #

修改 app/templates/application.hbs

handlebars
<header class="header">
  <h1>Todo App</h1>
  <nav>
    <LinkTo @route="todos">首页</LinkTo>
    <LinkTo @route="about">关于</LinkTo>
  </nav>
</header>

<main class="main">
  {{outlet}}
</main>

<footer class="footer">
  <p>使用Ember.js构建</p>
</footer>

4.2 Todos模板 #

修改 app/templates/todos.hbs

handlebars
<section class="todoapp">
  <header class="todo-header">
    <h2>待办事项</h2>
  </header>

  <section class="todo-list">
    <ul>
      {{#each @model as |todo|}}
        <li class="{{if todo.completed 'completed'}}">
          <div class="view">
            <input type="checkbox" checked={{todo.completed}} />
            <label>{{todo.title}}</label>
            <button class="destroy">删除</button>
          </div>
        </li>
      {{/each}}
    </ul>
  </section>

  <footer class="todo-footer">
    <span class="todo-count">
      剩余 {{this.remainingCount}} 项
    </span>
  </footer>
</section>

4.3 About模板 #

修改 app/templates/about.hbs

handlebars
<div class="about">
  <h2>关于本应用</h2>
  <p>这是一个使用Ember.js构建的待办事项应用示例。</p>
  <p>功能包括:</p>
  <ul>
    <li>添加待办事项</li>
    <li>标记完成状态</li>
    <li>删除待办事项</li>
    <li>过滤显示</li>
  </ul>
  <LinkTo @route="todos">返回首页</LinkTo>
</div>

五、添加样式 #

修改 app/styles/app.css

css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #f5f5f5;
  min-height: 100vh;
}

.header {
  background: #667eea;
  color: white;
  padding: 20px;
  text-align: center;
}

.header nav a {
  color: white;
  margin: 0 15px;
  text-decoration: none;
}

.header nav a:hover {
  text-decoration: underline;
}

.main {
  max-width: 600px;
  margin: 30px auto;
  padding: 0 20px;
}

.todoapp {
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.todo-header {
  padding: 20px;
  border-bottom: 1px solid #eee;
}

.todo-header h2 {
  color: #333;
}

.todo-list ul {
  list-style: none;
}

.todo-list li {
  padding: 15px 20px;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
}

.todo-list li.completed label {
  text-decoration: line-through;
  color: #999;
}

.todo-list .view {
  display: flex;
  align-items: center;
  width: 100%;
}

.todo-list input[type='checkbox'] {
  margin-right: 15px;
  width: 20px;
  height: 20px;
}

.todo-list label {
  flex: 1;
  font-size: 16px;
}

.todo-list .destroy {
  background: #ff6b6b;
  color: white;
  border: none;
  padding: 5px 15px;
  border-radius: 4px;
  cursor: pointer;
}

.todo-list .destroy:hover {
  background: #ee5a5a;
}

.todo-footer {
  padding: 15px 20px;
  background: #f9f9f9;
  color: #666;
}

.footer {
  text-align: center;
  padding: 30px;
  color: #999;
}

.about {
  background: white;
  padding: 30px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.about h2 {
  margin-bottom: 20px;
  color: #333;
}

.about ul {
  margin: 15px 0 20px 20px;
}

.about li {
  margin: 5px 0;
}

六、创建组件 #

6.1 生成组件 #

bash
# 生成单个待办事项组件
ember generate component todo-item

# 生成添加待办事项组件
ember generate component todo-input

6.2 TodoItem组件 #

app/components/todo-item.hbs

handlebars
<li class="{{if @todo.completed 'completed'}}">
  <div class="view">
    <input
      type="checkbox"
      checked={{@todo.completed}}
      {{on "change" (fn this.toggleComplete @todo)}}
    />
    <label>{{@todo.title}}</label>
    <button
      class="destroy"
      type="button"
      {{on "click" (fn this.deleteTodo @todo)}}
    >
      删除
    </button>
  </div>
</li>

app/components/todo-item.js

javascript
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class TodoItemComponent extends Component {
  @action
  toggleComplete(todo) {
    todo.completed = !todo.completed;
  }

  @action
  deleteTodo(todo) {
    if (this.args.onDelete) {
      this.args.onDelete(todo);
    }
  }
}

6.3 TodoInput组件 #

app/components/todo-input.hbs

handlebars
<form class="todo-input" {{on "submit" this.addTodo}}>
  <input
    type="text"
    placeholder="添加新的待办事项..."
    value={{this.newTitle}}
    {{on "input" this.updateTitle}}
  />
  <button type="submit">添加</button>
</form>

app/components/todo-input.js

javascript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class TodoInputComponent extends Component {
  @tracked newTitle = '';

  @action
  updateTitle(event) {
    this.newTitle = event.target.value;
  }

  @action
  addTodo(event) {
    event.preventDefault();

    if (this.newTitle.trim()) {
      if (this.args.onAdd) {
        this.args.onAdd(this.newTitle);
      }
      this.newTitle = '';
    }
  }
}

七、更新主模板 #

7.1 更新Todos模板 #

修改 app/templates/todos.hbs

handlebars
<section class="todoapp">
  <header class="todo-header">
    <h2>待办事项</h2>
    <TodoInput @onAdd={{this.addTodo}} />
  </header>

  <section class="todo-list">
    <ul>
      {{#each @model as |todo|}}
        <TodoItem @todo={{todo}} @onDelete={{this.deleteTodo}} />
      {{/each}}
    </ul>
  </section>

  <footer class="todo-footer">
    <span class="todo-count">剩余 {{this.remainingCount}} 项</span>
  </footer>
</section>

7.2 创建控制器 #

bash
ember generate controller todos

app/controllers/todos.js

javascript
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class TodosController extends Controller {
  @tracked nextId = 4;

  get remainingCount() {
    return this.model.filter((todo) => !todo.completed).length;
  }

  @action
  addTodo(title) {
    this.model.pushObject({
      id: this.nextId++,
      title: title,
      completed: false,
    });
  }

  @action
  deleteTodo(todo) {
    this.model.removeObject(todo);
  }
}

八、运行项目 #

8.1 启动服务器 #

bash
ember serve

8.2 访问应用 #

打开浏览器访问 http://localhost:4200

8.3 功能测试 #

  1. 查看待办事项列表
  2. 添加新的待办事项
  3. 勾选完成待办事项
  4. 删除待办事项
  5. 导航到关于页面

九、项目总结 #

通过这个简单的待办事项应用,我们学习了:

9.1 核心概念 #

概念 说明
Router 定义URL与应用状态的映射
Route 加载数据并渲染模板
Template 使用Handlebars语法渲染视图
Component 可复用的UI组件
Controller 处理用户交互逻辑

9.2 Ember约定 #

  • 路由文件位于 app/routes/
  • 模板文件位于 app/templates/
  • 组件文件位于 app/components/
  • 控制器文件位于 app/controllers/

9.3 下一步学习 #

  • 使用Ember Data持久化数据
  • 添加更多功能(过滤、编辑)
  • 编写测试用例
  • 部署到生产环境

恭喜你完成了第一个Ember应用!接下来让我们深入了解Ember的项目结构。

最后更新:2026-03-28