Steps to integrate React SPA application with ASP.NET Core

Time:2022-9-15

The UI of AgileConfig using react is almost complete. Last time I got the jwt-based login mode (AntDesign Pro + .NET Core implements JWT-based login authentication), but there are still some problems. Now rewritten using react, agileconfig has become a real front-end and back-end separation project. So in fact, if you deploy it, you need to deploy it in two sites. The static content built on the front end is deployed on a website, and the server side is also deployed on a site. Then modify the baseURL of the front end so that the api requests of the spa all point to the server's website.
It's not impossible to do this, but it's not in the spirit of AgileConfig, which is simplicity. The asp.net core program itself is actually an http server, so the spa website can be used to host it. In this way, only one site needs to be deployed to run the spa and the backend server at the same time.
In fact, the easiest way is to throw all the built files under the wwwroot folder. Then visit:

http://localhost:5000/index.html

But this way our entry is index.html, which looks awkward and not friendly enough. Moreover, these files are directly thrown into the root directory of wwwroot, which will be mixed with other js, css and other content of the website, which is also very confusing.
Then we will solve these two files below. There are two goals we want to achieve:

  1. The entry path of spa is friendly, such as http://localhost:5000/ui
  2. The directory where spa static files are stored is independent, such as in the wwwroot/ui folder, or in other directories.

To achieve the above, all you need is a custom middleware.

wwwroot\ui

wwwroot\ui

We copy all the built static files to the wwwroot\ui folder to distinguish them from other static resources. Of course, you can also put it in any directory, as long as it can be read.

ReactUIMiddleware

namespace AgileConfig.Server.Apisite.UIExtension
{
    public class ReactUIMiddleware
    {
        private static Dictionary<string, string> _contentTypes = new Dictionary<string, string>
        {
            {".html", "text/html; charset=utf-8"},
            {".css", "text/css; charset=utf-8"},
            {".js", "application/javascript"},
            {".png", "image/png"},
            {".svg", "image/svg+xml"},
            { ".json","application/json;charset=utf-8"},
            { ".ico","image/x-icon"}
        };
        private static ConcurrentDictionary<string, byte[]> _staticFilesCache = new ConcurrentDictionary<string, byte[]>();
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;
        public ReactUIMiddleware(
           RequestDelegate next,
           ILoggerFactory loggerFactory
       )
        {
            _next = next;
            _logger = loggerFactory.
                CreateLogger<ReactUIMiddleware>();
        }

        private bool ShouldHandleUIRequest(HttpContext context)
        {
            return context.Request.Path.HasValue && context.Request.Path.Value.Equals("/ui", StringComparison.OrdinalIgnoreCase);
        }

        private bool ShouldHandleUIStaticFilesRequest(HttpContext context)
        {
            //The requested Referer is 0.0.0.0/ui, based on which to judge whether it is a static file required by reactui
            if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("."))
            {
                context.Request.Headers.TryGetValue("Referer", out StringValues refererValues);
                if (refererValues.Any())
                {
                    var refererValue = refererValues.First();
                    if (refererValue.EndsWith("/ui", StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        public async Task Invoke(HttpContext context)
        {
            const string uiDirectory = "wwwroot/ui";
            //handle /ui request
            var filePath = "";
            if (ShouldHandleUIRequest(context))
            {
                filePath = uiDirectory + "/index.html";
            }
            //handle static files that Referer = xxx/ui
            if (ShouldHandleUIStaticFilesRequest(context))
            {
                filePath = uiDirectory + context.Request.Path;
            }

            if (string.IsNullOrEmpty(filePath))
            {
                await _next(context);
            }
            else
            {
                //output the file bytes

                if (!File.Exists(filePath))
                {
                    context.Response.StatusCode = 404;
                    return;
                }

                context.Response.OnStarting(() =>
                {
                    var extType = Path.GetExtension(filePath);
                    if (_contentTypes.TryGetValue(extType, out string contentType))
                    {
                        context.Response.ContentType = contentType;
                    }
                    return Task.CompletedTask;
                });

                await context.Response.StartAsync();

                byte[] fileData = null;
                if (_staticFilesCache.TryGetValue(filePath, out byte[] outfileData))
                {
                    fileData = outfileData;
                }
                else
                {
                    fileData = await File.ReadAllBytesAsync(filePath);
                    _staticFilesCache.TryAdd(filePath, fileData);
                }
                await context.Response.BodyWriter.WriteAsync(fileData);

                return;
            }
        }
    }
}

Briefly explain the idea of ​​​​this middleware. The logic of this middleware is probably the component part.
1. Intercept the request whose path is /ui, directly read the content of the static file index.html from the ui folder and output it, which is equivalent to directly accessing /index.html. But such path form looks more friendly.
2. Intercept static resource files required by react spa, such as css files, js files, etc. It is more troublesome here, because when spa pulls static files, the path starts directly from the root of the website, such as http://localhost:5000/xxx.js, so how to distinguish that this file is needed by react spa? Let's judge the Referer header of the request. If the path of the Referer is /ui, then it means that it is a static resource required by react spa, which is also read from the ui folder.
It is also necessary to set the specified contentType for each response, otherwise the browser cannot accurately identify the resource.


   public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseMiddleware<ExceptionHandlerMiddleware>();
            }
            app.UseMiddleware<ReactUIMiddleware>();
        
        ...
        ...

        }

Use this middleware inside the Configure method of the Startup class. So our transformation is almost complete.

run it

Visit http://localhost:5000/ui to see that the spa is loaded successfully.

Summarize

In order to allow asp.net core to host the react spa application, we use a middleware for interception. When accessing the corresponding path, the static resources are read from the local folder and returned to the browser, so as to complete the loading of the resources required by the spa. This time I use react spa to demonstrate, in fact, it is the same operation to switch to any spa application.
The code is here:ReactUIMiddleware

The above is the detailed content of the steps of integrating React SPA application with ASP.NET Core. For more information on integrating React SPA with ASP.NET Core, please pay attention to other related articles on developpaer!