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氏に
深く感謝いたします.