AngularJS x Webpack を使った構成の紹介
AngularJSとWebpackを上手いこと連携させてメンテナンスしやそうな構成を試行錯誤したので、 現状の記録と整理を兼ねて記事にしてみる。
結構いいかんじなアプローチなのではないかと思いつつ、まだ詰め切れていないところも多いので今後ブラッシュアップを重ねていきたい。
関連技術
- AngularJS
- AngularUIRouter
- Webpack
- CoffeeScript
要点
- 役割でディレクトリを分ける
- 汎用化出来そうな仕組みはアプリケーションの外に置く
- DDDを意識してビジネスロジックを構築する
- Webpackのローダーを活用する
- Webpackのrequireをそこそこに活用する
- $injectorを活用する
- Angular UI-Routerの設定をもとにViewのディレクトリを作る
役割でディレクトリを分ける
AngularJSだと controller
, service
, config
, directive
のように機能でディレクトリを分けるのがスタンダードのようだけど、メンテナンスをしているとあっちいったりこっちいったりで結構面倒臭かったり、考えをごっちゃにしがちであまり使い勝手がよくなかった。
そんなわけでJSの役割を幾つかに別けて、それごとにディレクトリを切るようにしてみた。
app/config
- アプリケーションの設定
app/domains
app/views
- 見た目に関する部分、
controller
とtemplate
を内包する - view-modelとdomainを
controller
で連携させる
- 見た目に関する部分、
- 汎用モジュール
汎用化出来そうな仕組みはアプリケーションの外に置く
モーダルやローディングなど汎用的に使え、アプリケーションに依存するものを設定に切り出せるようなモジュールは基本的に app
(アプリケーションの本体)とは別のディレクトリにおく。
外出ししやすくなるし、管理を別けれるので一度FIXすれば後は気にしなくていい。
ディレクティブやView周りのproviderはだいたい汎用化出来そうなので、適当なプリフィクスを振って、 xx-modal
みたいな感じでディレクトリを切ってそこに置くようにしている。
DDDを意識してビジネスロジックを組む
DDDというか、意識していることは以下。
- 単一責務の原則
- 一つのクラスが持つ役割はひとつ
- 複数持つようなら分ける
- Aggregateパターン
- 複数のユーザストーリーを一つのクラスに集約することでViewから参照するクラスを制限する
- しかし責務は分かれてるようにしているので相互影響は最小。最悪クラスの差し替えも想定してる
- Repositoryパターン
いままであまりJavaScriptでOOPを意識してこなかったのだけど、単一責務の原則をしっかりまもって、I/Fをある程度制約かければ結構取り回しが効いて良さそうな感じはする。 ただ、どのくらいの規模までハンドルできるかはちょっと見えてないので、今後の発展を見たい。
Webpackのローダーを活用する
CoffeeScriptのコンパイルはもちろんだが、何より嬉しいのはHTMLをJavaScriptの文字列として展開できること。
これによって、 grunt-ng-template
のようなライブラリは不要になったし、遷移のたびにXHRを投げる必要もなくなった。
UI-Routerと連携しつつ、templateファイルを require
することで文字列をそのままわたせる。
ついでに、viewsのディレクトリの中で controller
とtemplate
を並べせることができて、「controller
とtemplate
はセットで編集したいのにディレクトリの移動面倒!」「ディレクトリ構造違くてわけわかんない」みたいな問題から開放された。
HTMLだけでなく、JSONやYAMLのローダーを使うことで設定などの読み込みも容易になるのでなにかと便利になる。
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を連携させる事例を紹介しました。 組み合わせて色々便利で管理も楽になっていいよ!