javascript-ember

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Expert Ember.js Development

专业Ember.js开发

Write modern, performant Ember.js applications following Ember Octane conventions and current best practices.
遵循Ember Octane规范和当前最佳实践,编写现代化、高性能的Ember.js应用。

Critical First Step: Use context7 MCP

关键第一步:使用context7 MCP

ALWAYS use context7 MCP before writing or editing ANY Ember code - Before generating, modifying, or reviewing any Ember code, you MUST use the context7 MCP to check for relevant documentation. Context7 provides project-specific conventions, architectural patterns, coding standards, and technical decisions that override these general guidelines.
在编写或编辑任何Ember代码之前,务必先使用context7 MCP - 在生成、修改或审查任何Ember代码之前,你必须使用context7 MCP查阅相关文档。Context7提供的项目特定约定、架构模式、编码标准和技术决策,优先级高于这些通用指南。

When to Use context7

何时使用context7

Use context7 MCP in these situations:
  • Before writing any new Ember component, route, service, or model
  • Before modifying existing Ember code
  • When implementing features that might have project-specific patterns
  • When unsure about architectural decisions
  • Before suggesting refactors or improvements
在以下场景中使用context7 MCP:
  • 编写新的Ember组件、路由、服务或模型之前
  • 修改现有Ember代码之前
  • 实现可能存在项目特定模式的功能时
  • 对架构决策不确定时
  • 提出重构或改进建议之前

How to Use context7

如何使用context7

javascript
// Example: Check for Ember component patterns before creating a component
// Use the context7 MCP to search for relevant documentation
// Query examples:
// - "ember component patterns"
// - "ember routing conventions" 
// - "ember data models"
// - "ember testing standards"
// - "glimmer component lifecycle"

// Apply the documentation from context7 to your implementation
If context7 returns relevant documentation, follow it EXACTLY even if it conflicts with this skill's general guidance. Project-specific conventions always take precedence.
javascript
// Example: Check for Ember component patterns before creating a component
// Use the context7 MCP to search for relevant documentation
// Query examples:
// - "ember component patterns"
// - "ember routing conventions" 
// - "ember data models"
// - "ember testing standards"
// - "glimmer component lifecycle"

// Apply the documentation from context7 to your implementation
如果context7返回了相关文档,请严格遵循,即使它与本技能的通用指导冲突。 项目特定约定始终优先。

Core Principles

核心原则

  1. Embrace Ember Octane - Use Glimmer components, tracked properties, and native classes
  2. Convention over configuration - Follow Ember's resolver patterns and file structure
  3. Use the platform - Prefer native JavaScript features over framework-specific abstractions when possible
  4. Composition through services and modifiers - Extract reusable logic into services and UI behavior into modifiers
  5. Data down, actions up (DDAU) - Maintain clear data flow patterns
  1. 拥抱Ember Octane - 使用Glimmer组件、tracked属性和原生类
  2. 约定优于配置 - 遵循Ember的解析器模式和文件结构
  3. 使用平台特性 - 尽可能优先使用原生JavaScript特性,而非框架特定抽象
  4. 通过服务和修饰器实现组合 - 将可复用逻辑提取到服务中,将UI行为提取到修饰器中
  5. 数据向下,动作向上(DDAU) - 保持清晰的数据流模式

Context7 MCP Workflow

Context7 MCP工作流程

Before implementing any Ember code, follow this workflow:
  1. Query context7 - Search for relevant documentation using specific terms:
    • Component type (e.g., "ember glimmer component")
    • Feature area (e.g., "ember routing", "ember data")
    • Specific pattern (e.g., "form validation", "authentication")
  2. Review results - Read any returned documentation carefully
    • Note project-specific naming conventions
    • Identify required patterns or abstractions
    • Check for mandatory testing requirements
    • Look for deprecated approaches to avoid
  3. Apply documentation - Implement code following context7 guidance
    • Use project-specific utilities and helpers
    • Follow established architectural patterns
    • Match existing code style and structure
    • Include required metadata or annotations
  4. Fall back to general patterns - If context7 has no relevant docs, use this skill's patterns
    • Apply standard Ember Octane conventions
    • Follow community best practices
    • Use examples from this skill as templates
Remember: context7 documentation always overrides this skill's general guidance.
在实现任何Ember代码之前,请遵循以下工作流程:
  1. 查询context7 - 使用特定术语搜索相关文档:
    • 组件类型(例如:"ember glimmer component")
    • 功能领域(例如:"ember routing", "ember data")
    • 特定模式(例如:"form validation", "authentication")
  2. 审查结果 - 仔细阅读返回的所有文档
    • 记录项目特定的命名约定
    • 识别所需的模式或抽象
    • 检查强制的测试要求
    • 留意需要避免的已弃用方案
  3. 应用文档指导 - 按照context7的指导实现代码
    • 使用项目特定的工具和助手
    • 遵循已确立的架构模式
    • 匹配现有代码的风格和结构
    • 包含所需的元数据或注解
  4. 回退到通用模式 - 如果context7没有相关文档,使用本技能中的模式
    • 应用标准的Ember Octane约定
    • 遵循社区最佳实践
    • 使用本技能中的示例作为模板
请记住:context7文档始终优先于本技能的通用指导。

Context7 Query Examples

Context7查询示例

When implementing different types of Ember code, use these query patterns:
For Components:
- "ember component patterns"
- "ember component architecture"
- "glimmer component conventions"
- "[specific component type]" (e.g., "button component", "form component")
- "component props" or "component arguments"
- "component lifecycle"
- "component testing"
For Routes:
- "ember routing patterns"
- "ember route conventions"
- "route data loading"
- "route guards" or "route authentication"
- "nested routes"
- "query parameters"
For Ember Data:
- "ember data models"
- "ember data adapters"
- "ember data serializers"
- "api integration"
- "data relationships"
- "data layer patterns"
For Services:
- "ember services"
- "service patterns"
- "shared state management"
- "[specific service]" (e.g., "authentication service", "api service")
For Testing:
- "ember testing"
- "component testing"
- "integration tests"
- "acceptance tests"
- "test patterns"
- "test data" or "fixtures"
For Styling:
- "ember styling"
- "css conventions"
- "component styles"
- "tailwind" or "[css framework]"
When Editing Existing Code:
- Search for the specific feature: "user profile", "login form", etc.
- Search for the pattern you're implementing: "form validation", "dropdown menu"
- Search for utilities you might need: "validation utilities", "date helpers"
当实现不同类型的Ember代码时,使用以下查询模式:
针对组件:
- "ember component patterns"
- "ember component architecture"
- "glimmer component conventions"
- "[specific component type]" (e.g., "button component", "form component")
- "component props" or "component arguments"
- "component lifecycle"
- "component testing"
针对路由:
- "ember routing patterns"
- "ember route conventions"
- "route data loading"
- "route guards" or "route authentication"
- "nested routes"
- "query parameters"
针对Ember Data:
- "ember data models"
- "ember data adapters"
- "ember data serializers"
- "api integration"
- "data relationships"
- "data layer patterns"
针对服务:
- "ember services"
- "service patterns"
- "shared state management"
- "[specific service]" (e.g., "authentication service", "api service")
针对测试:
- "ember testing"
- "component testing"
- "integration tests"
- "acceptance tests"
- "test patterns"
- "test data" or "fixtures"
针对样式:
- "ember styling"
- "css conventions"
- "component styles"
- "tailwind" or "[css framework]"
编辑现有代码时:
- 搜索特定功能:"user profile", "login form"等
- 搜索你要实现的模式:"form validation", "dropdown menu"
- 搜索你可能需要的工具:"validation utilities", "date helpers"

Modern Ember Patterns (Octane+)

现代Ember模式(Octane+)

Glimmer Components

Glimmer组件

javascript
// app/components/user-profile.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class UserProfileComponent extends Component {
  @service currentUser;
  @service router;
  
  @tracked isEditing = false;
  @tracked formData = null;

  constructor(owner, args) {
    super(owner, args);
    // Use constructor for one-time setup
    this.formData = { ...this.args.user };
  }

  @action
  toggleEdit() {
    this.isEditing = !this.isEditing;
    if (this.isEditing) {
      this.formData = { ...this.args.user };
    }
  }

  @action
  async saveUser(event) {
    event.preventDefault();
    
    try {
      await this.args.onSave(this.formData);
      this.isEditing = false;
    } catch (error) {
      // Handle error
      console.error('Save failed:', error);
    }
  }

  @action
  updateField(field, event) {
    this.formData = {
      ...this.formData,
      [field]: event.target.value
    };
  }
}
handlebars
{{! app/components/user-profile.hbs }}
<div class="user-profile">
  {{#if this.isEditing}}
    <form {{on "submit" this.saveUser}}>
      <label>
        Name:
        <input 
          type="text" 
          value={{this.formData.name}}
          {{on "input" (fn this.updateField "name")}}
        />
      </label>
      
      <label>
        Email:
        <input 
          type="email" 
          value={{this.formData.email}}
          {{on "input" (fn this.updateField "email")}}
        />
      </label>
      
      <button type="submit">Save</button>
      <button type="button" {{on "click" this.toggleEdit}}>Cancel</button>
    </form>
  {{else}}
    <div class="profile-display">
      <h2>{{@user.name}}</h2>
      <p>{{@user.email}}</p>
      
      {{#if this.currentUser.canEdit}}
        <button {{on "click" this.toggleEdit}}>Edit Profile</button>
      {{/if}}
    </div>
  {{/if}}
</div>
javascript
// app/components/user-profile.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class UserProfileComponent extends Component {
  @service currentUser;
  @service router;
  
  @tracked isEditing = false;
  @tracked formData = null;

  constructor(owner, args) {
    super(owner, args);
    // Use constructor for one-time setup
    this.formData = { ...this.args.user };
  }

  @action
  toggleEdit() {
    this.isEditing = !this.isEditing;
    if (this.isEditing) {
      this.formData = { ...this.args.user };
    }
  }

  @action
  async saveUser(event) {
    event.preventDefault();
    
    try {
      await this.args.onSave(this.formData);
      this.isEditing = false;
    } catch (error) {
      // Handle error
      console.error('Save failed:', error);
    }
  }

  @action
  updateField(field, event) {
    this.formData = {
      ...this.formData,
      [field]: event.target.value
    };
  }
}
handlebars
{{! app/components/user-profile.hbs }}
<div class="user-profile">
  {{#if this.isEditing}}
    <form {{on "submit" this.saveUser}}>
      <label>
        Name:
        <input 
          type="text" 
          value={{this.formData.name}}
          {{on "input" (fn this.updateField "name")}}
        />
      </label>
      
      <label>
        Email:
        <input 
          type="email" 
          value={{this.formData.email}}
          {{on "input" (fn this.updateField "email")}}
        />
      </label>
      
      <button type="submit">Save</button>
      <button type="button" {{on "click" this.toggleEdit}}>Cancel</button>
    </form>
  {{else}}
    <div class="profile-display">
      <h2>{{@user.name}}</h2>
      <p>{{@user.email}}</p>
      
      {{#if this.currentUser.canEdit}}
        <button {{on "click" this.toggleEdit}}>Edit Profile</button>
      {{/if}}
    </div>
  {{/if}}
</div>

Tracked Properties and Getters

Tracked属性与Getters

javascript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { cached } from '@glimmer/tracking';

export default class DataGridComponent extends Component {
  @tracked sortColumn = 'name';
  @tracked sortDirection = 'asc';
  @tracked filterText = '';

  // Use @cached for expensive computations that depend on tracked properties
  @cached
  get filteredData() {
    const { filterText } = this;
    if (!filterText) return this.args.data;
    
    const lower = filterText.toLowerCase();
    return this.args.data.filter(item => 
      item.name.toLowerCase().includes(lower) ||
      item.email.toLowerCase().includes(lower)
    );
  }

  @cached
  get sortedData() {
    const data = [...this.filteredData];
    const { sortColumn, sortDirection } = this;
    
    return data.sort((a, b) => {
      const aVal = a[sortColumn];
      const bVal = b[sortColumn];
      const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
      return sortDirection === 'asc' ? result : -result;
    });
  }
}
javascript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { cached } from '@glimmer/tracking';

export default class DataGridComponent extends Component {
  @tracked sortColumn = 'name';
  @tracked sortDirection = 'asc';
  @tracked filterText = '';

  // Use @cached for expensive computations that depend on tracked properties
  @cached
  get filteredData() {
    const { filterText } = this;
    if (!filterText) return this.args.data;
    
    const lower = filterText.toLowerCase();
    return this.args.data.filter(item => 
      item.name.toLowerCase().includes(lower) ||
      item.email.toLowerCase().includes(lower)
    );
  }

  @cached
  get sortedData() {
    const data = [...this.filteredData];
    const { sortColumn, sortDirection } = this;
    
    return data.sort((a, b) => {
      const aVal = a[sortColumn];
      const bVal = b[sortColumn];
      const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
      return sortDirection === 'asc' ? result : -result;
    });
  }
}

Services

服务

javascript
// app/services/notification.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class NotificationService extends Service {
  @tracked notifications = [];
  
  @action
  add(message, type = 'info', duration = 5000) {
    const id = Date.now() + Math.random();
    const notification = { id, message, type };
    
    this.notifications = [...this.notifications, notification];
    
    if (duration > 0) {
      setTimeout(() => this.remove(id), duration);
    }
    
    return id;
  }
  
  @action
  remove(id) {
    this.notifications = this.notifications.filter(n => n.id !== id);
  }
  
  @action
  success(message, duration) {
    return this.add(message, 'success', duration);
  }
  
  @action
  error(message, duration = 10000) {
    return this.add(message, 'error', duration);
  }
  
  @action
  clear() {
    this.notifications = [];
  }
}
javascript
// app/services/notification.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class NotificationService extends Service {
  @tracked notifications = [];
  
  @action
  add(message, type = 'info', duration = 5000) {
    const id = Date.now() + Math.random();
    const notification = { id, message, type };
    
    this.notifications = [...this.notifications, notification];
    
    if (duration > 0) {
      setTimeout(() => this.remove(id), duration);
    }
    
    return id;
  }
  
  @action
  remove(id) {
    this.notifications = this.notifications.filter(n => n.id !== id);
  }
  
  @action
  success(message, duration) {
    return this.add(message, 'success', duration);
  }
  
  @action
  error(message, duration = 10000) {
    return this.add(message, 'error', duration);
  }
  
  @action
  clear() {
    this.notifications = [];
  }
}

Custom Modifiers

自定义修饰器

javascript
// app/modifiers/click-outside.js
import { modifier } from 'ember-modifier';

export default modifier((element, [callback]) => {
  function handleClick(event) {
    if (!element.contains(event.target)) {
      callback(event);
    }
  }
  
  document.addEventListener('click', handleClick, true);
  
  return () => {
    document.removeEventListener('click', handleClick, true);
  };
});
handlebars
{{! Usage }}
<div {{click-outside this.closeDropdown}} class="dropdown">
  {{! dropdown content }}
</div>
javascript
// app/modifiers/click-outside.js
import { modifier } from 'ember-modifier';

export default modifier((element, [callback]) => {
  function handleClick(event) {
    if (!element.contains(event.target)) {
      callback(event);
    }
  }
  
  document.addEventListener('click', handleClick, true);
  
  return () => {
    document.removeEventListener('click', handleClick, true);
  };
});
handlebars
{{! Usage }}
<div {{click-outside this.closeDropdown}} class="dropdown">
  {{! dropdown content }}
</div>

Routing

路由

Route Definitions

路由定义

javascript
// app/router.js
import EmberRouter from '@ember/routing/router';
import config from 'my-app/config/environment';

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

Router.map(function () {
  this.route('dashboard', { path: '/' });
  
  this.route('users', function () {
    this.route('index', { path: '/' });
    this.route('new');
    this.route('user', { path: '/:user_id' }, function () {
      this.route('edit');
      this.route('settings');
    });
  });
  
  this.route('not-found', { path: '/*path' });
});
javascript
// app/router.js
import EmberRouter from '@ember/routing/router';
import config from 'my-app/config/environment';

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

Router.map(function () {
  this.route('dashboard', { path: '/' });
  
  this.route('users', function () {
    this.route('index', { path: '/' });
    this.route('new');
    this.route('user', { path: '/:user_id' }, function () {
      this.route('edit');
      this.route('settings');
    });
  });
  
  this.route('not-found', { path: '/*path' });
});

Route Class

路由类

javascript
// app/routes/users/user.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class UsersUserRoute extends Route {
  @service store;
  @service router;

  async model(params) {
    try {
      return await this.store.findRecord('user', params.user_id, {
        include: 'profile,settings'
      });
    } catch (error) {
      if (error.errors?.[0]?.status === '404') {
        this.router.transitionTo('not-found');
      }
      throw error;
    }
  }

  // Redirect if user doesn't have permission
  afterModel(model) {
    if (!this.currentUser.canViewUser(model)) {
      this.router.transitionTo('dashboard');
    }
  }

  // Reset controller state on exit
  resetController(controller, isExiting) {
    if (isExiting) {
      controller.setProperties({
        queryParams: {},
        isEditing: false
      });
    }
  }
}
javascript
// app/routes/users/user.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class UsersUserRoute extends Route {
  @service store;
  @service router;

  async model(params) {
    try {
      return await this.store.findRecord('user', params.user_id, {
        include: 'profile,settings'
      });
    } catch (error) {
      if (error.errors?.[0]?.status === '404') {
        this.router.transitionTo('not-found');
      }
      throw error;
    }
  }

  // Redirect if user doesn't have permission
  afterModel(model) {
    if (!this.currentUser.canViewUser(model)) {
      this.router.transitionTo('dashboard');
    }
  }

  // Reset controller state on exit
  resetController(controller, isExiting) {
    if (isExiting) {
      controller.setProperties({
        queryParams: {},
        isEditing: false
      });
    }
  }
}

Loading and Error States

加载与错误状态

javascript
// app/routes/users/user/loading.js
import Route from '@ember/routing/route';

export default class UsersUserLoadingRoute extends Route {}
handlebars
{{! app/templates/users/user/loading.hbs }}
<div class="loading-spinner">
  <p>Loading user...</p>
</div>
javascript
// app/routes/users/user/loading.js
import Route from '@ember/routing/route';

export default class UsersUserLoadingRoute extends Route {}
handlebars
{{! app/templates/users/user/loading.hbs }}
<div class="loading-spinner">
  <p>Loading user...</p>
</div>

Ember Data

Ember Data

Models

模型

javascript
// app/models/user.js
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';

export default class UserModel extends Model {
  @attr('string') name;
  @attr('string') email;
  @attr('date') createdAt;
  @attr('boolean', { defaultValue: true }) isActive;
  @attr('number') loginCount;
  
  @belongsTo('profile', { async: true, inverse: 'user' }) profile;
  @hasMany('post', { async: true, inverse: 'author' }) posts;
  
  // Computed properties still work but use native getters
  get displayName() {
    return this.name || this.email?.split('@')[0] || 'Anonymous';
  }
  
  get isNewUser() {
    const daysSinceCreation = (Date.now() - this.createdAt) / (1000 * 60 * 60 * 24);
    return daysSinceCreation < 7;
  }
}
javascript
// app/models/user.js
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';

export default class UserModel extends Model {
  @attr('string') name;
  @attr('string') email;
  @attr('date') createdAt;
  @attr('boolean', { defaultValue: true }) isActive;
  @attr('number') loginCount;
  
  @belongsTo('profile', { async: true, inverse: 'user' }) profile;
  @hasMany('post', { async: true, inverse: 'author' }) posts;
  
  // Computed properties still work but use native getters
  get displayName() {
    return this.name || this.email?.split('@')[0] || 'Anonymous';
  }
  
  get isNewUser() {
    const daysSinceCreation = (Date.now() - this.createdAt) / (1000 * 60 * 60 * 24);
    return daysSinceCreation < 7;
  }
}

Custom Adapters

自定义适配器

javascript
// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';
import { inject as service } from '@ember/service';

export default class ApplicationAdapter extends JSONAPIAdapter {
  @service session;
  
  host = 'https://api.example.com';
  namespace = 'v1';

  get headers() {
    const headers = {};
    
    if (this.session.isAuthenticated) {
      headers['Authorization'] = `Bearer ${this.session.data.authenticated.token}`;
    }
    
    return headers;
  }

  handleResponse(status, headers, payload, requestData) {
    if (status === 401) {
      this.session.invalidate();
    }
    
    return super.handleResponse(status, headers, payload, requestData);
  }
}
javascript
// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';
import { inject as service } from '@ember/service';

export default class ApplicationAdapter extends JSONAPIAdapter {
  @service session;
  
  host = 'https://api.example.com';
  namespace = 'v1';

  get headers() {
    const headers = {};
    
    if (this.session.isAuthenticated) {
      headers['Authorization'] = `Bearer ${this.session.data.authenticated.token}`;
    }
    
    return headers;
  }

  handleResponse(status, headers, payload, requestData) {
    if (status === 401) {
      this.session.invalidate();
    }
    
    return super.handleResponse(status, headers, payload, requestData);
  }
}

Custom Serializers

自定义序列化器

javascript
// app/serializers/application.js
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class ApplicationSerializer extends JSONAPISerializer {
  // Normalize date strings to Date objects
  normalizeDateFields(hash) {
    const dateFields = ['createdAt', 'updatedAt', 'publishedAt'];
    
    dateFields.forEach(field => {
      if (hash[field]) {
        hash[field] = new Date(hash[field]);
      }
    });
    
    return hash;
  }

  normalize(modelClass, resourceHash) {
    this.normalizeDateFields(resourceHash.attributes || {});
    return super.normalize(modelClass, resourceHash);
  }
}
javascript
// app/serializers/application.js
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class ApplicationSerializer extends JSONAPISerializer {
  // Normalize date strings to Date objects
  normalizeDateFields(hash) {
    const dateFields = ['createdAt', 'updatedAt', 'publishedAt'];
    
    dateFields.forEach(field => {
      if (hash[field]) {
        hash[field] = new Date(hash[field]);
      }
    });
    
    return hash;
  }

  normalize(modelClass, resourceHash) {
    this.normalizeDateFields(resourceHash.attributes || {});
    return super.normalize(modelClass, resourceHash);
  }
}

Testing

测试

Component Integration Tests

组件集成测试

javascript
// tests/integration/components/user-profile-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | user-profile', function (hooks) {
  setupRenderingTest(hooks);

  test('it displays user information', async function (assert) {
    this.set('user', {
      name: 'Jane Doe',
      email: 'jane@example.com'
    });

    await render(hbs`<UserProfile @user={{this.user}} />`);

    assert.dom('h2').hasText('Jane Doe');
    assert.dom('p').hasText('jane@example.com');
  });

  test('it allows editing when canEdit is true', async function (assert) {
    this.owner.lookup('service:current-user').canEdit = true;
    this.set('user', {
      name: 'Jane Doe',
      email: 'jane@example.com'
    });
    this.set('onSave', () => {});

    await render(hbs`
      <UserProfile @user={{this.user}} @onSave={{this.onSave}} />
    `);

    await click('button:contains("Edit Profile")');
    
    assert.dom('form').exists();
    assert.dom('input[type="text"]').hasValue('Jane Doe');
    
    await fillIn('input[type="text"]', 'Jane Smith');
    await click('button[type="submit"]');
    
    assert.dom('form').doesNotExist();
  });
});
javascript
// tests/integration/components/user-profile-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | user-profile', function (hooks) {
  setupRenderingTest(hooks);

  test('it displays user information', async function (assert) {
    this.set('user', {
      name: 'Jane Doe',
      email: 'jane@example.com'
    });

    await render(hbs`<UserProfile @user={{this.user}} />`);

    assert.dom('h2').hasText('Jane Doe');
    assert.dom('p').hasText('jane@example.com');
  });

  test('it allows editing when canEdit is true', async function (assert) {
    this.owner.lookup('service:current-user').canEdit = true;
    this.set('user', {
      name: 'Jane Doe',
      email: 'jane@example.com'
    });
    this.set('onSave', () => {});

    await render(hbs`
      <UserProfile @user={{this.user}} @onSave={{this.onSave}} />
    `);

    await click('button:contains("Edit Profile")');
    
    assert.dom('form').exists();
    assert.dom('input[type="text"]').hasValue('Jane Doe');
    
    await fillIn('input[type="text"]', 'Jane Smith');
    await click('button[type="submit"]');
    
    assert.dom('form').doesNotExist();
  });
});

Route/Acceptance Tests

路由/验收测试

javascript
// tests/acceptance/user-flow-test.js
import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';

module('Acceptance | user flow', function (hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  test('visiting /users and creating a new user', async function (assert) {
    await visit('/users');
    
    assert.strictEqual(currentURL(), '/users');
    assert.dom('h1').hasText('Users');
    
    await click('a:contains("New User")');
    assert.strictEqual(currentURL(), '/users/new');
    
    await fillIn('[data-test-name-input]', 'John Doe');
    await fillIn('[data-test-email-input]', 'john@example.com');
    await click('[data-test-submit]');
    
    assert.strictEqual(currentURL(), '/users/1');
    assert.dom('[data-test-user-name]').hasText('John Doe');
  });
});
javascript
// tests/acceptance/user-flow-test.js
import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';

module('Acceptance | user flow', function (hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  test('visiting /users and creating a new user', async function (assert) {
    await visit('/users');
    
    assert.strictEqual(currentURL(), '/users');
    assert.dom('h1').hasText('Users');
    
    await click('a:contains("New User")');
    assert.strictEqual(currentURL(), '/users/new');
    
    await fillIn('[data-test-name-input]', 'John Doe');
    await fillIn('[data-test-email-input]', 'john@example.com');
    await click('[data-test-submit]');
    
    assert.strictEqual(currentURL(), '/users/1');
    assert.dom('[data-test-user-name]').hasText('John Doe');
  });
});

Performance Optimization

性能优化

See references/performance.md for comprehensive optimization strategies.
请参阅references/performance.md获取全面的优化策略。

Quick Reference

快速参考

ProblemSolution
Unnecessary component re-rendersUse
@cached
for expensive getters
Large listsUse
ember-collection
or virtual scrolling
Slow Ember Data queriesOptimize includes, use custom serializers
Bundle sizeUse route-based code splitting, lazy engines
Memory leaksProperly clean up in willDestroy, cancel timers
问题解决方案
不必要的组件重渲染对昂贵的getter使用
@cached
大型列表使用
ember-collection
或虚拟滚动
缓慢的Ember Data查询优化includes,使用自定义序列化器
包体积过大使用基于路由的代码分割、惰性引擎
内存泄漏在willDestroy中正确清理,取消定时器

Critical Anti-patterns

关键反模式

javascript
// ❌ Mutating tracked properties directly
this.items.push(newItem); // Won't trigger reactivity

// ✅ Replace the entire array
this.items = [...this.items, newItem];

// ❌ Creating new functions in templates
{{on "click" (fn this.handleClick item)}}

// ✅ Use actions or stable references
@action handleItemClick(item) { /* ... */ }
{{on "click" (fn this.handleItemClick item)}}

// ❌ Not using @cached for expensive computations
get expensiveComputation() {
  return this.data.filter(/* complex logic */);
}

// ✅ Use @cached
@cached
get expensiveComputation() {
  return this.data.filter(/* complex logic */);
}
javascript
// ❌ 直接修改tracked属性
this.items.push(newItem); // 不会触发响应式更新

// ✅ 替换整个数组
this.items = [...this.items, newItem];

// ❌ 在模板中创建新函数
{{on "click" (fn this.handleClick item)}}

// ✅ 使用actions或稳定引用
@action handleItemClick(item) { /* ... */ }
{{on "click" (fn this.handleItemClick item)}}

// ❌ 不对昂贵的计算使用@cached
get expensiveComputation() {
  return this.data.filter(/* complex logic */);
}

// ✅ 使用@cached
@cached
get expensiveComputation() {
  return this.data.filter(/* complex logic */);
}

Project Structure

项目结构

app/
├── components/           # Glimmer components
│   └── user-profile/
│       ├── component.js
│       ├── index.hbs
│       └── styles.css
├── controllers/          # Controllers (use sparingly in Octane)
├── helpers/             # Template helpers
├── modifiers/           # Custom modifiers
├── models/              # Ember Data models
├── routes/              # Route classes
├── services/            # Services
├── templates/           # Route templates
├── adapters/            # Ember Data adapters
├── serializers/         # Ember Data serializers
├── styles/              # Global styles
└── app.js

tests/
├── integration/         # Component tests
├── unit/               # Unit tests (models, services, etc.)
└── acceptance/         # Full application tests
app/
├── components/           # Glimmer组件
│   └── user-profile/
│       ├── component.js
│       ├── index.hbs
│       └── styles.css
├── controllers/          # 控制器(Octane中尽量少用)
├── helpers/             # 模板助手
├── modifiers/           # 自定义修饰器
├── models/              # Ember Data模型
├── routes/              # 路由类
├── services/            # 服务
├── templates/           # 路由模板
├── adapters/            # Ember Data适配器
├── serializers/         # Ember Data序列化器
├── styles/              # 全局样式
└── app.js

tests/
├── integration/         # 组件测试
├── unit/               # 单元测试(模型、服务等)
└── acceptance/         # 全应用测试

Tooling Recommendations

工具推荐

CategoryToolNotes
CLIember-cliOfficial tooling
TestingQUnit + ember-qunitBuilt-in, well integrated
LintingESLint + ember-template-lintCatch template issues
FormattingPrettierUse with ember-template-lint
Mockingember-cli-mirageAPI mocking for tests
State managementServices + trackedBuilt-in, no extra deps
HTTPfetch or ember-fetchNative or polyfilled
类别工具说明
CLIember-cli官方工具
测试QUnit + ember-qunit内置,集成良好
代码检查ESLint + ember-template-lint捕获模板问题
格式化Prettier与ember-template-lint配合使用
模拟ember-cli-mirage测试用API模拟
状态管理Services + tracked内置,无需额外依赖
HTTPfetch或ember-fetch原生或polyfill

Common Patterns

常见模式

Form Handling with Validation

带验证的表单处理

javascript
// app/components/registration-form.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class RegistrationFormComponent extends Component {
  @service notification;
  
  @tracked email = '';
  @tracked password = '';
  @tracked confirmPassword = '';
  @tracked errors = {};
  @tracked isSubmitting = false;

  get isValid() {
    return (
      this.email &&
      this.password.length >= 8 &&
      this.password === this.confirmPassword &&
      Object.keys(this.errors).length === 0
    );
  }

  @action
  updateEmail(event) {
    this.email = event.target.value;
    this.validateEmail();
  }

  @action
  updatePassword(event) {
    this.password = event.target.value;
    this.validatePassword();
  }

  @action
  updateConfirmPassword(event) {
    this.confirmPassword = event.target.value;
    this.validateConfirmPassword();
  }

  validateEmail() {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    if (!this.email) {
      this.errors = { ...this.errors, email: 'Email is required' };
    } else if (!emailRegex.test(this.email)) {
      this.errors = { ...this.errors, email: 'Invalid email format' };
    } else {
      const { email, ...rest } = this.errors;
      this.errors = rest;
    }
  }

  validatePassword() {
    if (this.password.length < 8) {
      this.errors = { ...this.errors, password: 'Password must be at least 8 characters' };
    } else {
      const { password, ...rest } = this.errors;
      this.errors = rest;
    }
    
    // Re-validate confirm password if it's filled
    if (this.confirmPassword) {
      this.validateConfirmPassword();
    }
  }

  validateConfirmPassword() {
    if (this.password !== this.confirmPassword) {
      this.errors = { ...this.errors, confirmPassword: 'Passwords do not match' };
    } else {
      const { confirmPassword, ...rest } = this.errors;
      this.errors = rest;
    }
  }

  @action
  async submit(event) {
    event.preventDefault();
    
    if (!this.isValid) return;
    
    this.isSubmitting = true;
    
    try {
      await this.args.onSubmit({
        email: this.email,
        password: this.password
      });
      
      this.notification.success('Registration successful!');
    } catch (error) {
      this.notification.error(error.message || 'Registration failed');
    } finally {
      this.isSubmitting = false;
    }
  }
}
javascript
// app/components/registration-form.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class RegistrationFormComponent extends Component {
  @service notification;
  
  @tracked email = '';
  @tracked password = '';
  @tracked confirmPassword = '';
  @tracked errors = {};
  @tracked isSubmitting = false;

  get isValid() {
    return (
      this.email &&
      this.password.length >= 8 &&
      this.password === this.confirmPassword &&
      Object.keys(this.errors).length === 0
    );
  }

  @action
  updateEmail(event) {
    this.email = event.target.value;
    this.validateEmail();
  }

  @action
  updatePassword(event) {
    this.password = event.target.value;
    this.validatePassword();
  }

  @action
  updateConfirmPassword(event) {
    this.confirmPassword = event.target.value;
    this.validateConfirmPassword();
  }

  validateEmail() {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    if (!this.email) {
      this.errors = { ...this.errors, email: '邮箱为必填项' };
    } else if (!emailRegex.test(this.email)) {
      this.errors = { ...this.errors, email: '邮箱格式无效' };
    } else {
      const { email, ...rest } = this.errors;
      this.errors = rest;
    }
  }

  validatePassword() {
    if (this.password.length < 8) {
      this.errors = { ...this.errors, password: '密码长度至少为8位' };
    } else {
      const { password, ...rest } = this.errors;
      this.errors = rest;
    }
    
    // 如果确认密码已填写,重新验证
    if (this.confirmPassword) {
      this.validateConfirmPassword();
    }
  }

  validateConfirmPassword() {
    if (this.password !== this.confirmPassword) {
      this.errors = { ...this.errors, confirmPassword: '密码不匹配' };
    } else {
      const { confirmPassword, ...rest } = this.errors;
      this.errors = rest;
    }
  }

  @action
  async submit(event) {
    event.preventDefault();
    
    if (!this.isValid) return;
    
    this.isSubmitting = true;
    
    try {
      await this.args.onSubmit({
        email: this.email,
        password: this.password
      });
      
      this.notification.success('注册成功!');
    } catch (error) {
      this.notification.error(error.message || '注册失败');
    } finally {
      this.isSubmitting = false;
    }
  }
}

Infinite Scroll with Modifier

基于修饰器的无限滚动

javascript
// app/modifiers/infinite-scroll.js
import { modifier } from 'ember-modifier';

export default modifier((element, [callback], { threshold = 200 }) => {
  let isLoading = false;
  
  function handleScroll() {
    if (isLoading) return;
    
    const { scrollTop, scrollHeight, clientHeight } = element;
    const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
    
    if (distanceFromBottom < threshold) {
      isLoading = true;
      callback().finally(() => {
        isLoading = false;
      });
    }
  }
  
  element.addEventListener('scroll', handleScroll, { passive: true });
  
  return () => {
    element.removeEventListener('scroll', handleScroll);
  };
});
javascript
// app/modifiers/infinite-scroll.js
import { modifier } from 'ember-modifier';

export default modifier((element, [callback], { threshold = 200 }) => {
  let isLoading = false;
  
  function handleScroll() {
    if (isLoading) return;
    
    const { scrollTop, scrollHeight, clientHeight } = element;
    const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
    
    if (distanceFromBottom < threshold) {
      isLoading = true;
      callback().finally(() => {
        isLoading = false;
      });
    }
  }
  
  element.addEventListener('scroll', handleScroll, { passive: true });
  
  return () => {
    element.removeEventListener('scroll', handleScroll);
  };
});

TypeScript Support

TypeScript支持

Ember has strong TypeScript support. Enable it with:
bash
ember install ember-cli-typescript
typescript
// app/components/user-profile.ts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import type { TOC } from '@ember/component/template-only';

interface UserProfileArgs {
  user: {
    name: string;
    email: string;
    avatarUrl?: string;
  };
  onSave: (data: UserData) => Promise<void>;
  canEdit?: boolean;
}

interface UserData {
  name: string;
  email: string;
}

export default class UserProfileComponent extends Component<UserProfileArgs> {
  @tracked isEditing = false;
  @tracked formData: UserData | null = null;

  @action
  async saveUser(event: SubmitEvent): Promise<void> {
    event.preventDefault();
    
    if (!this.formData) return;
    
    await this.args.onSave(this.formData);
    this.isEditing = false;
  }
}

// Template-only component signature
export interface GreetingSignature {
  Element: HTMLDivElement;
  Args: {
    name: string;
  };
}

const Greeting: TOC<GreetingSignature> = <template>
  <div ...attributes>Hello {{@name}}!</div>
</template>;

export default Greeting;
Ember对TypeScript有良好的支持。通过以下命令启用:
bash
ember install ember-cli-typescript
typescript
// app/components/user-profile.ts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import type { TOC } from '@ember/component/template-only';

interface UserProfileArgs {
  user: {
    name: string;
    email: string;
    avatarUrl?: string;
  };
  onSave: (data: UserData) => Promise<void>;
  canEdit?: boolean;
}

interface UserData {
  name: string;
  email: string;
}

export default class UserProfileComponent extends Component<UserProfileArgs> {
  @tracked isEditing = false;
  @tracked formData: UserData | null = null;

  @action
  async saveUser(event: SubmitEvent): Promise<void> {
    event.preventDefault();
    
    if (!this.formData) return;
    
    await this.args.onSave(this.formData);
    this.isEditing = false;
  }
}

// Template-only component signature
export interface GreetingSignature {
  Element: HTMLDivElement;
  Args: {
    name: string;
  };
}

const Greeting: TOC<GreetingSignature> = <template>
  <div ...attributes>Hello {{@name}}!</div>
</template>;

export default Greeting;

Remember: Always Use context7 MCP First!

请记住:始终先使用context7 MCP!

Before implementing any Ember code, query the context7 MCP for relevant project documentation. Context7 provides project-specific guidelines that always supersede these general best practices.
在实现任何Ember代码之前,请查询context7 MCP获取相关项目文档。Context7提供的项目特定指南始终优先于这些通用最佳实践。

Quick context7 Query Guide

快速context7查询指南

Before writing components:
  • Query: "ember component patterns", "glimmer component", "component architecture"
Before routing work:
  • Query: "ember routing", "route patterns", "navigation"
Before Ember Data:
  • Query: "ember data models", "api integration", "data layer"
Before tests:
  • Query: "ember testing", "test patterns", "test requirements"
When editing existing code:
  • Query the specific feature or pattern you're working with
The context7 MCP is your source of truth for this project's Ember conventions.
编写组件之前:
  • 查询:"ember component patterns", "glimmer component", "component architecture"
处理路由之前:
  • 查询:"ember routing", "route patterns", "navigation"
使用Ember Data之前:
  • 查询:"ember data models", "api integration", "data layer"
编写测试之前:
  • 查询:"ember testing", "test patterns", "test requirements"
编辑现有代码时:
  • 查询你正在处理的特定功能或模式
context7 MCP是你获取本项目Ember规范的权威来源。