DMM.comの、一番深くておもしろいトコロ。

Nuxt.jsとFirebaseでSPA×SSR×PWA×サーバーレスを実現する

Nuxt.jsとFirebaseでSPA×SSR×PWA×サーバーレスを実現する

f:id:dmminside:20180614161548p:plain

はじめまして。DMM.comラボ エンターテインメント本部イベント開発部の上井(ウワイ)と申します。
インフラからフロントエンドまで幅広く担当しております。

今回は、業務で利用しているNuxt.jsとFirebaseを使って、SPA×SSR×PWA×サーバーレスのWebアプリケーションを構築する方法をご紹介いたします。

利用技術

Nuxt.js

ja.nuxtjs.org

Nuxt.jsは、ユニバーサルなVue.jsアプリケーションを作るためのフレームワークです。
簡単に言えば、サーバーサイドレンダリングや静的なVue.jsアプリケーションを簡単に作ることができるスグレものフレームワークです。Vuexストアは標準で利用可能、ミドルウェアやモジュールなど開発を手助けしてくれる多くの機能を搭載しています。
プレーンなVue.jsよりも簡単に、早く、先進的なアプリケーションを作ることができます。

Firebase

firebase.google.com

Firebaseは、Googleが提供するモバイル開発プラットフォーム(mBaaS)です。
手間の掛かるバックエンドのあれこれをマネジメントしてくれます。サーバーのスケーリングや監視、インフラセキュリティなど様々な面倒から開放されます。さらにランニングコストも安く抑えられる場合があります。
また、Google Cloud Platformとの統合が進められおり、GCPとFirebaseを連携させることも可能です。

今回は、Firebase HostingとCloud Functions for Firebase(以下、Cloud Functions)という2つのサービスを利用します。
Firebase Hostingは静的ファイルのホスティングサービスです。今回はクライアントから直接参照されるファイルのホスティングに利用します。SSL、HTTP/2、CDN配信を、何もせずとも自動でやってくれます。
Cloud FunctionsはいわゆるFaaSで、イベントをトリガーにスクリプトを実行するサービスです。今回は、HTTPリクエストをトリガーとし、アプリケーションサーバーとして利用します。

実現できること

Single Page Application(SPA, シングルページアプリケーション)

SPAとは、その名のとおり単一ページで構成されるWebアプリケーションです。
単一ページでありながら、JavaScriptによる動的レンダリングやHTML5 History APIを用いることで、まるでネイティブアプリケーションのような挙動を実現します。ページ遷移はせず動的にレンダリングを行うため、ユーザー操作のリアクションが非常に高速という特徴を持ちます。さらに、サーバーと通信を行う部分は、通常のWebページと違い必要な情報だけ読み込めば良いため、通信量の削減やレンダリングまでの大幅な時間短縮が可能です。

Server Side Rendering(SSR, サーバーサイドレンダリング)

単なるSPAは、ファーストビューのローディング時間が長いという欠点があります。また、クローラーがJavaScriptで動的にレンダリングされる箇所を正しく解釈できないために、SEO的にも問題があると言われています。
それらの弱点を解消するためのアプローチがSSRです。

SSRは、本来クライアント側でレンダリングする箇所を、サーバー側でレンダリングしてクライアントに返す仕組みです。一般的にSSRの実装コストは高いと言われていますが、Nuxt.jsであれば手軽にSSRアプリケーションを作ることができます。

※静的アプリケーションと動的アプリケーション
Nuxt.jsには、静的アプリケーションとしてビルドするgenerateという機能があります。これを用いることでも、ファーストビューのローディング時間とSEO的な問題はクリアできます。しかし、あくまでも静的アプリケーションであり、動的アプリケーションとは全く異なるものです。

具体例として、会員サイトを考えてみましょう。
あるユーザーが会員登録を行い、会員ID123が動的に割り当てられたとします。そして、そのユーザーの個人ページ/users/123を開こうとします。
この時、動的アプリケーションでは、このルーティングを認識し動的にページが生成されます。
一方で、generateでビルドした静的アプリケーションでは、このページの静的ファイルが存在しないため404エラーが発生します。もしこれを解消するならば、会員登録時にアプリケーションを再ビルドし、その際にルーティングを定義しなければなりません。これでは即時性を大きく損なってしまいます。

f:id:uwai-shinnosuke:20180404172705p:plain

今回は、ファーストビューのローディング時間とSEO的な問題クリアする、かつ動的アプリケーションという目的があったため、SSRを選択しました。

Progressive Web Apps(PWA, プログレッシブウェブアプリ)

PWAとは、Webアプリケーションでありながら、ネイティブアプリケーションの利点を兼ね備えたものです。 developers.google.com

PWAに対応することで、主に次のようなメリットが生まれます。

  • オフラインでも閲覧可能
  • プッシュ通知
  • インストール可能

PWAは、Service Workerというクライアントのバックグラウンドで動作するスクリプトによって実現されます。 developers.google.com

Nuxt.jsにはPWAモジュールというものが用意されています。一見ハードルが高そうなPWA対応も、これを用いることで簡単に導入が可能です。 github.com

これは個人的な意見ですが、PWAに完全に準拠せずとも、Service Workerの恩恵を享受するためだけにPWAモジュールを導入するのもアリかと思います。

サーバーレスアーキテクチャ

自前でサーバー(仮想サーバー含む)を管理せずに、クラウドのマネジメントサービスをフル活用するアプリケーション構成です。マネジメントサービスは、いわゆるPaaSやFaaS、BaaSなどを指します。今回ならFirebaseがこれにあたります。

私が感じるメリットは下記のとおりです。ひと言で言うなら「アプリケーション開発に集中できる」でしょうか。

  • サーバーのメンテナンスや監視から解放される。
  • サーバーのプロビジョニングが不要。
  • 高可用性
  • サーバーのスケーリング管理が不要。
  • インフラの知識が少なくとも簡単に運用できる。
  • 費用を安く抑えられる場合がある。

もちろんデメリットも存在します。

  • 制約が少なくない
  • 開発難易度が上がる
  • ベンダーロックイン

今回のケースで言うと、Cloud Functionsの制約の影響を大きく受けました。2018年4月2日時点ではCloud Functionsで利用できるNode.jsのバージョンが少し古い6.11.5のみのため、開発も同バージョンで行い、ライブラリの依存関係もこれに合わせる必要がありました。
一方で、開発難易度が上がったとはあまり感じませんでした。ドキュメントや実例がわずかしか見つからなかったため調査には時間がかかりましたが、実装してみると思いのほか簡単でした。

構成

下図の構成でWebアプリを構築します。

f:id:uwai-shinnosuke:20180404172648p:plain

クライアント側で参照されるファイル群をFirebase Hostingに、サーバー側のコードをCloud Functionsに設置します。 クライアントからのHTTPリクエストをFunctionsで受けてサーバーサイドレンダリングし、クライアントに返します。

パフォーマンス

実際に上記の技術構成でWebアプリケーションを作成し、パフォーマンスを測定しました。

WebPagetestによる測定結果 f:id:uwai-shinnosuke:20180326204027p:plain

Lighthouseによる測定結果 f:id:uwai-shinnosuke:20180326203617p:plain

パフォーマンスに関しては外部リソースが多かったためか思いのほか振るいませんでしたが、概ね十二分なスコアです。


HowTo

さて、前置きが少々長くなりましたが、ここから実装方法の解説に入ります。

事前にNode.jsのv6.11.5をローカル環境にインストールしてください。なお、今回の解説ではNode.jsのパッケージマネージャーにnpmのv5.7.1を使用していますが、お好みでYarnを使用していただいても構いません。
また、シェルによる操作も交えますので、Windows環境の方は適宜読み換えていただくか、Windows Subsystem for Linuxなどをご利用ください。

GitHubにサンプルコードを置いておきましたので、ぜひご参照いただければと思います。

github.com

1. Nuxt.jsのセットアップ

1.1 プロジェクト作成

まずはvue-cliをインストールします。

$ npm install -g vue-cli

// シェル再起動
$ exec $SHELL -l

次に、Nuxt.jsスターターテンプレートのインストールです。インストールの際、Firebaseのために公式ガイドとは異なるディレクトリ構成となります。具体的には、公式ガイドではプロジェクトディレクトリの直下にソースコードを設置しますが、今回はプロジェクトディレクトリの中にソースコード用とデプロイ用のディレクトリを構成します。

// プロジェクトディレクトリ作成
$ mkdir <project-name>

// Cloud Functions用ディレクトリ作成
$ cd <project-name>
$ mkdir functions

// Nuxt.jsスターターテンプレートをインストール
// ソースコード用ディレクトリは自動生成されます
$ vue init nuxt-community/starter-template src
//// プロジェクト情報を尋ねられるので適宜入力
? Project name <project-name>
? Project description <project-description>
? Author <your-name>

   vue-cli · Generated "src".

1.2 依存関係の解決

繰り返しになりますが、Cloud FunctionsではNode.js v6.11.5しか利用できません。そのため、Nuxt.jsのバージョンはNode.js v6.11.5をサポートしている1.0.0-rc11を利用し、依存関係も解決する必要があります。加えて、Babelのパッケージも必要となりますので追加します。

src/package.jsondependencies, devDependencies項目を次のように変更します。

"dependencies": {
  "ajv": "^6.0.0",
  "babel-plugin-module-resolver": "^2.7.1",
  "babel-preset-stage-0": "^6.24.1",
  "babel-plugin-transform-runtime": "^6.23.0",
  "nuxt": "1.0.0-rc11",
  "vue": "2.4.4",
  "vuex": "^3.0.1"
},
"devDependencies": {
  "@nuxtjs/pwa": "^2.0.8",
  "babel-eslint": "^8.2.1",
  "babel-preset-env": "^1.6.1",
  "eslint": "^4.15.0",
  "eslint-friendly-formatter": "^3.0.0",
  "eslint-loader": "^1.7.1",
  "eslint-plugin-vue": "^4.0.0"
}

また、functionsディレクトリにもパッケージをインストールしておく必要があります。こちらは、ローカル環境およびCloud Functions上のサーバーで実行するためのパッケージです。
下記のfunctions/package.jsonを設置してください。Cloud Functionsを利用するため、特有のパッケージも含まれています。

{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "private": true,
  "dependencies": {
    "ajv": "^6.0.0",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-stage-0": "^6.24.1",
    "babel-runtime": "^6.23.0",
    "clone": "^2.1.1",
    "debug": "^3.1.0",
    "es6-promise": "^4.1.1",
    "express": "^4.15.4",
    "firebase-admin": "5.10.0",
    "firebase-functions": "0.8.2",
    "lodash": "^4.17.5",
    "nuxt": "1.0.0-rc11",
    "vue": "2.4.4",
    "vue-meta": "^1.4.4",
    "vue-router": "^3.0.1",
    "vuex": "^3.0.1"
  }
}

上記2ファイルの設定後、それぞれのディレクトリでnpm installを実行してください。

$ cd /path/to/<project-name>/src
$ npm install

$ cd /path/to/<project-name>/functions
$ npm install

1.3 ビルド設定

先ほど作成したfunctionsディレクトリにアプリケーションをビルドするよう設定します。
src/nuxt.config.jsのビルド設定箇所を次のように編集します。

module.exports = {
  /*
  ** Build configuration
  */
  buildDir: '../functions/nuxt',
  build: {
    publicPath: '/assets/',
    extractCSS: true,
    babel: {
      presets: [
        'env',
        'stage-0'
      ],
      plugins: [
        ['transform-runtime', {
          polyfill: true,
          regenerator: true
        }],
      ],
    },
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  }
}

ここで一旦、動作確認をしてみましょう。
開発サーバーを立ち上げ、http://localhost:3000にアクセスしてみてください。下のような画面が表示されたらひとまず成功です。

$ npm run dev

ここまででSPA×SSRが出来ました。

1.4 PWA導入

続いて、アプリケーションのPWA化です。
先述のとおり、Nuxt.jsにはPWAモジュールが用意されているので、それを用います。公式ドキュメントに沿ってセットアップしましょう。

まずはインストールです。

$ cd /path/to/<project-name>/src
$ npm install --save-dev @nuxtjs/pwa

そして、src/nuxt.config.jsにPWAの設定を追記します。

module.exports = {
  modules: [
    '@nuxtjs/pwa'
  ],
  manifest: {
    name: 'project-name',
    lang: 'ja'
  }
};

src/.gitignoresw.*を追加しておきましょう。

# Service Worker
sw.*

あとは、PWA用のアイコン画像src/static/icon.pngを設置してください。もしアイコン画像が不要であれば、先ほどのsrc/nuxt.config.jsを次のように変更します。

modules: [
    ['@nuxtjs/pwa', { icon: false }],
],

それでは動作確認です。PWAは開発モードでは動作しないため、本番モードでビルド・サーバー起動します。

// ビルド
$ npm run build

// サーバー起動
$ npm run start

ちなみに、開発モードでもPWAを動作させる方法はあるのですが、Service Workerによるキャッシュが開発の妨げになることもしばしばあるため、あまり推奨はいたしません。

さて、http://localhost:3000にアクセスし、デベロッパーツールでService Workerが動作しているか確認してみましょう。
下の画像はChromeで確認したものです。Service Workerスクリプトであるsw.jsが読み込まれていますね。

以上でSPA×SSR×PWAを実現できました。

2. Firebaseのセットアップ

2.1 プロジェクト作成

Firebaseコンソールにサインインし、プロジェクトを作成します。

また、無料のSparkプランではアウトバウンドネットワーキングがGoogle専用となっているため、有料プランに変更する必要があります。
従量制のBlazeプランであれば無料利用枠があり、そのうえGCPとも連携が可能です。

firebase.google.com

2.2 Firebase初期設定

アプリケーションにFirebaseの設定を追加します。

まずはFirebase CLIをインストールします。

$ npm install -g firebase-tools

// シェル再起動
$ exec $SHELL -l

Firebaseにログインします。次のコマンドを実行後、ブラウザでログインページが表示されます。

$ firebase login
...
✔  Success! Logged in as hoge@fuga.com

Nuxt.jsプロジェクトのディレクトリでFirebaseの初期設定を行います。いくつか質問されるので、適宜選択してください。

$ cd /path/to/<project-name>
$ firebase init
...

=== Project Setup

// 先ほど作成したプロジェクトを選択
? Select a default Firebase project for this directory:
  [don't setup a default project]
❯ <project-name> (project-id)
  [create a new project]

...

// Hosting用のディレクトリ設定。デフォルトpublicのまま。
? What do you want to use as your public directory? (public)

// 全てのURLを/index.htmlに上書きしない。Noを選択。
? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) N

完了すると、プロジェクトディレクトリにfirebase.json.firebasercファイルが生成されています。firebase.jsonはHostingとCloud Functionsの設定、.firebasercにはFirebaseプロジェクトの設定が記述されています。
また、publicディレクトリも生成されていますが、ここにはHostingにアップロードするファイルを設置します。後ほど解説いたします。

2.3 HostingとCloud Functionsの設定

firebase.jsonを次のようにしてください。

{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [{
      "source": "**",
      "function": "ssrapp"
    }]
  },
  "functions": {
    "source": "functions"
  }
}

2.4 Cloud Functions実装

Cloud Functionsで実行されるコードを実装します。
アプリケーションサーバーとしてExpressを利用し、Nuxt.jsアプリケーションを実行します。

functions/index.jsを作成し、次のようにしてください。

const functions = require('firebase-functions');
const express = require('express');
const { Nuxt } = require('nuxt');

const app = express();
const nuxt = new Nuxt({
    dev: false,
    buildDir: 'nuxt',
    build: {
        publicPath: '/assets/'
    }
});

function handleRequest(req, res) {
    res.set('Cache-Control', 'public, max-age=600, s-maxage=1200');
    return new Promise((resolve, reject) => {
        nuxt.render(req, res, (promise) => {
            promise.then(resolve).catch(reject);
        });
    });
}

app.use(handleRequest);
exports.ssrapp = functions.https.onRequest(app);

3. ビルド&デプロイ🚀

ここまでくれば、あとはビルドとデプロイを残すのみです!

3.1 ビルド

Firebase用にビルドするには少々手間がかかります。

まずはNuxt.jsアプリケーションのビルドです。

// 事前にnpm installを実行しておく
$ cd /path/to/<project-name>/functions && npm install
$ cd /path/to/<project-name>/src && npm install

// ビルド
$ npm run build

そして、先ほど生成されたpublicディレクトリに静的ファイルを設置します。

// あらかじめpublicディレクトリの中をクリーンアップ
$ rm -rf public/*

// ビルド時に生成された静的ファイルを設置
$ cp -R functions/nuxt/dist/ public/assets

// 用意していた静的ファイルを設置
$ cp -R src/static/* public

これらの作業はpackage.jsonscriptsで自動化して良いと思います。

3.2 ローカル環境でのFIrebaseエミュレーション

Firebase CLIには、ローカルで動作をエミュレーションする機能があります。Firebaseにデプロイする前に動作確認をしましょう。

$ firebase serve --only hosting,functions

http://localhost:5000にアクセスしてみます。

無事動作しました!

3.3 デプロイ

最後はデプロイです!
Firebaseへのデプロイは非常に簡単です。

$ firebase deploy

=== Deploying to '<project-id>'...

i  deploying functions, hosting
...
Function URL (ssrapp): https://us-central1-<project-id>.cloudfunctions.net/ssrapp

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/<project-id>/overview
Hosting URL: https://<project-id>.firebaseapp.com

末尾に表示されているHosting URLにアクセスしてみます。

以上で解説は終了です。Nuxt.js開発をエンジョイしてください!

まとめ

Nuxt.jsとFirebaseを使って、SPA×SSR×PWA×サーバーレスなアプリケーションを構築する方法をご紹介しました。フロントエンドの新しい技術をサーバーレスアーキテクチャで、しかもこれだけ簡単に構築できる、すごい時代になりました。

GCPにはGoogle App EngineCloud Spannerなど、先進的で便利なサービスが充実しています。今回はフロントエンドのみのアーキテクチャをご紹介しましたが、バックエンドも全てサーバーレスにすると簡単・先進的・メンテナンスレス・高可用性なアプリケーションを実現できるのではないでしょうか。

採用情報

現在、DMM.comでは、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい! dmm-corp.com