【AngularJS】ファイル分割オレオレプラクティス

# AngularJS with ES6/TypeScript and Browserify

『[AngularJSモダンプラクティス - Qiita][modern-practice]』を参考に，僕がAngularJSでなんかつくるときのディレクトリ構成とかそういうのを雑にまとめてみました．

## 対象
* TypeScript 1.5 or ECMAScript 6
* AngularJS >= 1.3.0
* Browserify

## 方針
* 基本的には**TypeScript**で記述
* 型定義ファイルは[**dtsm**][dtsm]で管理
* エントリーポイントを定め，そこに**全部`import`**する
* app nameやdirectiveのprefix等は定数にして`export`
  * 各ファイル内で`angular.module(appName)`してangularのmoduleを取り出す
  * `directive`や`factory`の**登録は各ファイル内**で行う
* `tsc`でコンパイルしたファイルを一時ディレクトリにおいて，それを`browserify`に投げる
  * 元ファイル: `./ui/assets/{javascript,template,typing}s`
  * `tsc`の`outDir`: `./tmp/assets/javascripts`
  * `browserify`の出力先: `./public/javascripts/bundle.js`（本番環境は`./public/assets/bundle.js`）
  * （ディレクトリ名が変なのはRailsアプリ内で利用してるやつだからです）
* directiveのtemplateは[gulp-angular-templatecache][gulp-angular-templatecache]でひとまとめにする
* テストはES6で書いたものを`spec/javascripts`に配置し，`browserify`+`babelify`に喰わせる
  * テストをTypeScriptで書くとmockまわりとかで超面倒になる
  * テスト対象は`tsc`と`browserify`通した後の最終出力（ここ微妙かも…）
* directiveを中心とした，**Component志向なAngularJS**を目指す


## ディレクトリ構造
`app.ts`をエントリーポイントとして`tsc`及び`browserify`に喰わせる．
`angular.module`を複数に分割する場合は，`javascripts`以下にネームスペース的ディレクトリ噛ませて，それぞれにエントリーポイントを作ればいい．

```text:directories
ui/assets/javascripts
├── app.ts
├── constants.ts
├── directives
│   ├── task_list.ts
│   └── index.ts
├── factories
├── resources
│   ├── index.ts
│   └── tasks.ts
└── routes.ts
```

`tsconfig.json`はこんな感じ．
`browserify`に喰わせるための`*.js`を適当なディレクトリに吐かせる．

```json:tsconfig.json
{
  "version": "1.5.0-beta",
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "outDir": "tmp/assets/javascripts"
  },
  "files": [
    "./ui/assets/javascripts/app.ts"
  ]
}
```

`browserify`にはエントリーポイントになるファイル（e.g. `app.js`）+ 内部で`require`してないライブラリ的なファイル（e.g. `angular-route.js`）を喰わせる．
これらは適当なところに記述しとけばOK（e.g. `package.json`）.
[gulp-angular-templatecache][gulp-angular-templatecache]等でテンプレートファイルを.jsに変換してる場合，そのファイルも`browserify`のエサにする．

```json:package.json
{
  "browserify": {
    "entries": [
      "./tmp/assets/javascripts/app.js",
      "./tmp/assets/javascripts/templates.js",
      "./node_modules/angular-route/angular-route.js",
      "./node_modules/angular-resource/angular-resource.js"
    ]
  }
}
```

### 定数: `constants.ts`
適当な定数たち（module名とかdirective名のprefixとか）を持たせる．

```ts:constants.ts
export const appName = "myApp";
export const prefix = "my";
export const externalModules = ["ngRoute", "ngResource"];
export const apiBaseUrl = "/api/v1/";
```

### エントリーポイント: `app.ts`
ここで`angular.module`の初期化を行い，その後，各moduleたちを`import`する．
`import "./hoge";`は`require("./hoge");`にコンパイルされるので`node.d.ts`や`browserify.d.ts`は不要（[TypeScript 1.5][ts5]から？）．

```ts:app.ts
import angular = require("angular");
import {appName, externalModules} from "./constants";

angular.module(appName, externalModules);

import "./routes";
import "./resources/index";
import "./directives/index";
```

これ以下のファイルから`app.ts`を参照した場合，ビルドはできるが実行時に`module "../app" not found`吐くという意味不明なことになる．

### resources
`resources/index.ts`で`resources`以下を一気にimportしてしまう．

```ts:resources/index.ts
import "./task";
import "./user";
```

リソースの具体例・注意点は以下．

* 属性値とかは`ng.resource.IResource<T>`実装クラスに定義
* `$resource()`は`ng.resource.IResourceClass<T>`になる
* `$resource()`返り値をそのまま or それをラップしたやつをfactoryとして登録

```ts:resources/task.ts
/// <reference path="../../typings/vendor/angularjs/angular-resource.d.ts" />

import angular = require("angular");
import {appName, apiBaseUrl} from "../constants";

export interface TaskResource extends ng.resource.IResource<TaskResource> {
  title: string;
  body: string;
  doneAt: string;
}

export interface TaskResourceClass extends ng.resource.IResourceClass<TaskResource> {
}

export function taskFactory($resource: ng.resource.IResourceService) : TaskResourceClass {
  const url = `${apiBaseUrl}/tasks/:taskId.json`;
  const params = { taskId: "@taskId" };
  let queryAction: ng.resource.IActionDescriptor = {
    method: "GET",
    isArray: true
  };
  return <TaskResourceClass> $resource(url, params, { query: queryAction });
};

let app = angular.module(appName);
app.factory("Task", ["$resource", taskFactory]);
```

URLのプレフィクスとか（もしくはURL自体も）constantに書いたほうがいいのかもしれない．

普通のfactoryやserviceも同じようなノリで扱えばOK.

### directives
resourcesと同様に`directives/index.ts`でディレクティブを一気に読み込む．

```ts:directives/index.ts
import "./task_list";
import "./profile";
```

具体例・注意点は以下．

* ディレクティブのクラスはあくまで[**Directive Definition Object**](https://docs.angularjs.org/api/ng/service/%24compile#directive-definition-object)
* イベント処理とかそんなんは全部Controllerにやらせる
* controllerAsちゃんとつかう（名前が競合したら死ぬのでprefix除いたディレクティブ名そのままがいいかもしれない）
* AngularJS 1.4以降の`bindToController`は神なので積極的に使いましょう（参考: [AngularJS1.4とbindToController - Qiita][bindToController]）

```ts:directives/task_list.ts
/// <reference path="../../../typings/vendor/angularjs/angular-route.d.ts" />
/// <reference path="../../../typings/vendor/angularjs/angular-resource.d.ts" />

import angular = require("angular");
import {appName, prefix} from "../../constants";
import {TaskResource, TaskResourceClass} from "../../resources/task";

class TaskListController {
  tasks: Array<TaskResource>;

  constructor(private Task: TaskResourceClass) {
    getTasks();
  }

  getTasks() {
    this.tasks = this.Task.query();
  }
}

class TaskListDirective {
  restrict = "E";
  controller = ['TaskList', TaskListController];
  controllerAs = 'tasklist';
  scope = {};
  bindToController = true;
  templateUrl = "task_list.html";
}

let app = angular.module(appName);
app.directive(`${prefix}TaskList`, () => {
  return new TaskListDirective();
});
```

### controllers
<blockquote class="twitter-tweet" lang="en"><p lang="ja" dir="ltr">というかやはりControllerと云う概念が負の遺産なんだよ　Componentこそが正解</p>&mdash; らこ (@laco0416) <a href="https://twitter.com/laco0416/status/605225419676119040">June 1, 2015</a></blockquote>

### routings
ルーティングの定義．
controllerを使わない代わりに，汚いところを全部受け持つdirectiveを一番外側に作って，それをtemplateとして配置してます．

```ts:routes.ts
/// <reference path="../typings/vendor/angularjs/angular-route.d.ts" />

import angular = require("angular");
import {appName} from "./constants";

let app = angular.module(appName);

app.config(($routeProvider: ng.route.IRouteProvider, $locationProvider: ng.ILocationProvider) => {
  $locationProvider.html5Mode(true);
  $routeProvider
    .when("/tasks", { template: "<task-list></task-list>" });
});
```

# まとめ
本記事ではAngularJS利用プロジェクトでの自己流ファイル分割について紹介しました．
とくにCommon JS（Browserifyの`require`やES6の`import`等）を利用したファイル分割について，罠も多くやり方も人によって様々だと思います．これをきっかけに「こうした方がいいんじゃないか」「これはやめた方がいい」みたいな議論を呼びこむことが出来ればいいと考えてます．

みんなでベストプラクティスを作り上げていきましょう．

# 謝辞
本記事の内容は『[AngularJSモダンプラクティス][modern-practice]』及び，そこで紹介されているAngularJSアプリケーション『[likr/interactive-sem][interactive-sem]』に非常に影響を受けています．
先の記事の作者であり，Twitter上でAngularJSやTypeScriptについて非常に多くのアドバイスをしていただいた@armorik83氏に
深く感謝いたします．

# References
* [AngularJSモダンプラクティス - Qiita][modern-practice]
* [likr/interactive-sem][interactive-sem]
* [What's new in TypeScript · Microsoft/TypeScript Wiki][ts5]
* [vvakame/dtsm][dtsm]
* [gulp-angular-templatecache][gulp-angular-templatecache]
* [AngularJS1.4とbindToController - Qiita][bindToController]

[modern-practice]: http://qiita.com/armorik83/items/5542daed0c408cb9f605
[interactive-sem]: https://github.com/likr/interactive-sem
[ts5]: "https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#typescript-15"
[dtsm]: https://github.com/vvakame/dtsm
[gulp-angular-templatecache]: https://www.npmjs.com/package/gulp-angular-templatecache
[bindToController]: http://qiita.com/shinsukeimai/items/131780e2f0d82cbf799a