のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

AngularJS x Webpack を使った構成の紹介

AngularJSとWebpackを上手いこと連携させてメンテナンスしやそうな構成を試行錯誤したので、 現状の記録と整理を兼ねて記事にしてみる。

結構いいかんじなアプローチなのではないかと思いつつ、まだ詰め切れていないところも多いので今後ブラッシュアップを重ねていきたい。

関連技術

要点

  • 役割でディレクトリを分ける
  • 汎用化出来そうな仕組みはアプリケーションの外に置く
  • DDDを意識してビジネスロジックを構築する
  • Webpackのローダーを活用する
  • Webpackのrequireをそこそこに活用する
  • $injectorを活用する
  • Angular UI-Routerの設定をもとにViewのディレクトリを作る

役割でディレクトリを分ける

AngularJSだと controller, service, config, directive のように機能でディレクトリを分けるのがスタンダードのようだけど、メンテナンスをしているとあっちいったりこっちいったりで結構面倒臭かったり、考えをごっちゃにしがちであまり使い勝手がよくなかった。

そんなわけでJSの役割を幾つかに別けて、それごとにディレクトリを切るようにしてみた。

  • app/config
    • アプリケーションの設定
  • app/domains
    • ビジネスロジックかつ見た目にかかわらない情報
    • DDDに影響を受けていたのでドメインと呼称している
    • 名前はともかく、controllerの肥大化を避けられる
  • app/views
    • 見た目に関する部分、 controllertemplate を内包する
    • view-modelとdomainを controller で連携させる
  • 汎用モジュール

汎用化出来そうな仕組みはアプリケーションの外に置く

モーダルやローディングなど汎用的に使え、アプリケーションに依存するものを設定に切り出せるようなモジュールは基本的に app (アプリケーションの本体)とは別のディレクトリにおく。

外出ししやすくなるし、管理を別けれるので一度FIXすれば後は気にしなくていい。

ディレクティブやView周りのproviderはだいたい汎用化出来そうなので、適当なプリフィクスを振って、 xx-modal みたいな感じでディレクトリを切ってそこに置くようにしている。

DDDを意識してビジネスロジックを組む

DDDというか、意識していることは以下。

  • 単一責務の原則
    • 一つのクラスが持つ役割はひとつ
    • 複数持つようなら分ける
  • Aggregateパターン
    • 複数のユーザストーリーを一つのクラスに集約することでViewから参照するクラスを制限する
    • しかし責務は分かれてるようにしているので相互影響は最小。最悪クラスの差し替えも想定してる
  • Repositoryパターン
    • APIやStorageへのアクセスを抽象化するようにしている
    • $resourceを使うことが多いけど、時には$localStorageなども使いたい
    • ビジネスロジック的にはどっちも同じように扱いたいので、localStorageを一段wrapして$resourceと同じような振る舞いをするようにしていたりする
    • 中途半端感あるのでこれはブラッシュアップしていきたいところ

いままであまりJavaScriptOOPを意識してこなかったのだけど、単一責務の原則をしっかりまもって、I/Fをある程度制約かければ結構取り回しが効いて良さそうな感じはする。 ただ、どのくらいの規模までハンドルできるかはちょっと見えてないので、今後の発展を見たい。

Webpackのローダーを活用する

CoffeeScriptコンパイルはもちろんだが、何より嬉しいのはHTMLをJavaScriptの文字列として展開できること。 これによって、 grunt-ng-template のようなライブラリは不要になったし、遷移のたびにXHRを投げる必要もなくなった。

UI-Routerと連携しつつ、templateファイルを require することで文字列をそのままわたせる。 ついでに、viewsのディレクトリの中で controllertemplateを並べせることができて、「controllertemplateはセットで編集したいのにディレクトリの移動面倒!」「ディレクトリ構造違くてわけわかんない」みたいな問題から開放された。

HTMLだけでなく、JSONYAMLのローダーを使うことで設定などの読み込みも容易になるのでなにかと便利になる。

Webpackのrequireをそこそこに活用する

webpackの拡張requireはあんまり使っていない。 index化したり拡張子の省略が主だけど、これが思いの外使い勝手が良かった。

Webpackのextensionsオプションに拡張子を登録しておけば、以下の様に拡張子を省略してかいた場合、 app.(js|coffee) あるいは app/index.(js|coffee) を探しにいってくれる。

var app = require("app");

設定的には、extensions: ["", "coffee"] とか登録しとくとCoffeeScriptも読んでくれるので便利。

indexファイルはファイルの機能によって何を書くかをわけている。

View系

exportするのはController。ユーザストーリーの入り口的な意味合いを持たせている。 DDDのAggregate的な扱いをしている雰囲気。雰囲気なのであまり厳密にやってない。

例としては、以下の様な感じ。

  • ユーザのサマリを出すページは user/index.js
  • ユーザの所持品を出すページは user/friend.js
  • ヘルプのページは help/index.js

モジュール系

angularの拡張としてproviderやfactoryを提供する場合。 ドメイン系はだいたいproviderとして提供しているし、汎用的なクラスだったりするとfactoryを提供する。

indexファイルでproviderやfactoryを定義し、別のファイルにクラスの定義をしている。

例として、ユーザクラスと、ユーザが持ってる友達のクラスを書くとしたら以下の様な構成にする。

+- domains/
  +- user/
    +- index.js
    +- user.js
    +- friend.js

$injectorを活用する

domainsなんかはクラスをたくさん作って、providerでインスタンス化している。

愚直にかくと以下のような書き方になる。

angular.module(module.exports = "user-domain", []).provider ()->
  User = require "./user"
  return {
    '$get': -> new User()
  }
  

requireで呼び出すことはできるけど、例えば $resource$q を使いたいときが結構ある。 呼び出すにしても $injector 呼ばなければならない。

$getでDIして、コンストラクタに渡す?でも無駄なプロパティは抱えたくない。

そこで、$injector.invoke を採用した。

こいつを使うと、以下のようにかける。

index.js:

angular.module(module.exports = "user-domain", []).provider () ->
  return {
    '$get': ($injector) ->
      new ($injector.invoke(require "./user"))
  }

user.js:

module.exports = ($resource) ->
  class User
    # Userクラスの定義

DIもクラス側に書けてメンテナンスがしやすくなった。

さらに、$injector.instantiateを使って、パラメータのバリデーションをしつつインスタンスを生成するようにする。

index.js:

angular.module(module.exports = "user-domain", []).provider () ->
  return {
    '$get': ($injector) ->
      $injector.instantiate($injector.invoke(require "./user"), {})
  }

$injector.instantiate を使うとコンストラクタに渡る値が存在しないときにErrorを投げてくれるので、「うっかり引数変え忘れた!」とかに気付ける。

若干鬱陶しいという気はしつつ、たくさんコードを書いているとどうしても漏れがでてくるのでエラーを投げてくれるのはありがたい。

Angular UI-Routerの設定をもとにViewのディレクトリを作る

state の構造と views のディレクトリ構造が合致するのが望ましい。

こうするとファイルをソースを見なくても探しにいけるし、なによりファイル名を決めるときに考えることが少ない。

templateとcontrollerを同じディレクトリにおいているので考える手間が一つになる。

$stateProvider
  .state "app",
    abstruct: true
    controller: require "app/views/app/index.coffee"
    template:   require "app/views/app/index.html"
  .state "app.user",
    url: "^/user/"
    controller: require "app/views/app/user/index.coffee"
    template:   require "app/views/app/user/index.html"
  .state "app.user.friend",
    url: "^/user/friend"
    controller: require "app/views/app/user/friend.coffee"
    template:   require "app/views/app/user/friend.html"

ディレクトリ構成

というわけで、大体以下の様な感じにディレクトリが出来上がる。

+- scripts
  +- xx-modal/
  +- xx-loading/
  +- xx-preload/
  +- app/
    +- config
    | +- router.coffee
    | +- resource.coffee
    | +- xx-modal.coffee
    +- domains/
    | +- user/
    | | +- user.coffee
    | | +- index.coffee
    | +- friend/
    +- views/
    +- app/        
    |  +- contents/
    |  |  +- header/
    |  |  +- index.coffee
    |  |  +- index.html
    |  |+- index.coffee
    |  +- index.html
    +- index.coffee

まとめ

AngularJS と Webpackを連携させる事例を紹介しました。 組み合わせて色々便利で管理も楽になっていいよ!