Angular CLI で作ったアプリを Heroku にデプロイして動くようにした
Angular CLI で生成したアプリを Heroku にデプロイして動作するようにするには、いくつか設定変更が必要だったので紹介。
目次
- 環境情報
- Angular プロジェクトを作る
package.jsonのdevDependenciesをdependenciesに移動するpackage.jsonにenginesを記述するpostinstall時にng buildを実行させるangular.jsonを書き換えてファイルがdist/直下に生成されるようにする- 多分任意 : HashLocationStrategy を使っておく
- Express サーバを用意する
npm startで Express サーバを起動してもらうようpackage.jsonを修正する- ココまでの対応内容まとめ
- Heroku にデプロイする
- 以上
環境情報
- Angular CLI : v7.0.4
- Node.js : v10.7.0
- npm : v6.1.0
- OS : macOS Mojave・Windows10 で検証
以下を前提条件とする。
- Angular CLI に関するひととおりの知識があること (コマンド等は詳しく説明しない)
- Heroku CLI がインストールしてあり、Angular プロジェクトと Heroku アプリの紐付けなどは自分でできること (細かいところまで説明しない)
Angular プロジェクトを作る
まずは Angular CLI で Angular プロジェクトを作る。ココは普通に $ ng new。
package.json の devDependencies を dependencies に移動する
Heroku でのデプロイは、Heroku 上で git clone 相当の資材取得を行い、npm install --production 相当のコマンドで npm install が行われる。
すなわち、devDependencies に記載されたパッケージはインストールされないのだ。この仕様により、Angular CLI で生成したプロジェクトは、typescript パッケージなどが devDependencies に記載されているため、このままではビルドが上手く行われないのだ。
そこで、package.json の devDependencies のうち、最低でも以下のパッケージを dependencies に移動しておく必要がある。
- @angular-devkit/build-angular
- @angular/cli
- @angular/compiler-cli
- @angular/language-service
- typescript
考え方としては、ng build コマンドの動作に必要なパッケージを移動しておけば良い、ということだが、心配なら全ての devDependencies の内容を dependencies に移植しても問題はない (デプロイ時の npm install に時間がかかるようになるだけ)。
自分の場合は Karma・Jasmine・Protractor など、テスト関連ツールを省き、それ以外は全て dependencies に書くことにした。@types はトランスパイル時に必須ではないだろうし、tslint も別に要らない、ts-node はよく分からない、という感じだが、この辺はとりあえず dependencies に移植しておいた。
package.json に engines を記述する
Heroku 上で npm install 等を動作させるために、使用する Node.js および npm のバージョンを指定する必要があるので、package.json に engines プロパティを記述しておく。バージョン番号は 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 側でのリダイレクト処理などが省けるかな、という狙い。
- 参考 : Angular Docs
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.json に start コマンドを記述しておく。
"scripts": {
"postinstall": "npm run build",
"build": "ng build --prod",
"start": "node server.js",
"dev": "ng serve"
}
Angular CLI で生成したとおりの package.json だと、ng serve コマンドを実行するために start コマンドが割り当てられているが、コレは外しておかないといけない。適当に dev コマンドでも割り当てておけば開発中にも使えるだろう。
ココまでの対応内容まとめ
ココまでの対応内容をおさらいする。
angular.jsonoutputPathをdist/に変更
package.json- ビルド・デプロイ時に必要なパッケージを
devDependenciesからdependenciesに移植する enginesプロパティを記述するpostinstall時にng build --prodを実行するように設定するstartコマンドでnode server.jsを実行するように設定する
- ビルド・デプロイ時に必要なパッケージを
server.jsng buildで生成されたdist/ディレクトリ配下のファイルを静的に返すルーティングを実装しておく
app-routing.module.ts- HashLocationStrategy を使用するように変更 (任意)
コレで準備完了。
Heroku にデプロイする
ココまでできたら、いよいよ Heroku へのデプロイだ。デプロイの仕方は通常の Node.js プロジェクトと同様、$ git push heroku master コマンドで Heroku に git push すれば良い。
$ git push heroku master
Heroku に向けて git push すると、git clone と npm 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 アプリケーションを作っていきたいと思う。