これの続き
ちなみに SPA は Single Page Application の略らしい。Smart Phone Application かと思ってた。
命名規則
View, Controller, Service, Model, API の各レイヤで作るサービスの名前のルール。
- {layer}.{category}.{name}
- layer
- view, ctrl, srv, model, api のどれか
- category
- 省略可能
- 必要に応じて「.」を増やし、階層化する
- lowerCamelCase
- name
- サービス名
- View, Service, Model は、一度生成されたものを使いまわすので、小文字始まり
- Controller, API は、都度生成されるものなので 大文字始まり
- layer
また、ディレクトリは、サービス名の 「.」を「/」に変えるだけで行けるようにしたい。
以下は各層のサービスの例
View
views/loading.coffee
"use strict"
angular.module("app").factory "view.loading", [
"$q", "$ionicLoading",
($q, $ionicLoading) ->
return (stmt) ->
$ionicLoading.show { template: "Loading..." }
deferred = $q.defer()
done = (x) ->
deferred.resolve(x)
done stmt done
return deferred.promise.finally $ionicLoading.hide
]
- $ionicLoading で、画面を暗くして操作を出来なくし、その間に処理を行う、という操作を実現出来る。
- もともと各ページに書いてあったのを、view のサービスとして外出ししたもの。
Controller
controllers/product/gift/getForm.coffee
"use strict"
angular.module("app").controller "ctrl.product.gift.GetForm", [
"$state", "$stateParams", "view.loading", "view.toaster", "srv.login", "srv.product", "srv.account", "srv.gift", "srv.message"
($state, $stateParams, loading, toaster, loginSrv, productSrv, accountSrv, giftSrv, messageSrv) ->
loading (done) ->
loginSrv.isLoggingIn().then (isLoggedIn) ->
if not isLoggedIn
return $state.go "user-login-form", {backToState: $state.current.name, stateParams: angular.toJson $stateParams }
pProduct = productSrv.getProduct $stateParams.productId
pFriendList = accountSrv.getFriendList()
return $q.all [pProduct, pFriendList]
.then ([product, friendList]) ->
$scope.product = product
$scope.friendList = friendList
$scope.continue = (friendId) ->
giftSrv.validate product.id, friendId
.then ->
$state.go "product-gift-confirm", {productId: product.id, friendId: friendId}
.catch (errorCode)
toaster.add messageSrv.message errorCode
]
- これで 23 行
- ページ遷移のための簡単な if 分岐を除き、基本的には model からもらったデータを view($scope)に渡しているだけ。
- promise を駆使しているので、「どの時点でどのデータを取得できているか」は見失わないようにしたい。
Service
services/gift.coffee
angular.module("app").factory "srv.gift", [
"model.inventory", "model.account", "model.gift"
(inventoryModel, accountModel, giftModel) ->
giftSrv =
validate: (productId, friendId) ->
pHasProduct = inventoryModel.hasProduct productId
pHasFriend = accountModel.hasFriend friendId
return $q.all [pHasProduct, pHasFriend]
.then ->
giftModel.validate productId, friendId
giftTo: (productId, friendId) ->
@validate productId, friendId
.then ->
giftModel.giftTo productId, friendId
return giftSrv
]
- Model を組み合わせて、ビジネスロジックを実現している。
- 各種エラーは、Model が生成しているので Service では何もしていない
- 今回はたまたまそうだというだけで、Service が Error を生成してはいけないわけではない
Model
models/gift.coffee
angular.module("app").factory "model.gift", [
"model.message", "api.gift.Validate", "api.gift.Post"
(messageModel, ValidateGiftAPI, PostGiftAPI) ->
giftModel =
validate: (productId, friendId) ->
api = new ValidateGiftAPI()
return api.request productId, friendId
.then (response) ->
if not response.data.hasError
return true
switch response.data.error_code
when "noThankYou" then $q.reject messageModel.warn.REFUSED_YOUR_GIFT
when "haveAlreadyGot" then $q.reject messageModel.warn.FRIEND_HAVE_ALREADY_GOT
giftTo: (productId, friendId) ->
api = new PostGiftAPI()
return api.request productId, friendId
]
- ほぼ API を呼び出すだけ。
- API が返すエラーを promise を reject する形で上の層へ伝えている。
- API の通信エラーや 401エラー などは、各種 Model では記述せず、$httpProvider.intercepts に登録するべきだ。
API
apis/gift/validate.coffee
anguler.module("app").factory "api.gift.Validate", [
"$http", "api.Base"
($http, BaseAPI) ->
class Validate extends BaseAPI
path = "/gift/validate"
useSsl: -> true
getPath: -> super path
getParams: (productId, friendId) ->
return super
productId: productId
friendId: friendId
request: (productId, friendId) ->
return $http.get @getPath(),
params: @getParams productId, friendId
return Validate
]
- 一番単純。$http を使って API を叩くだけ。
- 自動生成する仕組みを作ってもいいかもしれない。
- BaseAPI に、URL の共通部分や、API の version, デフォルトで渡す Params などが置いてある。
その他
キャッシュ周り、セキュリティ周りはまだ勉強中。
かなり実践に近いコードを書いてみました。何かの参考になれば幸いです。