LoginSignup
84
81

More than 5 years have passed since last update.

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

Posted at

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を取り出す
    • directivefactory登録は各ファイル内で行う
  • tscでコンパイルしたファイルを一時ディレクトリにおいて,それをbrowserifyに投げる
    • 元ファイル: ./ui/assets/{javascript,template,typing}s
    • tscoutDir: ./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まわりとかで超面倒になる
    • テスト対象はtscbrowserify通した後の最終出力(ここ微妙かも…)
  • directiveを中心とした,Component志向なAngularJSを目指す

ディレクトリ構造

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

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を適当なディレクトリに吐かせる.

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等でテンプレートファイルを.jsに変換してる場合,そのファイルもbrowserifyのエサにする.

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とか)を持たせる.

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.tsbrowserify.d.tsは不要(TypeScript 1.5から?).

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.tsresources以下を一気にimportしてしまう.

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

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

  • 属性値とかはng.resource.IResource<T>実装クラスに定義
  • $resource()ng.resource.IResourceClass<T>になる
  • $resource()返り値をそのまま or それをラップしたやつをfactoryとして登録
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でディレクティブを一気に読み込む.

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

具体例・注意点は以下.

  • ディレクティブのクラスはあくまでDirective Definition Object
  • イベント処理とかそんなんは全部Controllerにやらせる
  • controllerAsちゃんとつかう(名前が競合したら死ぬのでprefix除いたディレクティブ名そのままがいいかもしれない)
  • AngularJS 1.4以降のbindToControllerは神なので積極的に使いましょう(参考: AngularJS1.4とbindToController - Qiita
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

routings

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

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モダンプラクティス』及び,そこで紹介されているAngularJSアプリケーション『likr/interactive-sem』に非常に影響を受けています.
先の記事の作者であり,Twitter上でAngularJSやTypeScriptについて非常に多くのアドバイスをしていただいた@armorik83氏に
深く感謝いたします.

References

84
81
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
84
81