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:
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?
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.
Related
I have a Blazor Server app that is a multi-step "wizard" form. After each relevant step the state is adjusted, and new HTML is shown/hidden via conditional statements (simple example below).
if (IsStepSignature)
{
<div>Signature HTML here</div>
}
This all works just fine. My problem comes when I need to invoke some JS logic on the dynamically generated HTML from above (e.g. click handlers to hook up external JS libraries). When I handle the "Next" click, I can invoke the JS just fine...but it is not yet seeing the dynamic HTML from above. Is there a way to invoke some JS, and control it so that it doesn't execute until after the page is redrawn from the C# code execution?
5/18/2020 Update from Nik P
Can leverage some flags and use OnAfterRenderAsync to control this ordering. This does work, but it does require some extra hops to/from the server. Below is what I see when implementing this. This may just be the nature of Blazor Server, as one of the pros/cons is some known added chattiness. In total these requests were 2.5K, so extremely small.
CLIENT --> Browser Dispatch Event (NEXT CLICK)
Render <-- SERVER
CLIENT --> Render Complete
Invoke JS <-- SERVER
CLIENT --> Invoke Complete
The issue you are having has to do with the element not existing in the client side HTML at all until after the re-render takes place. So one way to do this is to set a boolean flag in your C# code that says there is code that needs to be run after the render, and populate support fields that you will need for your JS Interop. Whenever you need to run the JS interop, set your flag to true, set your support fields to the values you need for the JS interop call, and then do something that kicks a DOM diff calculation. (Even StateHasChanged should be enough, but adding your items conditionally as you mentioned will also do it) Then, override your OnAfterRenderAsync method as follows:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
{
// any first render code
}
if(yourFlag)
{
YourJSInteropMethod(supportField1, supportfield2);
yourflag = false;
}
}
The simplicity in this approach is that the DOM update will always happen ahead of the OnAfterRenderAsync call, so your HTML will be populated with what you are targeting with JS.
An exception breakpoint once led me to a render function generated by the Vue template compiler for one of my Vue components.
This striked me as a "Hey, I now understand how this template system works!", but I didn't pay attention to where that was in the webpack tree.
Now I'd like to inspect the code for other components and may be set a breakpoint here and there, but when I browse the active javascript sources in the browser debugger pane, I can't find any of this generated code.
It looks like the compiled render functions can be found in the sources tree under the . folder of webpack://. It can be a bit tricky to find the right one though because there are multiple files for each .vue file. The other files will contain other bits of the component. The file containing the render function should be pretty obvious when you find it, it should start var render = function() {. For example, in one of my test applications I can see the render function for app.vue at webpack:///./src/app.vue?91e4, though that last part will vary.
If you want to insert a breakpoint for your own component then you can sneak it in via a method. This can also be a quick way to find the right file.
Within the template, make a call to a method, I've called it breakpoint:
{{ breakpoint() }}
Then in the method:
breakpoint () {
debugger
return ''
}
You can then walk one level up the stack to see the compiled render function.
Of course you don't necessarily have to use a debugger statement for this. You could just set a browser breakpoint in a suitable method (or introduce one if one doesn't already exist). So long as the method is called within the template it should give you access to the compiled render function.
To set a breakpoint that way you should just need to navigate to the relevant .vue file in the webpack:// section of the sources tree. That file is usually easy to find directly under the webpack:// heading.
Update:
Once you've found the file containing the render function using a breakpoint you can then find the file in the sources tree using 'Reveal in sidebar':
I have an old-style, multiple-page website, with a multiple steps checkout process. This all works with JS disabled, and it is critical that it keeps doing so.
The checkout form has no JS at all, at the moment, and I'd like to improve it progressively (eg. dynamically showing or hiding fields, doing live validation, etc...).
I have already wrapped the entire website with an #app div, and I mounted a Vue instance to it. Then I created a few components which work correctly (but are not critical, so if JS is disabled then the whole thing keeps working and the components are just empty).
Now I have a long checkout form which is generated server-side (say: <form id='address-form'>).
The best course would be to put it into a component (say <checkout-form>) and use it. I can't do this, because 1) the form is generated server-side 2) it needs to work without JS.
Ideally, I would love to create a component with no template, and attach it to the existing HTML.
Is this even possible?
Edit: continuing to dig the Internet, I found this tutorial. This is exactly my problem, but if this is the only way to do it, then I will revert to JQuery :) Manually duplicating the entire HTML (one server side, the other in Vue) is definitely not a good idea.
I am currently developing a web application that is used to display elements for events on a map provided by HERE Maps. I am using Vue.
I have some components, but the relevant component is the component HereMaps.vue which initializes the map using the HERE Maps Api.
The HERE Maps Api provides the possibility to place so called InfoBubbles on the map showing additional information. These InfoBubbles can be provided some HTML-code in order to customize their appearance.
Please refer to the documentation for additional information
Following the documentation the code looks something like this:
let bubble = new H.ui.InfoBubble(marker.getPosition(), {
content: "<div class='someClass'>Some Content</div>"
});
this.ui.addBubble(bubble)
This is happening after mount in the "mounted" method from Vue in the "HereMaps" component.
The Bubbles are added in a "closed" (hidden) form and dynamically "opened" to reveal their content when the corresponding marker icon on the map is clicked. Therefore the HTML-code is present on the DOM after the component is mounted and is not removed at a later stage.
Now instead of supplying custom code within each bubble added to the UI i want to just add a component like this:
let bubble = new H.ui.InfoBubble(marker.getPosition(), {
content: "<myDynamicComponent></myDynamicComponent>"
});
this.ui.addBubble(bubble)
It does not matter to me wether the component is initialized using props or if it is conditionally rendered depending on the state of a global variable. I just want to be able to use the "myDynamicComponent" in order to customize the appearance in a different file. Otherwise the design process gets very messy.
As far as i know this is not possible or at least i was not able to get it work. This is probably due to the fact that the "myDynamicComponent" is not used within the "template" of the "HereMaps" component und thus Vue does not know that it needs to render something here after the directive is added to the DOM in the "mounted" method.
This is what the InfoBubble looks using normal HTML as an argument:
This is what the InfoBubble looks using the component as an argument:
It appears to just be empty. No content of the "myDynamicComponent" is shown.
Does anyone have any idea how i could solve this problem.
Thank You.
Answer is a bit complicated and I bet you wouldn't like it:)
content param can accept String or Node value. So you can make new Vue with rendered your component and pass root element as content param.
BTW, Vue does not work as you think, <myDynamicComponent></myDynamicComponent> bindings, etc exists in HTML only in compile time. After that all custom elements(components) are compiled to render functions. So you can't use your components in that way.
Give us fiddle with your problem, so we can provide working example:)
Windows Phone 8 app, WebBrowser control. I load a chunk of HTML via NavigateToString (after setting IsScriptEnabled=true). Some time later (long after it's loaded), I'm invoking some JavaScript on the page with InvokeScript.
When I invoke a JavaScript function that's defined inline inside a <script> element, it works as expected. When I invoke one that's defined in an external JS file, it doesn't, and an exception from HRESULT 0x80020006 ("name not found") is thrown.
The external script file is loaded from my app package. In the HTML string, there's a <base> element which contains a file:// URL to the package's folder (retrieved via Package::Current->InstalledLocation), and the <script> element contains just the file name. There are also styles and images in that folder - they load fine.
The HTML has no DOCTYPE and no xmlns - I know those things can sometimes throw JavaScript off.
The external script file is valid - it came straight from Android where it worked on the respective WebView control. The function I'm trying to invoke is empty anyway, to be on the safe side, JavaScript syntax-wise.
This could in theory be some kind of a cross-domain scripting issue. Technically, the script comes from a file:// URL while the page itself comes from no URL at all. Some piece of system code that makes sure no fishy script is called could've gotten in the way.
Found one workaround: load the external script file into a string on startup, once the HTML is loaded (LoadCompleted fires), feed it to the document using JavaScript eval.
Here is example of how to inject some script dynamically
Browser.InvokeScript("eval", new string[] { FileUtils.ReadFileContent("app/www/js/console.js") });
Where ReadFileContent could be defined as following
https://github.com/sgrebnov/IeMobileDebugger/blob/master/Libraries/Support/FileUtils.cs
Full example
https://github.com/sgrebnov/IeMobileDebugger/blob/master/Libraries/IE.Debug.Core/WebPageDebugger.cs
PS. instead of reading script from file you can pass hardcoded string, etc
Are you sure that your script is being loaded? One thing you can do is tuck an alert in there to make sure it is being loaded. My suspicion is that it isn't being loaded.
Any time I have run into this before that has been the case although admittedly I haven't loaded a JS file from Isolated Storage before.