Ember依赖注入 #

一、依赖注入概述 #

Ember使用依赖注入(DI)模式来管理组件和服务之间的依赖关系。

1.1 DI的优势 #

  • 解耦组件和服务
  • 便于测试
  • 单例管理
  • 延迟初始化

1.2 DI架构 #

text
┌─────────────────────────────────────────────────────────┐
│                    Application                           │
├─────────────────────────────────────────────────────────┤
│                    Registry                              │
│    ┌─────────┐  ┌─────────┐  ┌─────────┐               │
│    │Service A│  │Service B│  │Service C│               │
│    └─────────┘  └─────────┘  └─────────┘               │
│         ▲              ▲              ▲                 │
│         │              │              │                 │
│    ┌────┴────┐    ┌────┴────┐    ┌────┴────┐          │
│    │Component│    │Component│    │Controller│          │
│    └─────────┘    └─────────┘    └─────────┘          │
└─────────────────────────────────────────────────────────┘

二、服务注入 #

2.1 基本注入 #

javascript
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service session;
  @service store;
  @service router;
}

2.2 注入别名 #

javascript
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service('session') auth;
  @service('shopping-cart') cart;
}

2.3 注入选项 #

javascript
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service({
    name: 'session',
    as: 'auth',
  })
  session;
}

三、注入目标 #

3.1 组件注入 #

javascript
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class UserAvatarComponent extends Component {
  @service session;

  get user() {
    return this.session.currentUser;
  }
}

3.2 控制器注入 #

javascript
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default class ApplicationController extends Controller {
  @service session;
  @service theme;
}

3.3 路由注入 #

javascript
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class ApplicationRoute extends Route {
  @service session;
  @service notifications;
}

3.4 服务注入服务 #

javascript
import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default class ApiService extends Service {
  @service session;
  @service config;

  get headers() {
    return {
      Authorization: `Bearer ${this.session.token}`,
      'X-API-Key': this.config.apiKey,
    };
  }
}

四、初始化器 #

4.1 创建初始化器 #

bash
ember generate initializer session

4.2 注册服务 #

javascript
// app/initializers/session.js
export function initialize(application) {
  application.register('service:session', SessionService);
}

export default {
  name: 'session',
  initialize,
};

4.3 注入服务 #

javascript
// app/initializers/session.js
export function initialize(application) {
  // 注册服务
  application.register('service:session', SessionService);

  // 自动注入到所有组件
  application.inject('component', 'session', 'service:session');

  // 自动注入到所有路由
  application.inject('route', 'session', 'service:session');

  // 自动注入到所有控制器
  application.inject('controller', 'session', 'service:session');
}

export default {
  name: 'session',
  initialize,
};

五、实例初始化器 #

5.1 创建实例初始化器 #

bash
ember generate instance-initializer session

5.2 实例初始化器 #

javascript
// app/instance-initializers/session.js
export function initialize(applicationInstance) {
  const session = applicationInstance.lookup('service:session');

  // 从本地存储恢复会话
  session.restore();
}

export default {
  name: 'session',
  initialize,
};

5.3 初始化器与实例初始化器区别 #

类型 执行时机 用途
Initializer 应用启动时 注册、全局注入
Instance Initializer 实例创建时 查找服务、初始化状态

六、查找服务 #

6.1 使用lookup #

javascript
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class ApplicationRoute extends Route {
  @service session;

  beforeModel() {
    // 手动查找服务
    const store = this.owner.lookup('service:store');
    const config = this.owner.lookup('service:config');
  }
}

6.2 在组件中查找 #

javascript
import Component from '@glimmer/component';
import { getOwner } from '@ember/application';

export default class MyComponent extends Component {
  constructor(owner, args) {
    super(owner, args);

    // 获取owner
    const appOwner = getOwner(this);

    // 查找服务
    const session = appOwner.lookup('service:session');
  }
}

七、依赖关系 #

7.1 服务依赖链 #

javascript
// app/services/api.js
import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default class ApiService extends Service {
  @service session;
  @service config;
}

// app/services/auth.js
import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default class AuthService extends Service {
  @service api;
  @service session;
}

// app/services/user.js
import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default class UserService extends Service {
  @service auth;
  @service store;
}

7.2 循环依赖 #

避免循环依赖:

javascript
// 错误 - 循环依赖
// Service A → Service B → Service A

// 解决方案:使用事件或共享状态

八、测试与DI #

8.1 注入测试服务 #

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

module('Integration | Component | my-component', function (hooks) {
  setupRenderingTest(hooks);

  hooks.beforeEach(function () {
    // 注册测试服务
    this.owner.register('service:session', MockSessionService);
  });

  test('it renders', async function (assert) {
    await render(hbs`<MyComponent />`);
    assert.dom(this.element).hasText('');
  });
});

8.2 查找服务进行测试 #

javascript
test('uses session service', async function (assert) {
  const session = this.owner.lookup('service:session');
  session.login({ name: 'Test User' });

  await render(hbs`<UserMenu />`);

  assert.dom().includesText('Test User');
});

九、最佳实践 #

9.1 显式注入 #

javascript
// 好的做法 - 显式注入
export default class MyComponent extends Component {
  @service session;
  @service store;
}

// 避免 - 全局注入过多
// 只在初始化器中注入真正需要全局访问的服务

9.2 避免过度注入 #

javascript
// 好的做法 - 按需注入
export default class MyComponent extends Component {
  @service session;
}

// 避免 - 注入不需要的服务
export default class MyComponent extends Component {
  @service session;
  @service store;
  @service router;
  @service theme;
  // ...
}

9.3 服务命名 #

javascript
// 好的命名
@service session;
@service shoppingCart;
@service apiClient;

// 避免
@service s;
@service cart;

十、总结 #

依赖注入要点:

概念 说明
@inject 注入装饰器
lookup 手动查找
Initializer 应用初始化
Instance Initializer 实例初始化

理解依赖注入是掌握Ember架构的关键。

最后更新:2026-03-28