Cloud Functions for FirebaseでAngular Universal

名前: ちきさん
所属: オプト
GitHub/Twitter/Qiita: @ovrmrw

---

今回のGitHubリポジトリ 

https://github.com/ovrmrw/angular-universal-with-firebase-hosting

---

参考にしたもの

https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/universal-rendering.md

https://medium.com/@cdeniz/angular-universal-on-firebase-dynamic-hosting-4fdd034af3db

https://medium.com/@cyrilletuzi/angular-server-side-rendering-in-node-with-express-universal-engine-dce21933ddce

---

#### Angular Universalやってみたい :blush:

---

#### Expressが必要... :pensive:

---

#### Firebase好きだし、Firebase HostingでAngular Universalやりたい :blush:

---

#### Firebase HostingにはExpressアプリをホスティングできない... :pensive: 

---

#### Cloud Functions for Firebaseでワンチャンあるかも...! :smiling_imp:

---

Angular CLI 1.3.0-rc.3以上とFirebase CLIをインストールする

```
$ npm install -g @angular/cli@next firebase-tools
```

---

ng new で新しいプロジェクトを作成する。

```
$ ng new angular-universal-with-firebase-hosting
```

---

Firebaseプロジェクトにする。

```
$ cd angular-universal-with-firebase-hosting
$ firebase init
```

※ HostingとFunctionsは必須。その他は下記のように設定する。

```
Do you want to install dependencies with npm now? (Y/n): N
What do you want to use as your public directory? (public): public
Configure as a single-page app (rewrite all urls to /index.html): Y
```

---

@angular/platform-serverをインストールする。

```
$ npm install --save-dev @angular/platform-server
```

---

src/app/app.module.ts を編集する。

```ts:src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    // BrowserModule
    BrowserModule.withServerTransition({ appId: 'my-app' }) // ←ここ
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

---

src/app/app.server.module.ts を追加する。

```ts:src/app/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule, // ←ここにAppModuleを入れる。
    ServerModule,
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule { }
```

---

src/main.server.ts を追加する。

```ts:src/main.server.ts
import { enableProdMode } from '@angular/core';
export { AppServerModule } from './app/app.server.module';

enableProdMode();
```

---

src/tsconfig.server.json を追加する。

```json:src/tsconfig.server.json
{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "module": "commonjs"
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
}
```

---

angular-cli.json を編集する。

```json:.angular-cli.json
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "angular-universal-with-firebase-hosting"
  },
  "apps": [
    {
      .......
    },
    {
      "platform": "server",
      "root": "src",
      "outDir": "functions/dist-server",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  .......
}
```

---

src/index.html を functions/dist-serverディレクトリ　にコピーする。

```
$ cp src/index.html functions/dist-server
```

---

functions/package.json の `dependencies` を編集する。この後 functionsディレクトリに移動して `npm install` する。

```json:functions/package.json

{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "dependencies": {
    "firebase-admin": "~4.2.1",
    "firebase-functions": "^0.5.7",
    "@angular/animations": "^4.0.0",
    "@angular/common": "^4.0.0",
    "@angular/compiler": "^4.0.0",
    "@angular/core": "^4.0.0",
    "@angular/forms": "^4.0.0",
    "@angular/http": "^4.0.0",
    "@angular/platform-browser": "^4.0.0",
    "@angular/platform-browser-dynamic": "^4.0.0",
    "@angular/platform-server": "^4.0.0",
    "@angular/router": "^4.0.0",
    "core-js": "^2.4.1",
    "express": "^4.15.3",
    "rxjs": "^5.4.2",
    "zone.js": "^0.8.16"
  }
  "private": true
}
```

---

functions/index.js を編集する。

```js:functions/index.js
require('zone.js/dist/zone-node');
const functions = require('firebase-functions');
const express = require('express');
const path = require('path')

const renderModuleFactory = require('@angular/platform-server').renderModuleFactory;

const AppServerModuleNgFactory = require('./dist-server/main.bundle').AppServerModuleNgFactory;

const index = require('fs').readFileSync(path.resolve(__dirname, './dist-server/index.html'), 'utf8');

const app = express();

app.get('/', (req, res) => {
  renderModuleFactory(AppServerModuleNgFactory, { document: index, url: '/' })
  .then(html => {
    res.send(html);
  })
  .catch(err => {
    console.log(err)
  });
});

exports.ssr = functions.https.onRequest(app);
```

---

firebase.json　を編集する。

```json:firebase.json
{
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "**",
        "function": "ssr"
      }
    ]
  }
}
```

---

publicフォルダを追加し、空の hoge.css というファイルを配置する。

(ファイルが何もないとそのフォルダをFirebase Hostingにデプロイできないため)

---

package.json の `scripts` を編集する。

```json:package.json
{
  "scripts": {
    .....,
    "build": "ng build --prod --app 1 --output-hashing=none",
    "copy": "cp src/index.html functions/dist-server", 
    "deploy": "npm run build && npm run copy && firebase deploy",
    .....
  }
}
```

---

デプロイする。

```
$ npm run deploy
```

---

動作確認

https://easy-ng-universal.firebaseapp.com/

---

#### なにかいろいろもんだいはありそうだけど、くわしいことはよくわからない。

---

### Thanks :raised_hands_tone1: