この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Vue.jsのフレームワークであるNuxt.jsを利用し、SSR(Server Side Rendering)のレスポンスとして全体的なHTMLではなく、断片的なHTMLが欲しい状況に遭遇し、レスポンスをカスタマイズする方法を調べました。
検証に使用したNuxt.jsのバージョンはv2.8.1です。
断片的なHTMLの例
<div class="container">
Hello World
</div>
Nuxt.jsデフォルトの状態で試す
まずNuxt.jsのデフォルトのレスポンスを確認するために、プロジェクトを作って試します。
$ npx create-nuxt-app nuxt-sample
create-nuxt-app v2.9.0
✨ Generating Nuxt.js project in nuxt-sample
? Project name nuxt-sample
? Project description My peachy Nuxt.js project
? Author name shoito
? Choose the package manager Yarn
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
$ cd nuxt-sample
$ yarn dev
yarn run v1.16.0
$ nuxt
╭─────────────────────────────────────────────╮
│ │
│ Nuxt.js v2.8.1 │
│ Running in development mode (universal) │
│ │
│ Listening on: http://localhost:3000/ │
│ │
╰─────────────────────────────────────────────╯
これでプロジェクトの雛形とindexページが作られ、3000番ポートでListening状態になりました。
プログラムが起動できることは確認したので、 pages/index.vue
を検証用に編集してレスポンスをシンプルにします。
変更後の pages/index.vue
<template>
<div class="container">
Hello World
</div>
</template>
次に、curlコマンドでレスポンスを確認します。
$ curl localhost:3000
<!doctype html>
<html data-n-head-ssr data-n-head="">
<head data-n-head="">
<title data-n-head="true">nuxt-sample</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 peachy 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/index.js" as="script"><link rel="preload" href="/_nuxt/pages/index.398aef1ad33db90ea3e7.hot-update.js" as="script"><style data-vue-ssr-id="17cfdfa9:0">
.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;
}
</style><style data-vue-ssr-id="6da220d7:0">
.nuxt__build_indicator[data-v-71e9e103] {
box-sizing: border-box;
position: absolute;
font-family: monospace;
bottom: 20px;
right: 20px;
background-color: #2E495E;
padding: 5px 10px;
border-radius: 5px;
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.2);
color: #00C48D;
width: 84px;
z-index: 2147483647;
}
.v-enter-active[data-v-71e9e103], .v-leave-active[data-v-71e9e103] {
transition-delay: 0.2s;
transition-property: all;
transition-duration: 0.3s;
}
.v-leave-to[data-v-71e9e103] {
opacity: 0;
transform: translateY(20px);
}
svg[data-v-71e9e103] {
width: 1.1em;
position: relative;
top: 1px;
}
</style><style data-vue-ssr-id="aab9a468:0">
html {
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
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;
}
</style>
</head>
<body data-n-head="">
<div data-server-rendered="true" id="__nuxt"><!----><!----><div id="__layout"><div><div class="container">
Hello World
</div></div></div></div><script>window.__NUXT__={layout:"default",data:[{}],error:null,serverRendered:true,logs:[]};</script><script src="/_nuxt/runtime.js" defer></script><script src="/_nuxt/pages/index.js" defer></script><script src="/_nuxt/pages/index.398aef1ad33db90ea3e7.hot-update.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>
Hello World
とそれをラップするdivタグ以外にも沢山の情報が付いてきてますね。
これは期待するレスポンスではありません。
レイアウトを変更して試す
Nuxt.jsはレイアウトの機能があり、ページ毎にレイアウトを指定することができます。
https://ja.nuxtjs.org/api/pages-layout/
上記で編集した pages/index.vue
では layout
の指定はしていないのでデフォルト( layouts/default.vue
)が使われます。
変更前の layouts/default.vue
<template>
<div>
<nuxt />
</div>
</template>
<style>
html {
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
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;
}
</style>
curlコマンドのレスポンスに含まれていた style
の一部は layouts/default.vue
で指定されているものでした。
今回の検証では不要なので、消してシンプルにします。
変更後の layouts/default.vue
<template>
<nuxt />
</template>
この状態で、再度curlコマンドでレスポンスを確認します。
$ curl localhost:3000
<!doctype html>
<html data-n-head-ssr data-n-head="">
<head data-n-head="">
<title data-n-head="true">nuxt-sample</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 peachy 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/app.45da00be42239c2f97e8.hot-update.js" as="script"><link rel="preload" href="/_nuxt/pages/index.js" as="script"><style data-vue-ssr-id="17cfdfa9:0">
.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;
}
</style><style data-vue-ssr-id="6da220d7:0">
.nuxt__build_indicator[data-v-71e9e103] {
box-sizing: border-box;
position: absolute;
font-family: monospace;
bottom: 20px;
right: 20px;
background-color: #2E495E;
padding: 5px 10px;
border-radius: 5px;
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.2);
color: #00C48D;
width: 84px;
z-index: 2147483647;
}
.v-enter-active[data-v-71e9e103], .v-leave-active[data-v-71e9e103] {
transition-delay: 0.2s;
transition-property: all;
transition-duration: 0.3s;
}
.v-leave-to[data-v-71e9e103] {
opacity: 0;
transform: translateY(20px);
}
svg[data-v-71e9e103] {
width: 1.1em;
position: relative;
top: 1px;
}
</style>
</head>
<body data-n-head="">
<div data-server-rendered="true" id="__nuxt"><!----><!----><div id="__layout"><div class="container">
Hello World
</div></div></div><script>window.__NUXT__={layout:"default",data:[{}],error:null,serverRendered:true,logs:[]};</script><script src="/_nuxt/runtime.js" defer></script><script src="/_nuxt/pages/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><script src="/_nuxt/app.45da00be42239c2f97e8.hot-update.js" defer></script>
</body>
</html>
layouts/default.vue
から削除した style
は含まれていませんが、まだまだ不要な情報があります。
これも期待するレスポンスではありません。
アプリテンプレートを変更して試す
次にNuxt.jsのアプリテンプレートを変更します。
https://ja.nuxtjs.org/guide/views/#アプリテンプレート
デフォルトのテンプレートとしてこちらが使われます。
https://github.com/nuxt/nuxt.js/blob/2.x/packages/vue-app/template/views/app.template.html
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>
テンプレートを変更するためには、プロジェクトのルートフォルダに app.html
を作成するとのことなので、断片的なHTMLを返すように以下のようにします。
{{ APP }}
この状態で、再度curlコマンドでレスポンスを確認します。
$ curl localhost:3000
<div data-server-rendered="true" id="__nuxt"><!----><!----><div id="__layout"><div class="container">
Hello World
</div></div></div><script>window.__NUXT__={layout:"default",data:[{}],error:null,serverRendered:true,logs:[]};</script><script src="/_nuxt/runtime.js" defer></script><script src="/_nuxt/pages/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>
期待しているレスポンスに近づいてきましたが、まだ不要な script
タグがあります。
これも期待するレスポンスではありません。
injectScriptsオプションを変更して試す
この script
タグはNuxt.jsで挿入されているタグなので、 nuxt.config.js
の render
プロパティの injectScripts
オプションに false
を指定することで取り除けます。
https://ja.nuxtjs.org/api/configuration-render/#injectscripts
変更後の nuxt.config.js
の一部抜粋
export default {
mode: 'universal',
render: {
injectScripts: false
},
...
この状態で、再度curlコマンドでレスポンスを確認します。
$ curl localhost:3000
<div data-server-rendered="true" id="__nuxt"><!----><!----><div id="__layout"><div class="container">
Hello World
</div></div></div>
前の <div data-server-rendered="true" id="__nuxt"><!----><!----><div id="__layout">
と後ろの </div></div>
が余計ですが、だいぶ削れました。
(これはNuxt.jsアプリケーションのルート要素とレイアウト要素になります)
しかし、これも期待するレスポンスではありません。
あと、もう一歩!
bundleRendererオプションを変更して試す
時間内に、残ってしまったNuxt.jsアプリケーションのルート要素である <div data-server-rendered="true" id="__nuxt">...
を付与しないオプションは見つけられませんでした。
app.html
アプリテンプレートに残した、 {{ App }}
ですが、Nuxt.jsのコードを見ると、こちらのrender関数を使ってる限り変更はできなそうに見えました。
https://github.com/nuxt/nuxt.js/blob/2.x/packages/vue-app/template/App.js#L24
最終手段として nuxt.config.js
の render
プロパティの bundleRenderer
オプションの template
を変更して、 String.prototype.replace()
により前後のdivタグを削ってみました。
https://ja.nuxtjs.org/api/configuration-render/#bundlerenderer https://ssr.vuejs.org/ja/api/#レンダラオプション
export default {
mode: 'universal',
render: {
injectScripts: false,
bundleRenderer: {
template: (result, context) => {
result = result.replace(
/^<div data-server-rendered="true" id="[^>]+">[<!->]*<div id="__layout">/i,
''
)
result = result.replace(/(<\/div>){2}$/i, '')
return `${result}`
}
}
},
なお、 render:route
の hooks
でHTMLを書き換える形で同様のことは可能です。
https://ja.nuxtjs.org/api/internals-renderer/#フック
この状態で、再度curlコマンドでレスポンスを確認します。
$ curl localhost:3000
<div class="container">
Hello World
</div>
無理やり感満載ですが、やっと求めていた断片的なHTMLがレスポンスとして返りました!
さいごに
今回のレスポンスのカスタマイズのやり方では、アプリテンプレート(app.html)を変更しているので、Nuxt.jsアプリケーション全体のレスポンスが断片的なHTMLに変わってしまいます。
このやり方では、ルーティング次第で全体的なHTMLを返すエンドポイントと、断片的なHTMLを返すエンドポイントがNuxt.jsアプリケーション内で共存はできないので注意が必要です。