[FULL STATIC] Nuxt generate<4つのアプローチまとめ>

最近ContentfulとNetlify、Nuxt.jsを利用してブログを立ち上げました。

ヘッドレスCMSとNuxt generateで静的なサイトを！と思ったのですが、実際にはクライアントからのアクセスのたびにContentfulへの通信が発生していました。
![Screenshot from.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89564/115eb022-29ed-d0ec-f192-3e619169d666.png)

理由は明らかです。
asyncDataでContentfulからのデータ取得処理を行っているからです。サーバー側だけでなく、クライアント側でも再度取得処理が実行されてしまっているのですね...。

Contentfulの記事更新時にNetlify上で generate⇒デプロイ まで自動で行うように設定しているので、記事は常に最新です。クライアントが新着問い合わせを行う必要はありません。「不要なアクセスがあるのは気持ち悪いな〜」と思って調べてみました。

# この記事のまとめ
はじめに結論をまとめておきます。
## 通信を減らす・無くす手法は大きく4つに分けられます
### A.nuxtServerInitで全データ取得＆JSONに保存。
非常に直感的な手法です。完全に0にはできませんが、そこそこ減らせるはずです。個人ブログなどの場合はこれでも良いかもしれません。tagやキーワード検索などの機能が豊富なCMSも多いのですが、vueのfilterなどでリアルタイム検索すれば十分そうです。ただし、不要な通信は依然として残ります。

### B.window.\_\_NUXT__からJSONに保存。
nuxt generateで吐き出されたhtmlには window.\_\_NUXT__ からはじまる箇所にJavaScriptが記載されています。また、サーバーサイドで取得したデータも含まれています。そのデータをJSONにキャッシュして利用しようという考え方です。
こちらのアプローチを取っているのは[nuxt-payload-extractor](https://github.com/DreaMinder/nuxt-payload-extractor)です。下記で軽く触れますが、詳しくは実装を参考ください。

asyncDataで取得している場合はシンプルに導入できます。ただし、fetchやstore（ひとまとめにvuex）では不具合が起こる可能性があるとreadmeに記載されています。実は、full staticなgenerateはネイティブでの実装も検討されています。その1つの方策としても魅力的なようですが、課題もあるようです。

### C.APIリクエスト時にJSONに保存。
こちらも直感的ですね。個人でのアプローチ例も多い手法です。
[nuxt-static](https://github.com/stursby/nuxt-static)ではプラグインとしてaxiosリクエスト時にキャッシュしています。
また、下記で触れますがQiitaでも2例紹介されています。
トラブル時にも原因を突き止めやすそうで安心です:smiley:

### D.htmlからJavaScriptを削除。
正反対かつド直球なアプローチ。htmlからscriptを削除してしまいます。セキュリティの観点からがきっかけですが、以下のブログ記事でcheerioを使用した方法が紹介されています。
[Nuxt.js Generate後の＜script＞window.\_\_NUXT__=を消したい](https://ondwn.com/blog-20190718/)


## さらにパフォーマンスを求めるなら
### JSONデータをキャッシュする場合(A,B,C)
⇒下記Qiita記事で紹介されているように、webworkerでクライアント側にもキャッシュさせる。
[Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る](https://qiita.com/miyaoka/items/9774f0250953da419a58#api%E3%82%92%E9%9D%99%E7%9A%84%E5%8C%96%E3%81%99%E3%82%8B)

### htmlからJavaScriptを削除する場合
⇒AMPの導入。

以降ではGithub Issue、Qiita、ブログ記事の3つのソースから上記の4つの方法をもう少し詳細に紹介します。

# 目次

- 1.Github Issueから(generate時にJSONに保存)
- 2.Qiitaから(generate時にJSONに保存)
- 3.ブログ記事から(htmlからJavaScriptを削除)

## 1.JSONに保存（Github Issueから）
[Full static generated mode #22](https://github.com/nuxt/rfcs/issues/22)で静的なgenerateについて議論されています。

別の形で解決しましたが、以前にもnuxt generateには不要なJavaScriptがあるというIssueは上がっていました。現在は上記のIssueが中心のようです。（以前のIssue:[Lots of unnecessary JavaScript in generated Nuxt static build](https://github.com/nuxt/nuxt.js/issues/5260)）

Full static generated mode #22では

```
nuxt generate --full-static
```
という新しいオプションも提案されています。
ただし、queryの処理など課題も多いようです。

### Issueでは個人での対処例が2つ紹介されています。

- [stursby/nuxt-static](https://github.com/stursby/nuxt-static/)
- [nuxt-payload-extractor](https://github.com/DreaMinder/nuxt-payload-extractor)

どちらもJSONファイルにデータを保存し、

```
if(process.static && process.client)
```
で条件分岐し、クライアントサイドでの通信であれば、JSONファイルからのフェッチに変更しています。

仕組みは非常にシンプルなので、それぞれ簡単に紹介します。
#### [stursby/nuxt-static](https://github.com/stursby/nuxt-static/)
axiosでの通信時に条件分岐。JSONとして保存しておき、ベースURLを変更。
[src/plugins/axios.js](https://github.com/stursby/nuxt-static/blob/master/src/plugins/axios.js)のファイルがメインの処理部分です。以下Githubからの引用にコメントを追記したものです。

```javascript:src/plugins/axios.js
import axios from 'axios'

let baseURL = 'https://jsonplaceholder.typicode.com'

if (process.browser && process.static) {
  baseURL = '/data'  //クライアントサイドではaxiosのbaseurlを変更していますね！
}

const api = axios.create({ baseURL })

//クライアントでの実行時
if (process.browser && process.static) {
  //interceptorsでリクエスト前にリクエスト先のurlを書き換えています
  api.interceptors.request.use(config => {
    config.url = config.url + '.json'
    return config
  })
}

//サーバーサイドでの実行時
if (process.server && process.static) {
  const mkdirp = require('mkdirp-promise')
  const { join, dirname } = require('path')
  const { writeFileSync } = require('fs')

  api.interceptors.response.use(
    async function(response) {
      // Do something with response data
      //envファイルで予め格納先を指定しておいて、リクエストのパス名で取得データを保存
      const path = join(process.env.dataDir, response.request.path + '.json')
      console.log('Save', path)
      await mkdirp(dirname(path))
      writeFileSync(path, JSON.stringify(response.data))
      return response
    },
    function(error) {
      // Do something with response error
      return Promise.reject(error)
    }
  )
}

export { api }
```


#### [nuxt-payload-extractor](https://github.com/DreaMinder/nuxt-payload-extractor)
こちらはgenerate時にデータをタイムスタンプ付きでJSONファイルに保存しています。
以下moduleから抜粋

```nuxt-payload-extractor/lib/module.js 
//略
//生成されたhtmlから取得したデータが格納されている箇所を抜き出して保存
let extractPayload = function(html, route, base, timestamp){
  let chunks = html.split('<script>window.__NUXT__=')
  let pre = chunks[0]
  let payload = chunks[1].split('</script>').shift()
  let post = chunks[1].split('</script>').slice(1).join('</script>')
  let path = route === '/' ? '' : route

  return {
    html: pre + '<script defer src="' + base + path + '/payload' + timestamp + '.js"></script>' + post,
    payload
  }
}
//略
//nuxt.hook
this.nuxt.hook('generate:page', async page => {
    if(!this.nuxt.options.generate.subFolders) throw new Error('generate.subFolders should be true for nuxt-payload-extractor')
    if(blacklist && blacklist.includes(page.route)) return page
    //上記処理extractPayloadをgenerate時に呼び出しています
    let { html, payload } = extractPayload(page.html, page.route, base, timestamp)
    writePayload(payload, page.route, distDir, timestamp)

    page.html = html

    return page
  })
//略
```

その後、asyncData内で処理を分岐しています。(※$payloadURLはmodule読み込み時に指定するblacklistです)

以下nuxt-payload-extractorの[サンプル](https://github.com/DreaMinder/nuxt-payload-extractor/blob/master/example/pages/extracted.vue)からです。

```nuxt-payload-extractor/example/pages/extracted.vue
//略
<script>
export default {
  async asyncData({ $axios, $payloadURL, route, error }){
    try {
      if(process.static && process.client && route.path !== '/'){
        //route.path !== '/' - because this route is blacklisted for nuxt-payload-extractor
        let {data} = await $axios.get($payloadURL(route))
        return data
      }
      let post = await $axios.$get(`/post.json`)
      return {
        post
      }
      //Or alternative way
      // let payload = {};
      // if(process.static && process.client && route.path !== '/')
      //   payload = await app.$axios.$get(document.location.origin + route.path + '/payload.json')
      // else
      //   payload.post = await app.$axios.$get(`/post.json`)
      //
      // return payload
    } catch (e) {
//略
```

どちらも導入も簡単ですが、nuxt-payload-extractorを利用される際はreadmeの以下の注意点もご確認ください。

> Caveats
> There may be issues with vuex data requests and nested routes. Keep in mind that payload.json has not hash in its name, so it shouldn't be cached in browser.

翻訳
> 注意事項
> vuexデータリクエストとネストされたルートに問題がある可能性があります。 payload.jsonの名前にはハッシュがないため、ブラウザーにキャッシュしないでください。

workerでブラウザキャッシュしたい場合は次項のQiitaが参考になるかと思います。

## 2.JSONに保存（Qiitaから）
上記2つの例と同様、JSONファイルへの保存というアプローチを紹介しているQiita記事があります。

- [Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る](https://qiita.com/miyaoka/items/9774f0250953da419a58#api%E3%82%92%E9%9D%99%E7%9A%84%E5%8C%96%E3%81%99%E3%82%8B)
- [Nuxtでビルド時にAPIを静的化して、完全にサーバーへのリクエストをなくすト](https://qiita.com/soichiro_nitta/items/7ca238d76072eba2ed97)

1つめの記事はFirebase向けのCMS Flamelinkの紹介のほか、JAMstackの説明なども丁寧に解説されています。素敵な記事ですね。その一環でJSONファイルへの保存も紹介しつつ、また、webworkerでキャッシュすることによって更にパフォーマンスを上げていることが紹介されています。先述したnuxt-payload-extractorはブラウザにキャッシュできませんでしたが、パフォーマンスを求めるならこちらを参考にしたほうが良さそうですね。

2つめの記事は1つめの記事が参照されています。JSONへの保存・読み込み処理部分だけの記事なので参考にしやすいと思います。(なくすト.js:thumbsup:)


## 3.直接htmlから削除する（ブログ記事から）
nuxt generateで生成されたhtmlには

```
＜script＞window.__NUXT__=
```
からはじまる箇所にJavaScriptの処理が記載されています。先述した、nuxt-payload-extractorはこの記載箇所からデータを取得しているので、そちらのソースも参照するとわかりやすいかもしれません。（そのほかの例はaxiosから直接データを取得しています）

単純ですが、htmlからこの記載を削除してしまえば通信も発生しません。
サクッと導入するには以下の記事で紹介されているcheerioを利用した方法が手軽かと思います。

[Nuxt.js Generate後の＜script＞window.\_\_NUXT__=を消したい](https://ondwn.com/blog-20190718/)

以下上記ブログ記事からの引用です。

```nuxt.config.js
    const cheerio = require('cheerio')
    export default {
      // 省略
      hooks: {
        'generate:page': page => {
          const doc = cheerio.load(page.html);
          doc(`body script`).remove();
          page.html = doc.html();
        },
      },
      // 省略
    }
```
行っているのはnuxt.config.jsにcheerioで直接該当箇所を削除する処理を書くことだけです。アニメーションなどのために、クライアントサイドでJavaScriptを使う場合はscript全部ではなく、payload部分のみを削除すれば良さそうですね。（テストなどはしていません）

# ケースごとのまとめ
リンクも再掲します。

## case1　サクッと手軽になくしたい
htmlを軽くしたい。クライアントサイドのJavaScriptはいらない。既存のプロジェクトから不要なリクエストをとにかくサクッとなくしたい。セキュリティも気になる。
⇒D.直接htmlから削除する　が手軽です。
[Nuxt.js Generate後の＜script＞window.\_\_NUXT__=を消したい](https://ondwn.com/blog-20190718/)

## case2　webworkerやPWAは使わない
サクッと手軽になくしたい。webworkerやPWAは使わない。一番多そうなケースです。
⇒A.nuxtServerInitで全データ取得＆JSONに保存。
⇒B.window.\_\_NUXT__からJSONに保存。
⇒C.APIリクエスト時にJSONに保存。

中でもB.[nuxt-payload-extractor](https://github.com/DreaMinder/nuxt-payload-extractor)か、C.「[Nuxtでビルド時にAPIを静的化して、完全にサーバーへのリクエストをなくすト](https://qiita.com/soichiro_nitta/items/7ca238d76072eba2ed97)」が導入しやすそうだなと思います。

## case3　パフォーマンスを向上させたい
サイトのパフォーマンスを向上させたい。
⇒A.nuxtServerInitで全データ取得＆JSONに保存。
⇒C.APIリクエスト時にJSONに保存。

加えてwebworkerでキャッシュ。更にhtmlから不要な処理は削除しても良いかもしれません。
[Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る](https://qiita.com/miyaoka/items/9774f0250953da419a58#api%E3%82%92%E9%9D%99%E7%9A%84%E5%8C%96%E3%81%99%E3%82%8B)

⇒更に更に、軽さを追求する場合は、AMPの導入を検討してみても良いかもしれません。
Nuxtは公式でAMPのサンプルもあげられています。[nuxt.js/examples/with-amp/](https://github.com/nuxt/nuxt.js/tree/dev/examples/with-amp)


以上です。
至らない点あるかもしれません。不備などありましたら、ご指摘いただけるとありがたいです。