Nuxt.jsアプリケーションのレスポンスを断片的なHTMLになるようにカスタマイズしてみた
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アプリケーション内で共存はできないので注意が必要です。