javascript-ember
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExpert 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 implementationIf 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
核心原则
- Embrace Ember Octane - Use Glimmer components, tracked properties, and native classes
- Convention over configuration - Follow Ember's resolver patterns and file structure
- Use the platform - Prefer native JavaScript features over framework-specific abstractions when possible
- Composition through services and modifiers - Extract reusable logic into services and UI behavior into modifiers
- Data down, actions up (DDAU) - Maintain clear data flow patterns
- 拥抱Ember Octane - 使用Glimmer组件、tracked属性和原生类
- 约定优于配置 - 遵循Ember的解析器模式和文件结构
- 使用平台特性 - 尽可能优先使用原生JavaScript特性,而非框架特定抽象
- 通过服务和修饰器实现组合 - 将可复用逻辑提取到服务中,将UI行为提取到修饰器中
- 数据向下,动作向上(DDAU) - 保持清晰的数据流模式
Context7 MCP Workflow
Context7 MCP工作流程
Before implementing any Ember code, follow this workflow:
-
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")
-
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
-
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
-
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代码之前,请遵循以下工作流程:
-
查询context7 - 使用特定术语搜索相关文档:
- 组件类型(例如:"ember glimmer component")
- 功能领域(例如:"ember routing", "ember data")
- 特定模式(例如:"form validation", "authentication")
-
审查结果 - 仔细阅读返回的所有文档
- 记录项目特定的命名约定
- 识别所需的模式或抽象
- 检查强制的测试要求
- 留意需要避免的已弃用方案
-
应用文档指导 - 按照context7的指导实现代码
- 使用项目特定的工具和助手
- 遵循已确立的架构模式
- 匹配现有代码的风格和结构
- 包含所需的元数据或注解
-
回退到通用模式 - 如果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
<div class="user-profile">
<form >
<label>
Name:
<input
type="text"
value=
/>
</label>
<label>
Email:
<input
type="email"
value=
/>
</label>
<button type="submit">Save</button>
<button type="button" >Cancel</button>
</form>
<div class="profile-display">
<h2></h2>
<p></p>
<button >Edit Profile</button>
</div>
</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
<div class="user-profile">
<form >
<label>
Name:
<input
type="text"
value=
/>
</label>
<label>
Email:
<input
type="email"
value=
/>
</label>
<button type="submit">Save</button>
<button type="button" >Cancel</button>
</form>
<div class="profile-display">
<h2></h2>
<p></p>
<button >Edit Profile</button>
</div>
</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
<div class="dropdown">
</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
<div class="dropdown">
</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
<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
<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
快速参考
| Problem | Solution |
|---|---|
| Unnecessary component re-renders | Use |
| Large lists | Use |
| Slow Ember Data queries | Optimize includes, use custom serializers |
| Bundle size | Use route-based code splitting, lazy engines |
| Memory leaks | Properly clean up in willDestroy, cancel timers |
| 问题 | 解决方案 |
|---|---|
| 不必要的组件重渲染 | 对昂贵的getter使用 |
| 大型列表 | 使用 |
| 缓慢的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 testsapp/
├── 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
工具推荐
| Category | Tool | Notes |
|---|---|---|
| CLI | ember-cli | Official tooling |
| Testing | QUnit + ember-qunit | Built-in, well integrated |
| Linting | ESLint + ember-template-lint | Catch template issues |
| Formatting | Prettier | Use with ember-template-lint |
| Mocking | ember-cli-mirage | API mocking for tests |
| State management | Services + tracked | Built-in, no extra deps |
| HTTP | fetch or ember-fetch | Native or polyfilled |
| 类别 | 工具 | 说明 |
|---|---|---|
| CLI | ember-cli | 官方工具 |
| 测试 | QUnit + ember-qunit | 内置,集成良好 |
| 代码检查 | ESLint + ember-template-lint | 捕获模板问题 |
| 格式化 | Prettier | 与ember-template-lint配合使用 |
| 模拟 | ember-cli-mirage | 测试用API模拟 |
| 状态管理 | Services + tracked | 内置,无需额外依赖 |
| HTTP | fetch或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-typescripttypescript
// 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-typescripttypescript
// 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规范的权威来源。