SEO、OGP……Vue.js製SPAの「困った」を解決できる「Nuxt.js」が便利だ!

2017/09/08

Olayinka Omole

115

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Vue.jsでSPAを作ったものの、検索エンジンのクローラーやSNSのOGP取得といった問題で困ったことはありませんか? サーバーサイドレンダリングを簡単に構築できるNuxt.jsの活用方法を解説します。

ユニバーサル(Isomorphic)JavaScriptはJavaScriptコミュニティで一般的な用語になりました。ユニバーサルJavaScriptとは、クライアントとサーバーの両方で実行できるJavaScriptコードのことです。

Vue.jsを含むモダンJavaScriptフレームワークの多くは、シングルページアプリケーション(Single Page Application : SPA)の構築を目的に作られています。シングルページアプリケーションはページがリアルタイムで更新されるので、アプリの動きが軽快でユーザーエクスペリエンスが向上します。さまざまな利点がありますが、欠点もあります。ブラウザーがJavaScriptバンドルを取得するため、初期ロード時の「コンテンツ表示時間」が長くなったり、検索エンジンのWebクローラーやソーシャルネットワークのロボットが、ロードが完了したあとのアプリをクロールしなかったりします。

JavaScriptのサーバーサイドレンダリングとは、JavaScriptアプリケーションをWebサーバーでプリロードし、ブラウザーからのページリクエストに対してレンダリングされたHTMLを送信することです。

サーバーサイドレンダリングを用いたJavaScriptアプリの構築は、設定項目がたくさんあり、手間がかかります。コーディングがなかなか始められません。Nuxt.jsの目的は、Vue.jsアプリケーションの問題を解消することです。

Nuxt.jsについて

Nuxt.jsはサーバーサイドレンダリングを用いたVue.jsアプリケーションを簡単に作成するためのフレームワークです。Nuxt.jsは非同期データ、ミドルウェア、ルーティングなどを管理するために必要な設定のほとんどを抽象化します。AngularAngular Universalや、ReactNext.jsに似ています。

Nuxt.jsのドキュメントによれば「Nuxt.jsの主な目的はクライアント用ディストリビューションとサーバー用ディストリビューションを区別せずにユーザーインターフェイスをレンダリングすること」です。

静的サイト生成

Nuxt.jsの特徴のひとつは、generateコマンドを用いた静的Webサイトの生成です。generateコマンドはJekyllなど人気の静的サイト生成ツールと同様の機能を持つ優れものです。

Nuxt.jsの内部

Nuxt.jsはVue.js 2.0に加え、Vue-RouterVue-MetaVuexストアオプションを使うときのみ)をインクルードしているため、サーバーサイドレンダリングを用いたVue.jsアプリケーションを開発するために必要な各種ライブラリーを手作業でインクルードし設定する必要はありません。Nuxt.jsは各種ライブラリーがあらかじめ使える状態に設定されているのに、合計サイズは28kb min+gzipです(vuex使用の場合は31kb)。

Nuxt.jsはコードをバンドル、分割、圧縮するため、Webpackvue-loaderbabel-loaderと組み合わせて使用します。

動作の仕組み

ユーザーがNuxt.jsのアプリにアクセスしたり、<nuxt-link>経由で特定のページに移動したりするときの動作を示します。

  1. Nuxt.jsはユーザーがアプリにアクセスしたときに、ストアにnuxtServerInitアクションが定義されていれば、呼び出しストアを更新する
  2. Nuxt.jsはユーザーがアクセスしたページに存在するミドルウェアをすべて実行する。Nuxt.jsは最初にnuxt.config.jsファイルを確認してグローバルミドルウェアを実行し、次に、リクエストしたページのレイアウトファイルのマッチングを確認、最後にページと子ページを確認してミドルウェアを実行する。ミドルウェアの優先順位は以上の順に設定されている
  3. ユーザーがアクセスしているルートが動的ルートで、validate()メソッドが存在するなら、ルートを検証する
  4. asyncData()メソッドとfetch()メソッドを呼び出しページレンダリング前にデータをロードする。asyncData()メソッドはサーバーサイドでデータを取得しレンダリングするために用いる。fetch()メソッドはページレンダリング前にストアにデータを入れるために用いる
  5. 適切なデータをすべて格納したページをレンダリングする

Nuxtのドキュメントから引用した次の略図は、以上の動作を表現したものです。

Nuxt.js Schema

Nuxt.jsによるサーバーレス静的サイトの作成

それでは実際にコードを書きます。Nuxt.jsを使って、静的に生成されるシンプルなブログを作成します。記事はAPIから取得し、レスポンスを静的なJSONファイルでモックアップします。

本記事に沿って作業するには、Vue.jsの使い方の知識が必要です。Vue.jsを学ぶには、Jack Franklin氏によるVue.js 2.0の「ReactとAngularのいいとこ取り? 2017年こそ学びたいVue.jsの始め方」がおすすめです。また、ES6の構文も使います。関連記事を読んで復習してください。

作成するアプリのイメージです。

Nuxt SSR Blog

本記事のコードはすべてGitHubで確認できます。デモはここにあります。

アプリケーションのセットアップと設定

Nuxt.jsを簡単に始めるために、Nuxtチームが作成したテンプレートを利用します。vue-cliを使えばテンプレートをプロジェクト(ssr-blog)にインストールできます。

vue init nuxt/starter ssr-blog

補足:vue-cliをインストールしていない場合は、npm install -g vue-cliを実行してインストールしてください。

プロジェクトの依存オブジェクトをインストールします。

cd ssr-blog
npm install

これでアプリを起動できます。

npm run dev

http://localhost:3000にアクセスするとNuxt.jsのスターターテンプレートのページが表示されます。ページのソースを確認すると、ページ上に生成されたコンテンツはサーバーでレンダリングされHTMLとしてブラウザーに送信されたと分かります。

次に、nuxt.config.jsファイルを編集し、オプションを2つ追加します。

// ./nuxt.config.js

module.exports = {
  /*
   * Headers of the page
   */
  head: {
    titleTemplate: '%s | Awesome JS SSR Blog',
    // ...
    link: [
      // ...
      { 
        rel: 'stylesheet', 
        href: 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.2/css/bulma.min.css' 
      }
    ]
  },
  // ...
}

上の設定ファイルではtitleTemplateオプションで、アプリケーションで使うタイトルのテンプレートを指定します。各ページやレイアウトでtitleオプションをセットすると、レンダリング前にtitleTemplate%sプレースホルダーにtitleの値が入ります。

プリセットされたスタイルを利用するために、個人的に好みのCSSフレームワークBulmaを取得します。これはlinkオプションで実現します。

補足:Nuxt.jsはvue-metaを使ってアプリのヘッダーやHTML属性を更新します。ヘッダーがどうセットされるのか理解するには上記リンクを確認してください。

次のステップの、ログページや機能の追加に進みます。

ページレイアウトの操作

すべてのページ用のカスタムベースレイアウトを定義します。layouts/default.vueファイルを更新してNuxt.jsのメインレイアウトを拡張します。

<!-- ./layouts/default.vue -->

<template>
  <div>
    <!-- navigation -->
    <nav class="nav has-shadow">
      <div class="container">
        <div class="nav-left">
          <nuxt-link to="/" class="nav-item">
            Awesome JS SSR Blog!
          </nuxt-link>
          <nuxt-link active-class="is-active" to="/" class="nav-item is-tab" exact>Home</nuxt-link>
          <nuxt-link active-class="is-active" to="/about" class="nav-item is-tab" exact>About</nuxt-link>
        </div>
      </div>
    </nav>
    <!-- /navigation -->

    <!-- displays the page component -->
    <nuxt/>

  </div>
</template>

カスタムベースレイアウトはWebサイトのナビゲーションヘッダーを追加するだけです。<nuxt-link>コンポーネントでブログに設置するルートへのリンクを生成します。このコンポーネントの動作の仕組みは<nuxt-link>のドキュメントが参考になります。

<nuxt>コンポーネントはページコンポーネントの表示や、レイアウト作成に重要なコンポーネントです。

独自のドキュメントテンプレートやエラーレイアウトの定義など、できることがありますが、今回の「シンプルなブログ」には不要です。できることを知りたいなら、Nuxt.jsドキュメントのビューを参照してください。

シンプルなページとルート

Nuxt.jsでは、pagesディレクトリ内に単一ファイルコンポーネントとしてページが作成されます。ディレクトリ内にあるすべての.vueファイルをアプリケーションのルートに自動的に変換します。

ブログのTOPページを作成する

pagesディレクトリ内にある、Nuxt.jsテンプレートから生成されたindex.vueファイルを更新すると、ブログにTOPページを追加できます。

<!-- ./pages/index.vue -->
<template>
  <div>
    <section class="hero is-medium is-primary is-bold">
      <div class="hero-body">
        <div class="container">
          <h1 class="title">
            Welcome to the JavaScript SSR Blog.
          </h1>
          <h2 class="subtitle">
            Hope you find something you like.
          </h2>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
  export default {
    head: {
      title: 'Home'
    }
  }
</script>

titleオプションを指定すると、その値がページのレンダリング前にtitleTemplateの値に入ります。

アプリを再読み込みすると変更が反映されます。

Aboutページの作成

Nuxt.jsの優れた点は、pagesディレクトリ内のファイル変更の監視です。新たなページを追加したときアプリケーションの再起動が不要です。

単純なabout.vueページでテストします。

<!-- ./pages/about.vue -->
<template>
  <div class="main-content">
    <div class="container">
      <h2 class="title is-2">About this website.</h2>
      <p>Curabitur accumsan turpis pharetra <strong>augue tincidunt</strong> blandit. Quisque condimentum maximus mi, sit amet commodo arcu rutrum id. Proin pretium urna vel cursus venenatis. Suspendisse potenti. Etiam mattis sem rhoncus lacus dapibus facilisis. Donec at dignissim dui. Ut et neque nisl.</p>
      <br>
      <h4 class="title is-4">What we hope to achieve:</h4>
      <ul>
        <li>In fermentum leo eu lectus mollis, quis dictum mi aliquet.</li>
        <li>Morbi eu nulla lobortis, lobortis est in, fringilla felis.</li>
        <li>Aliquam nec felis in sapien venenatis viverra fermentum nec lectus.</li>
        <li>Ut non enim metus.</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  head: {
    title: 'About'
  }
}
</script>

http://localhost:3000/aboutにアクセスするとアプリを再起動しなくても、aboutページが表示されます。

ブログ記事をTOPページに表示する

現状のTOPページには、ほぼなにも表示されません。そこで、ブログの最新記事を表示します。<posts>コンポーネントを作成し、index.vueページに表示します。

その前に、JSON形式のブログ記事をアプリのルートフォルダーのファイル内に保存します。ファイルはここからダウンロードするか、以下のJSONをposts.jsonとしてルートフォルダーに保存します。

[
    {
        "id": 4,
        "title": "Building universal JS apps with Nuxt.js",
        "summary": "Get introduced to Nuxt.js, and build great SSR Apps with Vue.js.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "Jane Doe",
        "published": "08:00 - 07/06/2017"
    },
    {
        "id": 3,
        "title": "Great SSR Use cases",
        "summary": "See simple and rich server rendered JavaScript apps.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "Jane Doe",
        "published": "17:00 - 06/06/2017"
    },
    {
        "id": 2,
        "title": "SSR in Vue.js",
        "summary": "Learn about SSR in Vue.js, and where Nuxt.js can make it all faster.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "Jane Doe",
        "published": "13:00 - 06/06/2017"
    },
    {
        "id": 1,
        "title": "Introduction to SSR",
        "summary": "Learn about SSR in JavaScript and how it can be super cool.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "John Doe",
        "published": "11:00 - 06/06/2017"
    }
]

補足:記事はAPIかリソースから取得します。たとえば、Contentfulが適したサービスです。

コンポーネントをcomponentsディレクトリに格納します。ディレクトリ内に単一ファイルコンポーネント<posts>を作成し、以下を追加します。

<!-- ./components/Posts.vue -->
<template>
  <section class="main-content">
    <div class="container">
      <h1 class="title has-text-centered">
        Recent Posts.
      </h1>
      <div class="columns is-multiline">
        <div class="column is-half" v-for="post in posts">
          <div class="card">
           <header class="card-header">
            <p class="card-header-title">
              {{ post.title }}
            </p>
          </header>
          <div class="card-content">
            <div class="content">
              {{ post.summary }}
              <br>
              <small>
                by <strong>{{ post.author}}</strong> 
                \\ {{ post.published }}
              </small>
            </div>
          </div>
          <footer class="card-footer">
            <nuxt-link :to="`/post/${post.id}`" 
              class="card-footer-item">
              Read More
            </nuxt-link>
          </footer>
        </div>
      </div>
    </div>
  </div>
</section>
</template>

<script>
  import posts from '~/posts.json'

  export default {
    name: 'posts',
    data () {
      return { posts }
    }
  }
</script>

保存したJSONファイルから記事データをインポートし、コンポーネントのpostsの値に割り当てます。コンポーネントのテンプレート内でv-forディレクティブを使ってすべての記事をループ処理し、必要な記事属性を表示します。

補足:~シンボルは/ディレクトリのエイリアスです。Nuxt.jsが提供するエイリアスや、リンク先ディレクトリはこのドキュメントで確認してください。

<posts>コンポーネントをTOPページに追加します。

<!-- ./pages/index.vue -->
<template>
<div>
    <!-- ... -->
    <posts />
</div>
</template>

<script>
import Posts from '~components/Posts.vue'

export default {
  components: {
    Posts
  },
  // ...
}
</script>

動的ルートの追加

記事の動的ルートを追加し、URL: /post/1などで記事にアクセス可能にします。

pagesディレクトリにpostディレクトリを追加して構造を以下の形にします。

pages
└── post
    └── _id
        └── index.vue

構造に応じてアプリケーションの動的ルートが生成されます。

router: {
  routes: [
    // ...
    {
      name: 'post-id',
      path: '/post/:id',
      component: 'pages/post/_id/index.vue'
    }
  ]
}

個別記事ファイルを更新します。

<!-- ./pages/post/_id/index.vue -->
<template>
  <div class="main-content">
    <div class="container">
      <h2 class="title is-2">{{ post.title }}</h2>
      <div v-html="post.content"></div>
      <br>
      <h4 class="title is-5 is-marginless">by <strong>{{ post.author }}</strong> at <strong>{{ post.published }}</strong></h4>
    </div>
  </div>
</template>

<script>
  // import posts saved JSON data
  import posts from '~/posts.json'

  export default {
    validate ({ params }) {
      return /^\d+$/.test(params.id)
    },
    asyncData ({ params }, callback) {
      let post = posts.find(post => post.id === parseInt(params.id))
      if (post) {
        callback(null, { post })
      } else {
        callback({ statusCode: 404, message: 'Post not found' })
      }
    },
    head () {
      return {
        title: this.post.title,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: this.post.summary
          }
        ]
      }
    }
  }
</script>

Nuxt.jsのページコンポーネントは開発プロセスを簡単にするための独自メソッドを用意しています。一部のメソッドの個別記事ページでの使い方を説明します。

  • validateメソッドでルートパラメーターを検証できます。今回のvalidateメソッドは、ルートパラメーターに渡された値が数値か確認します。falseなら、404エラーページをローディングします。詳細はここを確認ください
  • asyncDataメソッドはブラウザーにレスポンスを送信する前にサーバーサイドでデータを取得しレンダリングします。さまざまな方法でデータを返します。今回は、コールバック関数でルートのidパラメーターと同じid属性を持つ記事を返します。関数の使用方法はここを確認ください
  • headメソッドは、ページのヘッダーを設定できます。今回は、ページタイトルを記事のタイトルに変更し、記事の要約をページのメタディスクリプションとして追加します

ブログに再度アクセスするとすべてのルートとページが正しく動きます。ページのソースを見るとHTMLが生成されていることを確認できます。サーバーサイドでレンダリングされた機能的なJavaScriptアプリケーションができました。

静的ファイルの生成

ページの静的HTMLファイルを生成します。

Nuxt.jsはデフォルトでは動的ルートを無視するので微調整します。動的ルートの静的ファイルを生成するには、./nuxt.config.jsファイル内でルートを明示的に指定します。

コールバック関数で動的ルートの一覧を返します。

// ./nuxt.config.js

module.exports = {
  // ...
  generate: {
    routes(callback) {
      const posts = require('./posts.json')
      let routes = posts.map(post => `/post/${post.id}`)
      callback(null, routes)
    }
  }
}

generateプロパティはこのドキュメントを確認ください。

以下のコマンドを実行してすべてのルートを生成します。

npm run generate

Nuxtは生成された静的ファイルをdistフォルダーに保存します。

Firebase Hostingへのデプロイ

最後はFirebaseのホスティングを活用して静的Webサイトを公開します。

Firebase CLIをインストールしていない場合はインストールします。

npm install -g firebase-tools

以下を入力してサイトを初期化しdistフォルダーをパブリックディレクトリに指定します。

firebase init

アプリをデプロイします。

firebase deploy

これで、<YOUR-FIREBASE-APP>.firebaseapp.comにWebサイトが立ち上がります。本記事によるデモアプリはhttps://nuxt-ssr-blog.firebaseapp.com/で確認できます。

最後に

サーバーサイドレンダリングによるJavaScriptアプリケーションをVue.jsを使って作成する際に、Nuxt.jsを活用する方法について説明しました。また、Nuxt.jsのgenerateコマンドでページの静的ファイルを生成し、Firebase Hostingなどのサービスで素早くデプロイする方法も説明しました。

Nuxt.jsはVue.js公式GitBookのSSRの項でもおすすめされているフレームワークです。私自身、多くのSSRプロジェクトでNuxt.jsを利用し、実力を引き出すのが本当に楽しみです。

本記事はGraham Coxが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Nuxt.js: A Universal Vue.js Application Framework

[翻訳:薮田佳佑/編集:Livit

Copyright © 2017, Olayinka Omole All Rights Reserved.

Olayinka Omole

Olayinka Omole

ソフトウェア開発、デザイン、エレクトロニクス工学、人工知能に興味を持つラゴスのエンジニアです。ツイートしたり、学術研究をしたり、写真を撮ったり、デザインしたりコードを書いたりして、日常を楽しんでいます。

Loading...