ASP.NET Core SPA templates and Vue.js
Since version 2.1, ASP.NET Core has moved all SPA templates, previously available via the Microsoft.AspNetCore.SpaTemplates
package, to the core repository. When that was done, all .NET developers that love VueJs were negatively surprises, since the Vue template was simply removed from the new set of templates. So, when you create a new ASP.NET Core Web Application, you have a choice between Angular, React
and React with Redux. The issue is described in details by Owen Caulfield in his post on Medium. Owen refers to the GitHub issue.
How to deal with that
Ideally, the .NET community needs to look at the issue and create a new template to address the issue. Below, I will go through the requirements for such a template and explain how to work around the problem before we get the template working.
The React template
Let's have a quick look at how the React+Redux SPA template works.
An application created with that template contains a folder in the web project that is called ClientApp
. This folder is a home for the React application, which uses WebPack.
To build the SPA, there are some additional items in the csproj
file. You can look at it youself since I will not include these items in the post for the sake of brevity. In short, there is one ItemGroup
there to include the ClientApp
folder as content to the output of the build, and two Target
tags to execute npm
at the build and publish stages.
There are also some lines of code in the Startup.cs
file that are important to make the whole thing work.
First, in the ConfigureServices
method we can find this line:
services.AddSpaStaticFiles(configuration =>
{ configuration.RootPath = "ClientApp/build"; });
and in the Configure
method we have a few more lines too:
app.UseStaticFiles();
app.UseSpaStaticFiles();
// MVC configuration is skipped but still needed
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
Making it work with Vue
So, as we can see, there are no massive changes to the whole application setup to make the SPA work, and hence the UseReactDevelopmentServer
accepts the npm
command, it might be easily replaced to run another command instead.
Replace the client app
So, let's start by replacing the React app with the Vue app. To do that, I created a Vue app in another directory, using the vue create myapp
command. I added some options like using TypeScript and PWA, but it doesn't really matter. The Vue CLI 3 only uses the WebPack configuration, so the whole build configuration of the ASP.NET Core application should work as before. To check if this is the case, I removed the content of the ClientApp
folder in my .NET project and replaced it with the content of my new Vue application directory:
You can see that my ClientApp
folder contains the Vue app instead of the React app. I can try building the whole solution now, and it builds as expected.
Middleware
However, if I run the app, I get an exception in the ReactDevelopmentServerMiddleware
, because it tries to execute npm run start
, but the Vue development server is started by npm run serve
. It appears to be an easy fix, so I only needed to change the line in my Startup.cs
:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "serve");
}
});
But now, when I start the application, it opens the browser window that continuously hangs trying to load the home page. At the console output, however, I can clearly see that the Vue development server has started successfully and there are no exceptions.
The reason for the hang is this code in the ReactDevelopmentServerMiddleware
class:
Match match = await npmScriptRunner.StdOut.WaitForMatch(new Regex("Starting the development server", RegexOptions.None, ReactDevelopmentServerMiddleware.RegexMatchTimeout));
As you can see, it starts the npm
with a given command, which we can replace, but it waits for Node to produce a certain console output, which is hardcoded to Starting the development server
. If you look close to the output of the npm run serve
for Vue, you can see that it says Starting development server
. So, the code above waits for the output until it times out and throws.
Change the output message
So, here comes a hack, since everything we did before was rather legit. Now, we need to replace the output message. It can be done by changing the serve.js
file in the ClientApp/node_modules/@vue/cli-service/lib/commands
directory. Here is my change:
}, async function serve (args) {
info('Starting the development server...')
Now, if I run the application again, it starts the browser, but I get an exception that the middleware cannot proxy the request to the development server:
HttpRequestException: Failed to proxy the request to http://localhost:54252/, because the request to the proxy target failed. Check that the proxy target server is running and accepting requests to http://localhost:54252/
(the port number can vary)
At the same time, I can see that at the time the development server of Vue was still building and linting the app. When that is done, I refreshed the page, and everything worked as expected.
Note on Browser Sync
It is possible to use the Browser Sync by installing the Vue CLI plugin by executing the vue add browser-sync
in the ClientApp
directory and using the serve:bs
as an argument for the middleware instead of serve
. But then the whole thing stops working again. That's because the plugin uses its own code to handle the serve:bs
command. But it can also be fixed by changing the text to Starting the development server
in the ClientApp/node_modules/vue-cli-plugin-browser-sync/index.js
file.
Publishing
If you run the dotnet publish
command for the React app, you will see that the distribution version for the SPA is built to the build
directory in the ClientApp
. That also corresponds with this line in the Startup.cs
file:
services.AddSpaStaticFiles(configuration =>
{ configuration.RootPath = "ClientApp/build"; });
and this line in the csproj
file:
<DistFiles Include="$(SpaRoot)build\**; $(SpaRoot)build-ssr\**"/>
As you can see, it is very easy to fix by changing build
to dist
in both places. The build-ssr
part can be safely removed if you don't use the Server-Side Rendering. So, the code would be:
services.AddSpaStaticFiles(configuration =>
{ configuration.RootPath = "ClientApp/dist"; });
in the Startup.cs
and
<DistFiles Include="$(SpaRoot)build\**"/>
in the csproj
file.
When those changes are done, you can start developing and publishing your Vue SPA app hosted in the .NET Core web application service.
Shortcut
It is not nice to hack the code that run npm
commands for the Vue CLI, so you might want to use the complete code for the Vue development server middleware that I've composed from the React development server middleware. Unfortunately, many helper classes for the middleware are internal, so I had to include those classes as well. All that code has the Apache 2.0 licence so it is not a problem to use the modified version of it as soon as the origin of the code is stated clearly. Here is my gist. If you copy this file to your project, you can just use it:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseVueDevelopmentServer(npmScript: "serve"); // use serve:bs for Browser Sync
}
});
Top comments (4)
Nice post.
There are clearly 2 different use cases when it comes to integrating vuejs with asp.net.
Thanks for the feedback Paul :)
I was doing this when I was writing my first UI code for the book, so you can see the article as a side-effect of being involved in writing about DDD :)
I have no clear preference when it comes to choosing where the UI part of the application should live. I think in many cases it makes sense to have the whole app combined together. Speaking from DDD perspective, I'd rather have small apps that encapsulate the whole bounded context and combine such apps if necessary using UI composition in some kind of umbrella app.
The idea of my solution was to use Kestrel to host everything and when I build an app to a container, I just run the container and it all works.
Hi Alexey.
I think we share the same spirit about UI: make it possible in the most simple and best maintainable way.
My best bet is currently blazor. Having one code paradigm, being able to use the full refactoring features of a real typed language (typescript still "evaporates", as it is called), etc ... can be a huge benefit.
Wow, great work Alexey. Even more by giving the middleware extension.
I'll try to test it out later in a project.
Thanks for bringing it. :D