Angular CLI で作ったアプリを Heroku にデプロイして動くようにした

Angular CLI で生成したアプリを Heroku にデプロイして動作するようにするには、いくつか設定変更が必要だったので紹介。

目次

環境情報

以下を前提条件とする。

Angular プロジェクトを作る

まずは Angular CLI で Angular プロジェクトを作る。ココは普通に $ ng new

package.jsondevDependenciesdependencies に移動する

Heroku でのデプロイは、Heroku 上で git clone 相当の資材取得を行い、npm install --production 相当のコマンドで npm install が行われる。

すなわち、devDependencies に記載されたパッケージはインストールされないのだ。この仕様により、Angular CLI で生成したプロジェクトは、typescript パッケージなどが devDependencies に記載されているため、このままではビルドが上手く行われないのだ。

そこで、package.jsondevDependencies のうち、最低でも以下のパッケージを dependencies に移動しておく必要がある。

考え方としては、ng build コマンドの動作に必要なパッケージを移動しておけば良い、ということだが、心配なら全ての devDependencies の内容を dependencies に移植しても問題はない (デプロイ時の npm install に時間がかかるようになるだけ)。

自分の場合は Karma・Jasmine・Protractor など、テスト関連ツールを省き、それ以外は全て dependencies に書くことにした。@types はトランスパイル時に必須ではないだろうし、tslint も別に要らない、ts-node はよく分からない、という感じだが、この辺はとりあえず dependencies に移植しておいた。

package.jsonengines を記述する

Heroku 上で npm install 等を動作させるために、使用する Node.js および npm のバージョンを指定する必要があるので、package.jsonengines プロパティを記述しておく。バージョン番号は x を使用して曖昧指定もできるので、現行の最新メジャーバージョンである Node.js v10、npm v6 系を指定しておこう。

"engines": {
  "node": "10.x",
  "npm": "6.1.x"
}

postinstall 時に ng build を実行させる

先程も書いたように、Heroku へのデプロイ時に、自動的に npm install コマンドが動作するので、この挙動を利用して、postinstall 時に ng build を実行するように package.json に npm-run-scripts を書いておこう。

"scripts": {
  "postinstall": "npm run build",
  "build": "ng build --prod"
}

build コマンドは Angular CLI が最初から用意していると思うので、それを呼び出すだけの postinstall コマンドにしておくと良いだろう。

angular.json を書き換えてファイルが dist/ 直下に生成されるようにする

Angular v6 以降、一つの Angular プロジェクト内に複数のアプリやライブラリを持てるようになった関係で、ng build 時の成果物ファイルは dist/ 直下ではなく、dist/【アプリ名】/ ディレクトリに出力されるようになった。

今回は複数アプリを作る要件はないので、Angular v5 までと同様、dist/ 直下に index.html やら bundle.js やらが生成されるように設定を変更しようと思う。

ビルド時の出力先ディレクトリを決めているのは、angular.json の次の部分。

"architect": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": {
      "outputPath": "dist/【アプリ名】",  // ← ココ!

この outputPath 部分を、dist/【アプリ名】 から dist のみに修正する。

"options": {
  "outputPath": "dist",

コレで ng build による成果物の生成先ディレクトリを1階層上げることができた。

多分任意:HashLocationStrategy を使っておく

このあと、この Angular アプリを Express にて配信するのだが、余計なルーティングの設定を避けるために、Angular アプリ内の URL 表現を、通常の PathLocationStrategy ではなく、HashLocationStrategy に変更しておこうと思う。

何を言っているかというと、アプリ内のルーティングによる URL 変更を、http://localhost/my-page/hoge といった見た目にするのではなく、http://localhost/#/my-page/hoge と、ハッシュ # 付きの URL にしよう、というワケ。この URL なら、必ずルートパスからのハッシュリンクの形で遷移できるので、Express 側でのリダイレクト処理などが省けるかな、という狙い。

HashLocationStrategy を使うには、RouterModule.forRoot()imports に設定する箇所で、useHash: true オプションを渡してやるだけ。通常 --routing オプションによって app-routing.module.ts を生成していれば、このファイルに追記して設定すれば良い。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true })],  // ← useHash を追加
  exports: [RouterModule]
})
export class AppRoutingModule { }

ココらへんはどれだけ Express 上の設定をするかに応じて調整できるかと。今回は余計な問題を避けるためハッシュ利用にしておく。

Express サーバを用意する

最後に、これら Angular アプリを配信するための Express サーバを用意する。ng build で生成された dist/ ディレクトリ配下の静的なファイルを配信するだけなので、コードとしては簡素なモノで済ませられる。

# Express をインストールして dependencies に追加する
$ npm install --save express

# Express サーバの設定を記述するファイルを作る
$ touch server.js

server.js の中身は以下のとおり。

const path = require('path');
const express = require('express');

// サーバをインスタンス化する 
const app = express();

// 以下の設定だけで dist/index.html も返せてはいる
app.use(express.static(`${__dirname}/dist`));

// ルートへのアクセス時は念のため dist/index.html を確実に返すようにしておく
app.get('/', (req, res) => {
  res.sendFile(path.join(`${__dirname}/dist/index.html`));
});

// サーバ起動
const server = app.listen(process.env.PORT || 8080, () => {
  const host = server.address().address;
  const port = server.address().port;
  console.log(`Listening at http://${host}:${port}`);
});

一番のキモはこの部分。

app.use(express.static(`${__dirname}/dist`));

コレは

app.use('/', express.static(`${__dirname}/dist`));

とほぼ同義で、ルート配下のパスを指定されたら、そのパスに合致するファイルを静的に返すという指定。で、参照先を dist/ 配下にしているので、ng build コマンドで dist/ 配下に生成された、bundle.js やら favicon.ico やらがこの記述で取れる、というワケ。

この記述によって、http://localhost:8080/index.html はも返せるのだが、http://localhost:8080/ とアクセスした時に確実に index.html を返すために、その下で res.sendFile() を使ったルーティングを定義している。

__dirname という Node.js 組み込みの変数を使っているのは、この server.js のパスに依存した相対パスを利用せず、フルパスで指定するため。

npm start で Express サーバを起動してもらうよう package.json を修正する

あとはこの Express サーバを npm start で起動できるよう、package.jsonstart コマンドを記述しておく。

"scripts": {
  "postinstall": "npm run build",
  "build": "ng build --prod",
  "start": "node server.js",
  "dev": "ng serve"
}

Angular CLI で生成したとおりの package.json だと、ng serve コマンドを実行するために start コマンドが割り当てられているが、コレは外しておかないといけない。適当に dev コマンドでも割り当てておけば開発中にも使えるだろう。

ココまでの対応内容まとめ

ココまでの対応内容をおさらいする。

コレで準備完了。

Heroku にデプロイする

ココまでできたら、いよいよ Heroku へのデプロイだ。デプロイの仕方は通常の Node.js プロジェクトと同様、$ git push heroku master コマンドで Heroku に git push すれば良い。

$ git push heroku master

Heroku に向けて git push すると、git clonenpm install が行われ、postinstall コマンドが呼ばれて ng build --prod 処理が実行される。成果物ファイルは angular.json の設定どおり、dist/ ディレクトリ直下に出力される。

その後 npm start コマンドが呼ばれ、server.js に書いた Express サーバが起動する。

ココまでできたら、https://【アプリ名】.herokuapp.com/ にアクセスしてみよう。ビルドされた Angular アプリが動作しているはずだ。

以上

以上の作業で、とりあえず Angular アプリを Heroku 上で動作させることはできたと思う。

クライアントサイドで完結する Angular アプリなら、別に GitHub Pages に Push しても同じだが、今回は PostgreSQL も付いてくる Heroku にデプロイしたので、この後はサーバサイドも実装してみて、Angular アプリと Express サーバがうまいことやり取りして、DB も絡んで、セッション管理とかもするような Web アプリケーションを作っていきたいと思う。