Use case:
I'm developing a Server-Side Renderer for Vue (which works with Express, Koa & etc. Will increase migration to Vue). For the SSR's head management to work, it needs a stable API to render VNodes to text.
The way my Vue SSR package will function:
master.vue
<template>
<div id="app">
<slot name="content"></slot>
</div>
</template>
<script>
export default {
created: function(){
if(this.$isServer){
this.$ssrContext.head = "HEAD HERE" // Something needed like: renderVNodesToString(this.$slots.head)
}
},
}
</script>
home.vue
<template>
<master>
<template slot="content">
Hello World
</template>
<template slot="head">
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<title>Hello</title>
</template>
</master>
</template>
<script>
import master from "layouts/master.vue"
export default {
components: {
master
}
}
</script>
My goal is getting home.vue's head slot rendered into a string and injecting it into the this.$ssrContext so it can be read and injected on the server-side
in master.vue, I can access this.$slots.head with no issue, and it contains the correct VNodes
my question is, how can I render them into a string? a way to basically do:
this.$ssrContext.head = renderVNodesToString(this.$slots.head)
From my research, I have been unable to find an easy way to do this.
To understand how the renderer works
const renderer = createBundleRenderer(bundle.server, {
runInNewContext: false,
inject: false,
template: `<!DOCTYPE html>
<html>
<head>
{{{ head }}}
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
</head>
<body>
<!--vue-ssr-outlet-->
<script>${ bundle.client }</script>
</body>
</html>`
})
This is the code for the serverbundlerenderer
/**
* @param {VNode}
*
* @returns {string} - VNode rendered to a html string
*/
Vue.renderVNode = function(VNode){
//...
}
Technically, you should be able to do this by using a function that returns the vnode and render using what Vue already exports (https://ssr.vuejs.org/guide/#rendering-a-vue-instance).
Not sure of the utility of a feature like this.
FYI you can use https://ssr.vuejs.org/guide/#rendering-a-vue-instance 馃檪
the issue is, the VNode cannot be passed to the server, besides by putting it in the template (but since it is an object it turns into [object Object]). You can't use complex functions in template brackets, as they are single-expression only.
<head>
{{{ head }}}
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
</head>
Note
It cannot be rendered within the created() function inside the .vue file because rendering is an async operation, which means it cannot assign a variable to this.$ssrContext safely, as created() is synchronous
If I had access to the proposed function, I would be able to pass it to the server easily via the this.$ssrContext variable
master.vue's script
export default {
created: function(){
if(this.$isServer){
var VNodeArray = this.$slots.head
var html = ""
for(let i in VNodeArray){
html += Vue.renderVNode(VNodeArray[i])
}
this.$ssrContext.head = html
}
}
}
output ($ssrContext)
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<title>Hello</title>
And I understand the use-case may be rare, but this is something that can and will increase server-side adoption and would be even useful for Nuxt.
This would finally allow a head management system to exist in the html/template section of SFCs and would make SSR, SEO and head management much easier
If this is not possible, do you know a way to find a solution to this issue?
@posva if it cannot be added to the core, there is an alternate place it could go:
https://ssr.vuejs.org/guide/build-config.html#client-config scroll to Manual Asset Injection
there should be a context.renderVNode(vnode) / context.renderVNodes(vnodes) in the context API.
This would allow:
createBundleRenderer(serverBundle, {
inject: false,
template: `<!DOCTYPE html>
<html>
<head>
{{{ renderVNodesToString(headVNodes) }}}
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
</head>
<body>
<!--vue-ssr-outlet-->
<script>${ clientBundle }</script>
</body>
</html>`
})
where headVNodes is set by this.$ssrContext.headVNodes = this.$slots.head from within a .vue file
I'd be happy to implement an API with tests in vue-server-renderer package for rendering VNodes if given the all-clear.
Proposed API
const { createBundleRenderer, renderVNodesToString } = require("vue-server-renderer")
var renderer = createBundleRenderer(serverBundle, {
inject: false,
template: `<!DOCTYPE html>
<html>
<head>
{{{ renderVNodesToString(headVNodes) }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>`
})
// headVNodes set by this.$ssrContext = this.$slots.head in vue file
renderer.renderToString({
renderVNodesToString
})
This would allow for a far greater head management system for Vue that's based in the <template>, and would increase server-side adoption in the process. This API should add virtually no over-head to users who decide not to use it. Size increase would be minimal, and limited to the vue-server-renderer package.
My further findings show that if the function is provided, it would need to be either:
template of the bundle renderer{{{ ... }}} syntax in the template so await may be used.@yyx990803 I would be happy to try implement this.
My implementation would involve making ssr template able to parse awaits (and getting rid of lodash template compiler in the process), and then adding a new exposed API for rendering VNode(s)
Accidental close
@yyx990803 I think that it was accidentally moved from Todo to Done in 2.6 project, once @DominusVilicus accidentally closed the issue. I'm sorry if bothering.
I think I originally misunderstood the request when I added it to 2.6 - after looking at it in more details I think this can be done in userland.
There are a few things that I have concern about landing this in Vue itself:
This method cannot be exposed on the Vue runtime, since it's a server only utility (and thus should not be included in the universal runtime)
It's better exposed on this.$ssrContext as an injected helper. (This means you can implement it yourself)
The renderToString exposed by vue-server-renderer can only be async because there may be async components or async data prefetch functions down the tree. We cannot expose a sync API because it will not work correctly in all cases. In comparison, it's much easier to write a simple VNode -> string render function that only handles predictable <head> content.
The use case also seems niche, so I think we'd be better off to test an implementation in userland first.
@yyx990803
I'd be happy to implement this in userland, but the issue is that there is no exposed API that I could use to render VNodes to a string
The reason you can't use renderToString is because the operation is async, and the bundle renderer won't wait for promises in the {{{ renderVNodes(headVNodes) }}} operation
const { createBundleRenderer} = require("vue-server-renderer")
var renderer = createBundleRenderer(serverBundle, {
inject: false,
template: `<!DOCTYPE html>
<html>
<head>
{{{ renderVNodes(headVNodes) }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>`
})
async function renderVNodes(vnodes){
// create bundle renderer for the head
// await renderToString
// return rendered string
// error because template can't handle promise values
}
//pass $ssrContext the VNode renderer
renderer.renderToString({
renderVNodes
})
From studying the internals, it seems the two options are:
template settingvue-server-rendererThe simplest option would be rewriting the template compiler for the vue-server-renderer to allow await expressions, this would also remove the Lodash template compiler dependency
I think this overcomplicates the problem. (Adding API surface for a niche case)
It's quite straightforward if what you need is just sync rendering of head elements (with a very predictable range of element/attributes, so very few edge cases to watch out for). VNodes are just objects in the shape of { tag, data: { attrs: { [key]: value }}}. You are essentially writing a function that serializes a few such objects into HTML strings... it probably is just 30 lines of code without having to patch anything in Vue itself.
You're right, I will probably just do that.
But I still actually like the function-based template.
{{{ }}}With template function
const renderer = createBundleRenderer(bundle.server, {
async template(result, context){
return `
<!DOCTYPE html>
<html${ context.htmlattr ? ' ' + context.htmlattr : '' }>
<head>
${ await context.renderVNodes(context.head) }
${ context.renderResourceHints() }
${ context.renderStyles() }
${ context.renderState({ windowKey: '__INITIAL_STATE__', contextKey: "data"}) }
</head>
<body>
${result}
<script>${ bundle.client }</script>
</body>
</html>`
}
})
vs string template
const renderer = createBundleRenderer(bundle.server, {
inject: false,
template: `<!DOCTYPE html>
<html{{{ htmlattr ? ' ' + htmlattr : '' }}}>
<head>
{{{ head }}}
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
{{{ renderState({ windowKey: '__INITIAL_STATE__', contextKey: "data"}) }}}
</head>
<body>
<!--vue-ssr-outlet-->
<script>${ bundle.client }</script>
</body>
</html>`
})
You're right. The usage example makes it much clearer. Let's consider adding this to 2.6.
Awesome! I managed to create my own custom VNode renderer for the specific task of head management (you can see it in #code-review in Vue Land)
I did make a working version with the function based template in #9324 but tests were failing for some reason. It was a non-breaking change too (users could optionally still use the string template)
For me the this.$ssrContext inside a component is undefined. I am using v2.5.22 and the vue-hackernews-2.0 template. Based on what the documentation says, it should be exposed in the component automatically
Finally a working draft:
Vue.mixin({
created: function () {
var title = this.$options.title
if (title) {
if(typeof document !== 'undefined')
document.title = title;
else
this.$ssrContext.title = title
}
}
})
and inside a component:
export default {
name: 'home',
title: 'hello!',
...
Now need to populte this with API data.
Have you had a look at https://github.com/futureaus/servue?
@DominusVilicus I have the following task and am wondering if you are trying to accomplish something similar with this request.
Environment:
Dot Net Core 2.1
Task:
[PLACEHOLDERS] with proper vue component, such as replacing [MAIN_NAV] placeholder with <main-nav :code="navHtml" /> component.Reading up on https://github.com/futureaus/servue now to see if this is a viable solution, but wanted to toss a note out to you for your feedback.
@alucidwolf What are you trying to accomplish? You shouldn't be rendering HTML code from a server, if you need a dynamic menu, send the menu data as the data, and let your .vue files render it
@DominusVilicus I want to create different page layouts from this Html so it is not just data I need for the menu but also the Html structure for the page layout, which can be different.