10年前のEmberアプリが今も動く理由
はじめに
フロントエンドのフレームワークって、数年もすれば書き方が様変わりして、過去のコードが動かなくなる……そんな経験、ありませんか。ReactもVueも、メジャーバージョンが上がるたびに移行作業に追われた人は少なくないはずです。
Emberは、その悩みに正面から向き合ってきたフレームワークです。2011年の登場から10年以上、「Stability without Stagnation(停滞なき安定性)」を掲げ、古いアプリを壊さずに新機能を取り込み続けています。npmパッケージember-sourceは現在バージョン7系まで進化していますが、その裏側では初期のEmberアプリが今なお動き続けているという実績があります。
派手さでは新興フレームワークに劣るかもしれません。でも、長期間メンテナンスするプロダクトを作るなら、この「壊れなさ」は何よりの武器になります。
特徴・メリット
1. Convention over Configuration(規約優先の設計)
Emberの根幹にあるのが「設定より規約」という思想です。
- ファイル名やディレクトリ構成に従うだけで、ルーティング・データ・テンプレートが自動的に結びつく
ember generateコマンドでコンポーネントやルートの雛形を一発生成- 「どう設計するか」に悩む時間が減り、チーム全員が同じ構造でコードを書ける
ReactやVueのように「状態管理は何を使う?」「ルーティングライブラリは?」と毎回選定する必要がないのは、地味に大きなメリットです。
2. 圧倒的な後方互換性
- LTS(長期サポート)リリースを定期的に提供し、各バージョンは30週間のセキュリティ修正対応
- Deprecationの仕組みが整備されており、非推奨機能は警告を出しつつ動作し続ける
- 10年近く前に書かれたアプリケーションでも、最新版へのアップグレードパスが用意されている
新機能を追加するたびに古いAPIを容赦なく壊す一部のフレームワークとは対照的なアプローチです。
3. Octane以降のモダンな書き方
2019年の「Octane」エディション以降、Emberの書き味は大きく刷新されました。
- Glimmerコンポーネント: 軽量なコンポーネントモデル
- Autotracking:
@trackedを付けたプロパティの変更を自動検知してUIを更新 - ネイティブクラス構文とデコレータ:
@tracked、@action、@serviceなどで宣言的に記述 - First-Class Component Templates(
.gjs/.gts): JavaScript/TypeScriptとテンプレートを1ファイルに統合
4. Ember DataとURL駆動のルーティング
- ルートの
model()フックでURLとデータ取得を自然に結びつける - ローディング中・エラー時の表示(loading substates / error substates)が規約として組み込まれている
- Ember Dataがモデル定義・API通信・キャッシュを一貫して扱う
5. アドオンエコシステム
ember-cliのアドオン機構により、認証・PDF生成・チャートなど、品質評価付きの拡張機能が豊富に揃っています。車輪の再発明をせずに機能追加できるのは、実務でありがたいポイントです。
インストール方法
まずはEmber CLIをグローバルにインストールします。
npm install -g ember-cli
バージョン確認:
ember --version
新規プロジェクトの作成
ember new my-ember-app
cd my-ember-app
ember serve
http://localhost:4200でアプリケーションが起動します。プロジェクト作成時には、TypeScript対応やCSSプリプロセッサなどをオプションで選択できます。
ember new my-ember-app --typescript
既存プロジェクトへのアドオン追加
ember install ember-data
ember install ember-cli-mirage # APIモックサーバー
ember install ember-fetch # fetchベースのHTTP通信
基本的な使い方
コンポーネントはember generateコマンドで雛形を作れます。
ember generate component greeting
Ember Octane以降の書き方では、JavaScriptのクラスとテンプレートを1つの.gjsファイルにまとめる「First-Class Component Templates」が使えます。@trackedが状態を、{{on}}修飾子がイベントを扱う中心的なAPIです。
// app/components/counter.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
export default class Counter extends Component {
@tracked count = 0;
@action
increment() {
this.count++;
}
<template>
<p>カウント: {{this.count}}</p>
<button {{on "click" this.increment}}>+1</button>
</template>
}
@trackedを付けたプロパティが変わると、それを参照しているテンプレートだけが自動的に再描画されます。React でいうuseState、Vueでいうrefに近い感覚ですが、Emberでは依存関係の宣言(useMemoのような明示的な依存配列)が不要な「Autotracking」という仕組みで実現されています。
実践的なユースケース
1. ルーティングとデータ取得
EmberはURLとデータをmodel()フックで結びつけるのが特徴です。ルートに入る前にデータ取得が完了し、テンプレート側は取得済みのデータをそのまま扱えます。
// app/router.js
import EmberRouter from '@ember/routing/router';
import config from 'my-ember-app/config/environment';
export default class Router extends EmberRouter {
location = config.locationType;
rootURL = config.rootURL;
}
Router.map(function () {
this.route('posts');
this.route('post', { path: '/posts/:post_id' });
});
// app/routes/post.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class PostRoute extends Route {
@service store;
async model(params) {
return this.store.findRecord('post', params.post_id);
}
}
post_idが見つからない場合や通信中の表示は、post-loading.gjsやpost-error.gjsといった規約に沿ったファイルを置くだけで自動的に切り替わります。
2. Ember Dataによるモデル定義
APIから取得するリソースは、Ember DataのModelとして宣言します。属性やリレーションを書くだけで、シリアライズ・キャッシュ・更新検知がひとまとまりになります。
// app/models/post.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr title;
@attr body;
@attr('date') publishedAt;
@hasMany('comment') comments;
}
// コンポーネント側での利用
import Component from '@glimmer/component';
import { service } from '@ember/service';
export default class NewPostForm extends Component {
@service store;
createPost = async (title, body) => {
const post = this.store.createRecord('post', { title, body });
await post.save();
};
}
this.store.findRecord()やcreateRecord()はキャッシュを内部で管理するため、同じレコードを複数箇所で参照しても不要なAPI呼び出しが発生しません。
3. サービスによる状態共有
コンポーネントをまたいで状態を共有したいときは、EmberのServiceを使います。DIコンテナに登録されたシングルトンとして、@serviceデコレータでどこからでも注入できます。
// app/services/cart.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class CartService extends Service {
@tracked items = [];
get totalPrice() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
addItem(item) {
const existing = this.items.find((i) => i.id === item.id);
if (existing) {
existing.quantity++;
// 配列内オブジェクトの変更を検知させるため参照を更新
this.items = [...this.items];
return;
}
this.items = [...this.items, { ...item, quantity: 1 }];
}
clear() {
this.items = [];
}
}
// app/components/cart-summary.gjs
import Component from '@glimmer/component';
import { service } from '@ember/service';
export default class CartSummary extends Component {
@service cart;
<template>
<p>合計金額: {{this.cart.totalPrice}}円</p>
<button {{on "click" this.cart.clear}}>カートを空にする</button>
</template>
}
グローバルな状態をServiceに集約しておけば、ヘッダーのカートアイコンとカート画面本体のように、離れたコンポーネント同士でも同じ状態を安全に共有できます。
まとめ
Emberは、派手な新機能競争から一歩引いた場所で、「壊れないこと」を価値として磨き続けてきたフレームワークです。
個人的に感じるEmberの良さをまとめると次のとおりです。
- 迷わない: Convention over Configurationで、ルーティング・データ・テンプレートの結びつき方に悩まない
- 壊れない: LTSリリースとdeprecationの仕組みで、長期運用に強い
- 進化している: Octane以降のGlimmerコンポーネントとAutotrackingで、書き味はモダン
- 一貫している: Ember DataとURL駆動のルーティングで、データ取得の設計に迷いがない
短期間で作って壊すプロトタイプにはオーバースペックかもしれません。でも、5年後・10年後も同じチームがメンテナンスし続けるプロダクトを作るなら、Emberの「レールに乗った安定感」は真剣に検討する価値があります。