Angular2アプリケーションのデプロイについて学ぶ
※この記事を執筆している時点の "Angular2" の最新バージョンは 2.4.7 、 "Quickstart" の最新バージョンは 2.4.0 です。情報の新鮮さの判断基準として考慮してください。
Angular2(TypeScript版)のガイド資料の「デプロイ」(Deployment)のセクションを読んで、Angularアプリケーションを本番稼動させる方法の概要を調べてみました(原典 2017/02/18取得)。
* * *
デプロイ
このページではAngularアプリケーションをデプロイし本番稼動向けに最適化するツールやテクニックについて解説している。
概要
このガイドでは、Angularアプリケーションをリモートで稼働しているサーバ〔≠開発マシン〕上にデプロイする方法を説明する。まずは簡単ではあるもののあまり効率的ではない方法からはじめて順々に複雑ではあるもののより効率的な方法へと進んでいく。
- もっともシンプルな方法は開発環境をまるごとサーバにコピーするというものだ。
- AOTコンパイルはいくつかの効率化戦略のうちの最初のものだ。AOTクックブックに示されたより詳しい説明を読む必要があるかもしれない。
- Webpackは、AOTのためのプラグインをも含む、よりリッチなエコシステムを持つパッケージングのためのツールとして人気があるものだ。まずはAngularのWebpackガイドを読んで初歩を学んでみてほしい。このページではさらなる効率化のためのアドバイスを示している。ともあれWebpackそのものについての独学も必要になるだろう。
- Angularの設定変更のセクションではクライアント側でアプリケーションをより高速に動作させるための方法を示している。
- サーバの設定変更のセクションでは、デプロイ方法に関わらずサーバ側で必要になる可能性のある変更について解説している。
もっともシンプルな方法
Angularアプリケーションをデプロイする方法のうちもっともシンプルなものは、開発環境からWebサーバへとファイルセットをごっそりとアップロードしてしまうものだ。
それそのものはすでにローカルできちんと動いている。それをほとんどそのままユーザがアクセスできる非ローカルのサーバにコピーしてしまうのだ。
- すべてを(あるいはほとんどすべてを)ローカルのプロジェクト・フォルダから非ローカルのサーバのフォルダにコピーする
- アプリケーションがサブフォルダで提供されるものである場合は、
index.html
の<base href>
の値を適切に編集する。例えば、index.html
にアクセスするためのURLがwww.mysite.com/my/app/
ならば、<base href="/my/app/">
となる。他方URLがwww.mysite.com/
であれば何もする必要はない。詳しくは後述する。 - サーバ側の設定を変更し存在しないファイルへの要求を
index.html
にリダイレクトするようにさせる。これも詳しくは後述する。 - 後述するようにプロダクション・モードを有効化する(これは必須ではない)。
これがもっともシンプルなデプロイの方法だ。
NOTE: これは本番稼働向けのデプロイではない。最適化されておらず高速に動作しないだろう。この方法は進行中の開発の進捗状況やアイデアをマネージャやチームメイト、その他の関係者たちに共有するのには十分かもしれない。後述の本番稼動に向けた最適化のセクションをぜひ読んでおいてほしい。
npmパッケージをWeb から読み込む(SystemJSを使用)
node_modules
フォルダ内のnpmパッケージ群はAngularアプリケーションをWebブラウザで稼動させる上で実際には必要でない余分のコードを多く含んでいる。クイックスタートのプロジェクトではこのフォルダに2万500以上のファイルがインストールされ、それらを合わせると180MB以上にもなる。アプリケーションが必要としているのはそのうちほんの一部だ。
これらの不要なファイル群をアップロードするには長い時間がかかるし、ユーザは細切れにされたファイルをダウンロードするために必要以上に待たされることとなる。
必要なファイルだけをWebから読み込むようにしよう。
①index.html
ファイルをコピーして、スクリプト群をnode_module
から読み込むよう指定している箇所をWebから読み込むように指定するものに置き換えよう。
<!-- Polyfills --> <script src="https://unpkg.com/core-js/client/shim.min.js"></script> <!-- Update these package versions as needed --> <script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script> <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
②systemjs.config.js
スクリプトを読み込む指定はsystemjs.config.server.js
を読み込む指定に置き換えよう。
<!-- This SystemJS configuration loads umd packages from the web --> <script src="systemjs.config.server.js"></script>
③systemjs.config.server.js
ファイル(内容については後述)をsrc/
フォルダに追加する〔訳者: systemjs.config.js
をコピーしたあと後述の変更を加える〕。このファイルはSystemJSのため設定ファイルの別バージョンであり、Angular(およびその他のサードパーティ・パッケージ群)のUMDバージョンをWebからロードするように指定するものだ。
systemjs.config.js
に対する変更はsystemjs.config.server.js
にも同じように適用する必要がある。
paths
の値に注目してほしい:
paths: { 'npm:': 'https://unpkg.com/' // path serves as alias },
〔訳者:SystemJSの設定ファイルである systemjs.config.js
はJSONではなくJavaScriptファイルだから、実際にはあえて別ファイル化せずに location.host
の値の次第でエイリアスを変更するようなコードにするほうが好ましいかもしれない。〕
もとのSystemJS設定ではnpm
パス・セグメントは node_modules/
を指すものに置き換えられる。一方デプロイ用の設定ではhttps://unpkg.comを指すものに置き換えられる。これはnpmパッケージをホストするサイトの1つだ。これでパッケージはWebから直接読み込まれるようになる。同じようなサービスは他にもある。
このような公開サービスからのパッケージ読み込みが好ましくないか不可能であるという場合、systemjs.config.server.js
の中で指定されているファイルとフォルダをサーバ上のライブラリ・フォルダにコピーする。そしてnpm
パス・セグメントがこのフォルダを指し示すように設定を変更する。
〔・・・中略・・・〕
本番稼動に向けた最適化
開発環境をまるごとデプロイする方法はたしかに正しく機能するが、まったくのところ効率的とは言い難いものだ。
クライアントは個々のアプリケーション・コードとテンプレート・ファイルをダウンロードするため多くの要求を行う必要がある。この事実はブラウザの開発者ツールのネットワーク・タブから容易に読み取ることができる。個々の小さなファイルのダウンロードはデータそのものの転送よりもサーバ側との交信のために多くの時間を要することもある。
開発環境のファイルは可読性やデバッグの便宜のためにコメントと空白文字を含んでいる。ブラウザはライブラリのコードのうちアプリケーションが必要とする部分だけでなく、ライブラリのコードの全体をダウンロードする。サーバからクライアントへ渡されるコードの容量(いわゆる「ペイロード」)はアプリケーションの実行に厳密に必要と云いうるものを著しく上回る可能性がある。
多くの要求と特大のペイロードによりアプリケーションはその起動に必要以上に長い時間を要する結果となる。ユーザがアプリケーションを使えるようになるまで数秒(あるいはそれ以上)の時間が必要になるだろう。
これが実際のところ問題となりうるかどうか、それはビジネスと技術のそれぞれに係る事項によって変わってくる。
これが問題となるのなら、要求の数を減らし、応答のサイズを小さくするためのツールと技術を使用することになる。
- AOTコンパイル: Angularコンポーネントのテンプレートを事前にコンパイルする。
- バンドル: モジュールを連結して単一ファイルにする。
- インライン化: テンプレートHTML〔外部ファイル化されたテンプレート〕とCSS〔同じく〕をコンポーネント・コード内に移動する。
- コード縮小: 余分な空白文字、コメント、そして必須ではない構文要素を除去する。
- コード難読化: 短く、暗号っぽい変数名や関数名に書き換える。
- デッドコードの除去: 参照されないモジュールと使用されないコードを削除する。
- ライブラリの切り詰め: 使用されないライブラリ参照をなくし、必要な機能以外を削ぎ落とす。
パフォーマンス計測: 最適化により目に見える効果があったか確認する。
ツールごとにその働きは異なる。それらは相互に補強し合うように働く。
使用するビルドシステムは開発者の好みによる。選択したシステムが何であれ、単一のステップのうちに本番稼動に向けたビルド作業を自動化してくれるものかどうか確認しよう。
AOTコンパイル
Angular AOTコンパイラはアプリケーション・コンポーネントとそのテンプレートをビルド・プロセスにおいて事前コンパイルする。
AOTコンパイルされたアプリケーションはいくつかの理由からより高速に動作する。
- アプリケーション・コンポーネントはクライアント側でのコンパイルなしに速やかに実行される。
- テンプレートはコンポーネントのコードに埋め込まれるので、クライアントからテンプレート・ファイルを要求する必要がない。
- Angularコンパイラはクライアントにダウンロードされない。コンパイラはそれ自体だいぶかさ張る。
- コンパイラは使用されないAngularディレクティブを削ぎ落とす。
AOTコンパイルについてより詳しくはAOTクックブックを参照のこと。AOTコンパイラをコマンドラインから実行し、バンドル、コード縮小、コード難読化、そして不要なコードの除去のためにRollupを使用する方法が解説されている。
Webpack(およびAOT)
Webpack 2はテンプレートとスタイルシートのインライン化、バンドル、コード縮小、そしてコード難読化のためのもう1つの有力な選択肢だ。Angularガイドの"Webpack: an introduction"を読めば、AngularとともにWebpackを使用する方法について初歩的な事項を学ぶことができるだろう。
Webpack 自体のコンフィギュレーションに関しては公式プラグインAngular AOT Webpackプラグインの使用を検討してみてほしい。このプラグインはTypeScriptアプリケーション・コードをトランスパイルし、遅延ロードされる独立したNgModule
群をバンドルし、AOTコンパイルを実行する。ソースコードに対する変更は必要ない。
Rollupでデッドコードを除去する
実行されないコードは何であれデッドコードである。アプリケーション本体やサードパーティ・ライブラリからこれらを削除することで、アプリケーション全体としてのサイズを小さくすることができる。
「ツリーシェイキング」〔Tree shaking〕とはJavaScriptモジュールがエクスポートする〔モジュール外に公開する〕APIの中からデッドコードを除去することを指す。アプリケーション本体のコードがインポートしないAPIはコード上から除去される。
ツリーシェイキングはRollupにより広く普及した。この人気のツールはバンドル、コード縮小、そしてコード難読化のためのプラグインを含むエコシステムを持つ。ツリーシェイキングとデッドコードの除去についてより詳しくはRollupの開発者リッチ・ハリスのこの投稿を参照のこと。
ライブラリの切り詰め
デッドコードの削除をすべて自動で済まそうと考えないこと。
使用していないライブラリは削除すること。とくにindex.html
で読み込んでいるスクリプトのうち不必要なものはそうすること。使用しているライブラリでも、よりサイズの小さい代替物がないか検討すること。
いくつかのライブラリはアプリケーションが必要とする機能だけを持つ、よりスリムなバージョンをビルドする方法を提供している。必要な機能だけを選んでインポートさせるライブラリもある。RxJSは好例だ。RxJS Observable
オペレータは個別にインポートすることができる。
まずはパフォーマンスを測定しよう
アプリケーションを遅くしている原因が何なのか明快かつ正確に理解をしていれば、何をどのように最適化すべきかという点についてより良い決定ができる。原因は開発者が想像するものとは違うかもしれない。目に見える成果がない最適化のためにたくさんの時間とお金を空費することになるかもしれない。したがってまずアプリケーションを実際に稼働する想定の環境において計測する必要がある。
Chromeブラウザの開発者ツールの資料のネットワーク・パフォーマンスに関するページはパフォーマンス計測について学び始めるのによいだろう。
WebPageTestはもう1つのよい選択肢である。このツールはデプロイ〔における最適化〕が首尾良く行われているかを検証するのを助けてくれる。
Angular の設定変更
Angularの設定変更によりアプリケーションを高速で起動させることもできるが、〔設定を誤ると〕まったく動作しないようにもできてしまう。
base
タグ
HTMLタグ<base href="..."/>
は画像やスクリプト、CSSなどのアセットの相対URLを解決するための基底パスを指定するものだ。例えば、< base href="/my/app/">
と記述すると、ブラウザはsome/place/foo.jpg
という画像ファイルの相対パスを見てmy/app/some/place/foo.jpg
というパスをサーバに対して要求する。Angularルーターはページ遷移の際このタグで指定された値をコンポーネント、テンプレート、そしてモジュール・ファイルの基底パスとみなす。
NOTE: 別の方法APP_BASE_HREFも参照のこと。
開発中は一般にindex.html
を格納するフォルダでアプリケーションを起動することが多いだろう。これがルート・フォルダだから、index.html
の上部には<base href="/">
が記述される。
しかし他の人間と共有するサーバや本番稼動向けのサーバでは、アプリケーションはサブフォルダに配置されるかもしれない。例えば、アプリケーションはhttp://www.mysite.com/mysrc/app/
のようなURLで読み込まれるかもしれない。この場合サブフォルダはmy/app/
であるから、サーバ上にデプロイするindex.html
には<base href="/my/app">
と記述する。
base
タグの設定を誤ると、アプリケーションの読み込みは失敗し、ブラウザのコンソールには 404 - Not Found
エラーが出力されるだろう。それらのファイルがどこに配置されているのかを確認し、適切なタグ記述をすること。
プロダクション・モードを有効化する
Angularアプリケーションはデフォルトでは開発モードで実行される。ブラウザのコンソールには次のようなメッセージが出力されるだろう:
Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode.
プロダクション・モードに切り替えることで、開発モード固有のチェクが無効化されアプリケーションはより高速に実行されるようになる。
アプリケーションをリモート・サーバ上で実行するにあたりプロダクション・モードを有効化するには、main.ts
に次のコードを追加する:
import { enableProdMode } from '@angular/core'; // ローカル以外で実行されているときはプロダクション・モードON if (!/localhost/.test(document.location.host)) { enableProdMode(); }
遅延読み込み
アプリケーションの起動時に絶対に必要なモジュール群だけを読み込むようにすることで、アプリケーションの起動時間は劇的に短縮される。
Angularルーターの設定を変更し、他のすべてのモジュール(とそれに関連するコード)を遅延読み込みさせよう。アプリケーションが起動するまで読み込みを待機するか、オンデマンドで遅延読み込みするかのいずれかを選ぶことができる。
遅延読み込みされるモジュールからAPIを先行読み込みしてはならない
これはよくある間違いだ。モジュールを遅延読み込みするようにしたのに、ルートのAppModule
のようにアプリケーションの起動時に先行読み込みされるファイルからうっかり当該モジュールをJavaScriptのimport
ステートメントを使って読み込む指定をしてしまったとする。すると当該モジュールについて直ちに読み込みが行われてしまうのだ。
バンドルの設定をするときは遅延読み込みを必ず考慮に入れなくてはならない。遅延ロードされるモジュールはJavaScriptのimportステートメントによってロードされるわけではないから、バンドル過程でそれらは除外されてしまう。バンドルを担当するツールはAngularルーターの設定を関知しないし、遅延読み込みされるモジュールのための分離されたバンドル・ファイルを生成したりもしない。それらのファイルは開発者が自分で用意しなくてはならない。
Angular AOT Webpackプラグインは遅延読み込みされるNgModules
を自動認識し、それらについて分離されたバンドル・ファイルを生成してくれる。
サーバの設定変更
このセクションではサーバやサーバにデプロイされるファイルに対して行うべき変更について取り扱っている。
ルーターを使用する場合必ずindex.html
にフォールバックさせる
Angularアプリケーションはシンプルな静的HTMLコンテンツを扱うサーバから提供することができる。Angularはページ・コンテンツの生成をクライアント側で実行するから、Webサーバ側で何かしらの仕組みを使って動的にそれを行う必要はないのだ。
アプリケーションでAngularルーターを使用する場合、クライアント側からサーバ上に存在しないコンテンツが要求されたとき、アプリケーションのホスト・ページ(index.html
)を戻すように、サーバ側の設定をしておく必要がある。
ルーター制御されたアプリケーションは「ディープリンク」〔deep link〕をサポートすべきだ。ディープリンクとはアプリケーションのコンポーネントのパスを指し示すURLのこと。例えば、 http://www.mysite.com/heroes/42
はID が42のヒーローの情報を表示する詳細ページのディープリンクだ。
ブラウザ上で実行されているアプリケーション内のリンクから当該のURLに遷移する分には何の問題もない。AngularルーターはURLを読み取ってそれが指し示すヒーローの詳細ページを画面上に表示する。
しかしEメールに記載されたリンクをクリックしたり、ブラウザのアドレスバーに直接入力をしたり、あるいはただ単にヒーローの詳細ページでブラウザの再読み込みボタンをクリックしたりすると・・・それらのアクションは〔ルーターではなく〕ブラウザ自身により制御され、実行中のAngularアプリケーションの制御を外れてしまう。ブラウザは当該のURLを直接Webサーバに要求することになる。
静的コンテンツを提供するWebサーバはhttp://www.mysite.com/
への要求であればいつでもindex.html
を返すが、 http://www.mysite.com/heroes/42
への要求の場合これを拒絶し、 404 - Not Found
エラーを返す。こうした場合index.html
を返すようあえて設定してやる必要がある。
フォールバック設定の例
これについてすべてのサーバで機能する単一の設定はない。続く各セクションではいくつかの有名なWebサーバで上述のえっ体を行う方法を解説している。この一覧は決して網羅的なものではない。しかし概要を理解するには適しているはずだ。
開発サーバ
○Lite-Server: クイックスタートのデフォルトのサーバ。 index.html
にフォールバックするよう予め設定済みだ。
○Webpack-Dev-Server:開発サーバ・オプションのhistoryApiFallback
エントリーに次のような設定を行う:
historyApiFallback: { disableDotRule: true, htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] }
プロダクション・サーバ
○Apache: こちらに示されているように リライト・ルールを.htaccess
ファイルに追加する:
RewriteEngine On # If an existing asset or directory is requested go to it as it is RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR] RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d RewriteRule ^ - [L] # If the requested resource doesn't exist, use index.html RewriteRule ^ /index.html
○NGinx: "フロント・コントローラ・パターンのWebアプリケーション"で解説されているようにtry_files
を使用する:
try_files $uri $uri/ /index.html;
○IIS: http://stackoverflow.com/questions/12614072/how-do-i-configure-iis-for-url-rewriting-an-angularjs-application-in-html5-mode/26152011#26152011:こちらのページで示されているようにして、web.config
にリライト・ルールを追加:
<system .webserver=""> <rewrite> <rules> <rule name="Angular Routes" stopprocessing="true"> <match url=".*"> <conditions logicalgrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchtype="IsFile" negate="true"> <add input="{REQUEST_FILENAME}" matchtype="IsDirectory" negate="true"> </add></add></conditions> <action type="Rewrite" url="/"> </action></match></rule> </rules> </rewrite> </system>
○GitHub Pages: GitHub Pagesのサーバでは直接的に設定することはできないが、404ページを追加することで代替させることができる。index.html
を404.html
という名前でコピーする。ファイルは404ステータスで返されてしまうが、ブラウザはページを読み込み、Angularアプリケーションはきちんと動く。masterブランチのdocs/にコンテンツを置き、https://www.bennadel.com/blog/3181-including-node-modules-and-vendors-folders-in-your-github-pages-site.htm:.nojekyllファイルを作成するのもよいだろう。
○Firebase hosting: リライト・ルールを追加する:
"rewrites": [ { "source": "**", "destination": "/index.html" } ]
異なるサーバからサービスをリクエストする(CORS)
Angularアプリケーションの開発者はアプリケーション自体がホストされているサーバ以外のサーバ上で稼働するサービスへの要求を行う過程でクロス・オリジン・リソース共有エラーに遭遇することもあるだろう。このような要求はサーバが明示的に許可していない限りブラウザはこれを禁止する。
これに関してクライアント・アプリケーションの側でできることは何もない。サーバ側で上述の要求を受け容れるよう設定する必要がある。https://enable-cors.org/server.htmlで個々のサーバ製品でCORSを有効化する方法を調べてみること。
* * *
以上、Angular2(TypeScript版)のガイド資料の「デプロイ」(Deployment)のセクションより抄訳(原典 2017/02/18取得)。