AngularJS with ES6/TypeScript and Browserify
『AngularJSモダンプラクティス - Qiita』を参考に,僕がAngularJSでなんかつくるときのディレクトリ構成とかそういうのを雑にまとめてみました.
対象
- TypeScript 1.5 or ECMAScript 6
- AngularJS >= 1.3.0
- Browserify
方針
- 基本的にはTypeScriptで記述
- 型定義ファイルは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でひとまとめにする
- テストはES6で書いたものを
spec/javascripts
に配置し,browserify
+babelify
に喰わせる- テストをTypeScriptで書くとmockまわりとかで超面倒になる
- テスト対象は
tsc
とbrowserify
通した後の最終出力(ここ微妙かも…)
- directiveを中心とした,Component志向なAngularJSを目指す
ディレクトリ構造
app.ts
をエントリーポイントとしてtsc
及びbrowserify
に喰わせる.
angular.module
を複数に分割する場合は,javascripts
以下にネームスペース的ディレクトリ噛ませて,それぞれにエントリーポイントを作ればいい.
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
を適当なディレクトリに吐かせる.
{
"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等でテンプレートファイルを.jsに変換してる場合,そのファイルもbrowserify
のエサにする.
{
"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とか)を持たせる.
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から?).
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してしまう.
import "./task";
import "./user";
リソースの具体例・注意点は以下.
- 属性値とかは
ng.resource.IResource<T>
実装クラスに定義 -
$resource()
はng.resource.IResourceClass<T>
になる -
$resource()
返り値をそのまま or それをラップしたやつをfactoryとして登録
/// <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
でディレクティブを一気に読み込む.
import "./task_list";
import "./profile";
具体例・注意点は以下.
- ディレクティブのクラスはあくまでDirective Definition Object
- イベント処理とかそんなんは全部Controllerにやらせる
- controllerAsちゃんとつかう(名前が競合したら死ぬのでprefix除いたディレクティブ名そのままがいいかもしれない)
- AngularJS 1.4以降の
bindToController
は神なので積極的に使いましょう(参考: AngularJS1.4とbindToController - Qiita)
/// <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
というかやはりControllerと云う概念が負の遺産なんだよ Componentこそが正解
— らこ (@laco0416) June 1, 2015
routings
ルーティングの定義.
controllerを使わない代わりに,汚いところを全部受け持つdirectiveを一番外側に作って,それをtemplateとして配置してます.
/// <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モダンプラクティス』及び,そこで紹介されているAngularJSアプリケーション『likr/interactive-sem』に非常に影響を受けています.
先の記事の作者であり,Twitter上でAngularJSやTypeScriptについて非常に多くのアドバイスをしていただいた@armorik83氏に
深く感謝いたします.