LoginSignup
24
24

More than 5 years have passed since last update.

なんだかんだでたどり着いたAngularの書き方

Last updated at Posted at 2015-12-24

AngularJS Advent Calendar 2015 24日目の投稿です。

知人からどんな感じで Angular を書いてるのか尋ねられたので、ボクはこうしてますよという投稿です。良い例か悪い例かはボク自身判断できないのですが、参考になればと。

とりあえずコード全部出す

ちょっと長いですが、全部出します。内容はただのTodoでRESTfulに通信してます。一覧をクリックすると編集でき、blur のタイミングで保存しにいくようにしてあります。ファイルはめんどうなのでわけてません。

(function(){
  'use strinoct';

  angular.module('MyApp', ['ngResource'])
    .controller('SampleTodoCtrl', ['$scope', 'SampleTodoService', function($scope, SampleTodoService){

      var vm = $scope.vm = {};

      function init(){
        SampleTodoService.getTodo(vm);
      }

      vm.clickAddBtn = function(){
        SampleTodoService.addTodo(vm);
      };

      vm.toEdit = function(index){
        vm.todoList[index].edit = true;
      };

      vm.editDone = function(todo){
        todo.edit = false;
        SampleTodoService.editTodo(vm, todo);
      };

      vm.clickDeleteBtn = function(index){
        SampleTodoService.deleteTodo(vm, index);
      };

      init();
    }])
    .factory('SampleTodoService', ['SampleTodoApi', function(SampleTodoApi){
      var factory = {};

      var api = SampleTodoApi.getApi();

      factory.getTodo = function(vm){
        api.get(function(result){
          vm.todoList = result.todoList || [];
        })
      };

      factory.addTodo = function(vm){
        api.save(vm.todo, function(){
          vm.todoList.push(angular.copy(vm.todo));
          vm.todo = '';
        });
      };

      factory.editTodo = function(vm, editTodo){
        api.save(editTodo);
      };

      factory.deleteTodo = function(vm, index){
        var deleteTodo = vm.todoList[index];
        api.delete(deleteTodo.id, function(){
          vm.todoList.splice(index, 1);
        });
      };

      return factory;
    }])
    .factory('SampleTodoApi', ['$resource', function($resource){
      var factory = {};

      factory.getApi = function(){
        return $resource('/api/todo');
      };

      return factory;
    }])

})();
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular-resource.js""></script>
  <script src="app.js"></script>
  <title>code sample</title>
</head>
<body ng-app="MyApp">

  <div ng-controller="SampleTodoCtrl">

    <div ng-form="sampleForm">
      <input type="text" name="todo" ng-model="vm.todo.title">
      <button ng-click="vm.clickAddBtn()">Add</button>

      <ul>
        <li ng-repeat="todo in vm.todoList" ng-form="sampleListForm">
          <div>
            <span ng-if="!todo.edit" ng-click="vm.toEdit($index)">{{todo.title}}</span>
            <span ng-if="todo.edit"><input type="text" name="editTodo" ng-model="todo.title" ng-blur="vm.editDone(todo)"></span>
            <button ng-click="vm.clickDeleteBtn($index)">削除</button>
          </div>
        </li>
      </ul>
    </div>

  </div>

</body>
</html>

結果こうなりました

$scope 直下にプリミティブ型のプロパティを生やさない

これはよくいわれているので、対応している人も多いはず。スコープの継承で、親子スコープ間でデータ共有できなくなります。
そのため、初期化時には

var vm = $scope.vm = {};

とします。

また、コントローラ内で $scope を書くのめんどうなので、ショートカットもつくっておきます。

コントローラの役割を厳格にする

昔はコントローラの引数に複数のサービスを読んで組み合わせていたんですけど、どうしても「サービス1の結果に対して分岐処理をする」みたいになってしまって、結果ロジック書いちゃってるじゃん!ってなってました。コントローラを薄くするためにもコントローラの役割を以下のように決めてしまいました。

コントローラは、

  • スコープの初期化
  • ビューからのイベントを補足して、サービスの該当処理を呼び出す
  • サービス(非同期通信など)で取得したデータをビューに渡す

のみ。それ以外はさせないようにしてます(フラグ値をかえすくらいならコントローラでやってもいいかなとは思ってます)。

  vm.clickAddBtn = function(){
    SampleTodoService.addTodo(vm);
  };

要は、追加ボタンが押されたら、Todoを追加する処理してね、ってだけです。

サービスはコントローラに対して 1:N

コントローラにロジックを書けなくなったので、サービスに書かざるを得ません。そのためコントローラから複数サービスを呼ぶのではなく、そのコントローラに対応するサービスを1つだけDIするようにして、そのサービス内で複数のサービスを組み合わせようにします。

.factory('SampleTodoService', ['SampleTodoApi', 'ここに', '並ぶ', function(SampleTodoApi, a, b){
  var factory = {};

  ...

この例だと SampleTodoApi だけですが、紹介している方法で書いていくと、この箇所でDIするサービスがたくさん並ぶことになります。

1:N の理由ですが、管理画面などの、入力/確認/完了/一覧 は共通的な機能が多いためです。
それぞれの画面に対して 1:1 で作るよりは、コードを使い回せるので楽できます。ただし、あまりにも1ファイルのサイズが大きくなった場合は 1:1 で作ったほうが良いです。

vm をサービスに渡してしまう

これが一番のポイントかもしれません。通常はこうしたくなるはず。

  vm.clickAddBtn = function(){
    // ここでパラメータ作る
    var param = ...
    vm.todoList = SampleTodoService.addTodo(param); // パラメータ渡す
  };

こうすると、サービスのためにパラメータを加工して渡すことが多くなります。
ただ、それすらコントローラに書きたくないという意地!

で、考えた結果、 vm ごと渡してしまいます。パラメータは渡した先で作るようにします。そして、コントローラでビューのデータをセットするのではなく、サービスに vm を渡してセットしてもらいます。
初期化の際に、vm = $socpe.vm しているため、双方向データバインディングされた状態です。
なので、サービスでセットしたデータはビューに反映されます。

「最小限のネストしたオブジェクト渡せばいいじゃん」、という方もいると思います、そう思います。
ただ、後から別のプロパティを参照しないといけなくなった場合、変更がめんどうなのでまるっと渡してしまう方針にしました。

で、結果これが良いな感じている今日この頃です。

失敗作として、

  .controller('SampleTodoCtrl', ['$scope', 'SampleTodoService', function($scope){
    $scope.vm = {};
    SampleTodoService.setViewModel($scope.vm);
    ...
  }]).
  .factory('SampleTodoService', ['SampleTodoApi', function(SampleTodoApi){
    var factory = {};
    var vm = {};

    factory.setViewModel = function(viewModel){
      vm = viewModel;
    };

    ...

のように、ビューモデル用の setter をつくろうとしたこともありました(サービスで保持させれば毎回 vm を渡さなくて良いので)。Angularの場合、サービスは singleton なので、毎回コントローラの初期化時にセットする必要性が生じてしまいます。忘れてしまって前回使用されたデータを参照してしまった、などのバグの温床になると考えやめました。

まとめ

Angularユーザのみなさんはどんな感じで書いてますか?
せっかく晒したのでレビュープリーズ。

24
24
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
24
24