Nuxt.jsのSSRの動きをHeadlessBrowserで確認してみる

この記事は、Nuxt.js #2 Advent Calendar 2018 の15日目の記事です。

14日目は POPOPON - Qiita さんの Nuxt でストアから GraphQL を扱う方法 - Qiita でした。

16日目は taroodr - Qiitaさんお願いします。

SSR(サーバーサイドレンダリング)してますか?

SSRにまつわる意義などの詳しい話はネット上にいくらでも転がっているモノだと思いますので割愛しますが、Nuxt.jsはそれ自体がVue.jsを使う上でSSRをより扱いやすくするべく生まれてきたものと(勝手に)認識しています。

SSRはSPAな今の世の中において必須の技術だと思います。

そして、最近のNuxt.js(とVue.js)の勢い・人気からすると、Nuxt.jsがいかに良く出来たフレームワークかがわかります。

実際、最近一番触っていて楽しいのがNuxt.jsです。

しかし、そんな私自身「実際、SSRってどんな動きなの?」と理解不足に感じることが多かったため(つまりアドベントカレンダー関係なく自分で理解したいために)、実際に動かして試してみることにしました。

予めお断りしますが、基本的にNuxt.jsのオフィシャルドキュメントに記載の内容を確認しているだけですのであしからず。

ja.nuxtjs.org/guide

Nuxt.js/Vue.jsのドキュメントはかなり充実していてありがたいですね。

Nuxt.jsにおけるSSRの動き

こちらもドキュメントの通りなのですが、

https://ja.nuxtjs.org/nuxt-schema.png

引用 - (ja.nuxtjs.org/guide#図解](https://ja.nuxtjs.org/nuxt-schema.png)

私の理解では、Componentが呼び出されると(あるいは違う言い方:当該Componentが必要となるURLに直接リクエストが来ると)、当該Componentに定義した asyncData() メソッドというものがHTMLレンダリングされる前に実行され、そのタイミングでサーバーなどからデータを取得し、同じく当該Componentの data() に値をセットすることで、クローラーなどが巡回してきたときにはすでに仮想DOMが出来上がっているといった認識です。

もしこの表現(特に「仮想DOMが出来上がっている」は怪しい)が間違っていたらご指摘いただきたいです。

localhostにてNuxt.jsでSSRを確認する方法

localhostにおいてNuxt.jsでSSRを行う場合、特に難しい設定は一切ありません。

nuxt コマンドで起動するだけです。

package.jsonで定義しても良いのですが、Node.jsのバージョンが最新であれば npx (直接packageのコマンドが打てる)を利用できますので、下記のコマンドでOKです。

$ npx nuxt

これだけで基本的にSSRを伴う動きとなるようです。

nuxt コマンドを実行すると開発サーバーが起動します。このサーバーはホットリローディング及び Vue Server Renderer を備えており、アプリケーションが自動的にサーバーサイドレンダリングするよう設定されています。

引用 - ja.nuxtjs.org/guide#サーバーサイドレンダリング-ユニバーサル-ssr-

そして、SSRしたいComponentにて、値を data() へ設定する処理を行う asyncData() を定義するだけです。

  asyncData (context) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          title: "title from asyncData",
          async_data_time: new Date()
        })
      }, 5000);
    }).then((res) => {
      return res
    })
  }

setTimeout を利用し、少し長めに5秒ほどのサーバー通信を擬似的に行っています。

localhostにてNuxt.jsでSSRさせない方法

反対に、SSRをさせない単純なSPAとする場合は、 --spa オプションをつけて nuxt コマンドを実行するだけです。

$ npx nuxt --spa

もし何らかの理由でサーバーサイドレンダリングを使いたくない、あるいはアプリケーションを静的にホスティングする必要があるときは nuxt --spa を使って、シンプルに SPA モードを使うことができます。generate 機能と組み合わせて使うことで、Node.js ランタイムや特別なサーバー処理を利用する必要なしに、SPA のパワフルなデプロイを実現できます。

引用 - ja.nuxtjs.org/guide#シングルページアプリケーション-spa-

HeadlessBrowserを使って確認する理由

SSRの期待するところは、なんといってもSPAなアプリケーションであってもSEO効果を期待させるためだと認識しています。

また、通常のブラウザでアクセスした場合、SPAモードで起動していたとしても仮想DOMは生成されます。

したがって、擬似的にクローラーの動きを真似るという意味でHeadlessBrowserで確認した次第です。

今回使うHeadlessBrowserはGoogleChromeに搭載されているものを利用します。

詳しくは こちら - ヘッドレス Chrome ことはじめ を参照してみてください。

上記ページを参考に、下記のようなコマンドをCLIにて実行して確認していきます。

$ chrome --headless --disable-gpu --dump-dom http://localhost:3000/about

確認用に作成したアプリ

トップ画面とアバウト画面の2画面だけの簡素なアプリです。

f:id:omiend:20181214170622p:plain

mountedが実行されると mounted_time に、 asyncData() が実行されると async_data_time にそれぞれ new Date() を設定しています。

トップ画面はただの静的な画面です。

また、ソースは下記にあります。もしよかったらどうぞ。

github.com

SPAモードで起動し、仮想DOMが生成されないことを確認

では実際に確認していきましょう。

下記のコマンドを実行し、SPAモードで起動します。

$ npx nuxt --spa

HeadlessBrowserでアクセスします。

$ chrome --headless --disable-gpu --dump-dom http://localhost:3000/about
<!DOCTYPE html>
<html data-n-head=""><head>
    <title data-n-head="true">omiend_nuxt_advent_calendar_2018_2_day_one</title><meta data-n-head="true" charset="utf-8"><meta data-n-head="true" name="viewport" content="width=device-width, initial-scale=1"><meta data-n-head="true" data-hid="description" name="description" content="My glorious Nuxt.js project"><link data-n-head="true" rel="icon" type="image/x-icon" href="/favicon.ico"><link rel="preload" href="/_nuxt/runtime.js" as="script"><link rel="preload" href="/_nuxt/commons.app.js" as="script"><link rel="preload" href="/_nuxt/vendors.app.js" as="script"><link rel="preload" href="/_nuxt/app.js" as="script">
  <style type="text/css">
.__nuxt-error-page {
  padding: 1rem;
  background: #F7F8FB;
  color: #47494E;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  font-family: sans-serif;
  font-weight: 100 !important;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.__nuxt-error-page .error {
  max-width: 450px;
}
.__nuxt-error-page .title {
  font-size: 1.5rem;
  margin-top: 15px;
  color: #47494E;
  margin-bottom: 8px;
}
.__nuxt-error-page .description {
  color: #7F828B;
  line-height: 21px;
  margin-bottom: 10px;
}
.__nuxt-error-page a {
  color: #7F828B !important;
  text-decoration: none;
}
.__nuxt-error-page .logo {
  position: fixed;
  left: 12px;
  bottom: 12px;
}

/*# sourceURL=/Users/endo/my/omiend_nuxt_advent_calendar_2018_2_day_one/.nuxt/components/nuxt-error.vue */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9lbmRvL215L29taWVuZF9udXh0X2FkdmVudF9jYWxlbmRhcl8yMDE4XzJfZGF5X29uZS8ubnV4dC9jb21wb25lbnRzL251eHQtZXJyb3IudnVlIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFtREE7RUFDRSxjQUFjO0VBQ2Qsb0JBQW9CO0VBQ3BCLGVBQWU7RUFDZixtQkFBbUI7RUFDbkIsY0FBYztFQUNkLHdCQUF3QjtFQUN4QixvQkFBb0I7RUFDcEIsdUJBQXVCO0VBQ3ZCLHdCQUF3QjtFQUN4Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBQzNCLCtCQUErQjtFQUMvQixvQ0FBb0M7RUFDcEMsbUJBQW1CO0VBQ25CLE9BQU87RUFDUCxRQUFRO0VBQ1IsU0FBUztFQUNULFVBQVU7Q0FDWDtBQUNEO0VBQ0UsaUJBQWlCO0NBQ2xCO0FBQ0Q7RUFDRSxrQkFBa0I7RUFDbEIsaUJBQWlCO0VBQ2pCLGVBQWU7RUFDZixtQkFBbUI7Q0FDcEI7QUFDRDtFQUNFLGVBQWU7RUFDZixrQkFBa0I7RUFDbEIsb0JBQW9CO0NBQ3JCO0FBQ0Q7RUFDRSwwQkFBMEI7RUFDMUIsc0JBQXNCO0NBQ3ZCO0FBQ0Q7RUFDRSxnQkFBZ0I7RUFDaEIsV0FBVztFQUNYLGFBQWE7Q0FDZCIsImZpbGUiOiJudXh0LWVycm9yLnZ1ZT92dWUmdHlwZT1zdHlsZSZpbmRleD0wJmxhbmc9Y3NzJiIsInNvdXJjZXNDb250ZW50IjpbIlxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cbi5fX251eHQtZXJyb3ItcGFnZSB7XG4gIHBhZGRpbmc6IDFyZW07XG4gIGJhY2tncm91bmQ6ICNGN0Y4RkI7XG4gIGNvbG9yOiAjNDc0OTRFO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBmb250LWZhbWlseTogc2Fucy1zZXJpZjtcbiAgZm9udC13ZWlnaHQ6IDEwMCAhaW1wb3J0YW50O1xuICAtbXMtdGV4dC1zaXplLWFkanVzdDogMTAwJTtcbiAgLXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OiAxMDAlO1xuICAtd2Via2l0LWZvbnQtc21vb3RoaW5nOiBhbnRpYWxpYXNlZDtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDA7XG4gIGxlZnQ6IDA7XG4gIHJpZ2h0OiAwO1xuICBib3R0b206IDA7XG59XG4uX19udXh0LWVycm9yLXBhZ2UgLmVycm9yIHtcbiAgbWF4LXdpZHRoOiA0NTBweDtcbn1cbi5fX251eHQtZXJyb3ItcGFnZSAudGl0bGUge1xuICBmb250LXNpemU6IDEuNXJlbTtcbiAgbWFyZ2luLXRvcDogMTVweDtcbiAgY29sb3I6ICM0NzQ5NEU7XG4gIG1hcmdpbi1ib3R0b206IDhweDtcbn1cbi5fX251eHQtZXJyb3ItcGFnZSAuZGVzY3JpcHRpb24ge1xuICBjb2xvcjogIzdGODI4QjtcbiAgbGluZS1oZWlnaHQ6IDIxcHg7XG4gIG1hcmdpbi1ib3R0b206IDEwcHg7XG59XG4uX19udXh0LWVycm9yLXBhZ2UgYSB7XG4gIGNvbG9yOiAjN0Y4MjhCICFpbXBvcnRhbnQ7XG4gIHRleHQtZGVjb3JhdGlvbjogbm9uZTtcbn1cbi5fX251eHQtZXJyb3ItcGFnZSAubG9nbyB7XG4gIHBvc2l0aW9uOiBmaXhlZDtcbiAgbGVmdDogMTJweDtcbiAgYm90dG9tOiAxMnB4O1xufVxuIl0sInNvdXJjZVJvb3QiOiIifQ== */</style><style type="text/css">
.nuxt-progress {
  position: fixed;
  top: 0px;
  left: 0px;
  right: 0px;
  height: 2px;
  width: 0%;
  opacity: 1;
  transition: width 0.1s, opacity 0.4s;
  background-color: #fff;
  z-index: 999999;
}
.nuxt-progress.nuxt-progress-notransition {
  transition: none;
}
.nuxt-progress-failed {
  background-color: red;
}

/*# sourceURL=/Users/endo/my/omiend_nuxt_advent_calendar_2018_2_day_one/.nuxt/components/nuxt-loading.vue */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9lbmRvL215L29taWVuZF9udXh0X2FkdmVudF9jYWxlbmRhcl8yMDE4XzJfZGF5X29uZS8ubnV4dC9jb21wb25lbnRzL251eHQtbG9hZGluZy52dWUiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQTRKQTtFQUNFLGdCQUFnQjtFQUNoQixTQUFTO0VBQ1QsVUFBVTtFQUNWLFdBQVc7RUFDWCxZQUFZO0VBQ1osVUFBVTtFQUNWLFdBQVc7RUFDWCxxQ0FBcUM7RUFDckMsdUJBQXVCO0VBQ3ZCLGdCQUFnQjtDQUNqQjtBQUVEO0VBQ0UsaUJBQWlCO0NBQ2xCO0FBRUQ7RUFDRSxzQkFBc0I7Q0FDdkIiLCJmaWxlIjoibnV4dC1sb2FkaW5nLnZ1ZT92dWUmdHlwZT1zdHlsZSZpbmRleD0wJmxhbmc9Y3NzJiIsInNvdXJjZXNDb250ZW50IjpbIlxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cbi5udXh0LXByb2dyZXNzIHtcbiAgcG9zaXRpb246IGZpeGVkO1xuICB0b3A6IDBweDtcbiAgbGVmdDogMHB4O1xuICByaWdodDogMHB4O1xuICBoZWlnaHQ6IDJweDtcbiAgd2lkdGg6IDAlO1xuICBvcGFjaXR5OiAxO1xuICB0cmFuc2l0aW9uOiB3aWR0aCAwLjFzLCBvcGFjaXR5IDAuNHM7XG4gIGJhY2tncm91bmQtY29sb3I6ICNmZmY7XG4gIHotaW5kZXg6IDk5OTk5OTtcbn1cblxuLm51eHQtcHJvZ3Jlc3MubnV4dC1wcm9ncmVzcy1ub3RyYW5zaXRpb24ge1xuICB0cmFuc2l0aW9uOiBub25lO1xufVxuXG4ubnV4dC1wcm9ncmVzcy1mYWlsZWQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZWQ7XG59XG4iXSwic291cmNlUm9vdCI6IiJ9 */</style><style type="text/css">
html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-weight: 300;
  font-size: 1rem;
  color: #35495e;
  letter-spacing: 1px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}
*,
*:before,
*:after {
  box-sizing: border-box;
  margin: 0;
}
.button--green {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #3b8070;
  color: #3b8070;
  text-decoration: none;
  padding: 10px 30px;
}
.button--green:hover {
  color: #fff;
  background-color: #3b8070;
}
.button--grey {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #35495e;
  color: #35495e;
  text-decoration: none;
  padding: 10px 30px;
  margin-left: 15px;
}
.button--grey:hover {
  color: #fff;
  background-color: #35495e;
}
.container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.title {
  display: block;
}

/*# sourceURL=/Users/endo/my/omiend_nuxt_advent_calendar_2018_2_day_one/src/layouts/default.vue */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9lbmRvL215L29taWVuZF9udXh0X2FkdmVudF9jYWxlbmRhcl8yMDE4XzJfZGF5X29uZS9zcmMvbGF5b3V0cy9kZWZhdWx0LnZ1ZSJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBT0E7RUFDRTtnREFDOEM7RUFDOUMsaUJBQWlCO0VBQ2pCLGdCQUFnQjtFQUNoQixlQUFlO0VBQ2Ysb0JBQW9CO0VBQ3BCLGtCQUFrQjtFQUNsQiwyQkFBMkI7RUFDM0IsK0JBQStCO0VBQy9CLG1DQUFtQztFQUNuQyxvQ0FBb0M7RUFDcEMsdUJBQXVCO0NBQ3hCO0FBRUQ7OztFQUdFLHVCQUF1QjtFQUN2QixVQUFVO0NBQ1g7QUFFRDtFQUNFLHNCQUFzQjtFQUN0QixtQkFBbUI7RUFDbkIsMEJBQTBCO0VBQzFCLGVBQWU7RUFDZixzQkFBc0I7RUFDdEIsbUJBQW1CO0NBQ3BCO0FBRUQ7RUFDRSxZQUFZO0VBQ1osMEJBQTBCO0NBQzNCO0FBRUQ7RUFDRSxzQkFBc0I7RUFDdEIsbUJBQW1CO0VBQ25CLDBCQUEwQjtFQUMxQixlQUFlO0VBQ2Ysc0JBQXNCO0VBQ3RCLG1CQUFtQjtFQUNuQixrQkFBa0I7Q0FDbkI7QUFFRDtFQUNFLFlBQVk7RUFDWiwwQkFBMEI7Q0FDM0I7QUFFRDtFQUNFLGtCQUFrQjtFQUNsQixjQUFjO0VBQ2Qsd0JBQXdCO0VBQ3hCLG9CQUFvQjtFQUNwQixtQkFBbUI7Q0FDcEI7QUFFRDtFQUNFLGVBQWU7Q0FDaEIiLCJmaWxlIjoiZGVmYXVsdC52dWU/dnVlJnR5cGU9c3R5bGUmaW5kZXg9MCZsYW5nPWNzcyYiLCJzb3VyY2VzQ29udGVudCI6WyJcblxuXG5cblxuXG5cbmh0bWwge1xuICBmb250LWZhbWlseTogJ1NvdXJjZSBTYW5zIFBybycsIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJyxcbiAgICBSb2JvdG8sICdIZWx2ZXRpY2EgTmV1ZScsIEFyaWFsLCBzYW5zLXNlcmlmO1xuICBmb250LXdlaWdodDogMzAwO1xuICBmb250LXNpemU6IDFyZW07XG4gIGNvbG9yOiAjMzU0OTVlO1xuICBsZXR0ZXItc3BhY2luZzogMXB4O1xuICB3b3JkLXNwYWNpbmc6IDFweDtcbiAgLW1zLXRleHQtc2l6ZS1hZGp1c3Q6IDEwMCU7XG4gIC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogMTAwJTtcbiAgLW1vei1vc3gtZm9udC1zbW9vdGhpbmc6IGdyYXlzY2FsZTtcbiAgLXdlYmtpdC1mb250LXNtb290aGluZzogYW50aWFsaWFzZWQ7XG4gIGJveC1zaXppbmc6IGJvcmRlci1ib3g7XG59XG5cbiosXG4qOmJlZm9yZSxcbio6YWZ0ZXIge1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBtYXJnaW46IDA7XG59XG5cbi5idXR0b24tLWdyZWVuIHtcbiAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIGJvcmRlcjogMXB4IHNvbGlkICMzYjgwNzA7XG4gIGNvbG9yOiAjM2I4MDcwO1xuICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gIHBhZGRpbmc6IDEwcHggMzBweDtcbn1cblxuLmJ1dHRvbi0tZ3JlZW46aG92ZXIge1xuICBjb2xvcjogI2ZmZjtcbiAgYmFja2dyb3VuZC1jb2xvcjogIzNiODA3MDtcbn1cblxuLmJ1dHRvbi0tZ3JleSB7XG4gIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgYm9yZGVyLXJhZGl1czogNHB4O1xuICBib3JkZXI6IDFweCBzb2xpZCAjMzU0OTVlO1xuICBjb2xvcjogIzM1NDk1ZTtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xuICBwYWRkaW5nOiAxMHB4IDMwcHg7XG4gIG1hcmdpbi1sZWZ0OiAxNXB4O1xufVxuXG4uYnV0dG9uLS1ncmV5OmhvdmVyIHtcbiAgY29sb3I6ICNmZmY7XG4gIGJhY2tncm91bmQtY29sb3I6ICMzNTQ5NWU7XG59XG5cbi5jb250YWluZXIge1xuICBtaW4taGVpZ2h0OiAxMDB2aDtcbiAgZGlzcGxheTogZmxleDtcbiAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbn1cblxuLnRpdGxlIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG4iXSwic291cmNlUm9vdCI6IiJ9 */</style><script charset="utf-8" src="/_nuxt/pages/about/index.js"></script></head>
  <body data-n-head="">
    <div id="__nuxt"><style>#nuxt-loading {  visibility: hidden;  opacity: 0;  position: absolute;  left: 0;  right: 0;  top: 0;  bottom: 0;  display: flex;  justify-content: center;  align-items: center;  flex-direction: column;  animation: nuxtLoadingIn 10s ease;  -webkit-animation: nuxtLoadingIn 10s ease;  animation-fill-mode: forwards;  overflow: hidden;}@keyframes nuxtLoadingIn {  0% {visibility: hidden;opacity: 0;  }  20% {visibility: visible;opacity: 0;  }  100% {visibility: visible;opacity: 1;  }}@-webkit-keyframes nuxtLoadingIn {  0% {visibility: hidden;opacity: 0;  }  20% {visibility: visible;opacity: 0;  }  100% {visibility: visible;opacity: 1;  }}#nuxt-loading>div,#nuxt-loading>div:after {  border-radius: 50%;  width: 5rem;  height: 5rem;}#nuxt-loading>div {  font-size: 10px;  position: relative;  text-indent: -9999em;  border: .5rem solid #F5F5F5;  border-left: .5rem solid #fff;  -webkit-transform: translateZ(0);  -ms-transform: translateZ(0);  transform: translateZ(0);  -webkit-animation: nuxtLoading 1.1s infinite linear;  animation: nuxtLoading 1.1s infinite linear;}#nuxt-loading.error>div {  border-left: .5rem solid #ff4500;  animation-duration: 5s;}@-webkit-keyframes nuxtLoading {  0% {-webkit-transform: rotate(0deg);transform: rotate(0deg);  }  100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);  }}@keyframes nuxtLoading {  0% {-webkit-transform: rotate(0deg);transform: rotate(0deg);  }  100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);  }}</style><script>window.addEventListener('error', function () {  var e = document.getElementById('nuxt-loading');  if (e) e.className += ' error';});</script><div id="nuxt-loading" aria-live="polite" role="status"><div>Loading...</div></div><!-- https://projects.lukehaas.me/css-loaders --></div>
  <script type="text/javascript" src="/_nuxt/runtime.js"></script><script type="text/javascript" src="/_nuxt/commons.app.js"></script><script type="text/javascript" src="/_nuxt/vendors.app.js"></script><script type="text/javascript" src="/_nuxt/app.js"></script>

</body></html>

なにやらうわ~っと出ていますが、最後の方に

<div id="nuxt-loading" aria-live="polite" role="status"><div>Loading...</div></div>

と出ているのがわかるかと思います。

おそらく、ここが仮想DOMに置き換わる場所だと思います。

chrome の HeadlessBrowserには screenshot を撮る機能がありますので、試しに撮ってみました。

まずはトップ画面。

f:id:omiend:20181214170912p:plain

静的な画面ですが、mountedは実行されるんですね。

次に、動的なアバウト画面。

f:id:omiend:20181214163656p:plain

真っ白です。

これではSEOもへったくれもありませんね・・・。

Universalモードで起動し、SSRされ、仮想DOMが生成されていることを確認

次に、Universalモードで起動してSSRがされるか試してみます。

$ npx nuxt

同じく、HeadlessBrowserでアクセスします。

$ chrome --headless --disable-gpu --dump-dom http://localhost:3000/about
<!DOCTYPE html>
<html data-n-head=""><head>
    <title data-n-head="true">omiend_nuxt_advent_calendar_2018_2_day_one</title><meta data-n-head="true" charset="utf-8"><meta data-n-head="true" name="viewport" content="width=device-width, initial-scale=1"><meta data-n-head="true" data-hid="description" name="description" content="My glorious Nuxt.js project"><link data-n-head="true" rel="icon" type="image/x-icon" href="/favicon.ico"><link rel="preload" href="/_nuxt/runtime.js" as="script"><link rel="preload" href="/_nuxt/commons.app.js" as="script"><link rel="preload" href="/_nuxt/vendors.app.js" as="script"><link rel="preload" href="/_nuxt/app.js" as="script"><link rel="preload" href="/_nuxt/pages/about/index.js" as="script">
  <style type="text/css">
.__nuxt-error-page {
  padding: 1rem;
  background: #F7F8FB;
  color: #47494E;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  font-family: sans-serif;
  font-weight: 100 !important;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.__nuxt-error-page .error {
  max-width: 450px;
}
.__nuxt-error-page .title {
  font-size: 1.5rem;
  margin-top: 15px;
  color: #47494E;
  margin-bottom: 8px;
}
.__nuxt-error-page .description {
  color: #7F828B;
  line-height: 21px;
  margin-bottom: 10px;
}
.__nuxt-error-page a {
  color: #7F828B !important;
  text-decoration: none;
}
.__nuxt-error-page .logo {
  position: fixed;
  left: 12px;
  bottom: 12px;
}

/*# sourceURL=/Users/endo/my/omiend_nuxt_advent_calendar_2018_2_day_one/.nuxt/components/nuxt-error.vue */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9lbmRvL215L29taWVuZF9udXh0X2FkdmVudF9jYWxlbmRhcl8yMDE4XzJfZGF5X29uZS8ubnV4dC9jb21wb25lbnRzL251eHQtZXJyb3IudnVlIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFtREE7RUFDRSxjQUFjO0VBQ2Qsb0JBQW9CO0VBQ3BCLGVBQWU7RUFDZixtQkFBbUI7RUFDbkIsY0FBYztFQUNkLHdCQUF3QjtFQUN4QixvQkFBb0I7RUFDcEIsdUJBQXVCO0VBQ3ZCLHdCQUF3QjtFQUN4Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBQzNCLCtCQUErQjtFQUMvQixvQ0FBb0M7RUFDcEMsbUJBQW1CO0VBQ25CLE9BQU87RUFDUCxRQUFRO0VBQ1IsU0FBUztFQUNULFVBQVU7Q0FDWDtBQUNEO0VBQ0UsaUJBQWlCO0NBQ2xCO0FBQ0Q7RUFDRSxrQkFBa0I7RUFDbEIsaUJBQWlCO0VBQ2pCLGVBQWU7RUFDZixtQkFBbUI7Q0FDcEI7QUFDRDtFQUNFLGVBQWU7RUFDZixrQkFBa0I7RUFDbEIsb0JBQW9CO0NBQ3JCO0FBQ0Q7RUFDRSwwQkFBMEI7RUFDMUIsc0JBQXNCO0NBQ3ZCO0FBQ0Q7RUFDRSxnQkFBZ0I7RUFDaEIsV0FBVztFQUNYLGFBQWE7Q0FDZCIsImZpbGUiOiJudXh0LWVycm9yLnZ1ZT92dWUmdHlwZT1zdHlsZSZpbmRleD0wJmxhbmc9Y3NzJiIsInNvdXJjZXNDb250ZW50IjpbIlxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cbi5fX251eHQtZXJyb3ItcGFnZSB7XG4gIHBhZGRpbmc6IDFyZW07XG4gIGJhY2tncm91bmQ6ICNGN0Y4RkI7XG4gIGNvbG9yOiAjNDc0OTRFO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBmb250LWZhbWlseTogc2Fucy1zZXJpZjtcbiAgZm9udC13ZWlnaHQ6IDEwMCAhaW1wb3J0YW50O1xuICAtbXMtdGV4dC1zaXplLWFkanVzdDogMTAwJTtcbiAgLXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OiAxMDAlO1xuICAtd2Via2l0LWZvbnQtc21vb3RoaW5nOiBhbnRpYWxpYXNlZDtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDA7XG4gIGxlZnQ6IDA7XG4gIHJpZ2h0OiAwO1xuICBib3R0b206IDA7XG59XG4uX19udXh0LWVycm9yLXBhZ2UgLmVycm9yIHtcbiAgbWF4LXdpZHRoOiA0NTBweDtcbn1cbi5fX251eHQtZXJyb3ItcGFnZSAudGl0bGUge1xuICBmb250LXNpemU6IDEuNXJlbTtcbiAgbWFyZ2luLXRvcDogMTVweDtcbiAgY29sb3I6ICM0NzQ5NEU7XG4gIG1hcmdpbi1ib3R0b206IDhweDtcbn1cbi5fX251eHQtZXJyb3ItcGFnZSAuZGVzY3JpcHRpb24ge1xuICBjb2xvcjogIzdGODI4QjtcbiAgbGluZS1oZWlnaHQ6IDIxcHg7XG4gIG1hcmdpbi1ib3R0b206IDEwcHg7XG59XG4uX19udXh0LWVycm9yLXBhZ2UgYSB7XG4gIGNvbG9yOiAjN0Y4MjhCICFpbXBvcnRhbnQ7XG4gIHRleHQtZGVjb3JhdGlvbjogbm9uZTtcbn1cbi5fX251eHQtZXJyb3ItcGFnZSAubG9nbyB7XG4gIHBvc2l0aW9uOiBmaXhlZDtcbiAgbGVmdDogMTJweDtcbiAgYm90dG9tOiAxMnB4O1xufVxuIl0sInNvdXJjZVJvb3QiOiIifQ== */</style><style type="text/css">
.nuxt-progress {
  position: fixed;
  top: 0px;
  left: 0px;
  right: 0px;
  height: 2px;
  width: 0%;
  opacity: 1;
  transition: width 0.1s, opacity 0.4s;
  background-color: #fff;
  z-index: 999999;
}
.nuxt-progress.nuxt-progress-notransition {
  transition: none;
}
.nuxt-progress-failed {
  background-color: red;
}

/*# sourceURL=/Users/endo/my/omiend_nuxt_advent_calendar_2018_2_day_one/.nuxt/components/nuxt-loading.vue */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9lbmRvL215L29taWVuZF9udXh0X2FkdmVudF9jYWxlbmRhcl8yMDE4XzJfZGF5X29uZS8ubnV4dC9jb21wb25lbnRzL251eHQtbG9hZGluZy52dWUiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQTRKQTtFQUNFLGdCQUFnQjtFQUNoQixTQUFTO0VBQ1QsVUFBVTtFQUNWLFdBQVc7RUFDWCxZQUFZO0VBQ1osVUFBVTtFQUNWLFdBQVc7RUFDWCxxQ0FBcUM7RUFDckMsdUJBQXVCO0VBQ3ZCLGdCQUFnQjtDQUNqQjtBQUVEO0VBQ0UsaUJBQWlCO0NBQ2xCO0FBRUQ7RUFDRSxzQkFBc0I7Q0FDdkIiLCJmaWxlIjoibnV4dC1sb2FkaW5nLnZ1ZT92dWUmdHlwZT1zdHlsZSZpbmRleD0wJmxhbmc9Y3NzJiIsInNvdXJjZXNDb250ZW50IjpbIlxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cbi5udXh0LXByb2dyZXNzIHtcbiAgcG9zaXRpb246IGZpeGVkO1xuICB0b3A6IDBweDtcbiAgbGVmdDogMHB4O1xuICByaWdodDogMHB4O1xuICBoZWlnaHQ6IDJweDtcbiAgd2lkdGg6IDAlO1xuICBvcGFjaXR5OiAxO1xuICB0cmFuc2l0aW9uOiB3aWR0aCAwLjFzLCBvcGFjaXR5IDAuNHM7XG4gIGJhY2tncm91bmQtY29sb3I6ICNmZmY7XG4gIHotaW5kZXg6IDk5OTk5OTtcbn1cblxuLm51eHQtcHJvZ3Jlc3MubnV4dC1wcm9ncmVzcy1ub3RyYW5zaXRpb24ge1xuICB0cmFuc2l0aW9uOiBub25lO1xufVxuXG4ubnV4dC1wcm9ncmVzcy1mYWlsZWQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZWQ7XG59XG4iXSwic291cmNlUm9vdCI6IiJ9 */</style><style type="text/css">
html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-weight: 300;
  font-size: 1rem;
  color: #35495e;
  letter-spacing: 1px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}
*,
*:before,
*:after {
  box-sizing: border-box;
  margin: 0;
}
.button--green {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #3b8070;
  color: #3b8070;
  text-decoration: none;
  padding: 10px 30px;
}
.button--green:hover {
  color: #fff;
  background-color: #3b8070;
}
.button--grey {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #35495e;
  color: #35495e;
  text-decoration: none;
  padding: 10px 30px;
  margin-left: 15px;
}
.button--grey:hover {
  color: #fff;
  background-color: #35495e;
}
.container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.title {
  display: block;
}

/*# sourceURL=/Users/endo/my/omiend_nuxt_advent_calendar_2018_2_day_one/src/layouts/default.vue */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9lbmRvL215L29taWVuZF9udXh0X2FkdmVudF9jYWxlbmRhcl8yMDE4XzJfZGF5X29uZS9zcmMvbGF5b3V0cy9kZWZhdWx0LnZ1ZSJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBT0E7RUFDRTtnREFDOEM7RUFDOUMsaUJBQWlCO0VBQ2pCLGdCQUFnQjtFQUNoQixlQUFlO0VBQ2Ysb0JBQW9CO0VBQ3BCLGtCQUFrQjtFQUNsQiwyQkFBMkI7RUFDM0IsK0JBQStCO0VBQy9CLG1DQUFtQztFQUNuQyxvQ0FBb0M7RUFDcEMsdUJBQXVCO0NBQ3hCO0FBRUQ7OztFQUdFLHVCQUF1QjtFQUN2QixVQUFVO0NBQ1g7QUFFRDtFQUNFLHNCQUFzQjtFQUN0QixtQkFBbUI7RUFDbkIsMEJBQTBCO0VBQzFCLGVBQWU7RUFDZixzQkFBc0I7RUFDdEIsbUJBQW1CO0NBQ3BCO0FBRUQ7RUFDRSxZQUFZO0VBQ1osMEJBQTBCO0NBQzNCO0FBRUQ7RUFDRSxzQkFBc0I7RUFDdEIsbUJBQW1CO0VBQ25CLDBCQUEwQjtFQUMxQixlQUFlO0VBQ2Ysc0JBQXNCO0VBQ3RCLG1CQUFtQjtFQUNuQixrQkFBa0I7Q0FDbkI7QUFFRDtFQUNFLFlBQVk7RUFDWiwwQkFBMEI7Q0FDM0I7QUFFRDtFQUNFLGtCQUFrQjtFQUNsQixjQUFjO0VBQ2Qsd0JBQXdCO0VBQ3hCLG9CQUFvQjtFQUNwQixtQkFBbUI7Q0FDcEI7QUFFRDtFQUNFLGVBQWU7Q0FDaEIiLCJmaWxlIjoiZGVmYXVsdC52dWU/dnVlJnR5cGU9c3R5bGUmaW5kZXg9MCZsYW5nPWNzcyYiLCJzb3VyY2VzQ29udGVudCI6WyJcblxuXG5cblxuXG5cbmh0bWwge1xuICBmb250LWZhbWlseTogJ1NvdXJjZSBTYW5zIFBybycsIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJyxcbiAgICBSb2JvdG8sICdIZWx2ZXRpY2EgTmV1ZScsIEFyaWFsLCBzYW5zLXNlcmlmO1xuICBmb250LXdlaWdodDogMzAwO1xuICBmb250LXNpemU6IDFyZW07XG4gIGNvbG9yOiAjMzU0OTVlO1xuICBsZXR0ZXItc3BhY2luZzogMXB4O1xuICB3b3JkLXNwYWNpbmc6IDFweDtcbiAgLW1zLXRleHQtc2l6ZS1hZGp1c3Q6IDEwMCU7XG4gIC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogMTAwJTtcbiAgLW1vei1vc3gtZm9udC1zbW9vdGhpbmc6IGdyYXlzY2FsZTtcbiAgLXdlYmtpdC1mb250LXNtb290aGluZzogYW50aWFsaWFzZWQ7XG4gIGJveC1zaXppbmc6IGJvcmRlci1ib3g7XG59XG5cbiosXG4qOmJlZm9yZSxcbio6YWZ0ZXIge1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBtYXJnaW46IDA7XG59XG5cbi5idXR0b24tLWdyZWVuIHtcbiAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIGJvcmRlcjogMXB4IHNvbGlkICMzYjgwNzA7XG4gIGNvbG9yOiAjM2I4MDcwO1xuICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gIHBhZGRpbmc6IDEwcHggMzBweDtcbn1cblxuLmJ1dHRvbi0tZ3JlZW46aG92ZXIge1xuICBjb2xvcjogI2ZmZjtcbiAgYmFja2dyb3VuZC1jb2xvcjogIzNiODA3MDtcbn1cblxuLmJ1dHRvbi0tZ3JleSB7XG4gIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgYm9yZGVyLXJhZGl1czogNHB4O1xuICBib3JkZXI6IDFweCBzb2xpZCAjMzU0OTVlO1xuICBjb2xvcjogIzM1NDk1ZTtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xuICBwYWRkaW5nOiAxMHB4IDMwcHg7XG4gIG1hcmdpbi1sZWZ0OiAxNXB4O1xufVxuXG4uYnV0dG9uLS1ncmV5OmhvdmVyIHtcbiAgY29sb3I6ICNmZmY7XG4gIGJhY2tncm91bmQtY29sb3I6ICMzNTQ5NWU7XG59XG5cbi5jb250YWluZXIge1xuICBtaW4taGVpZ2h0OiAxMDB2aDtcbiAgZGlzcGxheTogZmxleDtcbiAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbn1cblxuLnRpdGxlIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG4iXSwic291cmNlUm9vdCI6IiJ9 */</style></head>
  <body data-n-head="">
    <div id="__nuxt"><!----><div id="__layout"><div><section class="container"><div><a href="/" class="nuxt-link-active">to top</a> <h1 class="title">title from mounted</h1> <p>mounted_time : "2018-12-14T08:11:36.709Z"</p> <p>async_data_time : "2018-12-14T08:11:36.497Z"</p></div></section></div></div></div><script>window.__NUXT__={layout:"default",data:[{title:"title from asyncData",async_data_time:new Date(1544775096497)}],error:null,serverRendered:true};</script><script src="/_nuxt/runtime.js" defer=""></script><script src="/_nuxt/pages/about/index.js" defer=""></script><script src="/_nuxt/commons.app.js" defer=""></script><script src="/_nuxt/vendors.app.js" defer=""></script><script src="/_nuxt/app.js" defer=""></script>

</body></html>

下記の通り、sectionから始まるテンプレートが正しくマウントされてますね。

<section class="container"><div><a href="/" class="nuxt-link-active">to top</a> <h1 class="title">title from mounted</h1> <p>mounted_time : "2018-12-14T08:11:36.709Z"</p> <p>async_data_time : "2018-12-14T08:11:36.497Z"</p></div></section>

また、 <p>async_data_time : "2018-12-14T08:11:36.497Z"</p> もちゃんと出ました。

こちらも screenshot を撮ってみましょう。

$ chrome --headless --disable-gpu --screenshot http://localhost:3000/about

f:id:omiend:20181214171508p:plain

正しく表示されているようですね。

firebaseなどで確認するには?

$ npx nuxt --spa

と同様に

$ npx nuxt generate --spa

すれば同様に確認できると思います。

まとめ

SEOを気にする必要があり、サーバーとの通信如何でコンテンツが出来上がる画面においては、基本的に asyncData()SSRしたほうが良さそうです。

ざっくりとですが、個人的にはNuxt.jsにおけるSSRの理解が深まって良かったです。

また、試している最中に気づいたのですが、 asyncData() はその処理が終わるまでHTMLレンダリングを止めてくれる(すこし誤解を招きそうな表現)のですが、その分画面の表示は遅れました。重たい処理などはなるべく避けて、素敵なSSRライフを送りたいものです。

asyncData() を使わない静的サイトであれば、SPAモードで十分ですね。

以上です。

ここまでお読みくださいまして、ありがとうございました。