ぼくたちがフロントエンドエンジニアと呼ばれるまで

Angular1によるSPA開発 – 基本編

こんにちは、エンジニアの尾形です。

前回のブログではSassコーディングを行なう上で必要となる各種自動化の例についてお伝えしました。
今回からはAngular1によるSPA(シングルページアプリケーション)の開発について、数回にわたってご紹介していきたいと思います。

はじめに

「Angular2、Angular4がリリースされているのに、いまどき Angular1 !?」という声が聞こえてきそうですが、Angular1の話になります。その経緯について簡単に触れたいと思います。

2016年に現在開発中のSPAの前身となるプロトタイプを作成したのですが、その当時のAngular2はまだベータ版であったため、Angular1を採用しました。

2017年になってプロジェクトが本格始動する際に、Angular2が正式リリースされていたため、Angular1からAngular2への移行を試みたのですが、メジャーバージョンアップのため互換性がなく(ng-controllerの廃止など)、移行は一筋縄にはいきませんでした。

Angular1のまま進めると技術的負債となり、Angular2への移行を進めるとコストが膨らみローンチ時期も遅くなる、、、エンジニアとしてはAngular2への移行を推し進めたいところですが、会社としてやっている以上そうはいきません。弊社代表と相談した結果、「Angular1のまま進める。技術的負債の返済についてはサービスが流行ってから考える」という方針になり、いまに至ります。

TypeScriptでコーディングする

TypeScript はマイクロソフトによって開発され、メンテナンスされているフリーでオープンソースのプログラミング言語である。TypeScriptはJavaScriptに対して、省略も可能な静的型付けとクラスベースオブジェクト指向を加えた厳密なスーパーセットとなっている。

(Wikipediaより)

TypeScriptを採用することで以下の恩恵を受けることができます。
・コンパイル時の型チェックにより、JavaScriptの実行時エラーを未然に防ぐことができる。
・型定義が使用できるため、コードの自動補完、リファクタリングが容易になる。

小規模なアプリケーションの開発であれば、素のJavaScriptでも十分だと思いますが、複数人で開発する大規模なアプリケーションやSPAの場合には、TypeScriptの恩恵を十分に受けることができます。今回はSPA開発ということで、TypeScriptを使用して開発を行ないました。なお、Angular2からはTypeScriptが推奨の開発言語となっています。

TypeScriptの型定義ファイルについて

TypeScriptを使用するうえで欠かせないのが型定義ファイルです。
TypeScript 1系では、型定義ファイルを管理するツールとして Typings の使用が推奨されていましたが、TypeScript 2からは Node.js のパッケージ管理ツールである npm で型定義ファイルを管理できるようになりました。@types というパッケージを使用します。

Angular1の型定義ファイルをインストールするには以下のようにします。

npm install @types/angular --save-dev

以下のように複数まとめてインストールすることもできます。

npm install @types/{angular,jquery} --save-dev

npmで型定義ファイルをインストールすると、node_modulesディレクトリー配下に@typesディレクトリーが作成され、型定義ファイルがインストールされます。

node_modules/
`-- @types
    |-- angular
    |   |-- README.md
    |   |-- index.d.ts
    |   |-- package.json
    |   `-- types-metadata.json
    `-- jquery
        |-- README.md
        |-- index.d.ts
        |-- package.json
        `-- types-metadata.json

有名なライブラリーの型定義ファイルについては @types パッケージで概ね提供されていますが、ものによってはバージョンが古かったり、存在しなかったりと完全ではありません。
そのような場合には、Typings の型定義ファイルを使用したり、型定義ファイルを自作・修正したりして使用するようにしています。以下に例を挙げます。

Colorbox というjQueryのモーダルウィンドウプラグインについては、trapFocusのオプションが不足していたため次のように変更しています。

# diff typings/globals/jquery.colorbox/index.d.ts.org typings/globals/jquery.colorbox/index.d.ts
51a52,55
>      * If true, keyboard focus will be limited to Colorbox's navigation and content.
>      */
>     trapFocus?: boolean;
>     /**
#

また、angular-web-notification というAngular1のデスクトップ通知モジュールについては、型定義ファイルが存在しなかったため次の定義ファイルを追加しています。

declare module "angular-web-notification" {
    var _: string;
    export = _;

    import * as angular from 'angular';

    module 'angular' {
        namespace webnotification {
            interface WebNotificationOptions {
                body?: string;
                icon?: string;
                onClick?: () => void;
                autoClose?: number;
            }

            interface WebNotification {
                showNotification(
                    title: string,
                    options: WebNotificationOptions,
                    onShow: (error: Error, hide: Function) => void
                ): void;
            }
        }
    }
}

ライブラリーについて

SPAの開発を行なっていると、どうしてもjQueryに頼りたくなるケースがあると思います。(jQueryの〇〇〇ライブラリーを使って楽をしたい!など)
Angular1はjQueryとの親和性を意識して設計されたようなので、jQueryの使用を特に禁止することはせずに、Angular1とjQueryが共存する形で開発を行ないました。
ただ、Angular2ではjQueryが非推奨となっているようですので、Angular2への移行時には苦労しそうですね。。

なお、ライブラリーの管理は基本的に npm で行なっています。しかし、npm のリポジトリーには存在せず、クライアントサイド用のパッケージ管理ツールである Bower にだけ存在するライブラリーもあったりしますので、その場合に限り Bower を使用しています。

ディレクトリー構成

Angular開発用のディレクトリー構成の例を以下に示します。

(root)
|-- bower.json                        # ※1-1
|-- bower_components/                 # ※1-2
|-- package.json                      # ※1-3
|-- node_modules/                     # ※1-4
|-- gulpfile.js                       # ※1-5
`-- src/
    `-- frontend/
        `-- typescript/               # ※1-6
            |-- app.ts                # ※1-7
            |-- controllers/          # ※1-8
            |   `-- xxx-controller.ts
            |-- services/             # ※1-9
            |   `-- xxx-service.ts
            |-- components/           # ※1-10
            |   |-- xxx-component.tpl
            |   `-- xxx-component.ts
            |-- directives/           # ※1-11
            |   `-- xxx.ts
            |-- filters/              # ※1-12
            |   `-- xxx.ts
            |-- config/               # ※1-13
            |   |-- config.json
            |   |-- config.tpl.ejs
            |   `-- config.ts
            |-- constants/            # ※1-14
            |   `-- xxx.ts
            |-- models/               # ※1-15
            |   `-- xxx.ts
            |-- utils/                # ※1-16
            |   `-- xxx.ts
            |-- typings/              # ※1-17
            |   |-- globals/
            |   |-- modules/
            |   `-- index.d.ts
            `-- typings.json          # ※1-18

※1-1 Bowerの定義ファイルです。ライブラリーの情報を管理します。
※1-2 Bowerで管理するライブラリーがインストールされるディレクトリーです。
※1-3 npmの定義ファイルです。ライブラリーの情報を管理します。
※1-4 npmで管理するライブラリーがインストールされるディレクトリーです。
※1-5 Gulpの定義ファイルです。Gulpについては次の章で説明します。
※1-6 TypeScript/Angular1のプログラム用のディレクトリーです。コンポーネントの種類でディレクトリーを分けています。
※1-7 TypeScriptのコンパイル開始のエントリーポイントとなるファイルです。
※1-8 Angular1のcontroller用のディレクトリーです。
※1-9 Angular1のservice用のディレクトリーです。
※1-10 Angular1のcomponent用のディレクトリーです。
※1-11 Angular1のdirective用のディレクトリーです。
※1-12 Angular1のfilter用のディレクトリーです。
※1-13 環境固有の設定を管理します。
※1-14 定数を配置します。
※1-15 モデルを配置します。(APIのインターフェースの定義など)
※1-16 ユーティリティーを配置します。
※1-17 Typingsの定義ファイルです。型定義ファイルの情報を管理します。
※1-18 Typingsで管理する型定義ファイルがインストールされるディレクトリーです。

自動コンパイルについて

前回のブログで、タスクランナーの Gulp についてご紹介しました。
TypeScriptのコンパイルにも Gulp を使用します。
TypeScriptのコンパイルタスクは以下のようにしています。

// プラグインを読み込む
var gulp = require('gulp');
var gulpif = require('gulp-if');
var browserify = require('browserify');
var debowerify = require('debowerify');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var stripDebug = require('gulp-strip-debug');
var uglify = require('gulp-uglify');
var ngAnnotate = require('gulp-ng-annotate');

// TypeScriptコンパイルタスク
gulp.task('tsify', function() {
    var env = process.env.APP_ENV || 'development';
    var isProduction = (env === 'production');               // ※2-1

    browserify({                                             // ※2-2
            entries: ['./src/frontend/typescript/app.ts'],   // ※2-3
            debug: !isProduction
        })
        .plugin('tsify', {                                   // ※2-4
            target: 'es5',
            module: 'commonjs',
            moduleResolution: 'node',
            sourceMap: false,
            noImplicitAny: true,
            preserveConstEnums: false,
            removeComments: true
        })
        .plugin('licensify')                                 // ※2-5
        .transform(debowerify)                               // ※2-6
        .bundle()                                            // ※2-7
        .pipe(source('app.js'))                              // ※2-8
        .pipe(buffer())
        .pipe(gulpif(isProduction, stripDebug()))            // ※2-9
        .pipe(gulpif(!isProduction, sourcemaps.init({        // ※2-10
            loadMaps: true
        })))
        .pipe(ngAnnotate())                                  // ※2-11
        .pipe(uglify({                                       // ※2-12
            preserveComments: 'license'
        }))
        .pipe(sourcemaps.write('.'))                         // ※2-13
        .pipe(gulp.dest('../js'))                            // ※2-14
    ;
});

※2-1 環境変数APP_ENVによって開発と本番を切り替えます。
※2-2 Browserifyを使用してNode.jsのモジュールをブラウザーでも使用できるようにします。
※2-3 エントリーポイントとなるプログラムを指定します。
※2-4 tsifyプラグインを指定し、TypeScriptのコンパイルを行ないます。ES5、commonjs形式で出力するようにします。
※2-5 licensifyプラグインを有効にし、ライセンスコメントを残すようにします。
※2-6 debowerifyでbower経由のものも扱えるようにします。
※2-7 bundle()でBrowserifyを実行します。
※2-8 出力ファイル名をapp.jsとします。bundle()が返したファイルストリームをgulpが扱える vinyl に変換するためにvinyl-source-streamというモジュールを使用しています。
※2-9 本番ならjsのconsoleログを削除します。
※2-10 本番でなければsourcemapsを有効にします。
※2-11 ngAnnotateを使用してAngularのminify対策を行ないます。
※2-12 uglifyを使用してjsをminifyします。preserveCommentsのオプションを指定してライセンスコメントを残すようにします。
※2-13 sourcemapsを出力します。
※2-14 コンパイルされたjsの出力先を指定します。

minify対策について

Angular1の開発においては、コーディング時にminify(jsの圧縮)について考慮・対策する必要があります。

次のControllerを例にします。

export class SampleController {
    constructor(private $scope: ng.IScope) {
    }
}

このControllerをAngular1に登録します。

import { SampleController } from './controllers/sample-controller';

var appModule = angular.module('app', []);

// Angular1にControllerを登録
appModule.controller('sampleController', SampleController);

この書き方だと、jsをminifyしない場合には正常にControllerにDI(Dependency Injection)されて問題なく動作しますが、minifyしたときにDIに失敗してしまいます。
その理由は、Angular1のDIの仕組みとして、パラメーターの文字列を解析してControllerに渡すべきオブジェクトが決定されるところにあります。minifyすると、$scopeの変数名が短縮されてしまい、名前の解決ができなくなるためです。

そこで、次のようにDIに必要なパラメーターを明示的に指定してあげることで、minifyによって変数名が変わった場合でも正常にDIされるようになります。

// DIのパラメーターを指定し、Angular1にControllerを登録
appModule.controller('sampleController', ['$scope', SampleController]);

ただし、上記の記述方法では、パラメーターの増減があった場合に修正の手間がかかったり、修正漏れにより正常に動作しないケースが出てきます。そこで、ng-annotate というツールを使用して、自動でminify対策を行なうようにしています。DIしたいメソッドの一行目に "ngInject"; のキーワードを記述しておくことで、前述のGulpでのコンパイル時に自動で対策が行われます。

ng-annotateの使用例を次に示します。

export class SampleController {
    constructor(private $scope: ng.IScope) {
        "ngInject";   // ng-annotate用のキーワードを記述
    }
}

import { SampleController } from './controllers/sample-controller';

var appModule = angular.module('app', []);

// 明示的なパラメーターの指定は不要
appModule.controller('sampleController', SampleController);

これにより、コンパイル時に次のようなDIのコードが自動で追加され、minify対策が行われます。

// "t"という変数はSampleControllerを表しています
t.$inject = ["$scope"]

まとめと予告

今回はAngular1によるSPA開発の基本部分に書かせていただきました。
次回はAntular1の設計についてご紹介させていただきます

引用元・出典
TypeScript
AngularJSモダンプラクティス
AngularJSのDIの仕組み、minify対策は覚えておこう!

第5回
Angular1によるSPA開発 – 設計編
– コンポーネント指向設計について(Angular1からAngular2への移行を視野に入れコンポーネントを使っている話)
– ライブラリーの機能はwrapして使うべし($httpのカプセル化、$scope.$broadcast()のカプセル化など)
– 環境固有の設定について(サンプル紹介)
– バリデーション(入力チェック)について(サンプル紹介)

フロントエンドエンジニアへの道

1- HTML/CSSコーディングはじめるぞ〜HTML/CSSコーディングを行なうにあたっての事前準備
2- チーム作業をするための最適なCSS規約、Sassについて考えた
3- Sassコーディングを行なう上で必要となる各種自動化の話
4- Angular1によるSPA開発 – 基本編
5- Angular1によるSPA開発 – 設計編
6- Angular1によるSPA開発 – Tips編

LINEで送る
Pocket

この記事を書いた人・プロフィール
オガティー
ニックネーム: オガティー

2014年3月から現職。当社のベンチャービジネスに心惹かれて入社。
それ以前はソフトハウス(2社)に在籍。
エンジニア歴はもう少しで20年。通信キャリアーのシステム開発、サーバーサイドプログラミングの経験が長い。
エンジニアとしてのベースはJava屋。Androidアプリ開発も。サーバーサイドはPHP, Node.jsが多い。
フロントエンドエンジニアとしては駆け出し。
趣味:ギター、ドラム、フットサル、お酒