ディレクトリ構成の

ベストプラクティスを探る

第2回AngularJS勉強会 #ngCurry

株式会社LIG 菅原のびすけ

自己紹介

出身: 東北(宮城生まれの岩手育ち)

菅原のびすけ (LIG inc)

twitter: @n0bisuke

facebook: sugawara.ryousuke

LIGの新入社員です。4月に上京。

お手柔らかに

フットサルやってます。

誘ってください。

よろしくお願いします!

家: ギークハウス -> 0円シェアハウス

趣味: 雪合戦       特技:わんこそば

"RomoとChatworkを使った受付システム"

自己紹介

〜最近作ってるもの

相棒:Romo

お客さんがきたら...!

お客さん

きたよ!

angular.module('App', [])
.service('todos', ['$rootScope', '$filter', function ($scope, $filter) {
  var list = []; // ToDo リスト

  // ToDo リストの変更を監視し 全 $scope に対して change:list イベントを発行する
  $scope.$watch(function () {
    return list;
  }, function (value) {
    $scope.$broadcast('change:list', value);
  }, true);

  var where = $filter('filter');

  var done = { done: true };
  var remaining = { done: false };

  // リストが扱えるフィルタリング条件
  this.filter = {
    done: done,
    remaining: remaining
  };

  // 完了状態の ToDo のみを抽出して返す
  this.getDone = function () {
    return where(list, done);
  };

  // 要件を受け取り新しい ToDo をリストに加える
  this.add = function (title) {
    list.push({
      title: title,
      done: false
    });
  };

  // 引数の ToDo をリストから取り除く
  this.remove = function (currentTodo) {
    list = where(list, function (todo) {
      return currentTodo !== todo;
    });
  };

  // 完了状態の ToDo をリストから取り除く
  this.removeDone = function () {
    list = where(list, remaining);
  };

  // リスト内の ToDo すべての状態を引数に合わせる
  this.changeState = function (state) {
    angular.forEach(list, function (todo) {
      todo.done = state;
    });
  };
}])
.controller('RegisterController', ['$scope', 'todos', function ($scope, todos) {
  $scope.newTitle = '';

  $scope.addTodo = function () {
    todos.add($scope.newTitle);
    $scope.newTitle = '';
  };
}])
.controller('ToolbarController', ['$scope', 'todos', function ($scope, todos) {
  $scope.filter = todos.filter;

  $scope.$on('change:list', function (evt, list) {
    var length = list.length;
    var doneCount = todos.getDone().length;

    $scope.allCount = length;
    $scope.doneCount = doneCount;
    $scope.remainingCount = length - doneCount;
  });

  $scope.checkAll = function () {
    todos.changeState(!!$scope.remainingCount);
  };

  $scope.changeFilter = function (filter) {
    $scope.$emit('change:filter', filter);
  };

  $scope.removeDoneTodo = function () {
    todos.removeDone();
  };
}])
.controller('TodoListController', ['$scope', 'todos', function ($scope, todos) {
  $scope.$on('change:list', function (evt, list) {
    $scope.todoList = list;
  });

  var originalTitle;

  $scope.editing = null;

  $scope.editTodo = function (todo) {
    originalTitle = todo.title;
    $scope.editing = todo;
  };

  $scope.doneEdit = function (todoForm) {
    if (todoForm.$invalid) {
      $scope.editing.title = originalTitle;
    }
    $scope.editing = originalTitle = null;
  };

  $scope.removeTodo = function (todo) {
    todos.remove(todo);
  };
}])
.controller('MainController', ['$scope', function ($scope) {
  $scope.currentFilter = null;

  $scope.$on('change:filter', function (evt, filter) {
    $scope.currentFilter = filter;
  });
}])

/*
.controller('MainController', ['$scope','$filter', function ($scope, $filter) {
  $scope.todos = [];
  $scope.newTitle = ''; //入力された文字

  $scope.addTodo = function () {
    $scope.todos.push({
      title: $scope.newTitle,
      done: false
    });

    $scope.newTitle = ''; //init
  };

  // フィルタリング条件モデル
  $scope.filter = {
    done: { done: true },      // 完了のみ
    remaining: { done: false } // 未了のみ
  };

  // 現在のフィルタの状態モデル
  $scope.currentFilter = null;

  // フィルタリング条件を変更するメソッド
  $scope.changeFilter = function (filter) {
    $scope.currentFilter = filter;
  };

  var where = $filter('filter'); // filter フィルタ関数の取得
  $scope.$watch('todos', function (todos) {
    var length = todos.length;
    $scope.allCount = length;                             // 総件数モデル
    $scope.doneCount = where(todos, $scope.filter.done).length; // 完了件数モデル
    $scope.remainingCount = length - $scope.doneCount;          // 未了件数モデル
  }, true);

  var originalTitle;     // 編集前の要件
  $scope.editing = null; // 編集モードの ToDo モデルを表すモデル

  $scope.editTodo = function (todo) {
    originalTitle = todo.title;
    $scope.editing = todo;
  };

  $scope.doneEdit = function (todoForm) {
    if (todoForm.$invalid) {
      $scope.editing.title = originalTitle;
    }
    $scope.editing = originalTitle = null;
  };

  // 全て完了/未了
  $scope.checkAll = function () {
    var state = !!$scope.remainingCount; // 未了にするのか完了にするのかの判定

    angular.forEach($scope.todos, function (todo) {
      todo.done = state;
    });
  };

  // 完了した ToDo を全て削除
  $scope.removeDoneTodo = function () {
    $scope.todos = where($scope.todos, $scope.filter.remaining);
  };

  // 任意の ToDo を削除
  $scope.removeTodo = function (currentTodo) {
    $scope.todos = where($scope.todos, function (todo) {
      return currentTodo !== todo;
    });
  };

}])
*/
.directive('mySelect', [function () {
  return function (scope, $el, attrs) {
    // scope - 現在の $scope オブジェクト
    // $el   - jqLite オブジェクト(jQuery ライクオブジェクト)
    //         jQuery 使用時なら jQuery オブジェクト
    // attrs - DOM 属性のハッシュ(属性名は正規化されている)

    scope.$watch(attrs.mySelect, function (val) {
      if (val) {
        $el[0].select();
      }
    });
  };
}]);

何かのチュートリアルを真似して、

とりあえず動くものを作る。

動いて感動するけどapp.jsが悲惨なことに...

Angular.jsの勉強をしたときの思い出

Angular.jsの勉強をしたときの思い出

信じられないくらい日本語の情報が少ない!!

Angular流行ってるって言ってる割に...

日本語だと3つくらいしか

有用な記事がない

(function(angular) {
    var MODULES = [];
    var MODULE_GROUP = [
        'controllers',
        'filters',
        'services',
        'directives'
    ];
    for (var i = 0, len = MODULE_GROUP.length; i < len; i++) {
        angular.module(MODULE_GROUP[i],[]);
    }
    angular.module('App',MODULES.concat(MODULE_GROUP));
})(angular);
angular.module('App', [])
.service('todos', ['$rootScope', '$filter', function ($scope, $filter) {
  var list = []; // ToDo リスト

  // ToDo リストの変更を監視し 全 $scope に対して change:list イベントを発行する
  $scope.$watch(function () {
    return list;
  }, function (value) {
    $scope.$broadcast('change:list', value);
  }, true);

  var where = $filter('filter');

  var done = { done: true };
  var remaining = { done: false };

  // リストが扱えるフィルタリング条件
  this.filter = {
    done: done,
    remaining: remaining
  };

  // 完了状態の ToDo のみを抽出して返す
  this.getDone = function () {
    return where(list, done);
  };

  // 要件を受け取り新しい ToDo をリストに加える
  this.add = function (title) {
    list.push({
      title: title,
      done: false
    });
  };

  // 引数の ToDo をリストから取り除く
  this.remove = function (currentTodo) {
    list = where(list, function (todo) {
      return currentTodo !== todo;
    });
  };

  // 完了状態の ToDo をリストから取り除く
  this.removeDone = function () {
    list = where(list, remaining);
  };

  // リスト内の ToDo すべての状態を引数に合わせる
  this.changeState = function (state) {
    angular.forEach(list, function (todo) {
      todo.done = state;
    });
  };
}])
.controller('RegisterController', ['$scope', 'todos', function ($scope, todos) {
  $scope.newTitle = '';

  $scope.addTodo = function () {
    todos.add($scope.newTitle);
    $scope.newTitle = '';
  };
}])
.controller('ToolbarController', ['$scope', 'todos', function ($scope, todos) {
  $scope.filter = todos.filter;

  $scope.$on('change:list', function (evt, list) {
    var length = list.length;
    var doneCount = todos.getDone().length;

    $scope.allCount = length;
    $scope.doneCount = doneCount;
    $scope.remainingCount = length - doneCount;
  });

  $scope.checkAll = function () {
    todos.changeState(!!$scope.remainingCount);
  };

  $scope.changeFilter = function (filter) {
    $scope.$emit('change:filter', filter);
  };

  $scope.removeDoneTodo = function () {
    todos.removeDone();
  };
}])
.controller('TodoListController', ['$scope', 'todos', function ($scope, todos) {
  $scope.$on('change:list', function (evt, list) {
    $scope.todoList = list;
  });

  var originalTitle;

  $scope.editing = null;

  $scope.editTodo = function (todo) {
    originalTitle = todo.title;
    $scope.editing = todo;
  };

  $scope.doneEdit = function (todoForm) {
    if (todoForm.$invalid) {
      $scope.editing.title = originalTitle;
    }
    $scope.editing = originalTitle = null;
  };

  $scope.removeTodo = function (todo) {
    todos.remove(todo);
  };
}])
.controller('MainController', ['$scope', function ($scope) {
  $scope.currentFilter = null;

  $scope.$on('change:filter', function (evt, filter) {
    $scope.currentFilter = filter;
  });
}])

/*
.controller('MainController', ['$scope','$filter', function ($scope, $filter) {
  $scope.todos = [];
  $scope.newTitle = ''; //入力された文字

  $scope.addTodo = function () {
    $scope.todos.push({
      title: $scope.newTitle,
      done: false
    });

    $scope.newTitle = ''; //init
  };

  // フィルタリング条件モデル
  $scope.filter = {
    done: { done: true },      // 完了のみ
    remaining: { done: false } // 未了のみ
  };

  // 現在のフィルタの状態モデル
  $scope.currentFilter = null;

  // フィルタリング条件を変更するメソッド
  $scope.changeFilter = function (filter) {
    $scope.currentFilter = filter;
  };

  var where = $filter('filter'); // filter フィルタ関数の取得
  $scope.$watch('todos', function (todos) {
    var length = todos.length;
    $scope.allCount = length;                             // 総件数モデル
    $scope.doneCount = where(todos, $scope.filter.done).length; // 完了件数モデル
    $scope.remainingCount = length - $scope.doneCount;          // 未了件数モデル
  }, true);

  var originalTitle;     // 編集前の要件
  $scope.editing = null; // 編集モードの ToDo モデルを表すモデル

  $scope.editTodo = function (todo) {
    originalTitle = todo.title;
    $scope.editing = todo;
  };

  $scope.doneEdit = function (todoForm) {
    if (todoForm.$invalid) {
      $scope.editing.title = originalTitle;
    }
    $scope.editing = originalTitle = null;
  };

  // 全て完了/未了
  $scope.checkAll = function () {
    var state = !!$scope.remainingCount; // 未了にするのか完了にするのかの判定

    angular.forEach($scope.todos, function (todo) {
      todo.done = state;
    });
  };

  // 完了した ToDo を全て削除
  $scope.removeDoneTodo = function () {
    $scope.todos = where($scope.todos, $scope.filter.remaining);
  };

  // 任意の ToDo を削除
  $scope.removeTodo = function (currentTodo) {
    $scope.todos = where($scope.todos, function (todo) {
      return currentTodo !== todo;
    });
  };

}])
*/
.directive('mySelect', [function () {
  return function (scope, $el, attrs) {
    // scope - 現在の $scope オブジェクト
    // $el   - jqLite オブジェクト(jQuery ライクオブジェクト)
    //         jQuery 使用時なら jQuery オブジェクト
    // attrs - DOM 属性のハッシュ(属性名は正規化されている)

    scope.$watch(attrs.mySelect, function (val) {
      if (val) {
        $el[0].select();
      }
    });
  };
}]);

ファイルやディレクトリを分割することで

app.jsがかなりスッキリした

ただ、

このやり方がベストかは分からない...

Angular.jsの勉強をしたときの思い出

皆さん、

Angular.jsで開発するときの

ディレクトリ構成って

どうしてますか?

コンポーネント毎で分けるよ

ページ毎で分けるよ

あえて

ディレクトリに

分けないよ!

色々なケースがあると思います。

Angular.jsの

ディレクトリ構成パターン紹介と、

利用して感じた考察などを話します。

今日の内容

初心者でディレクトリ構成に悩んでる人

ベストプラクティスを求める人に響いて欲しい

1.ディレクトリ構成のパターン

2.組み込みとディレクトリ構成

1.1 Angular-Seed

・Angular.js公式

 

・Angular.jsの最小構成例

 

・初めてディレクトリ構成を考えるときに参考にしたい

 

angular-phonecatのコア部分

app
├─ css  //stylesheet
│  └─ app.css
├─ img //image
├─ js //angular.js files
│  ├─ app.js
│  ├─ controllers.js
│  ├─ directives.js
│  ├─ filters.js
│  └─ services.js
├─ lib //3rd party library
│  └─ angular
│     └─ angular.js
├─ partials // parts 
│  ├─ partial1.html
│  └─ partial2.html
├─ index-async.html
└─ index.html //main html

1.1 Angular-Seed

・基本的な分け方

 

・シンプルで分かりやすい

 

小さい規模向け

1.2 Angularjs Style Guide

・検索でヒットした4つのStyle Guide

 - mgechev/angularjs-style-guide

    - johnpapa/angularjs-styleguide

    - toddmotto/angularjs-styleguide 

    - gocardless/angularjs-style-guide

 

・Angular.js公式のものではない

 

・常に議論があって流動的

 

雰囲気で4種類(A〜D)に分類したので聞いて下さい笑

1.2.1  mgechev / Angularjs Style Guide

AngularJSアプリケーションのベストプラクティスとガイドラインを提供が目的

 

・日本語訳あり

https://github.com/mgechev/angularjs-style-guide/blob/master/README-ja-jp.md#%E5%85%A8%E8%88%AC

 

・ディレクトリ構成について最も言及している

Minko Gechev (mgechev)氏

1.2.1  mgechev / Angularjs Style Guide

.
├── app
│   ├── app.js
│   ├── controllers
│   │   ├── page1
│   │   │   ├── FirstCtrl.js
│   │   │   └── SecondCtrl.js
│   │   └── page2
│   │       └── ThirdCtrl.js
│   ├── directives
│   │   ├── page1
│   │   │   └── directive1.js
│   │   └── page2
│   │       ├── directive2.js
│   │       └── directive3.js
│   ├── filters
│   │   ├── page1
│   │   └── page2
│   └── services
│       ├── CommonService.js
│       ├── cache
│       │   ├── Cache1.js
│       │   └── Cache2.js
│       └── models
│           ├── Model1.js
│           └── Model2.js
├── lib
└── test

A:コンポーネントタイプ分類

・app : メインのプログラムファイル

・lib: サードパーティ性のライブラリなど

・test: テスト用テストコード

appの大枠はAngular-Seedと基本同じ

上の階層をコンポーネントタイプで分けて、下の階層は機能性(ページ)で分けるアプローチ

Seedの構成を拡張した形なので構成把握はしやすい

ページが多くなると細かい編集が厳しいかも。ディレクティブの使い回しとか...

1.2.1  mgechev / Angularjs Style Guide

.
├── app
│   ├── app.js
│   ├── common
│   │   ├── controllers
│   │   ├── directives
│   │   ├── filters
│   │   └── services
│   ├── page1
│   │   ├── controllers
│   │   │   ├── FirstCtrl.js
│   │   │   └── SecondCtrl.js
│   │   ├── directives
│   │   │   └── directive1.js
│   │   ├── filters
│   │   │   ├── filter1.js
│   │   │   └── filter2.js
│   │   └── services
│   │       ├── service1.js
│   │       └── service2.js
│   └── page2
│       ├── controllers
│       │   └── ThirdCtrl.js
│       ├── directives
│       │   ├── directive2.js
│       │   └── directive3.js
│       ├── filters
│       │   └── filter3.js
│       └── services
│           └── service3.js
├── lib
└── test

B:機能性分類

・app/common: 共通処理用ファイル

・app/page1: page1用ファイル

・app/page2: page2用ファイル

上の階層を機能性(ページ)で分けて、下の階層はコンポーネントタイプで分けるアプローチ

ページが多くなる場合に有効。

Seedからの拡張よりも、プロジェクト開始から設計しておきたい。

1.2.1  mgechev / Angularjs Style Guide

app
└── directives
    ├── directive1
    │   ├── directive1.html
    │   ├── directive1.js
    │   └── directive1.sass
    └── directive2
        ├── directive2.html
        ├── directive2.js
        └── directive2.sass

ディレクティブを作成するとき、全てひとつのフォルダ内に入れてディレクティブファイル(テンプレート、CSS/SASS, JavaScript)として関連付けてしまうと便利

コンポーネントタイプ分類/機能性分類のどちらでも使える

services
├── cache
│   ├── cache1.js
│   └── cache1.spec.js
└── models
    ├── model1.js
    └── model1.spec.js

1.2.1  mgechev / Angularjs Style Guide

特定のコンポーネントに変更を加えた時にそのテストを見つけるのが簡単になり、テスト自体がマニュアルやショーケースのように。

テストコードの場所

1.2.1  mgechev / Angularjs Style Guide

.
├── app
│   ├── app.js
│   ├── controllers
│   │   ├── page1
│   │   │   ├── FirstCtrl.js
│   │   │   └── SecondCtrl.js
│   │   └── page2
│   │       └── ThirdCtrl.js
│   ├── directives
│   │   ├── page1
│   │   │   └── directive1.js
│   │   └── page2
│   │       ├── directive2.js
│   │       └── directive3.js
│   ├── filters
│   │   ├── page1
│   │   └── page2
│   └── services
│       ├── CommonService.js
│       ├── cache
│       │   ├── Cache1.js
│       │   └── Cache2.js
│       └── models
│           ├── Model1.js
│           └── Model2.js
├── lib
└── test
.
├── app
│   ├── app.js
│   ├── controllers
│   │   ├── page1
│   │   │   ├── FirstCtrl.js
│   │   │   └── SecondCtrl.js
│   │   └── page2
│   │       └── ThirdCtrl.js
│   ├── directives
│   │   ├── page1
│   │   │   └── directive1.js
│   │   └── page2
│   │       ├── directive2.js
│   │       └── directive3.js
│   ├── filters
│   │   ├── page1
│   │   └── page2
│   └── services
│       ├── CommonService.js
│       ├── cache
│       │   ├── Cache1.js
│       │   ├── cache1.spec.js
│       │   ├── cache2.js
│       │   └── Cache2.spec.js
│       └── models
│           ├── Model1.js
│           ├── Model1.spec.js
│           ├── Model2.js
│           └── Model2.spec.js
└── lib

テストを内包

関連するコードを近くに置いた方が管理はしやすい。

ファイル数が量が増えてくると汚くなってしまう。

 

testにまとまっていた方がgulpタスクなどは書きやすい

1.2.2  johnpapa / angularjs-styleguide

・チーム開発のための AngularJS スタイルガイド

 

・toddomoddo氏とディスカッションして策定

 

・更新頻度が高い (最新はコミットは一昨日  数時間前)

 

・ディレクトリ構成についての記載あり

jonpapa

1.2.2  johnpapa / Angularjs Style Guide

app/
    app.module.js
    app.config.js
    app.routes.js
    components/       
        calendar.directive.js  
        calendar.directive.html  
        user-profile.directive.js  
        user-profile.directive.html  
    layout/
        shell.html      
        shell.controller.js
        topnav.html      
        topnav.controller.js       
    people/
        attendees.html
        attendees.controller.js  
        speakers.html
        speakers.controller.js
        speaker-detail.html
        speaker-detail.controller.js
    services/       
        data.service.js  
        localstorage.service.js
        logger.service.js   
        spinner.service.js
    sessions/
        sessions.html      
        sessions.controller.js
        session-detail.html
        session-detail.controller.js  

・Componnts: ディレクティブ

・layout: 共通部品

・people: ページ毎のファイル

・services: サービス群

・sessions: セッション機能

(機能毎にフォルダが増える)

C: コンポーネントタイプ分類 + 機能性分類の並列

ディレクトリがかなり多くなってしまうかも

階層が深くならないのでファイルは見つかりやすい

1.2.2  johnpapa / Angularjs Style Guide

app/
    app.module.js
    app.config.js
    app.routes.js
    controllers/
        attendees.js            
        session-detail.js       
        sessions.js             
        shell.js                
        speakers.js             
        speaker-detail.js       
        topnav.js               
    directives/       
        calendar.directive.js  
        calendar.directive.html  
        user-profile.directive.js  
        user-profile.directive.html  
    services/       
        dataservice.js  
        localstorage.js
        logger.js   
        spinner.js
    views/
        attendees.html     
        session-detail.html
        sessions.html      
        shell.html         
        speakers.html      
        speaker-detail.html
        topnav.html

D: コンポーネントタイプ分類からViewsを分離

 "CよりもA寄り"

ファイルが多くなってくると煩雑になるので、各ディレクトリで更にディレクトリを切ると良いかも

・controllers: コントローラー群

・directives: ディレクティブ

・services: サービス群

・views: ページ毎のファイル

Cからviewsを分離したイメージ

このタイプが割と多い

1.2.3  toddmotto / angularjs-styleguide

・チーム開発のための AngularJS スタイルガイド

 

・johnpapa氏とディスカッションして策定

 

・氏曰く、johnpapa氏のスタイルガイドより良い(?)

 

・日本語訳の"tama3bb/angularjs-styleguide"もある

https://github.com/tama3bb/angularjs-styleguide

 

・ディレクトリ構成についての記載は無かったので紹介のみ

toddmotto

1.2.4  gocardless / angularjs-style-guide

・前述したTodd Motto’s styleguide , John Papa’s styleguideMinko Gechev’s styleguideの3つを参考にしている。

 

 

・後述しますが、An AngularJS Style Guide for Closure Users at Googleも参照しているらしい

 

・ディレクトリ構成についての記載は少し

http://gocardless.com

/app
  /components
    /alert
      alert.directive.js
      alert.directive.spec.js
      alert.template.html
  /config
    main.config.js
  /constants
    api-url.constant.js
  /routes
    /customers
      /index
        customers-index.template.html
        customers-index.route.js
        customers-index.controller.js
        customers-index.e2e.js
  /helpers
    /currency
      currency-filter.js
      currency-filter.spec.js
    /unit
    /e2e
  /services
    /creditors
      creditors.js
      creditors.spec.js
  bootstrap.js
  main.js
/assets
  /fonts
  /images
  /stylesheets
404.html
index.html

1.2.4  gocardless / angularjs-style-guide

今までの例と比べ、詳細な設計。

初めに設計をしておけば死角は少ないかも。

・routes: ページ毎のファイル

・assets: フォントや画像,CSSなどの静的ファイル

サーバーサイドのフレームワークの構成に類似しているので、サーバーサイドエンジニアが理解しやすい。

C: コンポーネントタイプ分類 + 機能性分類の並列

1.3 AngularJS Best Practices: Directory Structure

kukicadnan氏によるコラム

 

 John Papa氏とTodd Mottoのスタイルガイドを参考にしているらしい

 

http://scotch.io/tutorials/javascript/angularjs-best-practices-directory-structure

1.3.1  Standard Structure

.
├──app
│   ├── controllers
│   │   ├── mainController.js
│   │   └── otherController.js
│   │
│   ├── directives
│   │   ├── mainDirective.js
│   │   └── otherDirective.js
│   │
│   ├── services
│   │   ├── userServices.js
│   │   └── itemServices.js
│   │
│   ├── js
│   │   ├── bootstrap.js
│   │   └── jquery.js
│   │
│   └── app.js
│ 
└--views
    ├── mainView.html
    ├── otherView.html
    └── index.html

・app : メインのプログラムファイル

・view: ページ毎のファイル

appフォルダ内はAngular-Seedと基本同じ

D: コンポーネントタイプよりの機能性分類

johnpapaの構成に似てるけど、あちらはapp/viewsという構成

htmlを独立させるので、htmlやCSSだけを触るコーダーやデザイナーがいるときは作業分担しやすい。

1.3.2  A Better Structure and Foundation

.
├──app
│   ├── shared 
│   │   ├── sidebar
│   │   │   ├── sidebarDirective.js
│   │   │   └── sidebarView.html
│   │   │
│   │   └── article
│   │       ├── articleDirective.js
│   │       └── articleView.html
│   │
│   ├── components 
│   │   ├── home
│   │   │   ├── homeController.js
│   │   │   ├── homeService.js
│   │   │   └── homeView.html
│   │   │
│   │   └── blog
│   │       ├── blogController.js
│   │       ├── blogService.js
│   │       └── blogView.html
│   │
│   ├── app.module.js
│   └── app.routes.js
│
├--assets
│   ├── img 
│   ├── css 
│   ├── js  
│   └── libs
│
└── index.html

B: 機能性分類

gocardlessmgechev(minko)構成に似てるけど、

sharedというフォルダに部品を格納

基本的にページ分類になるけど、共通部品ディレクトリを外部に出して管理しやすくしている。

1.4 Building Huuuuuge Apps

with AngularJS

.
├─ index.html
├─ scripts
│  ├─ controllers
│  │  └─ main.js
│  │  └─ ...
│  ├─ directives
│  │  └─ myDirective.js
│  │  └─ ...
│  ├─ filters
│  │  └─ myFilter.js
│  │  └─ ...
│  ├─ services
│  │  └─ myService.js
│  │  └─ ...
│  ├─ vendor
│  │  ├─ angular.js
│  │  ├─ angular.min.js
│  │  ├─ es5-shim.min.js
│  │  └─ json3.min.js
│  └─ app.js
├─ styles
│  └─ ...
└─ views
   ├─ main.html
   └─ ...

johnpapaの構成に似ている。

 

個人的にはvendor(他だとlib)の

位置が気になる。

D: コンポーネントタイプ分類からViewsを分離

1.5 AngularJS Folder Structure |

Stackoverflow

How do you layout a folder structure for a large and scaleable AngularJS application?

1.5.1 answer ①

johnpapa構成 D

johnpapa構成 C

/app
    /scripts
            /controllers
            /directives
            /services
            /filters
            app.js
    /views
    /styles
    /img
    /bower_components
    index.html
bower.json
  /scripts
        scripts.min.js (all JS concatenated, minified and grunt-rev)
        vendor.min.js  (all bower components concatenated, minified and grunt-rev)
    /views
    /styles
        mergedAndMinified.css  (grunt-cssmin)
    /images
    index.html  (grunt-htmlmin)

After build...

Before build...

1.5.2 answer ②

Seed拡張

1.6 An AngularJS Style Guide for Closure Users at Google

 

・恐らく公式

 

・ただ、更新があまりされていない(2013年止まり)

 

・ディレクトリ構成についてはなぜかDocsに記載

 

(恐らく時間が無いので省略)

sampleapp/                 the client app and its unit tests live under this directory        
   app.css
   app.js
   app-controller.js
   app-controller_test.js
   components/        module def'n, services, directives, filters, and related
   foo/                files live here. "foo" describes what the module does.
            foo.js     The 'foo' module is defined here.
              foo-directive.js
              foo-directive_test.js
              foo-service.js
              foo-service_test.js        
   index.html                        
e2e-tests/                                e2etests are outside the scope of this
sampleapp-backend/                        proposal, but could be at the same level as
backend/ and sampleapp/... we leave this
up to the developer.   

A Very Simple App Example

Consider a very simple app with one directive and one service:

sampleapp/ 
  app.css
  app.js                        
  app-controller.js
  app-controller_test.js
  components/
 bar/                                "bar" describes what the service does
    bar.js
    bar-service.js
    bar-service_test.js
 foo/                                "foo" describes what the directive does
    foo.js
    foo-directive.js
    foo-directive_test.js
index.html 

Or, in the case where the directive and the service are unrelated, we'd have:

A More Complex App Example

sampleapp/
        app.css
app.js                                top-level configuration, route def’ns for the app
app-controller.js
        app-controller_test.js
        components/
adminlogin/                                
        adminlogin.css                styles only used by this component
adminlogin.js              optional file for module definition
adminlogin-directive.js                         
adminlogin-directive_test.js        
                private-export-filter/
                        private-export-filter.js
                        private-export-filter_test.js
userlogin/
somefilter.js
somefilter_test.js
userlogin.js
userlogin.css                
userlogin.html                
userlogin-directive.js
userlogin-directive_test.js
userlogin-service.js
userlogin-service_test.js                
        index.html
subsection1/
subsection1.js
subsection1-controller.js
                subsection1-controller_test.js
                subsection1_test.js                         
                subsection1-1/                        
                        subsection1-1.css
                        subsection1-1.html
                        subsection1-1.js
                        subsection1-1-controller.js
subsection1-1-controller_test.js
                subsection1-2/                        
        subsection2/
subsection2.css
subsection2.html
subsection2.js
subsection2-controller.js
subsection2-controller_test.js
        subsection3/
                subsection3-1/
                        etc...

For a more complex app -- for example, a ticketing application that has a user and an admin login, where some sections of the UI and routing are common to both users while other subsections are only available to one or the other, we want to isolate files belonging to each component, thus:

1.7 Yeoman

・フロントエンド開発を便利にしてくれるジェネレータ

 

・Angular.jsのジェネレータも

 

O'Reilly本だとプロジェクト作成時にYeomanでひな形を作るのを推奨している

$ npm install -g yo
$ npm install -g generator-angular
$ yo angular [app-name]
$ yo angular:route myroute
$ yo angular:controller user
$ yo angular:directive myDirective
$ yo angular:filter myFilter
$ yo angular:view user
$ yo angular:service myService
$ yo angular:decorator serviceName

1.7 Yeoman/generator-angular

.
├── test
│   ├── spec
│   │   └── ...(app/scriptsと同じ構成)
│   ├── .jshintrc
│   └── karma.conf.js
├── bower_components
├── node_modules
├── app
│   ├── images
│   ├── scripts
│   │   ├── controllers
│   │   │   ├── main.js
│   │   │   ├── myroute.js
│   │   │   └── user.js
│   │   ├── decorators
│   │   │   └── servicenamedecorator.js
│   │   ├── directives
│   │   │   └── mydirectives.js
│   │   ├── filters
│   │   │   └── myfilters.js
│   │   ├── services
│   │   │   └── myservice.js
│   │   └── app.js
│   ├── styles
│   └── views
│       ├── main.html
│       ├── myroute.html
│       └── user.html
├── .bowerrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .jshintrc
├── .travis.yml
├── bower.json
├── Gruntfile.js
└── package.json

1.7 Yeoman/generator-angular

appの中に開発ファイルが全て入るので

最終的なbuild時のターゲットが分かりやすい。管理しやすい。

testが離れているとファイル追加時に手間がありそう。Yeomanで解決?

O'Reillyが推してるので、

割と公式認定?

D: コンポーネントタイプ分類からViewsを分離

1.ディレクトリ構成のパターン

2.組み込みとディレクトリ構成

ここまでの話はAngular.js単体で利用する場合の話でした。

ここからはサーバーサイドに

組み込むときに生じる問題や

ジレンマだったりを紹介します。

2.1 組み込み時の構成の実例

〜Expressへの組み込み〜

・Node.js向けのMVCフレームワーク

 

・RubyのSinatraライク

 

・今回の事例はv3系

.
├── node_modules
├── modules
├── public
├── routes
├── views
├── app.js
└── package.json

2.1 Express開発時の実際の構成

.
├── node_modules
├── modules
├── public
│ ├── stylesheets
│ ├── images
│ └── javascripts
│     ├── controllers
│     ├── filters
│     ├── services
│     └── app.js
├── routes
├── views
│ ├── view1.ejs
│ ├── view2.ejs
│ └── common
│     └── common.ejs
├── app.js
└── package.json

・組み込み時のディレクトリで開発

 

・API設計では無い

 

・タスクランナーは利用していない

こうした方が良かったかも(?)

root
├── node_modules
├── modules
├── public
│ ├── stylesheets
│ ├── images
│ └── javascripts
│     ├── controllers
│     ├── filters
│     ├── services
│     └── app.js
├── routes
├── views
│ ├── view1.ejs
│ ├── view2.ejs
│ └── common
│     └── common.ejs
├── app.js
└── package.json

フロント開発時のコードとビルド後の分離

root
├── app
│     ├── controllers
│     ├── filters
│     ├── services
│     ├── app.js
│     └── package.json
├── node_modules
├── modules
├── public
│ ├── stylesheets
│ ├── images
│ └── javascripts
│     └── app.min.js
├── routes
├── views
│ ├── view1.ejs
│ ├── view2.ejs
│ └── common
│     └── common.ejs
├── app.js
└── package.json

package.jsonがカオスになる問題

2.1 Express開発時の実際の構成

.
├── node_modules
├── modules
├── public
│ ├── stylesheets
│ ├── images
│ └── javascripts
│     ├── controllers
│     ├── filters
│     ├── services
│     └── app.js
├── routes
├── views
│ ├── view1.ejs
│ ├── view2.ejs
│ └── common
│     └── common.ejs
├── app.js
└── package.json

・組み込み時のディレクトリで開発

 

 

・API設計にはなっていない

 

 

 

・タスクランナーは利用していない

納品後はクライアントが保守

Socket.ioな開発はサーバーとフロントの境目が少ない

プロジェクトの途中からAngular.jsを導入

このケースから学んだ

ディレクトリ構成を考えるポイント

・サーバーエンジニアがフロントのコードを触る可能性があるか

 - サーバー側の言語はJavascriptか

 

・途中からAngular.jsを導入した場合

 

・タスクランナーを利用するか

〜CodeIgniter + Smartyへの組み込み〜

2.2 組み込み時の構成の実例

CodeIgniterの基本ディレクトリ構成

・今回のCodeIgniterバージョンは2系

・SmartyなどのAngular.jsと競合するエンジンが使われていないか

・PC版とSP版が分かれているか

〜WordPressへの組み込み〜

2.3 組み込み時の構成の実例

反省を踏まえた最近の弊社の構成

.
┣ bower_components/
┣ common.js
┣ app.js
┣ controllers/ 
┃┗ ○○/
┃ ┣ ○○Ctrl.js
┃ ┗ □□Ctrl.js
┣ services/
┃┣ ○○Model.js
┃┗ ○○.js
┣ directives/
┃┗○○/
┃ ┣ ○○.js
┃ ┗ ○○.html
┣ filters/
┃┗ ○○.js
┣ views/
┃ ┗ ○○/
┃  ┗ ○○.html
┗ test/
 ┣ controllers/
 ┃ ┗ ○○
 ┃  ┗ ○○CtrlSpec.js
 ┣ services
 ┃┗ □□Spec.js 
 ┣ directives
 ┃┗ ○○
 ┃ ┗ ○○Spec.js
 ┗ filters
  ┗ ○○Spec.js

スッキリした構成で今のところ最新

改善途中です。

servicesの中でmodelの分離はしていないけど今後どうしようか。など

common.js:

 bowerの外部ライブラリをまとめる

app.js:

 angular.jsのファイルをまとめる

・Angular.jsのディレクトリ構成パターンを調べてまとめた

・style guide 4つは是非見て欲しい

・実例であった話も紹介

今回紹介した事例が今後の開発の参考になればと思います。

ケースによって内容は変わると思うので

ウチではこうしてるよ!

みたいな事例などを懇親会でお聞きしたいです。

まとめ

おわり。

ディレクトリ構成のベストプラクティスを探る

By Sugawara Ryousuke

ディレクトリ構成のベストプラクティスを探る

第2回AngularJS勉強会 #ngCurry http://connpass.com/event/9364/ で話した内容です。

  • 7,823