Ameba Ownd(アメーバオウンド)のフロントエンドエンジニア、五藤(@ygoto3_)です。前々々々回前々々回に引き続き Ameba Ownd について、今回はフロントエンド開発のお話です。


本エントリーのアジェンダ:

  • はじめに
  • Ameba Ownd は3つのアプリケーションで構成
  • 再利用可能な機能単位でのパッケージング
  • 機能別のパッケージをまとめてモジュール化
  • 複数のアプリケーションで再利用可能な UI コンポーネント作成
  • 機能の独立性が高い大きなモジュールは非同期読み込みでファイル分割
  • 3つのアプリケーションで異なるルーティングの特徴
  • 階層が深いルーティング処理
  • 動的なルーティング生成
  • アプリケーション間の連携
  • おわりに

はじめに


Ameba Owndのフロントエンド開発は以下の技術を使用して開発しました。



Ameba Ownd は開発開始からリリースまで約8ヶ月の期間を経てリリースしましたが、初期開発メンバー全員が話し合い、当時みんなが(若干の偏見や趣向も交えながら)サービスにとってベストだと判断した技術を選択しました。


Ameba Ownd は3つのアプリケーションで構成



Ameba Ownd は「誰でもかんたんにオシャレなサイトが作ることができ、共有しあえるサービス」ですので、サイトを作成する機能閲覧する機能共有する機能、と大きく3つの機能のアプリケーションで構成されています。


  1. 作成する機能:サイト管理アプリケーション - https://m.amebaownd.com/
  2. 閲覧する機能:ユーザーが作成・公開した Web サイト - (例)スターバックス様の Ameba Ownd 公式ページ:https://starbucks.amebaownd.com/
  3. 共有する機能:Ameba Ownd 自体のポータル - https://www.amebaownd.com/

上記3つは Web ブラウザで使うアプリケーションですが、ここに iOS アプリAndroid アプリも加わり、Ameba Ownd という1つのサービスを構成しています。従って開発も大規模なものになりました。


本エントリーでは、Web ブラウザ版 Ameba Ownd を構成する3つのアプリケーションを開発する上でのフロントエンドの工夫について書いていきたいと思います。


再利用可能な機能単位でのパッケージング


機能単位でのパッケージング


3つのアプリケーションを同時に開発するのですが、3つ各々の役割は全く異なり、必要な機能も違います。


しかし、サービス全体で使いたい機能や共通のビジネスロジックもあり、共有できるところはできる限り共有したいです。開発当初は特定のアプリケーションでしか使わないはずだった機能が別のアプリケーションでも欲しくなったりすることもあります。


再利用可能なコードを増やすために機能単位で分けられたパッケージングをする必要がありました。


機能別のパッケージをまとめてモジュール化


モジュール構成


Ameba Ownd をパッケージごとに図にすると上記のようになっています。こういった構成を表現することが、AngularJS のモジュールシステムを利用したことで比較的簡単にできました。機能別に小さな単位でパッケージングしておいたものを、依存モジュールとして大きなモジュールに登録しておくことで1つの大きな単位の機能としてパッケージングすることが簡単にできます。


機能別のモジュールを、依存モジュールとして以下のように登録するだけで、別のモジュールにその機能を取り込むことができます。


angular.module('featureModule', []);


// featureModule を取り込む
angular.module('sharedModule', ['featureModule']);


// sharedModule を取り込むことにより featureModule も取り込んでいる
angular.module('mainModule', ['sharedModule']);

3アプリケーションをそれぞれ AngularJS のメインモジュールとして作成し、全アプリケーション共通で持たせたい機能は別モジュールとして作成します。これを3つのメインモジュールの依存モジュールとして読み込ませることで全てのアプリケーションから機能を呼び出せるようにしています。


複数のアプリケーションで再利用可能な UI コンポーネント作成


Ameba Ownd のディレクトリ構成は、コードを機能別にパッケージングして管理するために、モジュールごとにディレクトリが別れています。これら別れたファイルたちを依存関係を解決してバンドルする必要があります。バンドラとして利用しているのが WebPack です。


WebPack は Browserify とよく比較されるモジュールバンドラの1つですが、静的アセットを全て管理する目的で作られているため、比較的機能が盛り沢山という点で特徴的です。(WebPackについての詳細は、過去の 1 pixel の記事である RequireJS等はもう古い。WebPackとは?が参考になります。)


Ameba Ownd の UI の多くは、その UI に必要な JavaScript、HTML、CSS(に変換するための CoffeeScript、Jade、Stylus )を1つのディレクトリにパッケージングして管理しています。WebPack は JavaScript や CoffeeScript だけではなく、HTML、Jade、CSS、Stylus などもロードすることができるので、UI をコンポーネント化してまとめる役割をしてくれています。


NotificationSummary
├── index.coffee
├── indexSpec.coffee
├── template.jade
└── style.styl

1つのコンポーネントのディレクトリは上記のようなファイルが入っており、


module.exports = angular.module('components.notificationSummary', ['services.applyStyle'])

.directive 'notificationSummary', [

  'applyStyle'
  (applyStyle) ->
    return {
      restrict: 'E'
      replace: true

      # テンプレートファイルの読み込み
      template: require('jade!./template.jade')

      link: ->

        # スタイルファイルの読み込み
        style = require('!raw!stylus!./style.styl')
        applyStyle(style)

    }
]

上記のように CoffeeScript で書いた AngularJS の Directive 内で、Jade で書いたテンプレートや Stylus で書いたスタイルを読み込み、コンポーネントにバンドルさせます。このコンポーネントは必要なリソースを全て含んでいるので、別のアプリケーションからも再利用が可能になります。


再利用できるコンポーネント


また、WebPack は対象リソース用のローダーを別途追加することであらゆるリソースを JavaScript ファイルにバンドルできます。


サービスが大きくなってくると使用するサードパーティーライブラリも増えます。ある UI ライブラリが Sass などの Ameba Ownd では使用していない CSS プリプロセッサ用に実装されている場合もありましたが、そういった場合も言語の違いを気にせずに気軽にライブラリを試すことが可能です。


WebPack のおかげで、コンポーネント内で完結してる分には多少言語がバラついても影響範囲を最小限に抑えることができます。


また、将来的に現在 Ameba Ownd で採用している技術からほかの技術に移るような場合、例えば CoffeeScript から ES6 に変える必要があった場合にもコンポーネント単位でゆるやかに乗り換えていくことが可能なことも WebPack  を利用するメリットです。


独立性が高い大きなモジュールは非同期読み込みでファイル分割


開発が進むにつれて、サービスに必要な JavaScript のコードは膨大になり、それを1ファイルにまとめてしまうと複数ファイルに分かれている場合と違い並行ダウンロードができないため、結果的に読み込みに時間がかかってしまう場合があります。


Ameba Ownd の機能の中には、ユーザーがある画面に辿り着くまでは読み込む必要が全くない機能も存在します。こういった機能のモジュールは別ファイルとして非同期に読み込むことで、Ameba Ownd にランディングしたときの初期読み込みの負担を軽くすることができます。


WebPack では Browserify と同様に同期的なモジュールバンドルの機能も提供していますが、非同期的に別ファイルとして読み込むことが可能です。


非同期読み込み


例えば、自分のサイトのアクセス数を確認できる「アクセス統計」機能などです。この機能を提供するための JavaScript ファイルは、ユーザーが「アクセス統計」画面にアクセスするまで全く必要のないものなので、この機能を提供するモジュールだけ別ファイルとして遅延ロードさせる方が得策です。


AngularJS は通常、アプリケーションに必要な依存モジュールが先に登録された状態でメインモジュールが起動されます。しかし、非同期で依存モジュールを読み込んで追加する場合は、既にメインモジュールが起動した後なので、単純に追加しようとしても上手くいきません。


そこで、Ameba Ownd では、ocLazyLoad という Angular モジュールやコンポーネントを簡単に依存モジュールとして追加できるライブラリを使用しています。


$stateProvider.state 'sites.stats',
  url: '/stats'
  resolve:
    stats: [
      '$ocLazyLoad'
      '$q'
      (
        $ocLazyLoad
        $q
      ) ->
        return $ocLazyLoad(path_to_module).then(
          ->
            return null
          ->
            return $q.reject(key: 'Module load failure')
        )
    ]

例えば UI Router の resolve 機能と ocLazyLoad を組み合わせて、以上のようなコードで簡単に Angular モジュールを後読みすることができるので便利です。このコードではアクセス統計画面にルーティング処理されるときに、アクセス統計のモジュールを読み込み、それが完了した後にアクセス統計画面へ遷移されるようになっています。


3つのアプリケーションで異なるルーティングの特徴


3つのアプリケーションは、その役割の違いのため必要となる画面数やルーティングの特徴がそれぞれ異なります。


  • 作成する機能:サイト管理アプリケーション - 階層が深く、複雑なルーティング
  • 閲覧する機能:ユーザーが作成・公開した Web サイト - 階層は浅いが、動的なルーティング
  • 共有する機能:Ameba Ownd 自体のポータル - 階層も浅く、ルーティングもシンプル

Ameba Ownd は Single-page application(以下SPA)なのでフロントエンドでルーティング処理を行います。ルーティング処理はフレームワークである AngularJS を利用して行いますが、SPA でのルーティングを実現するオプションとして以下の3つがあります。



Angular New Router は開発初期当時の選択肢としては存在しなかったので省きますが、Ameba Ownd では画面遷移がそれほど複雑ではないアプリケーションについては ngRoute を使い、複雑なアプリケーションでは UI Router を使用しています。UI Router は ngRoute と比較して以下のようなメリットがあります。


  • 複数の view を表示できる
  • テンプレートを入れ子にできる
  • 親子関係の概念を持つステートにより、子は親のステートの設定を継承する

階層が深いルーティング処理


前述の通り、Ameba Ownd の3つのアプリケーションのうち、サイト管理の機能については、ルーティングが複雑です。まずアプリケーションの画面はペインが3つに分かれた構成になっているため、複数の view を別々に読み込む機能は必須です。


画面構成


ページを細かく作成するためのアプリケーションなので、より細かい編集を行う画面は必然的に階層が深くなります。そのとき親子関係を持ったルーティングの状態管理は必須になります。


例えば、個別のページを編集する機能を提供する画面では、該当のページを特定するためのページ ID が必要となりますが、下の階層でも同じページを編集する機能を提供するので、そのページ ID は引き続き必要です。


このとき UI Router のように、ページ編集機能を提供する画面のうち一番上の階層で設定した処理を下の階層も受け継ぐ機能を提供してくれると、格段にルーティングの管理が容易になります。


また画面に親子関係を設定できると、階層を意識させるためのトランジションアニメーションの設定も容易にできるようになります。


前述の通り、サイト管理アプリケーションは深いページ階層を持っています。そのため、ユーザーが現在いる階層を相対的に意識できるように、階層を下ったとき、上がったときをユーザーにフィードバックする必要があります。Ameba Ownd ではページ遷移時のサイドメニューのトランジションアニメーションでユーザーにフィードバックしています。


トランジション


上のように、階層を下ったときは右から新しいメニューが現在のメニューの上に被るように現れ、


トランジション


逆に階層を上がったときは現在のメニューが左に捌け、その捌けた下に新しいメニューがあるというアニメーションで表現しています。


このアニメーションのサイドメニューが動く方向は、UI Router で設定するステートの親子関係に管理することができます。しっかり URL 設計をしておけば URL 遷移時に、現在のステートと次のステートの親子関係をチェックするだけでアニメーションの方向を判定させることが可能です。


動的なルーティング生成


サイト管理アプリケーションは複雑な画面構造と深い階層構造を持ってるため、UI Router を使用してルーティングを設定しました。しかし、ユーザーが作成・公開した Web サイトと Ameba Ownd のポータルでは、深い階層構造などはないため、ngRoute を使用しています。これらは階層構造はシンプルなのですが、ユーザーが作成・公開した Web サイトに関しては、動的にルーティングを構築する必要があります。



Ameba Ownd では、ユーザーは自分のサイトに対して自由にページを追加・編集・削除することができます。ユーザーの Web サイトを表示するとき、それらのページデータをサーバーサイドから受け取り、ページの種類や選択されているテーマ情報から ngRoute に渡すルーティングデータを動的に生成します。


アプリケーション間の連携


それぞれ別のサイト管理アプリケーションとユーザーが作成したサイトを表示するアプリケーションは、連携します。


サイト管理アプリケーションとサイトを表示するアプリケーションは、お互いに iframe 間で通信するためのインターフェースを持っています。


アプリケーション間の連携


例えば、ユーザーがサイト管理アプリケーション側からサイトの背景色を変更するアクションを行った場合、背景色が変更することで影響される色を全て計算し、フォーマットに従った形で CSS 化したデータを postMessage で送ります。サイトを表示するアプリケーション側はその CSS データを受け取り、自身の head 要素にセットすることでサイトの色変更を行った際のリアルタイムプレビューが実現します。


おわりに


Ameba Ownd を構成する3つのアプリケーションを開発する上でのフロントエンドの工夫について書かせていただきました。


3つのアプリケーションを効率的に開発するために再利用性を高めるための機能のパッケージングや、異なる特徴を持ったルーティング処理、アプリケーション間の連携など、それぞれトライアンドエラーで開発をしてきた中でこういった工夫が生まれてきました。


フロントエンド開発をされている皆様にとって何かしら参考になれば幸いだと思っております。最後までお付き合いいただき、ありがとうございました。




フロントエンドエンジニア
五藤 佑典
@ygoto3_