20
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularAdvent Calendar 2016

Day 21

Angular アプリ開発の分類 & Angular 用 UI フレームワークを作った話

Last updated at Posted at 2016-12-21

この記事は Angular Advent Calendar 2016 21日目の記事です。

今年の下半期に angular2-onsenui という UI フレームワークを作ったので仕様をざっくりと紹介します。
前置きとして、Angular アプリ開発における UI 構築のパターンと、各パターンでの CSS フレームワーク・UI フレームワークとの付き合い方について述べます。

はじめに

Angular は今年9月に安定版(2.0.0)のリリースを迎え、もはや破壊的変更に右往左往することはなくなりました。加えて、TypeScript、デコレータ、階層的 DI、animations、Zone.js、RxJS、AoT コンパイル、Angular CLI、サーバサイドレンダリング (SSR) などといった Angular の周辺概念についてもこの半年でずいぶんと日本語の記事が増えました。それらを使いこなすための Tips もまとまってきました。

その気になればどんなコンポーネントでも作れるしどんなアプリでも作れる、という段階に至った人は多いのではないかと思います。

しかし全てをフルスクラッチで実装するのは無謀です。そういった人はライブラリを使って楽をすることを考えるのが次のステップになると思います。来年はその手の話が増えていくのではないかと思います。

ライブラリを使って楽をする対象にはバックエンド(クラウドサービス、オンプレミスサーバ、...)との連携や Flux アーキテクチャの実装などがありますが、この記事では UI の構築を楽にすることについて述べます。

UI の構成要素

UI を構成する要素は多岐に渡りますが、
この記事ではひとまず UI の構成要素を

  • スタイル:  CSS だけで実装できる部分
  • UI ロジック:  CSS だけでは実装できない部分(=複雑な視覚効果, UI 状態管理, ...)

の2つに抽象化して考えることにします(どちらもこの記事用の造語なのでご注意ください)。

スタイルと UI ロジックそれぞれに対する要求の違いによって、Angular アプリ開発は以下の4つのパターンに分類できます。

Angular アプリ開発のパターン

パターン1: 「スタイルは自前で用意したい」「UI ロジックも自前で用意したい」

デザインに関して要求の多い、PC サイトの受託開発や自社開発などで多く採用されていると思われるパターンです。

Angular は CSS のカプセル化により冗長な CSS コーディング規約から解放され、比較的簡単にスタイルを自前実装できる上、UI ロジックの自前実装についてもマルチタッチなどの複雑な入力イベントを使わない限りはほぼ困ることが無い程度にフレームワークとして至れり尽くせりなので、Angular の表現力を確かめるのも兼ねてこのパターンを採用している人が多いのではないかと思います。

しかし複雑な UI ロジックの自前実装は車輪の再発明になりがちで、バグの温床にもなります。ある程度複雑な UI ロジックが必要になってきた場合は後述のパターン4に移行することで UI の構築を楽にするのが得策です。

パターン2: 「スタイルはライブラリに頼りたい」「UI ロジックは自前で用意したい」

デザインよりもアプリケーションロジックが重要な PC サイト / モバイルサイトで採用されるパターンです。業務アプリの開発では主にこのパターンが使用されるのではないかと思います。

Angular を使っているとパターン1で述べた理由によりスタイルも全てフルスクラッチで書いてみたくなるところですが、特にスタイルにこだわりが無い場合は CSS フレームワークを利用するのが得策です。CSS に苦手意識を持っている人は是非 styles を書く手を一旦止めて、CSS フレームワークを利用してみると良いのではないかと思います。

(CSS フレームワークと聞くと Bootstrap を思い浮かべるかもしれませんが、今では他にも色々な選択肢があります。Qiita 内でたくさん紹介されていますので是非チェックしてみてください。)

パターン3: 「スタイルはライブラリに頼りたい」「UI ロジックもライブラリに頼りたい」

モバイルサイト開発や Cordova アプリ開発において、モバイル端末特有の操作(=スワイプ、ピンチイン・アウト、マルチタッチ、etc.)のサポートが必要な場合に選択することになるパターンです。

スクリーンショット 2016-12-21 22.56.16.png

angular.io のトップページに One framework. Mobile & desktop. と記載されているように、Angular はモバイルサイトでの利用も想定されています。

しかし、スワイプイベントやマルチタッチイベントは本質的に複雑であり、Angular の力を持ってしても自前での実装は地獄絵図です。例えば、まともなカルーセルやスライディングメニューを実装するには threshold やスワイプの加速度などを考慮して実装を行う必要があります。

また、モバイルブラウザで 60 fps のアニメーションを実現するには dirty hack が少なからず必要になります。

そんなモバイルサイトの開発において、パターン1やパターン2を採用するのは無謀です。
(PC サイトでもある程度複雑な UI ロジックを扱いたい場合はパターン1やパターン2は無謀です)


パターン3では、CSS フレームワークの代わりに UI フレームワークを利用することになります。CSS フレームワークがスタイルのみを提供する一方で、UI フレームワークはスタイルと UI ロジックの両方を提供します。すなわち UI フレームワークを使うと、スタイルと UI ロジックの両方で自前実装の必要がなくなり、楽ができます。

しかし UI フレームワークは CSS フレームワークと異なり、極端に選択肢が少なくなります。特定の JS フレームワークに特化した UI ロジックの提供は、CSS フレームワークに比べて極めてメンテナンスコストが大きいためです。

そんな数少ない Angular 用の UI フレームワークの中の有名どころとしては、PC/モバイル両用の Material Design for Angular 2 、また、PC/モバイル両用だがモバイル寄りの Ionic2 があります。あとは拙作の Onsen UI 2 があります。

そこら辺の話は 8 日目の @rdlabo さんの記事によくまとまっていますので、是非そちらをご覧ください。

【初心者向け】Angular2と一緒に使う有名なサードパーティを見回してみる – Tech Tech Kansai|テクテク関西
http://tec-kansai.tech/technology-2569.php

パターン4: 「スタイルは自前で用意したい」「UI ロジックはライブラリに頼りたい」

スタイルに関してライブラリから制約は受けたくないが、UI ロジックの自前実装はとてもじゃないがやっていられないので UI ロジックに関してはライブラリの恩恵を受けたいという場合に採用されるパターンです。

しかし「スタイルは提供せず UI ロジックのみを提供する」というようなライブラリは今のところ存在しないので、このパターンでは UI フレームワークに同梱されているスタイルを改造・上書きすることになります。

このとき、UI フレームワークの許容範囲を超えた改造を行うとスタイル周りのバグに悩まされることになりますが、それでもパターン1よりは最終的な工数は減ります。

ここまでのまとめ

以上、Angular アプリ開発の4つのパターンを紹介しました。
パターン1を採用している人は是非ともパターン2〜パターン4の採用を検討してみてください。

以下は余談です。

angular2-onsenui を作りました

今年の下半期に angular2-onsenui というパッケージを作ったので仕様をざっくりと紹介します(補足: 開発のほとんどは私ではなく @anatoo が行いました)。

angular2-onsenui は Angular から Onsen UI 2 を利用するためのパッケージです。パターン 3, 4 での利用を想定しています。

作れるもの

こんな感じのものが作れます。

img_3_trimmed.png

利用方法

npm で onsenuiangular2-onsenui をインストールし、OnsenModule をロードすると利用できます。
詳しい手順については @rdlabo さんが Angular CLI での利用方法をまとめてくれているので省略します。

monaca-cliはやめよう!OnsenUI2を使うにはangular-cliではじめた方がいい理由と使い方 – Tech Tech Kansai|テクテク関西
http://tec-kansai.tech/technology-2584.php

ドキュメントは以下の場所にあります。

使用可能なコンポーネント

OnsenModule をロードすると下記のセレクタで Onsen UI 2 のコンポーネントを利用できるようになります。

1.0.0-rc.3 時点での仕様です

種別 セレクタ コンポーネントクラス *1 コンポーネントファクトリクラス *2
ページマネージャ ons-navigator OnsNavigator なし
ページマネージャ ons-splitter なし *3 なし
ページマネージャ ons-splitter-side OnsSplitterSide なし
ページマネージャ ons-splitter-content OnsSplitterContent なし
ページマネージャ ons-tabbar なし なし
ページマネージャ ons-tab OnsTab なし
ページ ons-page なし なし
ページ ons-toolbar なし なし
ページ ons-toolbar-button なし なし
ページ ons-back-button なし なし
ページ ons-bottom-toolbar なし なし
モーダルウィンドウ ons-modal なし ModalFactory
ダイアログ ons-alert-dialog なし AlertDialogFactory
ダイアログ ons-dialog なし DialogFactory
ダイアログ ons-popover なし PopoverFactory
アイコン ons-icon なし なし
ウィジェット ons-pull-hook OnsPullHook なし
ウィジェット ons-carousel なし なし
ウィジェット ons-carousel-item なし なし
ウィジェット ons-list なし なし
ウィジェット ons-list-header なし なし
ウィジェット ons-list-item なし なし
ウィジェット ons-lazy-repeat OnsLazyRepeat なし
ウィジェット ons-input OnsInput なし
ウィジェット ons-button なし
ウィジェット ons-range OnsRange なし
ウィジェット ons-switch OnsSwitch なし
ウィジェット ons-fab なし なし
ウィジェット ons-speed-dial なし なし
ウィジェット ons-speed-dial-item なし なし
ウィジェット ons-progress-bar なし なし
ウィジェット ons-progress-circular なし なし
条件分岐 ons-if なし なし
グリッドレイアウト ons-row なし なし
グリッドレイアウト ons-col なし なし
タッチイベント ons-gesture-detector なし なし
視覚効果 ons-ripple なし なし

*1 厳密には各コンポーネントクラスは @Component ではなく @Directive を用いて実装しています。Onsen UI は @Componenttemplate ではなく Custom Elements V1connectedCallback, attributeChangedCallback フックを使ってテンプレートを append しているため、@Component を使う必要がなかったためです。

*2 モーダルウィンドウやダイアログのような動的に生成する必要のあるコンポーネントにはコンポーネントファクトリを用意しています。

*3 Input, Output もメソッドも不要なコンポーネントに対してはコンポーネントクラスを用意していません。

使い方その1: ウィジェット

ウィジェット系コンポーネントは特に何も考えずにテンプレート内に <ons-button (click)="onClick()">push</ons-button> 等を配置するだけで使えます。

import {
  Component,
  OnsenModule,
  NgModule,
  CUSTOM_ELEMENTS_SCHEMA
} from 'angular2-onsenui';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

declare var alert: Function;

@Component({
  selector: 'app',
  template: `
    <ons-button (click)="onClick()">push</ons-button>
    `
})
export class AppComponent {
  constructor() {
  }

  onClick() {
    alert('Clicked!');
  }
}

@NgModule({
  imports: [OnsenModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

動作サンプル (Plunker):
https://plnkr.co/edit/rySiuIg8JF3bOn33Gn0S

スクリーンショット 2016-12-22 2.22.28.png

使い方その2: ページマネージャ

ページマネージャ系コンポーネントの使い方は若干複雑になります。
以下では ons-navigator での例を示します。

まず、以下のコードでは DefaultPageComponent クラスと Page2Component クラスで「ページ」を定義し、それを ons-navigator 要素の Input に与えることでページを表示しています。
各ページ(実体はコンポーネント)は動的に生成する必要があるため、NgModule の entryComponentsDefaultPageComponent クラスと Page2Component クラスを事前に登録しています。
ページコンポーネントのセレクタには ons-page[page2]ons-page[default] のように属性の指定も行われていますが、これは ons-page を使用すると複数のページを定義した時にセレクタが衝突し、エラーを生じるためです。

各ページでは、DI で取得した OnsNavigator インスタンスのメソッドを叩いてページ遷移を行っています。this._navigator.element.popPage(); という文から分かるように、このサンプルは DOM 要素に紐づけられたメソッドを直接叩いていますが、これは内部で使用している Custom Elements V1 の仕様に由来するものです。

import {
  Component,
  ViewChild,
  Params,
  OnsNavigator,
  OnsenModule,
  NgModule,
  CUSTOM_ELEMENTS_SCHEMA
} from 'angular2-onsenui';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

@Component({
  selector: 'ons-page[page2]',
  template: `
    <ons-toolbar>
      <div class="left"><ons-back-button>Back</ons-back-button></div>
      <div class="center">Page2</div>
    </ons-toolbar>
    <div class="content">
      <div style="text-align: center; margin: 10px">
        <ons-button (click)="push()">push</ons-button>
        <ons-button id="pop" (click)="pop()">pop</ons-button>
      </div>
    </div>
  `
})
export class Page2Component {
  constructor(private _navigator: OnsNavigator, private _params: Params) {
  }

  push() {
    this._navigator.element.pushPage(Page2Component, {animation: 'slide');
  }

  pop() {
    this._navigator.element.popPage();
  }
}

@Component({
  selector: 'ons-page[default]',
  template: `
    <ons-toolbar>
      <div class="center">Page</div>
    </ons-toolbar>
    <div class="content">
      <div style="text-align: center; margin: 10px">
        <ons-button id="push" (click)="push(navi)">push</ons-button>
      </div>
    </div>
  `
})
class DefaultPageComponent {
  constructor(private _navigator: OnsNavigator) {
  }

  push() {
    this._navigator.element.pushPage(Page2Component);
  }
}

@Component({
  selector: 'app',
  template: `
  <ons-navigator animation="slide" [page]="defaultPage"></ons-navigator>
  `
})
export class AppComponent {
  defaultPage = DefaultPageComponent
}

@NgModule({
  imports: [OnsenModule],
  declarations: [AppComponent, DefaultPageComponent, Page2Component],
  entryComponents: [DefaultPageComponent, Page2Component],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

動作サンプル (Plunker):
https://plnkr.co/edit/MoLFcdTRTbD7C7v8A69C

3466c1cfa0fed7622006cc862e6ec9fa.gif

使い方その3: モーダルウィンドウ, ダイアログ

モーダルウィンドウやダイアログのような、アプリのコンポーネントツリー内に属するべきでないコンポーネントは、コンポーネントファクトリを用いて動的に生成して使用します。

以下の例ではダイアログを動的に生成し、ボタン押下をトリガーとしてダイアログを表示しています。

ダイアログの動的生成を行うには、まず ons-dialog 要素をテンプレートに持つ MyDialogComponent クラスをセレクタ無しで定義し、NgModule の entryComponents に登録しておきます。次に、DI で取得した DialogFactory インスタンスの createDialog メソッドを実行します。すると生成された ons-dialog 要素(+ダイアログコンポーネントを破棄するための関数)が非同期に返ってきます。後は this._dialog.show(); すればダイアログが表示されます。

angular2-onsenuiDialogFactory 以外にも AlertDialogFactory, PopoverFactory, ModalFactory を提供しています。

コンポーネントファクトリを使用する際は、ngOnDestroy 時に this._destroyDialog(); 等でコンポーネントを破壊するのを忘れないでください。

import {
  Component,
  DialogFactory,
  AfterViewInit,
  OnInit,
  OnDestroy,
  Params,
  OnsenModule,
  NgModule,
  CUSTOM_ELEMENTS_SCHEMA
} from 'angular2-onsenui';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

@Component({
  template: `
    <ons-dialog animation="default" cancelable #dialog>
      <div class="dialog-mask"></div>
      <div class="dialog">
        <div class="dialog-container" style="height: 200px;">
          <ons-page>
            <ons-toolbar>
              <div class="center">Name</div>
            </ons-toolbar>
            <div class="content">
              <div style="text-align: center">
                <p>{{message}}</p>
                <br>
                <ons-button id="close" (click)="dialog.hide()">Close</ons-button>
              </div>
            </div>
          </ons-page>
        </div>
      </div>
    </ons-dialog>
  `
})
class MyDialogComponent {
  message = '';

  constructor(params: Params) {
    this.message = <string>params.at('message');
  }
}

@Component({
  selector: 'app',
  template: `
  <ons-page class="page">
    <ons-toolbar>
      <div class="center">Dialog</div>
    </ons-toolbar>
    <div class="content">
      <div style="text-align: center;">
        <br>
        <ons-button id="open" (click)="show()">Open</ons-button>
      </div>
    </div>
  </ons-page>
  `
})
export class AppComponent implements OnInit, OnDestroy {
  private _dialog: any;
  private _destroyDialog: Function;

  constructor(private _dialogFactory: DialogFactory) {
  }

  ngOnInit() {
    this._dialogFactory
      .createDialog(MyDialogComponent, {message: 'This is just an example.'})
      .then(({dialog, destroy}) => {
        this._dialog = dialog;
        this._destroyDialog = destroy;
      });
  }

  show() {
    if (this._dialog) {
      this._dialog.show();
    }
  }

  ngOnDestroy() {
    this._destroyDialog();
  }
}

@NgModule({
  imports: [OnsenModule],
  declarations: [AppComponent, MyDialogComponent],
  bootstrap: [AppComponent],
  entryComponents: [MyDialogComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

動作サンプル (Plunker):
https://plnkr.co/edit/ZAu5BstNGklPIYliVBtt

75b785084c8d0956734ce599c7546b91.gif

まとめ

UI の構成要素をスタイルと UI ロジックの2つに分け、それによって定義できる4つの Angular アプリ開発のパターンを紹介しました。

また、拙作のパッケージ angular2-onsenui の仕様と使い方をざっくりと紹介しました。

余談

Onsen UI の開発目的についてよく聞かれるのでお答えしておくと、Onsen UI は特に直接的な利益に結びつけるために開発しているものではありません(というか OSS 全般がそうだと思います)。
元々自社サービス用に内製していたものをどうせなので OSS として公開した、という感じです。

詳しい経緯については Onsen UI のプロジェクトリーダーの @massie が記事を書いているのでそちらをご覧ください。

Onsen UIが生まれたきっかけ : アシアルブログ
http://blog.asial.co.jp/1490

20
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?