AngularJsを勉強してつまづいたところを振り返る【コントローラー編】

# はじめに
先日、AngularJsを初めて勉強したのですが、
これまでFWを用いたフロントエンドの改修経験が全くなく、
勉強の過程で色々つまづいたので個人的に重要そうなところを備忘録として残します。

今回はAngularJsのコントローラーについて取り上げます。

:::note warn
バージョンについて
本記事で扱うフレームワークはAngular（ver2以降）ではなく、AngularJs（ver1.x系）です
:::

# 初めてコントローラーの定義を見た時の感想
タイトルにもある通り、AngularJsのコントローラーの定義の理解に時間がかかりました。

AngularJsを解説しているサイトを色々見ていると
サンプルコードとして以下のようなソースが取り上げられるのですが、

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="mdxEEyW" data-user="takashi-ebina" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/takashi-ebina/pen/mdxEEyW">
  Untitled</a> by takashi.ebina (<a href="https://codepen.io/takashi-ebina">@takashi-ebina</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

上記のようなソースを見た時に、
**`MainController`側で定義されている`$scope.message` とHTMLの`{{message}}`の箇所がバインドされている**
というのはなんとなく理解することができました。

ですが、js側のコントローラーの定義に関して２つの疑問あり、腹落ちできない状態でした。

```javascript:
angular.module('App', [])
.controller('MainController', 
    ['$scope', function ($scope) {
        $scope.message = 'Hello world!';
        // Javascriptの処理・・
    }]);
```

* **疑問点１**
  * 配列内の文字列とfunctionの引数には何を指定したらよい・・？
    * 上記ソースの`['$scope', function ($scope) {...`の箇所
* **疑問点２**
  * 配列の中に文字列とfunctionを突っ込んでいるけどなんでこんな書き方しないといけない・・？
    * controllerメソッドの引数に配列を指定して、その配列の中には文字列とfunctionが存在するという構造がとても読みづらく、何をしたいのかよくわからないなというのが率直な感想。
    * 配列内の文字列とfunctionの引数名がどちらも`$scope`となっているが、この二つに関連性があるのか？

特に２つ目の疑問点に関しては、フレームワークなのでそういうものとして覚えるしかないのかなと初めは思ったのですが、
AngularJsについて理解を深めるにつれて、上記の疑問点がクリアになりました。

# コントローラーの定義について

## 疑問点１：配列内の文字列とfunctionの引数について

controllerメソッドの引数で渡している文字列およびfunctionに指定している引数の実態を理解するためには、
AngularJsの**サービス**という概念について知る必要があります。

### サービスについて
AngularJsが指すサービスは**アプリ上で特定のタスクを実行するためのコンポーネント**のことです。
（個人的にはAngularJsが用意している便利な機能をサービスという形で提供していると理解しています。）

一例ですが、サーバとの非同期通信を行うような機能は`$http`サービスという形で提供されています。

### サービスの利用方法について
では、サービスという便利な機能が利用できるようになるためにはどうすれば良いか？
先ほど取り上げた`$http`サービスを利用するためにはコントローラーの定義を以下の通りに修正する必要があります。

```diff_javascript
angular.module('App', [])
.controller('MainController', 
-    ['$scope', function ($scope) {
+    ['$scope', '$http', function ($scope, $http) {
        $scope.message = 'Hello world!';
        // Javascriptの処理・・
    }]);
```

**配列の文字列、functionの引数名にサービス名を追加**することでサービスを利用できるようになります。
今回の場合は`$scope`、`$http`サービスが利用可能になっています。
（この書き方そのもの説明に関しては、疑問点２のタイミングで解説いたします。）


また、今回の記事の本題である、コントローラーの定義から逸れますが、
`$http`サービスの使い方についてもざっくりと紹介します。

<details><summary>$httpサービスの使い方</summary>

`$http`サービスはサーバとの非同期通信を行うサービスですが、
今回は[RESAS-API](https://opendata.resas-portal.go.jp)を用いて都道府県データを取得する画面を作成いたします。

### サンプルソース

```js:app.js
angular.module('App', [])
.controller('MainController', 
    ['$scope', '$http', function ($scope, $http) {
        /* 【処理１】
         * GETで都道府県情報を取得する
         * 設定内容はRESAS-API仕様書参照
         * URL:https://opendata.resas-portal.go.jp/docs/api/v1/prefectures.html
         */
        $http({
            method: 'GET',
            url: 'https://opendata.resas-portal.go.jp/api/v1/prefectures',
            dataType: 'jsonp',
            jsonpCallback: 'callback',
            headers: {
                'X-API-KEY': 'APIキー' // 自身で発行したAPIキーを指定してください
            }
        }).
        /* 【処理２】
         * 非同期通信の結果で呼ばれるメソッド決まる
         * 正常の場合：successメソッド内のコールバック関数
         * エラーの場合：errorメソッド内のコールバック関数
         */
          success(function (data, status, headers, config) {
            // 非同期通信成功時に呼ばれる処理
            $scope.responseData = data;
          }).
          error(function (data, status, headers, config) {
            // 非同期通信失敗時に呼ばれる処理
            console.log(status);
          });  
    }]);
```

```html:index.html
<!DOCTYPE html>
<html lang="ja" ng-app="App">
<head>
    <title>AngularJS 入門</title>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta charset="UTF-8">
    <script src="//code.angularjs.org/1.5.0/angular.min.js"></script>
    <script src="./app.js"></script>
</head>
<body>
    <h1>RESAS-API 実行結果</h1>
    <div ng-controller="MainController">
        <div>{{responseData}}</div>
    </div>
</body>
```

### 実行結果
![スクリーンショット 2022-07-11 23.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292212/0e5101c5-61c3-459f-7f16-139de464a199.png)

### サンプルソースの解説
#### 処理１について
GETでHTTP通信を行い、都道府県の情報を取得しています。
都道府県の情報を取得するためのURLやパラメータなどは、[API仕様書](https://opendata.resas-portal.go.jp/docs/api/v1/prefectures.html)を参考にしてください。
（APIキーが必要ですが、別途発行してしてください。）


#### 処理２について
【処理１】の非同期通信の結果で呼ばれるメソッドが決まります。
* **正常の場合：successメソッド内のコールバック関数**
* **エラーの場合：errorメソッド内のコールバック関数**

このコールバック関数に渡される引数の値は以下となります。

| 引数名 | 概要 |
|:-----------|:------------|
| data       | レスポンスボディ     |
| status     | レスポンスのHTTPステータスコード      |
| headers    | ヘッダーを取得する関数  |
| config     | リクエストを作成するのに利用された構成オブジェクト  |

今回の場合は、レスポンスボディの情報を$scopeオブジェクトに設定して画面に出力しています。
</details>

## 疑問点２：配列の中に文字列とfunctionを指定する書き方について
この独特な書き方を理解するためには、AngularJsの**DI（依存性の注入）[^1]**　について理解する必要があります。

[^1]: DIについての説明は以下のサイトがわかりやすいです。
参考URL：https://qiita.com/okazuki/items/a0f2fb0a63ca88340ff6

### DI（依存性の注入）について
AngularJsでは、**引数の変数名からDIすべきオブジェクトを判断することが可能** となっており、
以下の書き方でサービスを利用することができます。

```js
angular.module('App', [])
.controller('MainController', function ($scope) {
        $scope.message = 'Hello world!';
    });
```

**疑問点１**でサービスを利用するために、**functionの引数名にサービス名を記載**すると説明しました。
これをDIの観点で言い換えると、コントローラーの処理実行時に
AngularJsがfunctionの引数名から必要なオブジェクト（上記ソースの場合は`$scope`サービス）を判断し、引数に渡すことで
コントローラの処理内でサービスが利用できると説明できます。

また、引数の変数名からDIすべきオブジェクトを判断するという仕組み上、この書き方の場合、
**「$scope」から「scope」（存在しないサービス名）に変更する**とエラーが発生します。

```js
angular.module('App', [])
.controller('MainController', 
        function (scope) { // 存在しないサービス名
        $scope.message = 'Hello world!';
    });
```

```:コンソール
angular.js:13236 Error: [$injector:unpr] http://errors.angularjs.org/1.5.0/$injector/unpr?p0=scopeProvider%20%3C-%20scope%20%3C-%20MainController
    at angular.js:38:1
    at angular.js:4397:19
    at Object.d [as get] (angular.js:4550:39)
    at angular.js:4402:28
    at d (angular.js:4550:39)
    at e (angular.js:4574:58)
    at Object.invoke (angular.js:4596:18)
    at T.instance (angular.js:9855:24)
    at u (angular.js:8927:34)
    at g (angular.js:8226:13)
(anonymous)	@	angular.js:13236
```

### コードのminifyを行う場合のDIについて
先ほどのfunctionの引数名とサービスの名前を一致させるというやり方の場合、
コードのminify[^2]を行う際に、DIが失敗しエラーとなってしまいます。

これは、minifyを行った際に、functionの引数名が短縮されてしまい、AngularJs側でDIすべきオブジェクトを特定することができなくなっているためです。

[^2]: コード上の不要な改行やインデントを削除することでファイルサイズを圧縮して読み込みの速度を向上させることです。
参考URL：https://ssaits.jp/promapedia/technology/minify.html

```js:minify後のソース
// 引数名が「$scope」→「a」に変更されているが、「a」という名前のサービスが存在しないためエラーとなる
angular.module("App",[]).controller("MainController",function(a){$scope.message="Hello world!"});
```


このようなコードのminifyすると動作ができないといったケースでは、別の実装をする必要があります。
その実装が**配列アノテーション**という手法であり、本記事で最初に取り上げたソースコードとなります。

#### 配列アノテーションの書き方
`['サービス名1','サービス名2', ・・・, function (変数名1, 変数名2, ・・・) {・・・ コントローラーの処理 ・・・}]`

```js:配列アノテーションを用いたコントローラーの宣言
angular.module('App', [])
.controller('MainController', 
    ['$scope', function ($scope) {
        $scope.message = 'Hello world!';
        // Javascriptの処理・・
    }]);
```

上記のような配列アノテーションを用いてコントローラーの宣言をした場合、
**サービス名が文字列リテラルとして指定されるため、minifyの対象となりません。**

```js:minify後のソース
angular.module("App",[]).controller("MainController",["$scope",function(a){a.message="Hello world!"}]);
```

minifyした場合、配列の一つ目の要素と、functionの第一引数の名前が一致しませんが、
配列アノテーションでは、**引数の並び順と、対応関係のある引数にDIを行う**ため、名前が一致しても問題ありません。

# おわりに
AngularJsのコントローラーの説明は以上となります。
コントローラーの実装はあまり見慣れない書き方で、個人的には難しかったのですが少しでも参考になる方がいましたら幸いです。

# 参考
https://html5experts.jp/canidoweb/16732/

https://js.studio-kingdom.com/angularjs/ng_service/$http

https://qiita.com/kawaz/items/363f430d21ec729f1b7d

https://atmarkit.itmedia.co.jp/ait/articles/1604/06/news025_2.html