How to host multiple React SPA apps on a single ASP.NET Core site?
Asked Answered
I

3

9

I'm trying to run multiple React SPA apps using ASP.NET Core 3.1 with the lastest SpaServices extension and have a problem serving static assets. Much of what I've built comes from a similar question at: How to configure ASP.net Core server routing for multiple SPAs hosted with SpaServices but that was related to Angular and an older version of SpaServices.

My code has two React apps, ClientApp and AdminApp and I have configured each using the following startup code:

app.Map("/client", clientApp =>
{
    clientApp.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseReactDevelopmentServer(npmScript: "start");
        }
    });
});

app.Map("/admin", adminApp =>
{
    adminApp.UseSpa(spa =>
    {
        spa.Options.SourcePath = "AdminApp";

        if (env.IsDevelopment())
        {
            spa.UseReactDevelopmentServer(npmScript: "start");
        }
    });
});

Going to the /client and /admin routes serves the correct React index.html file, but the linked bundled .js files do not load. Here's an example of the final index HTML for the ClientApp SPA:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <base href="/client" />
    <title>Client App</title>
  </head>
  <body>
    <h1>Client App</h1>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
    <script src="/static/js/0.chunk.js"></script>
    <script src="/static/js/main.chunk.js"></script>
  </body>
</html>

The problem is that the links are relative to the root of the site /static/js/bundle.js and need to be relative to the React app's path. The example file paths should be /client/static/js/bundle.js and /admin/static/js/bundle.js, respectively.

How do I get the system that writes the paths into the index.html file to use the correct root path for the static files? Thanks.

Illogicality answered 22/9, 2020 at 16:18 Comment(3)
did you make any progress on this? I'm working through the same challenge and found that adding the setting of the homepage property in package.json did add the new path for the script src paths. But now seeing uncaught syntaxerror logs in the console.Swanee
Adding the homepage property works, but only in the published output. It doesn't work when debugging, which means it is pointless for me. I ended up building a separate admin site unfortunately.Illogicality
Here in 2021, still haven't found a way to host both development environments with each SPA in a good way (with hot-reloading). The best solution I came up with is to build each react project so I could point to the prod build directories of each app. Which does defeat the purpose. Still haven't been able to get a proxy setup working just yet either.Chkalov
S
5

Come up with a possible solution. The expectation is that the build SPA will have the contents of its build directory copied into a folder in the wwwroot folder of the .NET Core project that uses the same name as the route the SPA will use. In this case we have two apps, guestuser and payinguser. The mapping in the Configure function will redirect the user request to pull the static files out of the appropriate wwwroot folder.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
   ...
   services.AddSpaStaticFiles();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...

   app.UseStaticFiles(new StaticFileOptions()
   {
       FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"))
   });

   app.Map("/guestuser", mappedSpa=>
   {
       mappedSpa.UseSpa(spa =>
       {
           spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions()
           {
               FileProvider =
                   new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/guestuser"))
           };
           spa.Options.SourcePath = "wwwroot/guestuser";
       });
   });

   app.Map("/payinguser", mappedSpa=>
   {
       mappedSpa.UseSpa(spa =>
       {
           spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions()
           {
               FileProvider =
                   new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/payinguser"))
           };
           spa.Options.SourcePath = "wwwroot/payinguser";
       });
   });
}

Within the ReactJS project you will want to update/add the homepage property to the package.json file for each project to also use the same name as the folder the site will be hosted in under the wwwroot folder. This will update the paths of the generated code to use the pathname in their file references.

{
  "name": "guest-user",
  "homepage": "guestuser",
  ...
}
Swanee answered 7/12, 2020 at 20:54 Comment(0)
H
1

I've come up with a solution that's working for me, based on the answer by @cminus.

First, I created this helper function, where codeFolderPath is the where my app folder is, and spaPath is the directory where I want the application hosted:

private void EnableReactApp(IApplicationBuilder app, IWebHostEnvironment env, string codeFolderPath, string spaPath)
        {
            if (env.IsDevelopment())
            {
                app.MapWhen(y => y.Request.Path.StartsWithSegments(spaPath) ||
                                 y.Request.Path.StartsWithSegments("/sockjs-node") ||
                                 y.Request.Path.StartsWithSegments("/static"), client =>
                {
                    client.UseSpa(spa =>
                    {
                        spa.Options.SourcePath = codeFolderPath;
                        spa.UseReactDevelopmentServer(npmScript: "start");
                    });
                });
            }

            else
            {
                app.Map(new PathString(spaPath), client =>
                {
                    client.UsePathBase(new PathString(spaPath));
                    client.UseSpaStaticFiles();
                    client.UseSpa(spa => {
                       spa.Options.SourcePath = codeFolderPath;});
                });
            }
        }

Then, I add the following lines to my Startup.cs:

        public void ConfigureServices(IServiceCollection services)
        {
            [...]

            services.AddSpaStaticFiles(configuration => { configuration.RootPath = "SpaApp1/build"; });
            services.AddSpaStaticFiles(configuration => { configuration.RootPath = "SpaApp2/build"; });

            [...]
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            [...]

            EnableReactApp(app, env, "SpaApp1", "/admin");
            EnableReactApp(app, env, "SpaApp2", "/sandbox");

            [...]
        }
Hoang answered 29/3, 2023 at 22:19 Comment(0)
S
0

What if I need to remain SpaApp1 hosted at / instead of /admin. Because if I changed /admin to / in the code above, the compile returns an exception saying that the path value should not end with /.

Sibell answered 7/7, 2023 at 12:21 Comment(1)
This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. To get notified when this question gets new answers, you can follow this question. Once you have enough reputation, you can also add a bounty to draw more attention to this question. - From ReviewUltrasound

© 2022 - 2024 — McMap. All rights reserved.