Nuxt.js初心者がAMPのサーバサイドレンダリングに挑戦!

シェアする:

この記事をおすすめしたい人

  • WordPressしか知らないのにNuxt.jsを始めようとしてる人
  • Nuxt.jsでAMPのSSRをやりたい人
  • Nuxt.jsでAMP Optimizerを使いたい人
  • つまりオレ

何も知らない超初心者が脱WordPressしたくてNuxt.jsでサイト構築していくシリーズです。

今回はNuxt.jsで出力したAMP HTMLのサーバサイドレンダリングに挑戦します。

今回はかなり無謀な挑戦だったようです。 途中で何度も絶望しました。

開発日記的な感じで時系列でやっていくと、いつまで経っても結論に到達しないので先に結論からお届けします!

WordPressの上でな!

※ このブログはWordPress製です

結論:AMP Optimizerを使う

AMP Optimizer」というモジュールを使うことはかなり初期に、というかそれを見つけたところがスタートだったんですが、それをどうNuxt.jsに組み込めばいいのかさっぱり分からず血反吐を吐きました。

その話は置いておいて、AMP Optimizerの導入、Nuxt.jsへの組み込み方をお話しします。

AMP Optimizerのインストール

Nuxt.jsのプロジェクトフォルダへ移動(cdコマンド)し、公式サイトの例に習い次のコマンド叩きます。


$ npm install @ampproject/toolbox-optimizer

インストール後、他のモジュールの場合は「nuxt.config.js」のmodulesプロパティに設定を追加しますが、AMP Optimizerの場合は不要です。

「@nuxtjs/amp」モジュールのインストール

AMP Optimizer単体でHTML5からAMPへ変換するとエラーが出ました。 詳細は後で書きます。

なのでまずはNuxt.jsが生成するHTMLをAMP化してくれるモジュールを組み込みます。


$ npm install @nuxtjs/amp

こちらはAMP Optimizerとは違い、「nuxt.config.js」に以下の設定を追加する必要があります。


export default {
  modules: [
    '@nuxtjs/amp',
  ],
}

AMPモジュールの動作については以前に書いた記事があるので、詳しくはそちらを見てください。

Nuxt.jsのhooksプロパティでAMP Optimizerを実行

AMP Optimizerは出力寸前の完成されたHTMLを引数として受け取り、戻り値で最適化されたAMP HTMLを返します。

なので、Nuxt.jsのhooksプロパティを使って出力前のHTMLを捕まえます。

「nuxt.config.js」に次のようなコードを追加してください。


export default {
  hooks: {
    render: {
      route: async (url, result, context) => {
        if ( context.req.isAMP ) {
          const AmpOptimizer = require('@ampproject/toolbox-optimizer');
          const ampOptimizer = AmpOptimizer.create();
          
          result.html = await ampOptimizer.transformHtml(result.html)
        }
      }
    },
  },
}

「result.html」に出力前のHTML全体が入っているので、AMP Optimizerで処理後、result.htmlに戻しています。

シンプルに見えるでしょ?

ここに至るまでの地獄の経緯は次に…。

そんなのどうでもいい人はここでおしまいです。

またの機会にお会いしましょう!

今回の実験の最大の目的はサーバサイドレンダリング

AMPのサーバサイドレンダリングに興味がありました。

サーバサイドレンダリング(以下SSR)というのは、僕が知っている範囲の知識だと、「本来はブラウザ側で処理するJavaScriptを、サーバ側で先に処理してしまい完成したHTMLを渡す」という機能です。

AMPは自前のJavaScriptが使えませんが、AMPのランタイムが同様の処理をしていて、ブラウザ側でAMPのコードを書き換えています。

例えばこんな↓の。


//元のHTML
<html lang="ja" amp>

//書き換え後のHTML
<html lang="ja" amp i-amphtml-layout i-amphtml-no-boilerplate transformed="self;v=1">

ランタイムをダウンロードし書き換えが終わるまでは、ボイラープレートというコードが画面を非表示にします。

つまり、表示が遅い。

これを改善するのがSSRです。 爆速が売りのAMPキャッシュにあるAMPもSSR済みのものだそうです。

どうやったらAMPをSSRできるのか

GoogleのブログでAMPのSSRを知りました。

ここによるとAMP OptimizerAMP Packagerを使うとSSR出来ますよと。

この時はNode.jsが何かも分かっていなかった(Goについては名前も知らなかった)ので、その下に書いてあった「Next.js 9ならSSRできますよ!」に惹かれ、Next.jsに興味を持ったのがこのサイトを作るきっかけだったりします。
(Nuxt.jsじゃなくてNext.jsの方です)

その辺の流れはNuxt.jsを選んだ理由を見てもらうとして、Nuxt.jsに乗り換えてからもAMP SSRに未練バリバリ。

そろそろNuxt.jsの操作にもほんのちょびっとだけ慣れてきたので「AMPとSSRに手を出してみるか!」と、今回の実験がスタートしました。

今はWordPressでもAMP SSRできるらしい

調べている過程で知ったのですが、WordPressのAMPプラグインでもSSRが出来るようになっているらしく、そっちの調査も開始しています。

プラグインを有効にするだけで勝手にAMP化とSSRをやってくれるとんでもない進化をとげていました。

ただ、個人的にはけっこう不満な点も見つかっていて、それはおいおい。

Nuxt.jsでのAMP化まで

まずはSSRを考えず、AMP化だけしてみました。

これには「@nuxtjs/amp」というモジュールを使っていてめっちゃ簡単です。


$ npm install @nuxtjs/amp

export default {
  modules: [
    '@nuxtjs/amp',
  ],
}

インストールしてちょこっとオプション追加するだけでパーフェクトなAMPに変換してくれます。

SSRする上で「@nuxtjs/amp」はおそらく必須

後から分かることですが、AMP Optimizer単体では正しいAMPへ変換することができませんでした。

AMP Validatorが吐いたエラーは次の3種類。

  • <style>が<style amp-custom>に変換されない
  • <style>がひとつの<style amp-custom>に統合されない
  • <script>が削除されず、<amp-script> にも変換されない

「@nuxtjs/amp」を使うとこの辺の処理をオートでやってくれます。

自分でやってもいいですが、「@nuxtjs/amp」は余計なことをしない優秀なモジュールっぽいのでこいつに頼った方が早いと思います。

AMP Optimizerを導入するまで

最初から詰まりました。

検索しても全然情報がないんです。

「"Nuxt.js" "AMP Optimizer"」でGoogle検索してみてください。
(ダブルクオーテーション付けないとNext.jsの方が引っかかります)

まともな情報が返ってきません。

もしかして前人未踏!?

そんなことはないと思いますが、予備知識もほぼ皆無な初心者が、先人の経験談なしで手探りでやっていくことになってしまいました…。

とりあえずAMP Optimizerをインストール

AMP Optimizerのreadmeに「ある程度知識がある人向け」のマニュアルが置いてあるので、知識のない僕は分かる範囲で追っかけていきます。

まずはインストール↓。


$ npm install @ampproject/toolbox-optimizer

難なく成功。

他のモジュールに習って「nuxt.config.js」に設定を追加↓。


export default {
  modules: [
    '@ampproject/toolbox-optimizer',
  ],
}

結果、エラー↓。


TypeError: Module should export a function: @ampproject/toolbox-optimizer

うーむ、よく分かりませんがNuxt.js用のモジュールは関数をエクスポートする形で実装されるべきところを、AMP Optimizerはその形になっていないんでしょう。

つまり、Nuxt.jsではAMP Optimizerが使えないか、別の使い方をする。

マニュアルに習って実行してみた

実行した場所については後で出てくるので、ここではコードと実行結果だけ紹介します。

AMP Optimizerのreadmeに書いてあるサンプルほぼそのままで実行しました。

入力したHTMLは最小構成のHTML↓。


<!doctype html>
<html>
<head>
</head>
<body>
</body>
</html>

実行したコード↓。


const AmpOptimizer = require('@ampproject/toolbox-optimizer')
const ampOptimizer = AmpOptimizer.create()

const originalHtml = '<!doctype html><html><head></head><body></body></html>'
const optimizedHtml = await ampOptimizer.transformHtml(originalHtml)

console.log(optimizedHtml)

出力結果↓。
(見やすいように改行と簡略化を行っています)


<!doctype html>
<html amp i-amphtml-layout i-amphtml-no-boilerplate transformed="self;v=1">
<head>
<meta data-auto charset="utf-8">
<style amp-runtime i-amphtml-version="012006180239002">
//ランタイムのスタイル
</style>
<meta data-auto name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<link rel="preload" href="https://cdn.ampproject.org/v0.js" as="script">
<script data-auto async src="https://cdn.ampproject.org/v0.js"></script>
<link data-auto rel="canonical" href=".">
</head>
<body>
</body>
</html>

オオォ!?

シンプルなHTMLでしたが、AMPに必要なコードを自動で追加して正しいAMPに変換してくれました。

非SSRなAMPでは必須の<style amp-boilerplate>はなく、SSRのコード(赤字の部分)が追加されているのでSSRにもなっています。

canonicalが変なのは僕が設定していないだけです。 ログでも指摘されていました。

さて、これでAMP Optimizer単体の動作確認が出来たので、後は出力前のHTMLをAMP Optimizerに食わせてやるだけです。

Nuxt.jsが出力する前のHTMLを捕まえる

詰まりました。

「詰まりました」ではお伝えできないくらい詰まりました。

しょうがないのでNuxt.jsの公式マニュアルを読みあさり、最初に見つけたのがrenderプロパティの「bundleRenderer」。

「bundleRenderer」で出力前のHTMLを捕まえる

どうもこれはベースとなっているVue.jsの機能のようで、「Vueのマニュアル読んでね」と書いてあったので今度はそっちを熟読。

結果、分かったこと。


export default {
  render: {
    bundleRenderer: {
      template: (result, context) => {
        //処理する部分
        return optimizedHtml
      }
    }
  },
}

上記のようなコードを書くと引数の「result」の中に出力前のHTMLが入っているようです。 なのでresultを処理したoptimizedHtmlを返せばページ内容が書き換わります。

さっそくresultの中身を確認↓。


<div data-server-rendered="true" id="__nuxt">
<!----><!---->
<div id="__layout">
<div>
<div class="container">
<div>
(略)
</div>
</div>
</div>
</div>
</div>
</div>

っと。

HTML全体ではなく<body>の中身ですね…。

これはNuxt.jsのアプリテンプレートという機能らしく、こういう↓テンプレート。


<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

先ほどの「result」の中身はテンプレートの「{{ APP }}」の部分だけで、この後Nuxt.jsがテンプレートに従って<head>やらを追加するようです。

Vue.jsでは「result」に<html>から</html>まで全部入ってるみたいなんですけどね…。

Nuxt.jsめ、余計なことしやがって!
残されたアプローチとしては、テンプレートの中身を「{{ APP }}」だけにしてしまって、bundleRenderer内で<head>を自分で構築する方法ならいけそうな気がします。

が、HTML_ATTRSやらHEAD_ATTRSをbundleRenderer内で取得する方法が分からず断念しました…。

「hooks」プロパティで出力前のHTMLを捕まえる

次に見つけたのが「hooks」プロパティ

フックというからにはいろいろなタイミングでNuxt.jsの処理に割り込めるんだろうと、いろいろなクラスのフックを実際にやってみて引数の中身を確認しました。

結果、Rendererクラスの「render:route」なら出力前にHTML全体を捕まえられることが分かりました。

書き方はこう↓。


export default {
  hooks: {
    render: {
      route: (url, result, context) => {
        //処理する部分
        result.html = optimizedHtml
      }
    },
  },
}

引数の「result.html」に<html>から</html>までのHTML全体が入っています。

bundleRendererと少し違うのは、戻り値で返すのではなく「result.html」に最適化済みのHTMLを戻してやる点。

後は「result.html」をAMP Optimizerに処理させて「result.html」に戻すだけです。

さっきのコードをそのまま持ってきてこう↓。


export default {
  hooks: {
    render: {
      route: (url, result, context) => {
        const AmpOptimizer = require('@ampproject/toolbox-optimizer');
        const ampOptimizer = AmpOptimizer.create();
        
        result.html = ampOptimizer.transformHtml(result.html)
      }
    },
  },
}

出力結果↓。


AssertionError: Assertion failed: Input document is not a string

えっ?

文字列ではありませんと。

result.htmlに代入するのはいったん諦めて、中身をconsole.logしてみると


Promise {
  <pending>
}

Promise…?pending…?

どうも処理が終わっていない状態で値が返っているみたいで、ブラウザがドキュメントを表示できませんよってことみたいです。

さて、ここから無知な僕とJavaScriptの非同期処理の戦いが始まるわけです。
(もちろん僕は非同期処理なんて言葉しか知りません)

細かいことはいずれJavaScriptの勉強のターンでやると思うので、今回はざっくりと。

自分でいろいろ調べましたが先へ進む気配が全く見られず、いつものアニキは忙しそうだったので、今回はアネキ(仮)にヘルプを出しました。 アネキはJavaScriptは得意だがNuxt.jsは触ったことがない、というスペック。

アネキの指導の下、試したことは以下の通り。

  • hooks内でawaitを使い、非同期処理が終わるのを待つ
  • hooks内でthenを使い、非同期処理が終わるのを待つ
  • 非同期処理部分を外の関数へ書き出し、async/awaitで同期処理
  • 上記3つの亜種多数

各段階での処理状況をconsole.logで確認しながら数時間付き合ってもらいましたが全てうまくいかず…。

「こんな↓感じで書けたりしないですかね…」「エラーでました…」「はい…」とアネキは去っていきました…。


route: async function (url, result, context) => {

専門外だったのにごめんなさい、そしてありがとう、アネキ。

ここまでの実験から残された道

分からないなりにここまでの実験から、残されていそうなアプローチを考えてみました。

bundleRendererで粘るパターン

アプリテンプレートを「{{ APP }}」だけにして、bundleRenderer内で自分で<head>他を構築する方法。

かなり頑張ればできるのかもしれない。

でもオレには出来ないのかもしれない。

hooksで粘るパターン

かなり惜しいところまでいっているので、多少強引にでも「transformHtml()」の処理待ちすることができればいけそう。

でもアネキに無理だったのに道は残されているのだろうか。

後は全く別の道ですかね…。

hooksに「async」付けるだけでよかった!?

次の日!
いつものアニキから別件で連絡が来たのでついでに聞いてみました!

すると、Nuxt.jsのマニュアルから該当部分を見つけてきてくれたのです!

そこに挙がっていたサンプルがコレ↓。


this.nuxt.hook('build:compile', async ({ name, compiler }) => {
  // コンパイラ (デフォルト: webpack) が始まる前に呼ばれます
})

書いている場所の都合、書き方がちょっと違いますが「build:compile」というフックに「async」が付いています!

アネキの最後のコレ!


route: async function (url, result, context) => {

functionを削るだけで動いたんです!!

何故試しに削ってみたなかったんだオレは…。

というわけで、この知識を加味してコードを書き直すとこう↓。


export default {
  hooks: {
    render: {
      route: async (url, result, context) => {
        const AmpOptimizer = require('@ampproject/toolbox-optimizer');
        const ampOptimizer = AmpOptimizer.create();
        
        result.html = await ampOptimizer.transformHtml(result.html)
      }
    },
  },
}

「async」が付けられることが分かったので、transformHtml()に「await」を付けることで無事に期待通りの動作になりました。

長かった…

AMPのみで実行するコードを追加

上のままだと非AMPページでも強引にAMP化されてしまうので、「AMPなら」という条件分岐をつけます。

「AMPなら」というのは以前の記事でこう↓書けばいいということが分かっています。


if ( this.$isAMP ) {

ですがhooks(あるいはrender:route)では「this」が使えないのかAMP/非AMPともにfalseが返ってしまいます。

今の僕はその程度では凹まされません…。

こんなもんはね、どうせrender:routeの引数「context」の中に入ってるんです。

contextの中身をconsole.logして確認すると「context.req.isAMP」に、「this.$isAMP」と同じ値が入っていることが確認できたので、書き直してこう↓。


export default {
  hooks: {
    render: {
      route: async (url, result, context) => {
        if ( context.req.isAMP ) {
          const AmpOptimizer = require('@ampproject/toolbox-optimizer');
          const ampOptimizer = AmpOptimizer.create();
          
          result.html = await ampOptimizer.transformHtml(result.html)
        }
      }
    },
  },
}

これでAMPでのみAMP Optimizerに処理させることができました。

まとめ

長かった!つらかった!

Nuxt.jsでAMP Optimizerを使うには次の3ステップだ!

  1. AMP Optimizerのインストール!
  2. 「@nuxtjs/amp」のインストール!
  3. 「nuxt.config.js」のhooksプロパティにコード追加!

export default {
  hooks: {
    render: {
      route: async (url, result, context) => {
        if ( context.req.isAMP ) {
          const AmpOptimizer = require('@ampproject/toolbox-optimizer');
          const ampOptimizer = AmpOptimizer.create();
          
          result.html = await ampOptimizer.transformHtml(result.html)
        }
      }
    },
  },
}

以上、WordPressからお届けしました!

おまけ

Nuxt.jsでのAMP Optimizerの解説記事が海外含めほんと見つからないんですけど、これは逆に需要もないってことだと思うんですよね。

でもそんな中でひとりくらい外国人が検索したりして、この記事読んだりして、その時はChromeで翻訳すると思うんですけど、僕の文章は一体どんな英語になるんだろうw
機械的には訳しにくい文章のはず…。

そもそも内容について呆れるだろうしw

He has the guts, while he's still a beginner!

※ 知りあいの知りあいであるボブに聞いたので、たぶん自虐味が薄味

Nuxt.js AMP編の目次

シェアする: