0からFirebase Cloud FunctionsとNode.jsとTypeScriptとVSCode開発環境の構築

## 背景
Cloud Functionsと Node.js, TypeScript をまともにいじったことなかったのですが、ひょんなことから0から始める機会があったので、流れをまとめました。
開発エディタには TypeScript と相性の良い VSCode(Visual Studio Code)を選びました。

## 読者対象

- Mac
- Firebase ちょっと使ったことある
- Firebase Functions 未経験
- Node.js 未経験
- TypeScript 未経験

#### 書いてること

TypeScript で書いたコードが Firebase-Tools で Cloud Functions にデプロイできるようになるまでについてまとめました。

#### 書いていないこと

- Node.js のイディオムやコードに関すること
- TypeScript のイディオムやコードに関すること


## 全体の流れ

1. :dog: Node.js 用バージョン管理ツール( nvm )のインストール
1. :cat: nvm で Firebase が推奨するバージョンをインストール
1. :bee: npm で Firebase-Tools パッケージをインストール
1. :house: Firebase-Tools でプロジェクト作成
1. :cow: npm で残りの必要パッケージをインストール
1. :train: TypeScriptに必要なパッケージやトランスパイル周りについて
1. :rocket: Firebase-Tools でデプロイ
1. :tools: VSCodeで開発しやすい環境準備
1. :notebook_with_decorative_cover: その他(調査中に見つけた情報)

## :dog: Node.js 用バージョン管理ツール( nvm )のインストール

nvm, nodenv, n, ndenv と色々ありますが、nvm を選択しました。
理由は Google トレンドの結果をシンプルに受け止めました。
こういったエコシステムは流行りよりも情報量で選んで、ささっと切り抜けないと本来の目的へ辿りつくのに時間がかかるリスクが増すためです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37274/7260ea67-55be-e51d-4256-a97fce6030bc.png)


Mac なので Homebrew でインストールします。
インストール成功すると追加設定するよう言われるので、言われたとおりやります。

```bash
$ brew install nvm

# ↓ インストール成功後
$ mkdir ~/.nvm

$ vim ~/.bash_profile

# ↓の2つを.bash_profileに追記します
export NVM_DIR=~/.nvm
source $(brew --prefix nvm)/nvm.sh

# インストール成功したか確認します
$ nvm --version
0.34.0
```

最後の確認でうまく動かない人はターミナルを再起動するか `$ source ~/.bash_profile` でリロードしてみてください。

## :cat: nvm で Firebase が推奨するバージョンをインストール
[公式ページ](https://cloud.google.com/functions/docs/concepts/nodejs-8-runtime)では `8.15.0` なのでローカルの Node.js のバージョンも合わせます。

```bash
$ nvm install v8.15.0
```

## :bee: npm で Firebase-Tools パッケージをインストール

npm とは Node.js 用パッケージマネージャーです。
他言語のパッケージマネージャー同様に、グローバルとローカルのインストール先を指定できます。

まず必要なパッケージは `firebase-tools` になります。
これがあれば Firebase に必要な開発環境の雛形を簡単に作ってくれます。

通常の Node.js 開発であれば `$ npm init` を叩けば Node.js プロジェクトが作られますし、そのときに `package.json` も一緒に作られます。
しかし `$ firebase init` を叩けば **Node.js ベースの Firebase のプロジェクトを作ってくれます。**

`firebase-tools` パッケージはグローバルにインストールします。
`-g` を指定することでグローバルパッケージとしてインストールされ、いろんな所で直接呼べるようになります。

```bash
$ npm install -g firebase-tools
```

※ グローバル環境を汚したくない人は後述する方法を参考にしてみてください。

残りの必要パッケージをインストールする前に先に Firebase プロジェクトを作成します。
そうすることで `package.json` が作成されて、そこにローカルパッケージとして追加できるからです。

## :house: Firebase-Tools でプロジェクト作成

firebase-toolsからFirebaseを操作するために認可させる必要があります。
```bash
$ firebase login
```

プロジェクト作成は
```bash
$ firebase init
```
実行すると質問されるので答えていくことで、最後に Firebase 開発に必要なファイル群が作成されます。

例えば

- Cloud Functions のみ
- default Firebase プロジェクトの指定
- TypeScript
- TSLintあり
- 依存解決する

とした場合の ファイルツリーと `package.json` は次のようになります。

```bash
$ tree -L 2
.
├── firebase.json
└── functions
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── src
    ├── tsconfig.json
    └── tslint.json
```

`package.json` の中身はこうなります。

```JSON
{
  "name": "functions",
  "scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "8"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^8.0.0",
    "firebase-functions": "^3.0.0"
  },
  "devDependencies": {
    "tslint": "^5.12.0",
    "typescript": "^3.2.2"
  },
  "private": true
}
```
`TypeScript` や `TSLint` と答えたことで依存パッケージが入っています。
また開発中しか使わないので `devDependencies` スコープになります。

`devDependencies` とは `$ npm install --production` のように本番向けだとインストールされません。
`--save` だと `dependencies` スコープ
`--save-dev` だと `devDependencies` スコープ
に入ります。



### 残りのパッケージをインストール

例えば `typed-rest-client` パッケージをインストールすると `package.json` に追記されます。

```bash
$ npm i --save typed-rest-client
```

↓は `package.json` の抜粋です。このように先程インストールしたパッケージが追記されます。

```JSON
{
  "dependencies": {
    "firebase-admin": "^8.0.0",
    "firebase-functions": "^3.0.0",
    "typed-rest-client": "^1.5.0"
  }
}
```

`express` パッケージも入れる場合は入れましょう。


### グローバル環境を汚染したくない人向け

これが正しい方法か自信そんなにないですが、

1. `$ npm init` で `package.json` 作成
2. `$ npm install --save-dev firebase-tools` で `package.json` に追記
3. `$ npm install` で `firebase-tools` をインストール
4. `$ npx firebase init` で Firebase のプロジェクト作成
5. 作成された`package.json`ファイルがあるディレクトリ上で `$ npm install --save-dev firebase-tools` をインストール

`npx` とは ローカルパッケージを実行するコマンドです。


## 他メンバーが入ってきたら

nvm のインストールは必要ですが、 npm のパッケージに関しては

```bash
$ npm install
```
と叩くだけで　`package.json` 内に記述されたパッケージ一覧をインストールしてくれます。

## :train: TypeScriptに必要なパッケージやトランスパイル周りについて

### トランスパイル
TypeScriptはご存知の通り型あり言語です。
そのままでは動かないので JavaScript にトランスパイルする必要があります。
トランスパイルには `tsc` というパッケージを使います。
`$ tsc src/ts/app.ts --outDir dist/ts` とすることで JavaScript ファイルに変換されます。
変換後の JavaScript を Firebase にデプロイします。
つまりデプロイ前にトランスパイルが必須となります。
`firebase-tools` によって TypeScript を指定して作成したプロジェクトはそこらへんの細かいファイル設定を全て作成してくれます。 :innocent: 

`tsconfig.json` だけでなく `package.json` の `script` にも反映されています。

```JSON
  "scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  }
```
`script` とは `$ npm run` で実行できるコマンド一覧を指します。
新しく作ることもできます。
`$ firebase init` で TypeScript を指定してあると、ここで事前に `npm run build` でトランスパイルされてから
それぞれのデプロイやローカル実行などが動きます。

### 型情報
JavaScriptのパッケージを使うには型情報(DefinitelyTyped) が必要です。
過去に TypeScript 1系などでは、 `tsd` や `Typings` というツールを使って
型情報管理を行っていましたが、 TypeScript 2系になってからはそれらツールは不要で
npm 内で完結できるようになりました。

例えば　Cloud Functions や Express, Node といった型情報をインストールしたい場合

```bash
$ npm install --save-dev @firebase/functions-types
$ npm install --save-dev @types/express
$ npm install --save-dev @types/node
```

のように `@types/[型情報]` とすることで型情報をインストールできます。
型情報が用意されているかどうかは、http://definitelytyped.org/ で調べることができます。
パッケージによっては始めから型情報が入っている場合もあります。

### コーディングスタイル
コーディングスタイルは[ここ](https://basarat.gitbooks.io/typescript/docs/styleguide/styleguide.html)を参考にしました。

## :rocket: Firebase-Tools でデプロイ

### 書いてみる

`functions/src/index.ts` に書いてみましょう。
エディタにはこだわりなければ `VSCode` をおすすめします。

```ts
import * as functions from 'firebase-functions'

export const helloWorld = functions.https.onRequest( (request, response) => {
  console.log('Hello world')
  response.status(200).send('Hello world')
}
```

### デプロイ

```bash
$ firebase deploy --only functions
```

`$ firebase init` で Firebase プロジェクトを選択済みであれば、このコマンドだけでデプロイが完了します。

### async/awaitを使う

下のコードは POSTリクエストを投げると Firestore にデータを書き込むサンプルです。
このサンプルは Firestore を使うため、 プロジェクトに Firestoreに関する情報が配置されている必要があります。
振り返りとして、もう一度 `$ firebase init` でプロジェクトを作り Firestore も選択したプロジェクトで試してみるといいです。

```ts
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
admin.initializeApp(functions.config().firebase);

export const helloWorld = functions.https.onRequest( async (request, response) => {
  if (request.method !== 'POST') {
      const json = {
          error_list: {
              error_code: '4000',
              message: 'This is not post request'
          }
      }
      response.status(400).send(json)
      return
  }

  await admin.firestore().collection('products').doc('1').set({
      first: "hanako",
      last: "yamada"
  })

  response.status(200).send({first: "hanako", last: "yamada"})
}
```


## :tools: VSCodeで開発しやすい環境準備

### ローカル開発

```bash
$ firebase serve
```
でエミュレーターによるローカルで立ち上がり http://localhost~ などでアクセスできるようになります。

また　npm script に↓が用意されているので, VSCode上で `npm serve` を実行するとトランスパイルして立ち上げてくれます。
`"serve": "npm run build && firebase serve --only functions,firestore"`

## :notebook_with_decorative_cover: その他(調査中に見つけた情報)

- `npm watch` を実行しながら `npm serve` を実行すると、コード変更がすぐに反映される
- `$ firebase deploy` は `--project` で Firebaseプロジェクトを指定できる.
- `--project` で指定をエイリアスに出来る `$ firebase use --add` を使う
- `--project` を未指定でどのエイリアスが使われるのかは `$ firebase use` で分かる
- Functionsだけのデプロイは `$ firebase deploy --only functions`
- 関数だけのデプロイは `$ firebase deploy --only functinos:helloWorld`
- 関数以外のデプロイは `$ firebase deploy --except functions`
- 関数の削除は `$ firebase functions:delete helloWorld`
- 関数リネームは `$ firebase deploy --only functions:newHelloWorld` 後 `$ firebase functions:delete helloWorld`
- リージョン変更も同様の流れ
- npm のインストールパッケージ一覧は `$ npm ls`で確認できる
- ルートパッケージだけの確認は `$ npm ls --depth=0`
- グローバルパッケージは `$ npm -g ls`
- Firestore エミュレータは `$ firebase setup:emulators:firestore` が必要
- エミュレーターがどのサービスをたちあげるかは`firebase.json`の中身に依存する


認証エラーは[こちら](https://qiita.com/camomile_cafe/items/7420a88db56e266dd05e)を参考にしました

## 最後に
- 駆け足で調査したので情報漏れなどがあるかもしれません。
- Google公式漁るときは、ノートに情報を抜粋しながらまとめないと、GCP側とFirebase側とで資料と用語がぶつかり、点と点の間の線がやばいことになる