Angular Universalについて語るスレ

SEOやOGP対応の文脈で必要になってくるであろう、Server-side Rendering (SSR) を実現する Angular Universal についての情報共有などをしましょう!

1 Like

本当にあったハマりポイント話

OGP対応をしたかったので、以下のような環境でSSR構築しました。
(構築中にアップデートとかもしちゃってるのでもしかしたら生成ファイルに差異があるかも?)

[akai@thinkpadArch hoge-project]$ ng version
     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
Angular CLI: 8.2.2
Node: 10.12.0
OS: linux x64
Angular: 8.2.2
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... platform-server, router
Package                                    Version
--------------------------------------------------------------------
@angular-devkit/architect                  0.802.2
@angular-devkit/build-angular              0.802.2
@angular-devkit/build-optimizer            0.802.2
@angular-devkit/build-webpack              0.802.2
@angular-devkit/core                       8.2.2
@angular-devkit/schematics                 8.2.2
@angular/cdk                               8.1.3
@angular/fire                              5.2.1
@ngtools/webpack                           8.2.2
@nguniversal/express-engine                8.1.1
@nguniversal/module-map-ngfactory-loader   8.1.1
@schematics/angular                        8.2.2
@schematics/update                         0.802.2
rxjs                                       6.5.2
typescript                                 3.5.3
webpack                                    4.38.0

ハマった対応

  1. requestAnimationFramesetTimeout に置き換えるといつまでも画面描写が終わらない

    • rAFループでアニメーションしていた部分が問題だった(サーバー内で処理ループすると終わらないっぽい)。 単純置き換えではなく文脈で動作のスイッチングが必要
  2. コンポーネント外での createdAt = firestore.Timestamp.now(); みたいな値の初期化

    • コンポーネントのレンダリング前に firestore とかのパッケージが呼ばれるとだめっぽい?
      • @angular/fire とかそのへんのパッケージも注意
  3. ws とか xhr2 とかfirebase x universalで必要になるらしい諸々のパッケージのインストール

    • 参考にしていた記事に出てこなかったのですっ飛ばしていたが、無いと変なところでレンダリングが失敗するようだった(公式では入れろと書いてある)
      • 中途半端に動くので原因に気づきにくい。 通信のポリフィルになるようなのだが、通信してないやろ〜みたいなところで必要だった
    • firebase x universalの公式イントロはこちら ですが、最終更新が結構古いのでモニョる
      • 最新の生成されるコードとちょっと差異がある… 下で言及してるけど、 webpack.config.js の externals とか結局どうするのが正しいのだろうか この辺のせいでうまく動かなかったし
  4. 生成された webpack.config.jsmodule.exports.externals (12行目) { './dist/server/main': 'require("./server/main")' } はコメントアウトして、
    [/^firebase]に置き換える https://github.com/angular/angularfire2/blob/master/docs/universal/getting-started.md

    • require(".server/main") をコメントアウトしないとサービング時にエラー
    • [/^firebase] を記述しないとfirestoreからのデータ取得画面をプリレンダリング時に以下のようなエラー
      • [2019-08-17T12:26:37.869Z]  @firebase/app: 
            Warning: This is a browser-targeted Firebase bundle but it appears it is being
            run in a Node environment.  If running in a Node environment, make sure you
            are using the bundle specified by the "main" field in package.json.
            If you are using Webpack, you can specify "main" as the first item in
            "resolve.mainFields":
            https://webpack.js.org/configuration/resolve/#resolvemainfields
            If using Rollup, use the rollup-plugin-node-resolve plugin and specify "main"
            as the first item in "mainFields", e.g. ['main', 'module'].
            https://github.com/rollup/rollup-plugin-node-resolve
        Discarding entity body for GET requests
        
  5. 生成されたnpmタスクの build:client-and-server-bundles の --bundleDependencies all オプションは削除しないとサービング時にエラー

  6. ng g pipe hoge コマンドで生成されたパイプの、手入れしていない不要なspecファイルを削除しないとビルドエラー

1 Like

URLの取得について

OGP対応をするためにmetaサービスを使って、og:urlなどに現在のURLを設定する必要があると思います。

Firabase FunctionsをSSRに利用していて DOCUMENT をDIしlocationにアクセスした場合、
FunctionsのURLを取得してしまうので、うまくOGP設定に反映されない問題に少しハマりました。

この問題に対して、以下のような対策が考えられるかと思います。

  1. 定数的にURLを書いてしまう (environment.prod.tsとかに書いて使う時にimportする)
  2. server.ts の ngExpressEngine の providers に ORIGIN_URL のようなものを登録し、リクエストのURLをAngularでDIできるようにする
  3. @nguniversal/express-engine/tokens から REQUEST@Optional でDIし、 this.request.header('X-Forwarded-Host') のようにしてFunctionsにリダイレクトされる前の要求されたホスト名やプロトコルを取得する

2の手法がお行儀いい気がするのですが、server.tsに変更を加えるのが億劫だったので、私は3の手法をとって対応しました。 (リダイレクト噛んでる場合なんか問題になるかな……? 要検証かも……)

参考にしてください。

2 Likes

URLを得るには DOCUMENT じゃなくて Location をDIしたほうがいいですが、それでもFunctionsのエンドポイントURLになりそうですね

https://github.com/angular/universal/blob/master/modules/express-engine/src/main.ts#L88 ここの options.url に server.ts側で入れちゃうのがいいんじゃないかなと思いましたが未検証です

1 Like

おおお具体的にありがとうございます!
ちょっとここにどうやって入れるのかとかパッとわからないですがあとで試してみます!!

render関数にURLを渡すことで、DIしたDOCUMENTのurl(this.document.location.hrefなど)が想定したものになることがわかりましたので、以下のようにserver.tsを実装し、やりたいことが実現できました!

app.get('*', (req, res) => {
  const WHITE_HOSTS   = ['example.com', 'example.hoge.com']; // 外部からリダイレクトされて想定外の挙動をするのを防ぐ
  const forwerdedHost = WHITE_HOSTS.find(x => x === req.get('x-forwarded-host'));
  const protocol      = forwerdedHost ? req.get('x-forwarded-proto') : req.protocol;
  const host          = forwerdedHost || req.hostname;
  const url           = `${protocol}://${host}${req.url}`;
  res.render('index', { req, url });
});

URLを得るには DOCUMENT じゃなくて Location をDIしたほうがいいですが、

ここが気がかりなのですが、どうやらLocationで行えるのはパス(ホストが無い)部分のみのようでした。

OGPで利用する「現在のページの絶対URL」を取得したいのでwindow.location(document.location)を使いたいような気がします:thinking:

2 Likes

Hey, guys.

Angularをv9にしたらUniversal with Firebaseが死にました!

結論から書くと、v9ではまだUniversal x Firebaseは早い、対応していないという感じなのですが、
試行錯誤の結果をざっくりと共有しますね。

前提環境ですが、Angular v8時代に https://github.com/angular/angularfire2/blob/master/docs/universal/getting-started.md でUniversal x Firebaseの仕組みを構築していたという感じです。
このスレッドを見ていただければ私の状態はなんとなく理解できるかと思います。

  1. angular関連のパッケージとfirebase関連のパッケージをアップデート
  2. 一旦対象プロジェクトから離れ、新たにng new hogeし、ng add @nguniversal/express-engineをすることでどんなファイルが生成され、どんなファイルに変更があるのかを確認
  3. https://github.com/angular/angularfire2/blob/master/docs/universal/getting-started.md で行ったUniversal x Firebase対応は基本的に遅れているものと認識し、↑で行ったng add @nguniversal/express-engineの変更に手動で合わせていく
    • webpack.server.config.jsの削除や、server.tsの変更が主で、私の環境では本作業によって以下のようなファイルが書き換えられ(削除され)ました
      • .circleci/config.yml angular.json firebase.json functions/src/index.ts karma.json package-lock.json package.json server.ts src/app/app-routing.module.ts src/app/app.component.spec.ts src/app/app.server.module.ts src/browserslist src/main.server.ts src/test.ts tsconfig.app.json tsconfig.json tsconfig.server.json tslint.json webpack.server.config.js
  4. 動かないので調査。 以下のIssueがよくまとまっていました
  5. @angular/fireは5.4.2を利用していたが、おおよそ↑の通り進めたらコンパイルが通るようになり、npm run dev:ssrでローカルサービングできるようになった
  6. externalDependencies@angular/fire/firestoreを含める対応を行っているためか、firestoreに依存したコンテンツ(私のプロジェクトではブログ記事)がSSRされなくなっていた
  7. circleciだのの設定をいい感じに修正(ng addのデフォルト設定を利用したら、dist内のパスがほんの少し変わったため)して、デプロイ
  8. Functionsのログくん Cannot find module '@angular/fire/firestore'
    • なんでなんだろう? ローカルでは怒られないんだけれども……。 Functionsのuniversal関数(↓)がダメなんかな?
    • export const universal = functions.https.onRequest((request, response) => {
        response.set('Cache-Control', 'max-age=86400');
        require(`${process.cwd()}/dist/ver1000000/server/main.js`).app(request, response);
      });
      
  9. angularfire開発チームに1週間位で対応したい気持ちがあるようなので、SSRは一時的に諦めて一旦は素SPAでデプロイ

firebaseへの対応度はともかく、ng add @universal/express-engineの仕組み、いいですね〜〜。
以前やったときはなんとなくHACKHACKみたいな書き方してた気がするんですが、ng addでやるとちゃんとAngularに乗っかってる感があります。

1 Like

ちょうど v9 Universalの話をメンテナの VikramさんがAngularAirで話してました

1 Like