Hacking the ASP.NET Core React SPA template for Vue.js

alexeyzimarev

Alexey Zimarev

Posted on January 26, 2019

Hacking the ASP.NET Core React SPA template for Vue.js

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
    }
});
💖 💪 🙅 🚩
alexeyzimarev
Alexey Zimarev

Posted on January 26, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related