Search code examples
vue.jsvuejs2asp.net-core-2.0server-side-renderingasp-net-core-spa-services

Server Side Rendering Vue with ASP.NET Core 2


I'm trying to understand the usage and limitations of server side rendering with vuejs when using aspnet core.

I used this starter kit for aspnet core and vuejs to setup a simple vue site, which is running based on the code here: https://github.com/selaromdotnet/aspnet-vue-ssr-test/tree/master

I then modified the project to update the aspnet-prerendering and added vue-server-renderer, compiling a hodgepodge of sources to cobble together this update: https://github.com/selaromdotnet/aspnet-vue-ssr-test/tree/ssr

If I run this project, the site appears to load fine, and if I turn off the javascript in the browser, I can see that it does appear that the server-side rendering executed and populated the html result:

enter image description here

however, because JavaScript is disabled, the content isn't moved into the dom as it looks like it is trying to...

My understanding of server-side rendering is that it would populate the html entirely and serve a completed page to the user, so that even if JS was disabled, they'd at least be able to see the page (specifically for SEO purposes). Am I incorrect?

Now I believe modern search engines will execute simple scripts like this to get the content, but I still don't want a blank page rendered if js is disabled...

Is this a limitation of server-side rendering, or perhaps specifically ssr with vue and/or aspnet core?

or am I just missing a step somewhere?

Edit: more information

I looked at the source code for what I believe is the method that prerenders the section here: https://github.com/aspnet/JavaScriptServices/blob/dev/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs

The line

output.Content.SetHtmlContent(result.Html); 

has a null value for result.Html. However, when I manually edit this value to put a test value, it also doesn't render to the output html, and the app div tag is still empty...

If I'm doing something wrong to populate the result.Html value with the expected output, that's one thing, and I would appreciate some help in doing that, especially since the output html appears to be found, since it's in the script that immediately follows...

However, even if I were to populate it, it appears it's being skipped, as evidenced by me manually changing the value. is this a bug in the code or am I doing somethigng wrong, or perhaps both?


Solution

  • As you correctly noticed, for your project, result.Html inside the tag helper is null. So that line cannot be the location where the output is being generated. Since the HTML output from your prerendering script also does not include a script tag, it is clear that something has to generate that. The only other line that could possible do this is the following from the PrerenderTagHelper:

    output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
    

    That would fit the observed output, so we should figure out where the globalsScript comes from.

    If you look at the PrerenderTagHelper implementation, you can see that it will call Prerenderer.RenderToString which returns a RenderToStringResult. This result object is deserialized from JSON after calling your Node script.

    So there are two properties of interest here: Html, and Globals. The former is responsible for containing the HTML output that finally gets rendered inside the tag helper. The latter is a JSON object containing additional global variables that should be set for the client side. These are what will be rendered inside that script tag.

    If you look at the rendered HTML from your project, you can see that there are two globals: window.html and window.__INITIAL_STATE__. So these two are set somewhere in your code, although html shouldn’t be a global.

    The culprit is the renderOnServer.js file:

    vue_renderer.renderToString(context, (err, _html) => {
        if (err) { reject(err.message) }
        resolve({
            globals: {
                html: _html,
                __INITIAL_STATE__: context.state
            }
        })
    })
    

    As you can see, this will resolve the result containing just a globals object with both html and __INITIAL_STATE__ properties. That’s what gets rendered inside of the script tag.

    But what you want to do instead is have html not as part of globals but on the layer above, so that it gets deserialized into the RenderToStringResult.Html property:

    resolve({
        html: _html,
        globals: {
            __INITIAL_STATE__: context.state
        }
    })
    

    If you do it like that, your project will properly perform server-side rendering, without requiring JavaScript for the initial view.