angularで外部のJavaScriptライブラリを利用する方法

justInCaseでフロントエンドを担当している@diostrayです。
12/1に入社したばかりのアラサーおじさんです:innocent: (入社は最近ですが、ジョインはもうちょっと前だったりします)
経歴上サーバサイドがメインだった(はず)なんですが、流れに身を任せた結果、気づいたらフロントエンドをやることになっていました。なぜだろう…フシギダネ

justInCaseのマイページ(契約者専用のWebポータル的なもの)でangularを使っているので、地味にハマった内容をシェアします。

# TL;DR
angular-cliプロジェクトは`node-js`を使うことになるので、`npm`に登録されているライブラリ（資産）がそのまま使えます。
けれども全てのJavaScriptライブラリがnpmに登録されているかと言われればそうではないわけで、外部のJavaScriptライブラリを`script`タグで読み込まないといけない場合があります。
例） `facebook-sdk` / `google-api-sdk`

そんな時にどうすればいいの?という疑問に対して、angular-cliプロジェクトに`GoogleのJavaScriptライブラリ`を利用して、GoogleアカウントでのSSOを実装した時の設定内容を元に解説します。

[ここの内容](https://qiita.com/shimitei/items/2201bd0b88746dfb550d)に近いんですが、違いは**外部のJavaScript**というところと、angularのバージョンが違うというところです。

## おことわり
- Google APIの使い方については触れません。
- Google APIを利用するために必要なもの(クライアントID etc...)は準備済みとします。

## Envirionment(Version info)
```text:envirionment
node-js     8.11.1
angular-cli 6.0.1
angular     6.0.1
```
[angular7出ちゃったよ…](https://dev.classmethod.jp/client-side/angular-7-releases/)

# Step1. 外部のJavaScriptライブラリをindex.htmlに追加する
これはそのままなので、index.htmlの差分を貼り付るだけにします。

```diff:index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>justincase</title>
    <base href="/">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
+    <script src="https://apis.google.com/js/platform.js?onload=onLoadCallback" async defer></script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>
```

### ポイント
- [Googleの参考サイト](https://developers.google.com/identity/sign-in/web/)だと`?onload=~~`は書いてないですが、ビルドとかの都合上入れる必要があります
- 理由は後で説明します

# Step2. typesを導入する
後で`ng build`した時とかにエラーで泣く羽目になるので、先に解消するための仕込みをします。
こういう地味なところで躓くのは良いことなのか悪いことなのか未だに判断つきません。

※ typesとはなんぞ?という方は[ここ](https://qiita.com/lacolaco/items/ed1aadf335f12cd3618d)を見てください。

## Step2-1. Google APIの型定義を導入する
https://github.com/DefinitelyTyped/DefinitelyTyped で定義されています。
こういうリポジトリのメンテナにはいつも感謝です。
必要な型定義は`gapi.auth2`(と依存する`gapi`)です。

```diff:package.json
{
  "name": "test-project",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
  },
  "private": true,
  "devDependencies": {
+    "@types/gapi.auth2": "0.0.47",
  }
}
```

`npm i`で終わりと思いきや、大きな罠があります。
npmのライブラリはこれだけで良いんですが、外部ライブラリの場合は個別に読み込む設定を追加しないと型情報を拾ってくれません。(拾ってくれないだけなら良いんですが、ビルドエラーになってしまいます)
そこで、`tsconfig.app.json`に型定義を読み込む設定を追加し、ソース内で型情報を認識させるようにします。

```diff:tsconfig.app.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "module": "es2015",
-    "types": ["node"]
+    "types": ["node", "gapi", "gapi.auth2"]
  },
  "exclude": [
    "src/test.ts",
    "**/*.spec.ts"
  ]
}
```

### ポイント
- tsconfig.app.jsonに書くのを忘れない
- もう一つの解決方法もありますが、そちらはデメリットがあるので省略します

## Step2-2. 個別の型定義を導入する
Step1で非同期インクルードの設定をしたのですが、(追加した`?onload=~~`の部分)この関数を定義しないとビルドで落ちる羽目になります。

そこで、定義ファイルを`src/types`の下にこんなファイルを追加することで読み込ませます。

```typescript:window.d.ts
interface Window {
  onLoadCallback: () => void;
}

declare var window: Window;
```

### ポイント
- 当然ですがindex.htmlとこの中の関数名は一緒にする

# Step3. Google APIライブラリをプロジェクトに取り込む
ライブラリを使う時は、大抵の場合初期化処理が必要です。
どこに書くかは好みの問題だと思いますが、`app.component.ts`にそのまま書いてしまいます。

```typescript:app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor() {
    // configure google api component.
    window.onLoadCallback = () => {
      gapi.load('client:auth2', () => {
        gapi.auth2.init({
          client_id: "XXXXXXXXXXXXXXXXXXXXXX",
          fetch_basic_profile: true,
          scope: 'profile'
        });
      });
    };
  }
}
```

### ポイント
- Step1で非同期インクルードの設定をしたのは、ここでの都合上
- componentの初期化とscriptの読み込みの開始時期を比較した時に、componentの初期化が早いと、未定義エラーになってしまうため、非同期インクルードの設定が**必須**

# Step5. `ng build`する
タイトルそのままなので省略…

# Step6. `ng test`用の設定を追加する
ググり力が低かったのか、なかなか見つけられなかったユニットテスト用の設定です。
(あまりやってないのか、それともハマってないのか…)

## Step 6-1. type設定を読み込む
Step2-1の設定はビルド向けの設定で、テスト用の設定は`tscofig.spec.json`に用意されています。
`tsconfig.app.json`と同じ設定をこちらにも追加します。

```diff:tsconfig.spec.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/spec",
    "module": "commonjs",
    "types": [
      "jasmine",
      "node",
+      "gapi",
+      "gapi.auth2"
    ]
  },
  "files": [
    "test.ts",
    "polyfills.ts"
  ],
  "include": [
    "**/*.spec.ts",
    "**/*.d.ts"
  ]
}

```

## Step7. karmaの設定を変える（外部ライブラリの場合は不要かも?）
karmaはユニットテストのフレームワークです。
ユニットテスト時には当然`index.html`なんてないわけで、index.htmlに書いてあるライブラリをどこかでインクルードしないと未定義エラーが出ます。

そのための設定を管理するのが`karma.conf.js`で、そこにインクルードする設定を`files`で書きます。
ローカルファイルの場合はここの設定をしないと`ng test`でエラーになりました。

※これで相当時間を取られたのはいい思い出…

```diff:karma.conf.js
 module.exports = function (config) {
   config.set({
+     files: [
+       // ↓のような感じで、index.htmlで列記しているものを読み込み
+       { pattern: './assets/lib/hogehoge/fugagfuga.js', included: true, watch: false },
+     ],
   });
 );
```

### ポイント
- `files`の位置は先頭が良い（位置は関係ないと思いたいですが、先頭じゃないと動かない時があったような気がします）
- index.htmlに書いてある順番と同じにする
- 手抜きして`{ pattern: './assets/lib/**/*.js', included: true, watch: false }`と書かない

# 終わりに（テンプレ）
angularやAIを含めて、**最新技術で保険業界を変えていきたい!**という方をjustInCaseは募集しています!
justInCase では一緒にアプリ・サービスを作り上げていただけるエンジニアを絶賛募集中です。

- [国内初P2P保険を創る！ブロックチェーンエンジニア募集！](https://www.wantedly.com/projects/257441)
- [フィンテック最期の砦:IT保険会社のインフラをサーバーレスで作る！](https://www.wantedly.com/projects/251109)
- [保険APIで保険フィンテック先駆けを作るサーバーサイドエンジニア！](https://www.wantedly.com/projects/251106)
- [フロントエンジニアfor保険テック保険会社！](https://www.wantedly.com/projects/169590)
上で絶賛募集中なので、興味のある方はぜひご応募くださいm(_ _)m

# 参考リンク
- https://developers.google.com/identity/sign-in/web/
- https://qiita.com/lacolaco/items/ed1aadf335f12cd3618d
- https://qiita.com/shimitei/items/2201bd0b88746dfb550d