Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ssr] Add Vue function to render VNode to html string #9205

Closed
AlbertMarashi opened this issue Dec 16, 2018 · 23 comments
Closed

[ssr] Add Vue function to render VNode to html string #9205

AlbertMarashi opened this issue Dec 16, 2018 · 23 comments

Comments

@AlbertMarashi
Copy link

AlbertMarashi commented Dec 16, 2018

What problem does this feature solve?

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

What does the proposed API look like?

/**
* @param {VNode}
* 
* @returns {string} - VNode rendered to a html string
*/
Vue.renderVNode = function(VNode){
    //...
}
@posva
Copy link
Member

posva commented Dec 16, 2018

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 🙂

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Dec 17, 2018

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

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Dec 17, 2018

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>

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Dec 17, 2018

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?

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Dec 18, 2018

@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

@AlbertMarashi
Copy link
Author

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.

@yyx990803 yyx990803 changed the title Add Vue function to render VNode to html string [ssr] Add Vue function to render VNode to html string Dec 22, 2018
@AlbertMarashi
Copy link
Author

My further findings show that if the function is provided, it would need to be either:

  • Fully synchronous, so it could be included in the template of the bundle renderer
  • Or, allowing async code in the {{{ ... }}} syntax in the template so await may be used.

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Dec 27, 2018

@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)

@AlbertMarashi
Copy link
Author

Accidental close

@AlbertMarashi AlbertMarashi reopened this Dec 27, 2018
@leopiccionia
Copy link

@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.

@yyx990803
Copy link
Member

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.

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Jan 15, 2019

@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
})

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Jan 15, 2019

From studying the internals, it seems the two options are:

  1. making the Lodash template compiler able to handle await (which would probably mean implementing a custom template compiler for the template setting
  2. exposing an API for synchronously rendering VNodes in the vue-server-renderer

The 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

@yyx990803
Copy link
Member

yyx990803 commented Jan 15, 2019

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.

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Jan 16, 2019

You're right, I will probably just do that.

But I still actually like the function-based template.

  • It allows async operations in the template
  • makes the bundle renderer easier to use, and more JS templating (strings & vars) instead of {{{ }}}
  • the documentation could be simplified
  • could potentially remove/deprecate string-based Lodash template dependency (in a major version)

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>`
        })

@yyx990803
Copy link
Member

You're right. The usage example makes it much clearer. Let's consider adding this to 2.6.

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Jan 16, 2019

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)

@mategvo
Copy link

mategvo commented Apr 4, 2019

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

@mategvo
Copy link

mategvo commented Apr 4, 2019

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.

@AlbertMarashi
Copy link
Author

Have you had a look at https://github.com/futureaus/servue?

@alucidwolf
Copy link

@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:

  1. Fetch HTML string from API in serverPrefetch(). This HTML can contain [PLACEHOLDERS].
  2. Replace [PLACEHOLDERS] with proper vue component, such as replacing [MAIN_NAV] placeholder with <main-nav :code="navHtml" /> component.
  3. Render updated string of HTML with newly added vue components on the server and deliver to browser with complete functionality for navigation.

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.

@AlbertMarashi
Copy link
Author

@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

@alucidwolf
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants