カテゴリー
Angular UniversalでSSR/Prerenderするときに躓いたら確認したい4つの方針
※ 当ページには【広告/PR】を含む場合があります。
2020/06/02
2022/08/10
近年では、WebサイトのSEO対応の新しいスタンダードとしてSSRやSPAページのプリレンダリングなどが盛んに議論されるようになりました。
すこし残念ですがまだあまり日本ではこういうトレンドはそんなに話題になってはいません。
邦人のブログ運用から言うと、最初からWordpressのようなオールインワンの機能を持ったリッチなCMSサービスを利用してブログサイトを立ち上げるケースがほとんどであり、そもそもフルスクラッチでブログサイトを自作する人はかなり小数なのかも知れません。
兎角、Angular Universalに限らず、JS系のフレームワークで、SSR/Prerenderingさせるテクニカルな内容を解説されているサイトは海外勢の技術ブログやドキュメントの盛り上がりとくらべると、日本での盛り上がりは少ないと言わざるを得ません。
諸事情等ありますが、Angular Universalのネタは海外サイトの情報を仕入れて、自主勉強するより他はなさそうです。
最近読んだ
Angular Universalを利用する際に、静的ページのプリレンダリング中に起こるおおよそのエラーは、サーバー(Nodejs)側とブラウザ(クライアント)側の仕組みの違いをあまり理解しないで、通常のAngularアプリケーションをビルドする感覚で起こっているといえます。
逆に言うとクライアント側のコードとサーバー側のコードをより意識的に別けて書くことで、Angular Universalをより良く活用できるはずです。
今回はSSR/プリレンダリングする時に吐き出されるエラーの傾向と、解決のためのおおまかな指針に関してまとめます。
その1 〜 Webブラウザのグローバルオブジェクトの扱い
ブラウザには標準的に備わっているグローバルオブジェクトもしくはブラウザオブジェクトという機能があります。
代表格の
window
document
location
通常のAngularのSPAでもDOMの仕組みを通じてhtmlを操作を可能としているのは、このグローバルオブジェクトの存在があるからと言えます。
対して、サーバーサイドレンダリングやプリレンダリングはNodejs(サーバー側)であらかじめ処理をしてから、クライアント側へレンダリングした結果を送り出すことを行っています。
Nodejsはブラウザと違って、DOMを操作するAPIは備わっていないので、いつものSPAをビルドする感覚でAngular Universalを使うと、以下ようなエラーに遭遇することがあります。
window is not defined
document is not defined
setTimout is not defined
#...などなど
Angular Universalがビルドしている主体はサーバーサイド側です。
ブラウザー側の機能であるグローバルオブジェクトが何なのかNodejsにはコンパイル中に解決出来ないために起こるエラーのようです。
このエラーは
polyfill.ts
今日日、サードパーティー製のnpmパッケージを利用しないプロジェクトなどは考えられないくらい何かしらのライブラリを活用されているかと思います。
ブラウザ上で動作することを想定して
window
document
domino
index.html
プロジェクトに
domino
$ npm install domino
#OR
$ yarn add domino -S
でパッケージインストールします。
domino
server.ts
main.server.ts
const domino = require('domino');
const fs = require('fs');
const path = require('path');
//ビルドした際に出力されたindex.htmlを擬似DOMテンプレートとして読み込む
const template = fs.readFileSync(path.join(__dirname, '.', 'dist', 'index.html')).toString();
//windowグローバルオブジェクトとdocumentグローバルオブジェクトを設定する
const window = domino.createWindow(template);
global['window'] = window;
global['document'] = window.document;
export { AppServerModule } from './app/app.server.module';
export { renderModule, renderModuleFactory } from '@angular/platform-server';
これで通常のSPAを出力するのと同じような感覚で、プリレンダリングが可能になります。
その2 〜 Angular Universalはできるだけ新しいバージョンを使う
ご存知のように、Angularのメジャーなバージョンアップが半年ごとにあり、頻繁にメソッドの用法・文法が変わっていきます。
その都度メソッドの呼び出しに細かい見直しがあったのを気づかずに、ブラウザビルドは問題なく通ったけれど、Angular Universalではコンパイルでエラーが起こる...という不可解な状況になることもあります。
特にサードパーティのnpmパッケージを利用するときに、そのメンバ関数からネイティブDOMにアクセスするようなライブラリを扱うときに注意が必要となります。
ElementRef
nativeElement
とあるサードパーティ・ライブラリからネイティブDOMを操作するメソッドを実装した
AwesomeRenderService
import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
import { AwesomeRenderService } from './awesome-render.service';
@Component({
selector: 'awesome-dom',
template: `
<span #tex_area></span>
`,
styleUrls: ['./awesome-dom.component.scss']
})
export class AwesomeDomComponent implements OnInit {
@Input() data: any;
@ViewChild('tex_area', {
read: ElementRef,
static: true
}) elementRef: ElementRef;
constructor(private awsomeRenderer: AwesomeRenderService) {}
ngOnInit() {
if (this.data) {
// AwesomeRenderServiceクラスのrenderメソッドの引数にネイティブDOMを指定する
this.awsomeRenderer.render(this.data.text, this.elementRef.nativeElement);
}
}
としてDOMを扱うパターンが最近のangularでは多いかと思います。
ただし、
ElementRef
nativeElement
Angular Universal
もしそれより以前のangularでもDOM操作しなければならない場合には、後方互換の手段で
Renderer2
DOM周りの実装すべてを
Renderer2
結局、苦労して後方バージョンのサポートするくらいであれば、プロジェクトの
angular universal
angular cli
その3 〜 メモリーリークの確認
主にメモリーリークを確認するのは、通常のAngular SPAの開発のとき同様、SSRやプリレンダリングで開発するときも有効な手段です。
JSヒープをモニタリングすることによって、Universalアプリケーションがメモリリークを起こしているかどうかを評価することが出来ます。
Chrome DevToolsによるパフォーマンス測定の方法に関しては、以下のサイトで詳しく解説されているのでご参考いただくとして、本ブログでの詳細の説明は割愛します。
Universalアプリケーションに限らず、Angularアプリケーションの主要なメモリリークの原因となるのは、実装したサービスクラスのサブスクリプションインスタンスを
unsubscribe()
Rxjsで
unsubscribe
その4 〜 遅延ロードの利用
これは主にSSRを使う時の注意点です。
サーバーサイドでレンダリングする際には、クライアント側からのリクエストを受け取ってからサーバー内部で様々な処理がされてから、レンダリング後のindex.htmlなどの結果がレスポンスとして返ってくる出力を受け取るという一連の流れでアプリケーションが動作しています。
問題になるのは、クライアント側からサーバー側での処理に時間的な負荷のかかるリクエストを、非同期処理させてしまうことです。
SSRする際にはサーバー側からのレスポンスを同期的に受け取る仕組みが必要ですし、リクエストの負荷が大きいとサーバー側の計算資源が枯渇してパンクしてしまいます。
そもそもSSRの目的は、クライアント側(ブラウザ)でのリソースの読み込み時間や描画時間を軽減させ、より高速・快適にWebページをレンダリングする仕組みですので、サーバー側で長い処理時間が掛かってしまうと本末転倒です。
そのような場合には遅延ロード(Lazy Loading)によって細かい粒度の処理リクエストに小分け・後出しすることで、サーバー側の負荷を軽減させるのがベターです。
クライアント側からのクリックやスクロールなどのイベントで
HttpClient
IntersectionObserver
下の参考サイトなどに実装方法が解説してあるので、興味があればご参照ください。
まとめ
最近ではangularに限ったことではないのですが、vuejsやReactでもUniversalアプリケーション対応が盛り上がってきているように感じます。
サーバーサイドレンダリングやプリレンダリングはまだまだ発展途上な技術な上に、フロントエンドとバックエンドの知識が混じり合うようなこともあり、コードの実装が分かりにくいのが現状です。
とはいえ、一度慣れてしまうとWebサイトの運営に多大なベネフィットを生み出す技術であると言えます。
今後も気が向く限りUniversalアプリケーションの話題を紹介できたら良いと考えております。
参照サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー