4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-26

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

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

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

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

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

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

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

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

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

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へ値を直接代入していますが、実際にはasyncDatafetch内でサーバー応答をバインドすることによって問題なくSSRできます。問題になるのは子コンポーネントであるMyComponentの描画にサーバー上の情報などコンポーネントの外の情報を使う必要がある場合で、これを正しくSSRするにはもう少し考慮が必要になります。

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

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

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は動作しません。サーバー側で処理が行われる(ページコンポーネントの)asyncDatafetchの中で値を設定する必要があります。

方針1: propsで渡す

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

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

@Component({})
export default class MyComponent extends Vue {
  @Prop()
  content = ''
}
</script>
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を使用可能な環境であればこの方針が一番管理しやすいと思います。

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>
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を行う事ができました。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?