M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Angular2アプリケーションのデプロイについて学ぶ

※この記事を執筆している時点の "Angular2" の最新バージョンは 2.4.7 、 "Quickstart" の最新バージョンは 2.4.0 です。情報の新鮮さの判断基準として考慮してください。

Angular2(TypeScript版)のガイド資料の「デプロイ」(Deployment)のセクションを読んで、Angularアプリケーションを本番稼動させる方法の概要を調べてみました(原典 2017/02/18取得)。

               * * *

デプロイ

このページではAngularアプリケーションをデプロイし本番稼動向けに最適化するツールやテクニックについて解説している。

概要

このガイドでは、Angularアプリケーションをリモートで稼働しているサーバ〔≠開発マシン〕上にデプロイする方法を説明する。まずは簡単ではあるもののあまり効率的ではない方法からはじめて順々に複雑ではあるもののより効率的な方法へと進んでいく。

もっともシンプルな方法

Angularアプリケーションをデプロイする方法のうちもっともシンプルなものは、開発環境からWebサーバへとファイルセットをごっそりとアップロードしてしまうものだ。

それそのものはすでにローカルできちんと動いている。それをほとんどそのままユーザがアクセスできる非ローカルのサーバにコピーしてしまうのだ。

  1. すべてを(あるいはほとんどすべてを)ローカルのプロジェクト・フォルダから非ローカルのサーバのフォルダにコピーする
  2. アプリケーションがサブフォルダで提供されるものである場合は、index.html<base href>の値を適切に編集する。例えば、index.htmlにアクセスするためのURLが www.mysite.com/my/app/ならば、<base href="/my/app/">となる。他方URLが www.mysite.com/であれば何もする必要はない。詳しくは後述する。
  3. サーバ側の設定を変更し存在しないファイルへの要求をindex.htmlにリダイレクトするようにさせる。これも詳しくは後述する。
  4. 後述するようにプロダクション・モードを有効化する(これは必須ではない)。

これがもっともシンプルなデプロイの方法だ。

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.jsJSONではなく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のようにアプリケーションの起動時に先行読み込みされるファイルからうっかり当該モジュールをJavaScriptimportステートメントを使って読み込む指定をしてしまったとする。すると当該モジュールについて直ちに読み込みが行われてしまうのだ。

バンドルの設定をするときは遅延読み込みを必ず考慮に入れなくてはならない。遅延ロードされるモジュールは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;

IIShttp://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 PagesGitHub Pagesのサーバでは直接的に設定することはできないが、404ページを追加することで代替させることができる。index.html404.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取得)。