the industrial

ブログと言うより自分のためのメモ以外の何モノでもないです。でも読んでくださってありがとうございます。

ドリーム

ドリーム (字幕版)

1961年のアメリカ南東部バージニア州ハンプトン。アメリカ南部において、依然として白人と有色人種の分離政策が行われていた時代。優秀な黒人女性のキャサリンは、同僚のドロシーとメアリーと共にNASAのラングレー研究所で計算手として働いていた。 ソ連人工衛星スプートニクの打ち上げ成功を受けて、アメリカ国内では有人宇宙船計画へのプレッシャーが強まっていた。そんな中、キャサリンは上司のミッチェルからスペース・タスク・グループ(英語版)(STG)での作業を命じられた。図らずも、キャサリンはグループ初の黒人でしかも女性スタッフとなったのだが、人種差別的な環境に苦しめられることとなった。

ドリーム (2016年の映画) - Wikipedia

素直に面白かった。

時代背景もあり、衣装もオシャレ。オシャレだけじゃなくて、人種差別ってこんなにひどかったんだと。

実話を元にしてはいるものの、結構脚色されているらしい。しかし、主人公の「キャサリン・ゴーブル・ジョンソン」なんかは「よく出来た映画でした」とおっしゃっているほど。

#Nuxt.jsのComponentをどの単位で分けるか悩んだ話

この記事は、空いていたので Nuxt.js #2 Advent Calendar 2018 の21日目の記事としました。

20日目は同僚である@ryamakuchiNuxt.js と Firebase(Firestore)を使って認証と DB 保存を実装するでした。

TL;DR

Nuxt.jsのComponentをどの単位で分けるか悩んだ話

  • Componentはページ自体を構成するComponentと、部品としていくつかのページで再利用できるComponetの2種類が存在すると定義。
  • ページ自体を構成するComponentはSectionsComponent、部品としていくつかのページで再利用できるはPartsComponetとして呼ぶように。
  • それぞれcomponents/sections/ディレクトリとcomponents/parts/ディレクトリに配置。

また、Vue.jsのオフィシャルスタイルガイドを受け

  • BaseとなるComponet(BaseButton.vueなど)は、PartsComponentに分類することに。
  • よくあるTheHeader.vueなどは、SectionsCopmonetに分類。
  • SectionsComponetの中だけで利用する子Componentを定義する場合(密結合となるComponent)は、親のComponentの名前をPrefixとして受け継いだSectionsComponentとしてcomponents/sections/に配置。

以上は実験的で今後変更があるかもしれませんが、今の所一番しっくり来ています。

NuxtのComponentどうしていますか?

弊社のNuxt.jsプロダクト開発において、ページを構成するComponentと、部品として再利用するComponentが混在しており、規模が大きくなるに連れてカオス化しそうでした。

そのため、同僚である@ryamakuchiといろいろ相談して、下記の様に決めました。

例えば

(細かい要素は抜きにして)こういった構造のページがあるとします。

f:id:omiend:20181227193940p:plain

このページの中では、ページ自体を構成するSection(あるいはHeaderやFooter)要素があり、それぞれをComponentに切り出すことがあるかもしれません。

f:id:omiend:20181227194154p:plain

同様に、他のページでも再利用できる要素(あえてdiv要素で定義してみました)についても、Componentに切り出したいでしょう。

f:id:omiend:20181227194351p:plain

すると、components/ディレクトリはこの様になるでしょう。

components/
├── ADiv.vue
├── ASection.vue
├── BDiv.vue
├── BSection.vue
├── CDiv.vue
├── CSection.vue
├── TheFooter.vue
└── TheHeader.vue

まだComponentは少ないので良いかもしれませんが、Componentが増えるにつれて確実に見通しが悪くなりそうだと思いました。

そこで、components/配下にディレクトリを切り出し、それぞれのComponentの責務(ページ自体を構成するComponentなのか、部品としていくつかのページで再利用できるComponetなのか)を明確にすることで、少なくとも見通しは良くなるのではないかと考えた次第です。

components/
├── parts
│   ├── ADiv.vue
│   ├── BDiv.vue
│   └── CDiv.vue
└── sections
    ├── ASection.vue
    ├── BSection.vue
    ├── CSection.vue
    ├── TheFooter.vue
    └── TheHeader.vue

また、当初は下記の様にcomponent/parts/だけ切り出そうかとも考えたのですが、ディレクトリの階層を揃えるという意味でcomponents/sections/を切り出しました。

components/
├── ASection.vue
├── BSection.vue
├── CSection.vue
├── TheFooter.vue
├── TheHeader.vue
└── parts
    ├── ADiv.vue
    ├── BDiv.vue
    └── CDiv.vue

ディレクトリを切るかどうか

ディレクトリを切らずに、直列で見れるほうが良いという方もいるそうです。

もしディレクトリが増えまくるとimport時が辛くなりそうな側面もあるかもしれません。

もちろん好みの問題もあるので、その際はファイル名SectionsComponent/PartsComponentを切り分けて工夫する感じですかね。

おわり

こういった地味だけど設計思想にも直結する話は、かなり大事なことだと認識しています。

話が逸れそうですが、そういった話をメンバーとできて、統一見解として内部で共有できることは、とても幸せだと感じました。

しばらく運用してみて、何か不都合があったらまたメンバーと一緒に考えて、ブログにするつもりです。

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

以上です。

MacOSで、ディレクトリ内部の.heicファイルを一括で.jpgファイルに変換する方法

このエントリーは 今すぐalias登録すべきワンライナー by ゆめみ① Advent Calendar 2018 の16日目のエントリーです。

僕には愛する愛する4歳の娘が居まして、たった4年間ではありますが、iPhoneで何気なく撮影した彼女の成長を捉えた写真が数えきれないほどあります。

そして、それらの写真はこれからもどんどん増えますw。

それほど愛しい愛娘の写真でも、流石にローカルへずっと保存しておけるほどの量では無くなってきたので、いつでもどこでも見られて、間違って消してしまうことも無いGoogle Photoにあずけることにしました。

しばらくは問題なかったのですが、しかし、事件はおきました。

iPhoneやブラウザでカワイイ可愛い娘の写真を見る分には全く問題が無かったのですが、Google Photoから写真をダウンロードしてみると(今はまだ)扱いづらい画像ファイル、そう、 .HEIC ファイルとなっていることに気づきました。

.HEIC ファイル自体はまあ良いのですが、まだ未対応のアプリも多く非常に不便だったりします。

HEICファイルとは

High Efficiency Image File Format (ハイ・エフィシエンシー・イメージ・ファイル・フォーマット、HEIF、ヒーフ) は、Moving Picture Experts Group (MPEG) によって開発され、 MPEG-H Part 12 (ISO/IEC 23008-12) で定義された画像ファイルフォーマットである。1枚の画像やイメージシーケンスに使用される。HEIF方式による画像ファイルにはheifやheicといった拡張子が使われる。

2017年6月、アップルはmacOS High SierraiOS 11でHEIFをサポートすると発表し、その秋に出荷した。HEVCのサポートにより、静止画/動画の圧縮率は最大2倍にまで高められる[4]。

Wikipedia - High Efficiency Image File Format

結論

そこで、 .HEIC ファイルをJPEGファイルに変換する処理をワンライナーで作ってみました。

時代に逆行する内容で本当に申し訳ないと思っていますが、世のお父様方の代表として、ここに公開したいと思います。

$ find . -name '*.heic' | xargs -IT basename T .heic | xargs -IT sips --setProperty format jpeg ./T.heic --out ./T.jpg;
~/Downloads/IMG_8738.heic
  ~/Downloads/IMG_8738.jpg

簡単に説明しますと、

  • findでダウンロードした .HEIC ファイルを一覧する
  • xargs で一つづつ受けとり、basenameを利用して *.heic の拡張子を一度除外
  • さらに xargs で受けとり、sipsコマンドで変換

ただし・・・

xargs -IT basename T .heic

を挟むのがやや納得いきません。

何故やっているのかというと、もともとはこんなふうに書けばいいかなと思ったのに

$ find . -name '*.heic' | xargs -IT sips --setProperty format jpeg T --out ${T%.*}.jpg;
~/Downloads/IMG_8738.heic
  ~/Downloads/.jpg

xargs内では変数展開(下記の ${T%.*}.jpg )が出来ない?っぽく、ファイル名が空になってしまったからです。

例えば、これだったら変数展開でできるのですが・・・

$ for target in ./*.heic; do
>   sips --setProperty format jpeg $target --out ./${target%.*}.jpg;
> done
~/Downloads/IMG_8738.heic
  ~/Downloads/IMG_8738.jpg

でも、やっぱりワンライナーで書きたいですよね。

以上です。

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

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モードで十分ですね。

以上です。

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

ブラックパンサー

「真っ黒が最高」でお馴染みのブラパンことブラックパンサー

というのは嘘で、MARVELヒーローズオーケストラきっての名作でした。

どことなくバーフバリに似たようなものを感じた。

祖国を守ることと、正義を守るはずが、その正義を守るために過ちを犯してしまう国王と、その真実に気づいた新たなる王の戦い。

すっごく面白かった。

この映画の新の主人公は、ブラパンことティ・チャラではなく、ライバル:キルモンガーだと思った。

自社サービスのRails4を、Rails5にアップグレードした話

会社のRails4.2.0で動いているプロダクトは、Rails5.2.0へとアップグレードしたので、メモがてら大変だったことなどを書いておこうと思います。

何故やろうと思ったのか

Railsをアップグレードすることで便利な機能が追加されたと、得られる嬉しい事がいろいろあると思います。

ですが、弊社はエンジニアの人数が限られた組織ですので、下記の理由からなかなかRails5へのアップグレードに踏み切れませんでした。

  • Rails5にすることで、影響範囲がわからない
  • Railsに詳しいわけではないので、そもそもRails4からRails5へのやり方を調べる必要がある
  • エンジニアの人数が少ないので、優先度としては
  • 調査時間や実際に対応する時間を捻出するのが難しい

しかし、フレームワークのバージョンアップはこの先もずっとついて回る事ですし、ここでやらないと一生やらないと思ったからです。

また、実はこれから春に向けて当該サイトのリニューアルを予定してます。

リニューアルは主にフロントエンド周りとなるのですが、そこで個人としても今一番注目している「Nuxt.js」を利用したいと考えています。

「Nuxt.js」は今をときめく「Vue.js」のフレームワークでして、「Nuxt.js」を使い今よりもよりモダンなフロントエンド開発にすることで、開発効率の向上となによりUI/UXをより向上させることができるのではないかと考えております。

そういった予定もあり、Rails5化はその前にやってしまいたかったというお話でした。

何が一番大変だったのか

まず、何よりも一番問題になったのが、サイトを開発したエンジニアがすでに居ない事でした。

さらに開発ドキュメントも存在せず、「ある機能が実装された経緯」一つとってもワカラナイ状態です。

Rails5にバージョンアップ後、各機能が正常に動作するかを確認していくのですが、エラーとなる箇所全てにおいてロジックを書き換えることになります。

そうすると、”知っている人”に聞くことができないまま、エラーになるたびに「この機能の意図する所はこうだ」とロジックを全て追って理解するという作業が必要となり、これがとにかく大変でした。

実際にやった作業

余談ですが、Rails5にアップグレードするにあたって、基本的に下記のページを参考にしました。

Upgrading Ruby on Rails

Gemの依存関係

さて、実際にRails4からRails5にアップグレードする上で、実際やった作業です。

まず、Gemfileのrailsをアップデートします。

- gem 'rails', '4.2.0'
+ gem 'rails', '~> 5.2.0'

すると、 bundle install する際にGemの依存関係でエラーとなりので、ひとつづつ依存を解決していく必要があります。

この依存関係をひとつづつ解決する作業がかなり大変で、しかも私自身がRuby on Railsを始めて1年ほど人間だったので、恐らく無理だろうと判断しましました。

そして、少しディストピア感はあるのですが、代わりに下記の様に対応しました。

  • 開発向けのGemは一旦すべて削除
  • 現在Gemfileに設定されているGemを一つづつ「何をする・しているGemなのか」調べる
  • 本当に必要なGemだけ残し、それ以外は一旦削除する
  • 各々のGemのアップデートで起きた依存解決エラーを解決

これで、なんとかRailsを立ち上げるところまでできました。

各々のGemのアップデートで起きたエラーを修正

・何故か mysql2 のinstallが通らなくなった

原因は結局ワカラズでしたが、とりあえず xcode-select --installXCodeをインストールしたら mysql2 をインストールすることができました。(必要なパッケージが無かったっぽい?)

gem 'therubyracer'gem 'mini_racer' に変更

詳しくは下記。

https://github.com/rails/rails/pull/29285

xxx_filterxxx_action に修正

  • before_filterbefore_action に変更
  • around_filteraround_action に変更

コレですね

DEPRECATION WARNING: before_filter is deprecated and will be removed in Rails 5.1. Use before_action instead.

・ scope でチェインするような書き方を修正

where{ (category_id == target_category_id) }

のように書かれていた箇所を

where({category_id: target_category_id})

の様に修正しました。

これは単純にエラーを解消させた感じです。

・ 画像表示周りで、 .url が無く、エラーになる

こちらも単純にエラーを解消させた感じです。

また、ついでなのですが、下記の様にリンク付き画像を定義されている、

= link_to(image_tag(record.image, alt: record.alt.to_s), record.link, target: "_blank")

といった書き方をされていた箇所を

= link_to record.link, target: "_blank" do
  = image_tag record.image.url, alt: record.alt.to_s

といった様に修正しました。

リンクはリンク、画像は画像で、見通しがよくなった様に思います。

SQLエイリアス

例えば、

employees belongs_to companies というAssociationがあったとして

class Emoloyee < ActiveRecord::Base
  belongs_to :company
end

とすると思いますが、Rails5から belongs_to は必須項目になったので

class Emoloyee < ActiveRecord::Base
  belongs_to :company, optional: true
end

に修正しました。

FactoryGirl から FactoryBot への修正

恐らくこの作業が一番大切だと考えています。

まず、アプリケーションのテストが全く書かれていませんでした。

なので、とりあえずRSpecで重要ロジックのテストを書いておこうと思ったのですが、その上で、FactoryGirlが FactoryBot に名前が変更されたので、ついでにFactoryBotにしました。

まとめ

作業内容をまとめると、

  • 「Gemの依存関係解決」難易度:★★★★
  • 「ちょっとしたロジックの修正」難易度:★★
  • 「(とりあえず)足りない重要ロジックのテストを書く」難易度:★★

といった感じでした。

とりあえずこのくらいの作業でRails5へのアップグレードは完了し、稼働させることができました。

やってしまえばなんてことはありませんが、手探り状態で1週間ほど対応に時間がかかりました。

その間新規機能の開発などが止まることを考えると、経営目線では「Rails5へのアップグレードは本当に必要な作業なの?」と問われることも多々あるかもしれません。

長い目で見ると、いつか爆発するかもしれない爆弾とも言えそうな「Gemの依存解決」、フレームワークの新機能を使ったスピード感ある「開発効率の向上」などなど、必ず良い事だらけだと思います。