Angular Universal なアプリケーションで webfontloader を使う

linkタグでWEBフォントを読み込むより、[webfontloader](https://github.com/typekit/webfontloader) で非同期に読み込む方が [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) のスコアが良くなります。

なので、Angular で webfontloader を使ってみましょう。

Angular Universal で SSR(サーバーサイドレンダリング) しているような場合は、ひと工夫しないとエラーが出てしまうため、この記事を参考にしてください。

GitHub に[サンプルコード](https://github.com/naopoyo/angular-webfontloader-example)を用意しています。実際に動かしてみてください :eyes:

この記事に出てくる[コマンドをまとめています](https://hackersheet.com/sheets/200)。こちらも見てください :star2:

## 前提

* Angular 9.1.12
* Angular Universal 9.1.11

なアプリケーションで webfontloader を使ってWebフォントを利用します。

## 準備

`npm i webfontloader --save` でインストールします。

`npm i @types/webfontloader --save-dev` で型定義もインストールすると良いです。

## まずは普通に使ってみる

Angular Universal で SSR していないようなアプリケーションで webfontloader を使ってみましょう。

```typescript:app.component.ts
import { Component, OnInit } from '@angular/core'
import * as WebFont from 'webfontloader'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  ngOnInit() {
    WebFont.load({
      google: {
        families: ['Modak', 'M+PLUS+1p:400,900'],
      },
    })
  }
}
```

AppComponent の ngOnInit でこんな風に呼び出してみましょう。

CSS に `font-family: Modak;` とすればフォントが適用されます。

## APP_INITIALIZER を使う

アプリケーション全体で読み込む必要があるのであれば、APP_INITIALIZER でアプリケーションの初期化時に webfontloader の設定をするようにしましょう。
`ng g service app-init` で AppInitService を作成して、以下のように修正します。

```typescript:app-init.service.ts
import { Injectable } from '@angular/core'
import * as WebFont from 'webfontloader'

export function AppInitFactory(appInit: AppInitService) {
  return () => appInit.init()
}

@Injectable({
  providedIn: 'root',
})
export class AppInitService {
  constructor() {}

  init() {
    this.initWebFont()
  }

  private initWebFont() {
    WebFont.load({
      google: {
        families: ['Modak', 'M+PLUS+1p:400,900'],
      },
    })
  }
}
```

次に、app.module.ts を修正します。

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

import { AppComponent } from './app.component'
import { AppInitService, AppInitFactory } from './app-init.service'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    // 以下を追加
    {
      provide: APP_INITIALIZER,
      useFactory: AppInitFactory,
      multi: true,
      deps: [AppInitService],
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
```
これでアプリケーションの初期化時にlinkタグが追加されます。

## Angular Universal で SSR

`ng add @nguniversal/express-engine` のあと `npm run dev:ssr` でアプリケーションを動かしてみましょう。

しかし、このままでは **ReferenceError: window is not defined** というエラーが出てサーバーが起動できませんので、今までのコードを修正していきましょう。

## Angular Universal のための修正

ここまでで作成した app-init.service.ts を以下のように修正します。

```typescript:app-init.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core'
import { isPlatformServer } from '@angular/common'

export function AppInitFactory(appInit: AppInitService) {
  return () => appInit.init()
}

@Injectable({
  providedIn: 'root',
})
export class AppInitService {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  init() {
    this.initWebFont()
  }

  private initWebFont() {
    // SSRの場合は実行しないように
    if (isPlatformServer(this.platformId)) {
      return
    }
    // ここでimportしないとReferenceError: window is not definedになる
    import('webfontloader').then((WebFont) => {
      WebFont.load({
        google: {
          families: ['Modak', 'M+PLUS+1p:400,900'],
        },
      })
    })
  }
}
```

これですべておしまいです。