一つのプロジェクトにAngularアプリを複数入れてみる

# はじめに

とあるWebアプリを開発するにあたって、一つのプロジェクトに複数のAngularアプリを構成したくなった。
したくなったので、やってみた。

「もっといい方法あるよー」とか「これ間違ってるよー」とかあったら教えてください。

> 2018/3/20 更新
> アプリでassetsを使っているときにpathが合わなくなることが判明。
> assets内のリソースへのpathを相対pathで指定することと、
> .angular-cli.jsonの設定に以下を追加することで対応できた。

> 2018/12/30 追記
> タグにもありますが、angular v5の時代の記事です。
> angular v7.1.4現在、`.angular-cli.json`は`angular.json`に置き換わっており、中身を見る限りマルチプロジェクトに対応しているようです。
> `angular.json`の解説は[2018 Angular advent calender内の記事](https://qiita.com/m4fg/items/6426375ae64762c93124)などあるので、そちらを参照ください。

```json:.angular-cli.json
  "apps": [
    {
      "baseHref": "./",
      ...
```

# フォルダ構成

書いてみたけどスーパー見づらい。

```sh
.
|-- coverage    // テストカバレッジ
|   |-- first-app
|   |-- second-app
|   `-- server
|-- dist    // ビルド結果
|   |-- first-app
|   |-- second-app
|   `-- server
|-- e2e    // e2eテストは一旦パス
|-- server    // バックエンド側ソース
|   |-- index.spec.ts
|   |-- index.ts
|   `-- tsconfig.json
|-- src    // フロント(Angular)アプリ側ソース
|   |-- first-app
|   |   |-- app
|   |   |   |-- app.component.css
|   |   |   |-- app.component.html
|   |   |   |-- app.component.spec.ts
|   |   |   |-- app.component.ts
|   |   |   `-- app.module.ts
|   |   |-- assets
|   |   |-- environments
|   |   |   |-- environment.prod.ts
|   |   |   `-- environment.ts
|   |   |-- favicon.ico
|   |   |-- index.html
|   |   |-- main.ts
|   |   |-- polyfills.ts
|   |   |-- styles.css
|   |   |-- test.ts
|   |   |-- tsconfig.app.json
|   |   |-- tsconfig.spec.json
|   |   `-- typings.d.ts
|   |-- second-app
|   |   |-- app
|   |   |   |-- app.component.css
|   |   |   |-- app.component.html
|   |   |   |-- app.component.spec.ts
|   |   |   |-- app.component.ts
|   |   |   `-- app.module.ts
|   |   |-- assets
|   |   |-- environments
|   |   |   |-- environment.prod.ts
|   |   |   `-- environment.ts
|   |   |-- favicon.ico
|   |   |-- index.html
|   |   |-- main.ts
|   |   |-- polyfills.ts
|   |   |-- styles.css
|   |   |-- test.ts
|   |   |-- tsconfig.app.json
|   |   |-- tsconfig.spec.json
|   |   `-- typings.d.ts
|   |-- polyfills.ts    // アプリ一括テスト用polyfill
|   |-- test.ts    // アプリ一括テスト用ブートストラップ
|   `-- tsconfig.spec.json    // アプリ一括テスト用コンフィグ
|-- README.md
|-- karma.conf.js
|-- package-lock.json
|-- package.json
|-- protractor.conf.js
|-- tsconfig.json
`-- tslint.json
```

# 複数アプリ構成になるまで

## それぞれのアプリのソースを置く

`ng new`で生成されたソースだと、`./src`以下にアプリのソースが集まっている。
複数アプリに対応できるように、ソースの移動&コピーを行う。

```sh
# './src' copy to './src/first-app' & './src/second-app'

mv ./src ./first-app
mkdir ./src
mv ./first-app ./src
cp -r ./src/first-app ./src/second-app
```

それぞれのtsconfig.xxx.jsonのパスがずれるので直す

```json:tsconfig.app.json(tsconfig.spec.jsonも同様)
{
  "extends": "../../tsconfig.json",  // <-- ここと
  "compilerOptions": {
    "outDir": "../../out-tsc/app",   // <-- ここ
    "baseUrl": "./",
    ...
```

アプリ側はこれだけ。

## .angular-cli.jsonを直す

Angular CLIが複数アプリを正しく扱えるように設定してやる。

```json:.angular-cli.json
  ...
  "apps": [
    {
      "name": "first-app",        // (Optional) 名前を付けてやるとあとでうれしいかも。
      "root": "src/first-app",    // ソースコードのルートディレクトリ
      "outDir": "dist/first",     // 出力先ディレクトリ
      "deployUrl": "/first-app",  // デプロイするurl (expressのルート設定と合わせる)
      ...
    },
    {
      "name": "second-app",        // 二つ目のアプリも同様に
      "root": "src/second-app",
      "outDir": "dist/second",
      "deployUrl": "/second-app",
      ...
    }
  ],
  ...
```

## パスによってExpressが返すページを変える

サーバのフレームワークはExpressを使ってます。
執筆時点では`express@4.16.3`でした。

また、AngularがTypescriptなのでサーバサイドもTypescriptで書いちゃいました。
tsconfig.jsonもフロント側のtsconfig.app.jsonあたりから持ってきてサクッと書いちゃいます。

```ts:(server)index.ts
import * as path from "path";
import * as express from "express";
const app = express();

// ...

// .angular-cli.jsonで指定したdeployUrlとパスを合わせること
app.use("/first-app", express.static(path.resolve(`${__dirname}/../first-app`));
app.use("/second-app", express.static(path.resolve(`${__dirname}/../second-app`));

// ...
```

```json:(server)tsconfig.json
{
  "extends": "../tsconfig.json", // Anguler CLIでルートディレクトリに吐き出される設定を使っとく
  "compilerOptions": {
    "outDir": "../dist/server",  // ビルド出力先を設定
    "baseUrl": "./",
    "module": "commonjs",        // イマイチ把握してない。誰か教えてください。
    "target": "es2015",          // 使っているNode.jsのバージョンに合わせて出力フォーマットを決める。(node v8系なのでes2015は動く)
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ]
}
```

## 複数アプリをそれぞれビルドする

今の所、.angular-cli.jsonのappsに複数アプリを書いても一括で全部ビルドしてくれたりserveしてくれたりする機能はないようです。
なので、個別にビルドします。
サーバサイドもついでにビルドします。

```sh
# .angular-cli.jsonにnameプロパティをちゃんと付けてたらアプリ名で指定できる
ng build --prod -a first-app
ng build --prod -a second-app
tsc -p server/

# nameプロパティ付けてない場合は配列のインデックスで。
# あとの工程でインデックスをずらすので注意。
ng build --prod -a 0
ng build --prod -a 1
tsc -p server/
```

こんだけ打つの面倒なので、npm scriptに書いちゃいましょう。
開発に使ってるPCがWindowsで、デプロイ先がDebian系だったりするので、npm-run-allのお世話になってます。
Linux系OS使わせて。

```json:package.json
  "scripts": {
    "build": "npm-run-all -s build:**:*",
    "build:first": "ng build --prod -a first",
    "build:second": "ng build --prod -a second",
    "build:server": "tsc -p server/",
  }
```

## 実行する！

```sh
node dist/server/index.js
```

`http://<deploy-domain>/first-app`と`http://<deploy-domain>/second-app`にアクセスするとそれぞれのアプリが表示されるはずです。

# 実行まではできたけど。

もうちょっとやりたいことがある。

* ng serveで画面を確認しながらコード書きたい。
* ユニットテストはどうやるの？

## ng serveしたい

これは、`ng serve`にオプションを付けるだけで出来ました。

```sh
ng serve --app first-app
ng serve --app second-app
```

`ng build`と同じですね。

## ユニットテストしたい

アプリをそれぞれ別個にテストするだけなら簡単。

```sh
ng test --app first-app
ng test --app second-app
```

これも`ng build`と同じくオプション付けるだけでした。
ここで疑問。

> ユニットテストのカバレッジってどう取る…？

それぞれ別個にカバレッジを取るだけならまぁ良い。

```sh
ng test --app first-app --code-coverage
ng test --app second-app --code-coverage
```

不満があるとすれば、片方実行するたびにもう一方のカバレッジ結果に上書きされてしまう点。
この方法だとこのプロジェクト全体のカバレッジを見ることができないんですね。

**プロジェクト全体のテストカバレッジを俯瞰したい！**

というわけで構成をもう少しいじります。

### src以下に全体ユニットテスト用ファイルを置く

全体ユニットテストって良く分かんない言葉ですね。でも他に思いつかないからそのまま書きます。
src以下に以下の3ファイルを置きましょう。

```ts:polyfills.ts
import "./first-app/polyfills";
import "./second-app/polyfills";
```

```text:test.ts
(デフォルトのtest.tsをそのままコピーしてくるだけ)
```

```text:tsconfig.spec.json
(デフォルトのtsconfig.spec.jsonをそのままコピーしてくるだけ)
```

### .angular-cli.jsonに全体ユニットテスト用構成を追加する

`apps`以下にこんなのを追加します。

```json:.angular-cli.json
  "apps": [
    {
      "name": "testAll",   // 分かりやすい名前にしましょう。
      "root": "src/",      // テスト用ファイルを置いたディレクトリをルートにする。
      "main": "test.ts",   // 実際使わないのでなんでもいい。"省略する"ことに対しては怒られるのでなんか書く。
      "test": "test.ts",   // さっき置いたファイル。
      "testTsconfig": "tsconfig.spec.json",  // さっき置いたファイル。
      "polyfills": "polyfills.ts"  // さっき置いたファイル。ブラウザ使ってテストするので、各アプリで必要なpolyfillを適用してやる必要がある。
    },
    ...
```

僕は`apps`以下の先頭にこいつを追加しました。
先頭にある場合は、`ng test`(appオプションを省略)したときの動作がデフォルトでこの構成になります。
アプリ名を指定しなかった場合は全体テスト、指定した場合はアプリ個別のテスト、というように使い分けています。

あくまでこの構成はテストしか考えていないので、`ng serve`や`ng build`(appオプションを省略)はどちらもエラーで通らなくなっています。
`serve`や`build`をしたいときは`--app 1`や`--app 2`を付けてあげましょう。（さっき書いてたインデックスとはずれてるので注意）

### テストしてみる

おもむろに`ng test`してみましょう。全てのアプリに対してテストが実行されたと思います。
`ng test --code-coverage`してやると、全てのアプリのテストカバレッジが取得できるようになりました。
めでたし。

# おわりに

あ、サーバサイドのテスト入れるの忘れてた…。