Vue.js + Nuxt.js でのSSR概要
Vue.jsなどのJavaScriptフレームワーク/ライブラリはサイト閲覧時に優れたUXを提供できます。
殆どの場合サーバーに毎回リクエストを送信することなくコンテンツを動的に変更する方法が提供されています。
ただしSPAにも問題がありWebサイトを最初にロードするときにブラウザは完全なページを受け取れません。
代わりにページ(HTML/CSS/その他の画像ファイル等)を構築するための断片と、それらを全てを纏める方法の指示(JavaScriptフレームワーク/ライブラリ)が送信されます。
ブラウザに実際に表示する前にこれらの情報をクライアント側で纏めるのは時間がかかります。
組み立て式の家具を購入したようなイメージで、最初に家具を作ってから物を載せる必要があります。
これに対する解決策はとてもシンプルです。
クライアント側でページを組み立てずにサーバーサイドでページを構築してからブラウザに渡すようにします、既製品の家具が届くのでそのまま物を置いて使い始める事ができます。
他にも多くの利点があります。
例えばAbout UsやContact Usページなど滅多に更新しないページはユーザーがリクエストするたびに構築する必要はありません。
そのためサーバーは一度作成してからページをキャッシュまたは保存しておく事ができます。
速度改善としては極わずかな効果しか得られませんが、応答までの時間がミリ秒(またはそれ以下)で測定される環境ではジワジワと効いてきます。
VueでのSSRに対するメリットの詳細情報はこちらを参照してください。
DjangoとNuxtを使用したサンプルはこちらを参照ください。
Nuxt.jsを使う理由
Nuxt.js は Next と呼ばれるReactライブラリのSSR実装に基づいています。
React + Nextの組み合わせに経験がある人はアプリケーション設計とレイアウトがよく似ている事がわかると思います。
ただしNuxtはVue向けの強力かつ柔軟なSSRソリューションを作成するためにVue固有の機能も提供しています。
Nuxtは現在もアクティブで十分なサポートが得られます。
特に素晴らしいことはNuxtを使用してプロジェクトを構築する時と他のVueプロジェクトを構築する時とでそれほど変わらないことです。
Nuxtプロジェクトの作成
まずはVueCLIを使用しプロジェクトを作成します。
# install VueCLI globally
$ npm install -g @vue/cli
# create a project using a nuxt template
$ vue init nuxt-community/starter-template my-nuxt-app
これによりmy-nuxt-app
フォルダの中にプロジェクトが作成されます。
次に依存関係をインストールして開発サーバーを起動します。
$ cd my-nuxt-app
$ npm install
$ npm run dev
ブラウザで localhost:3000
を開くと次のようにアプリがローディングされ、読み込みが終わるとHelloWorldが表示されます。
表示される内容は通常のVueAppと大きな差はありませんが、アプリの構造を見るとファイル数が通常のVueAppに比べそれほど多くないはずです。package.json
を見るとNuxt自体の依存関係は1つしかないこともわかります。
これはNuxtの各バージョンがVue、Vue-router、およびVuexの特定のバージョンで動作するように調整されており、それらをすべて一緒にバンドルしているためです。
また、nuxt.config.js
というファイルもプロジェクトルートにあります。
このファイルでNuxtが提供する一連の機能をカスタマイズできます。
デフォルトではヘッダータグ、読み込みバーの色、およびESLintルールが設定されています。
その他の設定可能なオプションはこちらのドキュメントをご覧ください。
この投稿でもいくつかのオプションについて説明します。
プロジェクトのレイアウト
作成されたディレクトリを参照するとすべてのディレクトリに簡単な概要とドキュメントへのリンクが記載されたReadmeが用意されています。
これはNuxtを使用する利点の1つでアプリケーションのデフォルト構造です。
またNuxtはビルド時にこれらを確認し、使われているディレクトリに応じてアプリケーションをビルドします。
それではNuxtが提供するディレクトリを1つずつ見てみましょう。
Pages
これが唯一の必須ディレクトリです。
このディレクトリ内のVueコンポーネントはファイル名とディレクトリ構造に基づいてvue-routerに自動的に追加されます。
これは非常に便利です。
通常はPagesやViewsといったディレクトリを用意しこれらの各コンポーネントをrouter.js
に手動で登録する必要があります。router.js
は大規模なプロジェクトではかなり複雑になる可能性があり、可読性を保つために分割が必要になる場合があります。
しかしNuxtでは我々の代わりにすべてのロジックを処理してくれます。
これを確かめてみましょう。
Pagesディレクトリ内にabout.vueというVueコンポーネントを作成し、 単純なテンプレートを追加してみましょう。
<template>
<h1>About Page</h1>
</template>
コンポーネントを保存するとNuxtがルートを再生成します。
/aboutに移動するとこのabout.vueが呼び出され表示されます、とてもシンプルです。
また、Nuxtでは特別なファイル名が1つあります。
それは index.vue
です。
この名前が付けられたディレクトリはホームページもしくはランディングページとしてルーティングされます。
Pagesの中に更にサブディレクトリを作成するとルートの構造化に役立ちます。
例えばView Documentページが必要な場合、Pagesディレクトリを次のようにします。
/pages
--| /documents
----| index.vue
----| view.vue
この状態でブラウザから/documents/view
へ移動するとview.vue
が読み込まれます。
勿論、/documents
へ移動した場合はdocumentsディレクトリ内のindex.vue
が読み込まれます。
/about
ページを作成したときと結果は同じだと思うかもしれませんが、この2つの構造には違いがあります。
別の新しいページを追加して、これを検証してみましょう。
例えば各ユーザ事にAboutページが必要だったとします。
Pagesディレクトリにaboutというサブディレクトリを作ってみましょう。
/pages
--| about.vue
--| /about
----| hogehoge-taro.vue
この状態で /about/hogehoge-taro
にアクセスしようとすると、/about
にアクセスした際に読み込まれるabout.vueが表示されてしまいます。
これはNuxtがネストされたrouteの生成を行っている事に起因しています。
この構造は永続的な/about
ルートが必要であり、そのルート内のすべてのものを独自のビュー領域にネストする必要があることを示しています。
通常のVueAppでは<router-view />
コンポーネント内にabout.vue
コンポーネントを指定することで実現されます。
Nuxtでも同じ概念なのですが<router-view/>
の代わりに<nuxt/>
を使用します。
早速about.vue
コンポーネントを更新して、ネストされたルートを許可してみましょう。
<template>
<div>
<h1>About Page</h1>
<nuxt/>
</div>
</template>
ここで、/about
に移動すると以前のabout.vue
コンポーネントがタイトルだけで取得されます。
ただし、/about/hogehoge-taro
に移動するとben-jones.vue
コンポーネントがレンダリングされます。
もう一つのルーティングパターンに切り替えるならディレクトリを再構築するだけです。 about.vue
コンポーネントを/about
ディレクトリ内に移動し、index.vue
という名前に変更するだけで大丈夫です。
/pages
--| /about
----| index.vue
----| hogehoge-taro.vue
最後にrootパラメーターを使用して特定のページを取得したいとします。
例えば64がpage_idである/documents/edit/64
に移動して編集できるようにします。
これは次の方法で実行できます。
/pages
--| /documents
----| /edit
------| _document_id.vue
_document_id.vueコンポーネントの先頭にあるアンダースコアに注目してください。
これは、$ route.paramsオブジェクトまたはNuxtのコンテキストのparamsオブジェクトでアクセス可能なルートパラメーターを示しています。
このパラメータのキーはアンダースコア以降のコンポーネント名(この場合はdocument_id)となる事に注意してください。
また、キーはプロジェクト内で一意に保つ必要があります。
_product_id.vueには次のコードを書き、/documents/edit/63
にアクセスするとルーティングされている事が確認できます。
<template>
<h1>Editing Document {{ $route.params.document_id }}</h1>
</template>
続いてもう少し複雑なレイアウトを構成してみましょう。
次のような構成にしてみます。
/pages
--| /categories
----| /_category_id
------| documents.vue
------| /documents
--------| _document_id.vue
この場合、/categories/2/documents/3
が何を表示するかを想像するのはそれほど難しくありませんね。
ネストされた category_id
とdocument_id
パラメータを持つ documents.vue
コンポーネントがあります。
これはvue-routerで構成するよりもはるかに簡単です。
他にルーター構成で採用される傾向があるのはルーターガードのセットアップです。
NuxtではbeforeRouteEnterを使用してコンポーネント自体でルーターを構築できます。
ルートパラメータを検証する場合、Nuxtはvalidateと呼ばれるコンポーネントメソッドを提供しており、コンポーネントをレンダリングする前にproduct_idが数値であるかどうかを確認したい場合 _document_id.vue
のスクリプトタグに次のコードを追加します。
export default {
validate ({ params }) {
// Must be a number
return /^\d+$/.test(params.product_id)
}
}
この状態で /categories/2/documents/somedocument
に移動すると’somedocument’ は有効になっていないため404エラーが返されます。
Pagesディレクトリについての説明はこれで終わりです。
ディレクトリでルートを適切に構成する方法を習得する事は不可欠であるため、Nuxtを最大限に活用するには少し時間をかけてでも色々試してみることが重要です。
公式のドキュメントにも目を通しておいてください。
上述したこのルーティング方法は適切に構成されていれば様々なプロジェクトに最適です。
また、Nuxtが自動的に生成もしくは再構築するよりも多くのルートをルーターに追加する必要がある場合Nuxtはルーターインスタンスをカスタマイズする方法も提供しています。
これにより新しいルートを追加し生成されるルートをカスタマイズできるため、イレギュラーなケースに遭遇した場合でも対応できる柔軟性があります。
Store
Pagesディレクトリ同様にストアディレクトリの構造に基づいてVuexストアを構築できます。
もしストアが必要ない場合はディレクトリごと削除してください。
ストアにはClassicとModuleの2つの動作モードがあります。
Classicではstoreディレクトリにindex.js
ファイルが必要なため、Vuexインスタンスを返す関数をエクスポートする必要があります。
import Vuex from 'vuex'
const createStore = () => {
return new Vuex.Store({
state: ...,
mutations: ...,
actions: ...
})
}
export default createStore
これで通常のVueプロジェクトでVuexを使用するのと同じように必要に応じてストアを作成できます。
Moduleモードでもstoreディレクトリにindex.js
ファイルを作成する必要がありますが、state/mutations/actionsのみエクスポートするだけでOKです。
以下の例は空のstateを指定しています。
export const state = () => ({})
次にストアディレクトリ内の各ファイルは独自の名前空間、またはモジュール内のストアに追加されます。
例として現在のドキュメントを保存する場所を作成します。
storeディレクトリにdocument.js
というファイルを作成するとストアの名前空間セクションが$ store.product
で利用可能になります。
試しに簡単な例を挙げてみます。
export const state = () => ({
_id: 0,
title: 'Unknown',
maintainer: 'None'
})
export const actions = {
load ({ commit }) {
setTimeout(
commit,
1000,
'update',
{ _id: 1, title: 'Document', maintainer: 'hedgehog' }
)
}
}
export const mutations = {
update (state, document) {
Object.assign(state, document)
}
}
ロードアクションのsetTimeout
でどこぞやのAPI呼び出しを行った場合を想定し、1秒後にResponseを使ってストアを更新する事にしました。
これを documents/view
ページで使用してみます。
<template>
<div>
<h1>View Document {{ document._id }}</h1>
<p>{{ document.title }}</p>
<p>Price: {{ document.maintainer }}</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
created () {
this.$store.dispatch('document/load')
},
computed: {
...mapState(['document'])
}
}
</script>
this.$store.dispatch
している document/load
アクションがDocumentの下で名前空間となっていることがわかりますね。
これによりstoreのどのセクションを扱っているかが明確になります。
そしてstateをプロパティにマッピングすることでテンプレートで使用できるようになります。
ただし、実行してみるとわかりますがこのままでは1つ問題があります。
(偽の)APIキック中に元のstateが一瞬表示されます。
これの対処は後ほどNuxtが提供するfetchという機能を使用し修正します。
Components
Componentsディレクトリには、ナビゲーションバー、画像ギャラリー、ページネーション、データテーブルなどの再利用可能なコンポーネントが含まれています。
これらをPagesディレクトリ内に配置してしまうと上述した通りルートに変換されてしまうため、ページを構成するパーツはComponentsディレクトリに配置するようにします。
これらのコンポーネントはインポートすることでページまたは他のコンポーネントで使用できます。
import Hoge from ~/components/Hogehoge.vue
Assets
Assetsディレクトリにはコンパイルされていないファイルが含まれています。
これはNuxtWebpackがファイルをロード及び処、理する方法に関係しています。
詳細は公式のドキュメントを読んでください。
Static
Staticディレクトリにはサイトのルートディレクトリにマップされる静的ファイルが含まれます。
例としてこのディレクトリに logo.png
という画像を配置すると、/logo.png
で利用できるようになります。
これは robots.txt
、favicon.ico
といったMetaファイルなどに適しています。
Layouts
VueAppには通常 App.vue
と呼ばれるrootコンポーネントがあり、アプリの基本レイアウトを設定できます。
NuxtではこれをLayoutsディレクトリで構成でき、特定のページに対してのみ他と異なるレイアウトを適用することも可能です。
例としてヘッダータグのみがありナビゲーションバーがない layouts/admin-layout.vue
を作してみましょう。
<template>
<div>
<h1>Admin Layout</h1>
<nuxt />
</div>
</template>
次にPagesディレクトリに admin.vue
ページを作成し、Nuxtが提供するlayout
というプロパティを使用してコンポーネントに使用するレイアウト名を指定します。
<template>
<h1>Admin Page</h1>
</template>
<script>
export default {
layout: 'admin-layout'
}
</script>
これだけで完了です。
他のページではデフォルトレイアウトを使用しますが、/admin
に移動するとadmin-layout.vue
レイアウトが使用されます。
ちなみに無効なURLを入力された際などに表示するエラーページは、デフォルトではNuxtが提供する独自のエラー画面が表示されます。
もしこれを編集したい場合は、error.vue
レイアウトを作成するだけで切替えできます。
ただし、error用のレイアウトには他のコンポーネントを含めることはできません。
Middleware
ミドルウェアはページまたはレイアウトをレンダリングする前に実行できる機能です。
例えばルートガードは、(コンポーネント自体でvalidateメソッドを使用する代わりに)有効なログインのVuexストアを確認したり、いくつかのパラメーターを検証したりすることができる一般的な使用法です。
これらの関数は非同期にすることができ、Nuxtのコンテキストにもアクセスできます。
後ほど説明します。
Plugins
Pluginsディレクトリを使用するとアプリケーションを作成する前にVueプラグインを登録できます。
これによりプラグインをVueインスタンス上のアプリ全体で共有し、任意のコンポーネントでアクセスできるようになります。
例としてvue-notifications
を使用してみましょう。
$ npm install vue-notifications --save
次に、プラグインディレクトリに vue-notifications.js
というファイルを作成します。
import Vue from 'vue'
import VueNotifications from 'vue-notifications'
Vue.use(VueNotifications)
そしてプロジェクトルートのnuxt.config.js
ファイルを編集し、module.exportsオブジェクトに次のエントリを追加します。
plugins: ['~/plugins/vue-notifications']
これでアプリ全体でvue-notifications
を使用できます。
これで、ディレクトリ構造の説明は以上です。
Nuxt’s Supercharged Components
NuxtではすべてのPageコンポーネントに追加機能を提供するために使用できる追加メソッドが用意されています。
例えば、valids
メソッドを使用してパラメータをチェックし無効な場合はユーザーをリダイレクトする際などにこの機能は使われています。
そしてNuxtプロジェクトで使用される主な機能は asyncData
メソッドとfetch
メソッドです。
どちらも概念が非常に似ており、コンポーネントが生成される前に非同期で実行されコンポーネントとストアのデータを取り込むために使用できます。
また、データベースまたはAPI呼び出しを待つ必要がある場合でもクライアントに送信する前にサーバー上でページを完全にレンダリングできるようにしてくれます。
asyncDataとfetchの違い
- asyncDataは、Pageコンポーネントのデータを取り込むために使用されます。
オブジェクトを返すと、レンダリングの前にデータの出力とマージされます。 - fetchは、Vuex Storeを作成するために使用されます。
プロミスを返すと、Nuxtはレンダリングされる前にプロミスが解決されるまで待機します。
早速これを試してみましょう。
先ほどの /products/view
ページでAPI呼び出しの実行中にストアの初期状態が短時間表示されるという問題がありました。
これを解決する方法の1つは、コンポーネントまたはストアにloading = trueなどの値を保存し、API呼び出しの終了まで読み込みコンポーネントを表示することです。
その後、loading = falseに設定しデータを表示します。
ではフェッチを使用しレンダリングの前にストアにデータを入力できるようにしましょう。 /documents/view-async
という新しいページで次のコードを記述します。
export default {
fetch () {
this.$store.dispatch('document/load')
},
computed: {...}
}
ただしこれらの「Supercharged」メソッドはコンポーネントが作成される前に実行されるため、ストアにアクセスできません。
それではストアにアクセスするにはどうすれば良いでしょうか。
The Context API
もちろん解決策があります。
Nuxtのすべてのメソッドで、Contextという非常に便利なオブジェクトを含む引数(通常は最初の引数)が提供されます。
ストアにアクセスするためにコンテキストを分解し、そこからストアを抽出できますがコンポーネントをレンダリングする前にNuxtが解決するのを待つことができるようにPromiseが返ることを確認する必要があります。
そのためにStoreアクションも少し調整します。
// Component
export default {
fetch ({ store }) {
return store.dispatch('documnt/load')
},
computed: {...}
}
// Store Action
load ({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit('update', { _id: 1, title: 'Document', maintainer: 'hedgehog' })
resolve()
}, 1000)
})
}
状況に応じてasync / await または他の方法を使用できます。
API呼び出しが終了しストアがレスポンスで更新されてからるようにNuxtに指示します。
これで /documents/view-async
に移動した際、初期化中のコンテンツがフラッシュ(チラつき)されなくなりました。
仮にSSRでの実装をしていなくてもVueAppでこれはとても役立ちます。
コンテキストはすべてのミドルウェアやストアが初期化される前に実行される特別なストアアクションであるNuxtServerInit
などの他のNuxtメソッドでも使用できます。
SSRでのセッション管理
これを考えるにあたってNuxtではクライアントとサーバーはそれぞれの別のエンティティと考えることが重要です。
ユーザが最初にページへアクセスすると、RequestがNuxtに送信されサーバーはそのページとアプリの残りの部分を可能な限りサーバ側で構築してから送信しクライアント側で必要に応じてチャンクをロードします。
この時サーバーはできる限り処理を進めておく必要がありますが、必要な情報にアクセスできない場合もありその際は代わりにクライアント側で処理が行われます。
更にクライアントによって処理された最終的なコンテンツがサーバー側で予期したものと異なる場合、クライアントはそれをゼロから再構築するように注意されます。
これが発生し始めるとブラウザのコンソールでもエラーが表示されます。
セッション管理の方法としてログイン機能のあるVueAppがあり、localStorageに保持することにしたトークン(JWTなど)を使用してセッションが保存されているとします。
サイトにアクセスするときにAPIに対してトークンを認証し、基本的なユーザー情報が返されたらストアに格納したとしましょう。
NuxtにはNuxtServerInitと呼ばれる便利なメソッドがあります。
このメソッドを使用すると初期ロード時にストアを非同期で1回読み込むことができます。
export const actions = {
nuxtServerInit ({ dispatch }) {
const token = localStorage.getItem('token')
if (token) return dispatch('user/load', token)
}
}
完璧なように思えますが、この状態でページを更新するとエラーが発生します。
何故ならこのメソッドはサーバー上で実行されるため、クライアントのlocalStorageに何が保存されているかわからないのです。
代替手段としてlocalStorageに最も似ているのは代わりにCookieを使用することです。
Nuxtはクライアントからサーバーへのリクエストとともに送信されるCookieにアクセスできます。
他のNuxtメソッドと同様に、nuxtServerInitはコンテキストにアクセスできます。
コンテキストではクライアントリクエストからのすべてのヘッダーおよびその他の情報を格納するreqオブジェクトにアクセスできます。
Node.jsではお馴染みですね。
よってCookieにトークンを保存した後、サーバー上でトークンにアクセスしましょう。
import Cookie from 'cookie'
export const actions = {
nuxtServerInit ({ dispatch }, { req }) {
const cookies = Cookie.parse(req.headers.cookie || '')
const token = cookies['token'] || ''
if (token) return dispatch('user/load', token)
}
}
デプロイ
Nuxtでのデプロイは非常に簡単です。
同じコードを使用してSSRアプリ、SPA、または静的ページを作成できます。
SSR
デプロイの基本的な考えとして選択したプラットフォームでビルドプロセスを実行し、いくつか構成を設定します。
公式ドキュメントに記載されているHerokuの例を使用します。
まず、package.jsonでHerokuのスクリプトを設定します。
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"heroku-postbuild": "npm run build"
}
次に、heroku-cliを使用してHeroku環境をセットアップします。
$ heroku config:set NPM_CONFIG_PRODUCTION=false
$ heroku config:set HOST=0.0.0.0
$ heroku config:set NODE_ENV=production
$ git push heroku master
これだけで完了です。
他のプラットフォームでは設定方法は異なりますがプロセスは似ています。
現在はAzure, GCP, AWSを初めGitHubなど様々なプラットフォームに対応しています。
SPA
Nuxtが提供する追加機能の一部を利用したいが、サーバーがページをレンダリングしようとするのを避けたい場合はSPAとしてデプロイできます。
まず、デフォルトでnpm run dev
はSSRをオンにして実行されるため、SSRなしでアプリケーションをテストしておくのが最善です。
これを実施するにはnuxt.config.js
ファイルを編集し、次のオプションを追加します。
mode: 'spa',
これでnpm run dev
を実行するとSSRがオフになり、アプリケーションがテスト用のSPAとして実行されます。
またこの設定によりビルド時にSSRが含まれなくなります。
静的ページ
S3などにホスティングする場合、コンテキスト内のreqおよびresにアクセスできないためコードが依存している場合は他の手段に置き換える必要があります。
例えば、先ほどnuxtServerInit関数はリクエストヘッダー内のCookieからトークンを取得しようとしているためエラーをスローします。
デプロイ自体はとても簡単で、nuxt.config.json
にオプションを追加します。
generate: { fallback: true },
まとめ
NuxtはSPAとSSRに静的ページと幅広くサポートしており、storeやrouter部分などVue単体でアプリケーションを作成する時の面倒な部分を簡略化してくれます。
初めて使用する際はつまずくポイントもあるかもしれませんが公式のDocumentが良く纏まっているのでまずは一読しておくことをオススメします。