技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
ember【数据与研究】Use this skill when the user says 'Ember.js', 'Ember app', 'Ember setup', 'Ember CLI', 'Ember Data', 'Ember Octane', 'Ember component', 'Ember route', 'Ember service', or when building ambitious web applications with Ember.js. This skill enforces: convention over configuration, Ember CLI for code generation, Ember Data for state management, Octane pa…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build ambitious web applications using Ember's convention-over-configuration approach — Ember CLI for scaffolding, Ember Data for state management, and Octane-era patterns for modern components.
Exact user phrases: "Ember setup", "Ember app", "Ember CLI", "Ember Data", "Ember Octane", "Ember component", "Ember route", "Ember service", "Ember project", "ember.js".
Before activating, verify:
No file output. Produces code snippets and structural guidance as text.
Code with Ember conventions:
// app/routes/index.ts
import Route from '@ember/routing/route'
import { service } from '@ember/service'
No preamble. No postamble. No explanations. Compress output — why use many token when few do trick.
~4096 tokens.
| Approach | Trade-off | When to Use |
|---|---|---|
| Glimmer component (native class) | Full lifecycle, tracked state | Interactive components with logic |
| Template-only (.hbs) | Zero JS, pure display | Static UI, presentational |
| Classic component | Legacy, deprecated | Old code, migration path only |
| Custom modifier | DOM interaction only | Event listeners, scroll handling |
| Helper function | Pure transformations | Formatting, math, string utils |
What is the component's purpose?
Pure display (no logic, no state) -> Template-only component (.hbs only)
Display with local state -> Glimmer component (native class + @tracked)
Encapsulated behavior + template -> Classic component (deprecated, avoid)
Reusable DOM interaction -> Custom modifier (use:)
Contextual helper -> Helper function (pure, no state)
Where does the data live?
Per-component, ephemeral -> @tracked property on the component
Shared across routes -> Route model hook + service caching
Global application state -> Service (@service injection)
Persisted server data -> Ember Data store (this.store.findAll)
URL state -> Query params on controller
Where is the data needed?
Single route -> model() hook in route
Multiple routes sharing data -> Parent route model + modelFor on child
Global (user, session) -> Service loaded in application route
Lazy / on-demand -> this.store.findRecord in component (Octane)
Real-time -> Ember Concurrency or tracked tasks
// app/routes/posts.ts
import Route from '@ember/routing/route'
import { service } from '@ember/service'
export default class PostsRoute extends Route {
@service declare store: Store
async model(params: { page: string }) {
const page = parseInt(params.page) || 1
return this.store.findAll('post', { page })
}
}
// app/components/post-card.ts
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import { action } from '@ember/object'
interface Args {
post: Post
onSelect: (id: string) => void
}
export default class PostCard extends Component<Args> {
@tracked isExpanded = false
get excerpt() {
return this.args.post.body.length > 100
? this.args.post.body.slice(0, 100) + '...'
: this.args.post.body
}
@action
toggleExpand() {
this.isExpanded = !this.isExpanded
}
@action
select() {
this.args.onSelect(this.args.post.id)
}
}
{{! app/components/post-card.hbs }}
<article class="post-card">
<h2>{{@post.title}}</h2>
<p>{{if this.isExpanded @post.body this.excerpt}}</p>
<button type="button" {{on "click" this.toggleExpand}}>
{{if this.isExpanded "Show less" "Show more"}}
</button>
</article>
{{! app/components/ui/button.hbs }}
<button class="btn btn--{{@variant}}" type="button" ...attributes>
{{yield}}
</button>
// app/services/post-store.ts
import Service from '@ember/service'
import { service } from '@ember/service'
import { tracked } from '@glimmer/tracking'
export default class PostStoreService extends Service {
@service declare store: Store
@tracked currentPostId: string | null = null
get currentPost() {
return this.currentPostId
? this.store.peekRecord('post', this.currentPostId)
: null
}
async findByTag(tag: string) {
return this.store.query('post', { tag })
}
async createPost(data: Partial<Post>) {
const post = this.store.createRecord('post', data)
await post.save()
return post
}
}
// app/modifiers/click-outside.ts
import Modifier from 'ember-modifier'
interface Args {
positional: []
named: { action: () => void }
}
export default class ClickOutsideModifier extends Modifier<Args> {
private handler: ((e: MouseEvent) => void) | null = null
didReceiveArguments() {
this.handler = (e: MouseEvent) => {
if (!this.element.contains(e.target as Node)) {
this.args.named.action()
}
}
document.addEventListener('click', this.handler, true)
}
willRemove() {
if (this.handler) {
document.removeEventListener('click', this.handler, true)
}
}
}
// Fetching
const posts = await this.store.findAll('post')
const post = await this.store.findRecord('post', id)
const results = await this.store.query('post', { category: 'tech' })
// Creating
const post = this.store.createRecord('post', { title: 'New', body: '...' })
await post.save()
// Updating
post.title = 'Updated'
await post.save()
// Deleting
await post.destroyRecord()
Services are singletons injected with @service. Use @tracked for reactive properties:
@service declare auth: AuthService
@service declare cart: CartService
// In template: {{this.auth.user.name}}
// app/controllers/posts.ts
import Controller from '@ember/controller'
import { tracked } from '@glimmer/tracking'
export default class PostsController extends Controller {
@tracked page = 1
queryParams = ['page']
}
ember-cli-bundle-analyzer to inspect bundle composition.tracked over computed for local state — computed has caching overhead.ember-concurrency's dropTask for rapidly firing events (typeahead).ember-concurrency's restartableTask.ember-collection or vertical-collection.package.json periodically.ember-auto-import for npm package imports.ember-cli-code-coverage for tracking unused code.ember-engines for large feature areas.ember build --environment=production enables minification and tree-shaking.// tests/integration/components/post-card-test.ts
import { module, test } from 'qunit'
import { setupRenderingTest } from 'ember-qunit'
import { render, click } from '@ember/test-helpers'
import { hbs } from 'ember-cli-htmlbars'
module('Integration | Component | post-card', function (hooks) {
setupRenderingTest(hooks)
test('it toggles expanded state', async function (assert) {
this.set('title', 'Test Post')
this.set('body', 'Test body')
await render(hbs`<PostCard @title={{this.title}} @body={{this.body}} />`)
assert.dom('article').exists()
await click('h2')
assert.dom('p').hasText('Test body')
})
})
module('Acceptance | posts', function (hooks) {
setupApplicationTest(hooks)
test('visiting /posts loads data', async function (assert) {
await visit('/posts')
assert.dom('[data-test-post]').exists({ count: 10 })
})
})
ember-qunit with @ember/test-helpers for DOM interaction.ember-cli-mirage for mocking Ember Data responses.settled() after async operations to wait for rendering.| Classic | Octane |
|---|---|
EmberObject.extend() | Native class extends Component |
computed() | @tracked + getter |
.observes() | @tracked + @action |
didInsertElement | {{did-insert}} modifier |
this.set('prop', val) | this.prop = val |
{{my-component}} | <MyComponent /> |
this._super() | No equivalent (native class) |
Ember.Component | @glimmer/component |
Migration order: 1) Update Ember CLI to v3.15+, 2) Convert components one by one, 3) Replace computed with @tracked, 4) Replace observers with native getters, 5) Use ember-cli-update for automated migration.
| React Pattern | Ember Equivalent |
|---|---|
useState | @tracked property |
useEffect | Modifiers ({{did-insert}}, {{did-update}}) |
useContext | @service injection |
| Props | @arg (named args in .hbs template) |
| JSX | Handlebars (.hbs) templates |
React.memo | Template-only component (no JS class) |
set() is for classic mode. Octane uses this.property = value.this.prop = value.| Aspect | Ember | React | Vue |
|---|---|---|---|
| Architecture | Convention over config | Library + choices | Framework |
| Build tool | Ember CLI | Vite/CRA | Vite/Vue CLI |
| State mgmt | Ember Data + Services | Zustand/Redux | Pinia/Vuex |
| Templating | Handlebars (.hbs) | JSX/TSX | .vue SFC |
| Routing | Built-in, config | React Router | Vue Router |
| TypeScript | First-class since v4 | Optional | Via vue-tsc |
| Learning curve | Steep | Moderate | Moderate |
ember generate component, ember generate routeember-cli-mirage — API mockingember-concurrency — async task managementember-truth-helpers — boolean template helpersember-composable-helpers — functional helpersember-test-selectors — data-test-* stripped from productionember-cli-update — automated migrationember-template-lint — template lintingember-cli-bundle-analyzer — bundle analysis@tracked for reactive properties, never set().@action decorator for event handlers.@service.app/ folder convention: routes/, components/, services/, models/, modifiers/.No artifact produced. Next skill: ember-data (if complex data layer) or frontend-testing. Carry forward: route/service pattern, Glimmer component conventions, @tracked/@action.
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
Does the component need lifecycle hooks or DOM access?
├── No → Template-only component (Glimmer) - fastest, no JS class
└── Yes → Does it manage state shared across components?
├── Yes → Service + component pattern
└── No → Classic Glimmer component with @tracked
Is the DOM interaction imperative (drag/drop, canvas)?
├── Yes → Use modifier (ember-modifier)
└── No → Use component lifecycle (constructor, willDestroy)
Is the route data-driven (model-dependent)?
├── No → Static route with template content
└── Yes → Does the model come from API?
├── Yes → Route model() hook with ember-data or fetch
└── No → Route model() with local computation
Are there multiple async dependencies?
├── Yes → RSVP.hash() in model() with parallel loading
└── No → Single async model() return
{{someProperty}} for auto-escaped output. For trusted HTML, use {{{htmlContent}}} only after sanitization via ember-cli-htmlbars or DOMPurify. Never triple-stash user content.<meta name="csrf-token" content="...">. For non-ember-data requests, read the meta tag and include in headers.config/content-security-policy.js. Ember's ember-cli-build can inject meta CSP tags. Set script-src 'self' and use nonces for inline scripts in production.ember-cli-deprecation-workflow to track deprecations. Use npm audit or yarn audit in CI. Pin major dependency versions. Avoid deprecated Ember addons without active maintenance.