Nuxt.jsで文字列からコンポーネントを描画してSSRする

この記事では`"<template><MyComponent /></template>"`のような文字列データを、Vueコンポーネントを正しく解釈した上で画面上に描画してSSRすることを目指します。

これを実現したい主要なモチベーションとしては、ブログ投稿を管理するシステムを構築するシーン等を想定してもらうとわかりやすいかもしれません。「 **ブログの投稿をVueの文法で書きたい** 」かつ「 **投稿は検索エンジンにインデックスされてほしい** 」ようなケースをこの記事では想定しています。

## HTML文字列をVueのテンプレートとして描画する

HTMLを記述した文字列オブジェクトをそのまま描画するだけであれば`v-html`で実現することができます。しかし、ここではHTML文字列中に`<MyComponent>`が含まれていることが問題です。これをそのままDOMに挿入しても、当然ブラウザが正しく解釈することはできません。

ここでは、以下のような単純なブログページを作るものとします。  

```html:pages/blog/index.vue
<template>
  <header></header>
  <main>
    <!-- 
    ここに
    "<article>
      <MyComponent />
    </article>"
    みたいな文字列を挟んで、正しくレンダリングしてほしい
    -->
  </main>
  <footer></footer>
</template>
```

この問題に関しては自前で`render`をゴリゴリ書いてもよいですが、`v-runtime-template`というライブラリが広く使われているので、これを用いることで解決できます。

- [v-runtime-template](https://github.com/alexjoverm/v-runtime-template)

(導入方法等の詳細はREADMEに記載があるので省略します)

`v-runtime-template`を用いて文字列からコンポーネントを描画する例は以下の通りです。

```vue:pages/blog/index.vue
<template>
  <header></header>
  <main>
    <v-runtime-template :template="template" />
  </main>
  <footer></footer>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import VRuntimeTemplate from 'v-runtime-template'
import MyComponent from '~/components/MyComponent.vue'

@Component({
  components: {
    VRuntimeTemplate,
    MyComponent
  }
})
export default class extends Vue {
  template = '<article><MyComponent /></article>'
}
</script>
```

これで(一番単純な例ですが)文字列をVueのテンプレートとして解釈して正しく描画することができました。
ここでは`template`へ値を直接代入していますが、実際には`asyncData`や`fetch`内でサーバー応答をバインドすることによって問題なくSSRできます。問題になるのは子コンポーネントである`MyComponent`の描画にサーバー上の情報などコンポーネントの外の情報を使う必要がある場合で、これを正しくSSRするにはもう少し考慮が必要になります。

## v-runtime-templateで解決されるHTMLをSSRする

以下のように`MyComponent`が内部でサーバー呼び出しを行うようなケースではSSRされません。(Nuxtを使うプロジェクトではあまりこういう設計にならない気もしますが)

```vue:components/MyComponent.vue
<template>
  <div>
    {{ content }}
  </div>
</template>

<script lang="ts">
import axios from 'axios'
import { Vue, Component } from 'nuxt-property-decorator'

@Component({})
export default class MyComponent extends Vue {
  content = ''

  async created() {
    this.content = await axios.get('/from/server')
  }
}
</script>
```

createdフックが動作するのはクライアントサイドですのでこの方針ではSSRは動作しません。サーバー側で処理が行われる(ページコンポーネントの)`asyncData`や`fetch`の中で値を設定する必要があります。

### 方針1: propsで渡す

ページコンポーネントの`asyncData`で取得した値を子コンポーネントのpropsに渡せばSSRが動作します。

```vue:components/MyComponent.vue
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'

@Component({})
export default class MyComponent extends Vue {
  @Prop()
  content = ''
}
</script>
```

```vue:pages/blog/index.vue
<script lang="ts">
import axios from 'axios'
import { Vue, Component } from 'nuxt-property-decorator'
import VRuntimeTemplate from 'v-runtime-template'
import MyComponent from '~/components/MyComponent.vue'

@Component({
  components: {
    VRuntimeTemplate,
    MyComponent
  }
})
export default class extends Vue {
  async asyncData() {
    // templateの文字列の中にバインドを書く
    template = '<article><MyComponent :content="content" /></article>'
    content = await axios.get('/from/server')
    return {
      template,
      content
    }
  }
}
</script>
```

ただし、これではコンポーネントの引数体系に合わせてテンプレートの内容を書き換える必要があるため、テンプレートのメンテナンス性が高くありません。
また、1階層の子コンポーネントであればこの程度ですが、ネストしたコンポーネントを考えるとpropsのバケツリレーが大変なことになりそうです。

### 方針2: ストアで渡す

Vuexを使用可能な環境であればこの方針が一番管理しやすいと思います。

```vue:components/MyComponent.vue
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'

@Component({})
export default class MyComponent extends Vue {
  content = this.$store.getters.content
}
</script>
```

```vue:pages/blog/index.vue
<script lang="ts">
import axios from 'axios'
import { Vue, Component } from 'nuxt-property-decorator'
import VRuntimeTemplate from 'v-runtime-template'
import MyComponent from '~/components/MyComponent.vue'

@Component({
  components: {
    VRuntimeTemplate,
    MyComponent
  }
})
export default class extends Vue {
  async asyncData() {
    // ストアを使えばpropsをバインドする必要がない
    template = '<article><MyComponent /></article>'
    content = await axios.get('/from/server')
    this.$store.commit('content', content)
    return {
      template
    }
  }
}
</script>
```

以上のようにしてHTMLのテンプレート文字列をVueのコンポーネントとして描画し、SSRを行う事ができました。