Ember常用服务示例 #

一、认证服务 #

1.1 Session服务 #

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

const TOKEN_KEY = 'auth_token';
const USER_KEY = 'current_user';

export default class SessionService extends Service {
  @tracked isAuthenticated = false;
  @tracked currentUser = null;
  @tracked token = null;
  @tracked isLoading = false;

  constructor() {
    super(...arguments);
    this.restore();
  }

  restore() {
    const token = localStorage.getItem(TOKEN_KEY);
    const userJson = localStorage.getItem(USER_KEY);

    if (token && userJson) {
      this.token = token;
      this.currentUser = JSON.parse(userJson);
      this.isAuthenticated = true;
    }
  }

  @action
  async login(email, password) {
    this.isLoading = true;

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || '登录失败');
      }

      const data = await response.json();

      this.token = data.token;
      this.currentUser = data.user;
      this.isAuthenticated = true;

      localStorage.setItem(TOKEN_KEY, data.token);
      localStorage.setItem(USER_KEY, JSON.stringify(data.user));

      return data;
    } finally {
      this.isLoading = false;
    }
  }

  @action
  async logout() {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        headers: this.headers,
      });
    } finally {
      this.clearSession();
    }
  }

  clearSession() {
    this.token = null;
    this.currentUser = null;
    this.isAuthenticated = false;

    localStorage.removeItem(TOKEN_KEY);
    localStorage.removeItem(USER_KEY);
  }

  get headers() {
    const headers = {
      'Content-Type': 'application/json',
    };

    if (this.token) {
      headers.Authorization = `Bearer ${this.token}`;
    }

    return headers;
  }

  get isAdmin() {
    return this.currentUser?.role === 'admin';
  }
}

1.2 使用认证服务 #

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

export default class ApplicationRoute extends Route {
  @service session;

  beforeModel() {
    if (this.session.isAuthenticated) {
      this.session.restore();
    }
  }
}
javascript
// app/routes/admin.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class AdminRoute extends Route {
  @service session;
  @service router;

  beforeModel(transition) {
    if (!this.session.isAuthenticated) {
      this.session.set('attemptedTransition', transition);
      this.router.transitionTo('login');
    } else if (!this.session.isAdmin) {
      this.router.transitionTo('forbidden');
    }
  }
}

二、主题服务 #

2.1 Theme服务 #

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

const THEME_KEY = 'app_theme';

export default class ThemeService extends Service {
  @tracked current = 'light';

  themes = ['light', 'dark', 'auto'];

  constructor() {
    super(...arguments);
    this.loadTheme();
    this.watchSystemTheme();
  }

  loadTheme() {
    const stored = localStorage.getItem(THEME_KEY);
    if (stored && this.themes.includes(stored)) {
      this.current = stored;
    }
    this.applyTheme();
  }

  watchSystemTheme() {
    if (window.matchMedia) {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
      mediaQuery.addEventListener('change', (e) => {
        if (this.current === 'auto') {
          this.applyTheme();
        }
      });
    }
  }

  @action
  setTheme(theme) {
    if (this.themes.includes(theme)) {
      this.current = theme;
      localStorage.setItem(THEME_KEY, theme);
      this.applyTheme();
    }
  }

  @action
  toggle() {
    const newTheme = this.current === 'light' ? 'dark' : 'light';
    this.setTheme(newTheme);
  }

  applyTheme() {
    const effectiveTheme = this.effectiveTheme;
    document.documentElement.setAttribute('data-theme', effectiveTheme);
  }

  get effectiveTheme() {
    if (this.current === 'auto') {
      return this.systemTheme;
    }
    return this.current;
  }

  get systemTheme() {
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      return 'dark';
    }
    return 'light';
  }

  get isDark() {
    return this.effectiveTheme === 'dark';
  }
}

2.2 使用主题服务 #

handlebars
{{! app/components/theme-toggle.hbs}}
<button {{on "click" this.theme.toggle}}>
  {{if this.theme.isDark "🌙" "☀️"}}
</button>

<select {{on "change" this.handleChange}}>
  {{#each this.theme.themes as |theme|}}
    <option value={{theme}} selected={{eq this.theme.current theme}}>
      {{theme}}
    </option>
  {{/each}}
</select>

三、通知服务 #

3.1 Notification服务 #

javascript
// app/services/notifications.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later, cancel } from '@ember/runloop';

export default class NotificationsService extends Service {
  @tracked items = [];
  defaultTimeout = 5000;

  @action
  add(message, options = {}) {
    const notification = {
      id: Date.now(),
      message,
      type: options.type || 'info',
      timeout: options.timeout ?? this.defaultTimeout,
      dismissible: options.dismissible ?? true,
      timestamp: new Date(),
    };

    this.items = [...this.items, notification];

    if (notification.timeout > 0) {
      this.scheduleDismiss(notification);
    }

    return notification;
  }

  scheduleDismiss(notification) {
    later(this, () => {
      this.remove(notification.id);
    }, notification.timeout);
  }

  @action
  remove(id) {
    this.items = this.items.filter((n) => n.id !== id);
  }

  @action
  clear() {
    this.items = [];
  }

  success(message, options = {}) {
    return this.add(message, { ...options, type: 'success' });
  }

  error(message, options = {}) {
    return this.add(message, { ...options, type: 'error' });
  }

  warning(message, options = {}) {
    return this.add(message, { ...options, type: 'warning' });
  }

  info(message, options = {}) {
    return this.add(message, { ...options, type: 'info' });
  }
}

3.2 使用通知服务 #

javascript
// app/controllers/posts/new.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class PostsNewController extends Controller {
  @service notifications;
  @service router;

  @action
  async savePost(post) {
    try {
      await post.save();
      this.notifications.success('文章保存成功');
      this.router.transitionTo('posts.show', post.id);
    } catch (error) {
      this.notifications.error('保存失败:' + error.message);
    }
  }
}

四、本地存储服务 #

4.1 Storage服务 #

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

export default class StorageService extends Service {
  @tracked data = {};

  constructor() {
    super(...arguments);
    this.load();
  }

  load() {
    try {
      const stored = localStorage.getItem('app_data');
      if (stored) {
        this.data = JSON.parse(stored);
      }
    } catch (e) {
      console.error('Failed to load from storage:', e);
    }
  }

  save() {
    try {
      localStorage.setItem('app_data', JSON.stringify(this.data));
    } catch (e) {
      console.error('Failed to save to storage:', e);
    }
  }

  get(key, defaultValue = null) {
    return this.data[key] ?? defaultValue;
  }

  set(key, value) {
    this.data = { ...this.data, [key]: value };
    this.save();
  }

  remove(key) {
    const { [key]: _, ...rest } = this.data;
    this.data = rest;
    this.save();
  }

  clear() {
    this.data = {};
    localStorage.removeItem('app_data');
  }
}

五、API服务 #

5.1 API服务 #

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

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

  baseUrl = '/api';

  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;

    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
    };

    if (this.session.isAuthenticated) {
      config.headers.Authorization = `Bearer ${this.session.token}`;
    }

    if (options.body && typeof options.body === 'object') {
      config.body = JSON.stringify(options.body);
    }

    try {
      const response = await fetch(url, config);

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || '请求失败');
      }

      return await response.json();
    } catch (error) {
      this.notifications.error(error.message);
      throw error;
    }
  }

  get(endpoint, params = {}) {
    const query = new URLSearchParams(params).toString();
    const url = query ? `${endpoint}?${query}` : endpoint;
    return this.request(url, { method: 'GET' });
  }

  post(endpoint, body = {}) {
    return this.request(endpoint, { method: 'POST', body });
  }

  put(endpoint, body = {}) {
    return this.request(endpoint, { method: 'PUT', body });
  }

  delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

六、总结 #

常用服务模式:

服务 用途
Session 用户认证
Theme 主题管理
Notifications 消息通知
Storage 本地存储
API API请求

服务是Ember应用中管理共享状态和功能的最佳方式。

最后更新:2026-03-28