【Angular Universal】SSR で 404 を返す ( クライアント側から express Response を操作する方法 )


![0_g1TwOOdncfeTD2CC.png](https://qiita-image-store.s3.amazonaws.com/0/41793/58633c21-c6a9-0d4c-e82a-1fdc7275c613.png)

# 解決する問題

存在しないページをリクエストされた場合、通常のサイトであれば Http Status Code を `404: Not Found` で返します

しかし、SPA の場合起点となる `index.html` が問題なく取得できてしまうため、適当に打ったパスにアクセスしようと常に `200: OK` が返ってしまいます

これは SPA を開発している人なら誰でも一度は悩まされる問題だと思います
今回は、この問題を Angular Universal で解決します

# サーバー側

- サーバー側は、通常通り `@nguniversal/express-engine` を使って SSR に備えるだけです
- 詳細は、[公式ドキュメント](https://angular.io/guide/universal) を参照してください

```typescript:server.ts
import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

...

const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(path.join(DIST_FOLDER, 'main.bundle'));

const server = express();

server.engine('html', ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [ provideModuleMap(LAZY_MODULE_MAP) ],
}));

...
```

# クライアント側

## 不明な URL のリダイレクト

- どの定義にもマッチしなかった URL を捕捉し、`not-found` へリダイレクトさせておきます

```typescript:app.routing.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
    ...
    { path: 'not-found', loadChildren: './features/not-found/not-found.module#NotFoundModule' },
    { path: '**', redirectTo: 'not-found' },
];
```

## Not-Found コンポーネントの作成

- 「存在しないページです🤖」的なメッセージが表示されるページをイメージしたコンポーネントです
- シンプルに実装すると こんな感じになるかと思います

```typescript:not-found.component.ts
import { Component, Inject, OnInit, Optional, PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { RESPONSE } from '@nguniversal/express-engine/tokens';
import { Response } from 'express';

@Component({
    selector: 'app-not-found',
    templateUrl: './not-found.component.html',
    styleUrls: [ './not-found.component.scss' ],
})
export class NotFoundComponent implements OnInit {
    constructor(
        @Optional()
        @Inject(RESPONSE)
        private response: Response,
        @Inject(PLATFORM_ID) private platformId: Object,
    ) {}

    public ngOnInit() {
        if (isPlatformServer(this.platformId)) {
            this.response.status(404);
        }
    }
}
```

注目するべき点は、コンストラクタで `response ` と `platformId` を受け取っている部分です

```typescript
    constructor(
        @Optional()
        @Inject(RESPONSE)
        private response: Response,
        @Inject(PLATFORM_ID) private platformId: Object,
    ) {}
```

### RESPONSE

`response` は、型を見て分かる通り express で使用するものと全く同じオブジェクトです
`RESPONSE` 及び `REQUEST` は、`@nguniversal/express-engine` が自動的に登録してくれているので、どこでも Inject することができます

なお `Optional` デコレータはその名の通り、あったりなかったりするプロパティに使います
今回の例では、アプリケーションがサーバー側で起動した場合にのみ  `response` が存在するため付与してあります

### PLATFORM_ID

`platformId` は、アプリケーションの実行環境を判定するために使用する値です

 `@angular/common` が `isPlatformServer` 及び `isPlatformBrowser` を提供してくれているので、`platformId` を使って現在の実行環境を判定することが出来ます

```typescript
        if (isPlatformServer(this.platformId)) {
            this.response.status(404);
        }
```

今回は、サーバーサイドで実行している場合のみ `response` を操作してステータスコードを `404: Not Found` に変更します

# 結果

- 適当な URL を入力してみる
- 無事 404 で Not-Found ページが取得できました

<img width="550" alt="スクリーンショット 2018-04-11 22.37.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/41793/14a44627-0b05-fa64-9a43-34e1d7f8f3d8.png">


# まとめ

Angular を Universal 化している場合は、こんな感じでステータスコードを操作できます
今回の例では 404 への変更だけでしたが、実際は 404 以外のステータスコードを返したり、ヘッダーを操作したり…ということがありそうなので、response を操作する処理をまとめたサービスを作成し、そこから `this.responseService.setNotFound()` のように使うのが良いかと思います

Universal 化を進めるにあたって [このリポジトリ](https://github.com/patrickmichalina/fusebox-angular-universal-starter) が大変参考になっています
今回紹介したレスポンスを操作する処理も綺麗にまとまっているので、現在 Universal チャレンジしている人は参考になりそうですヾ(｡>﹏<｡)ﾉﾞ