Skip to content
This repository has been archived by the owner on Apr 8, 2020. It is now read-only.

Host spa(s) from paths off MVC app #1518

Closed
tbertenshaw opened this issue Feb 6, 2018 · 18 comments
Closed

Host spa(s) from paths off MVC app #1518

tbertenshaw opened this issue Feb 6, 2018 · 18 comments

Comments

@tbertenshaw
Copy link

tbertenshaw commented Feb 6, 2018

Is it possible to define the spa routes as paths off an existing mvc site?
I.e. I want all normal defined mvc routing to work as expected, however if the relative path off the root is <base href="/memberspa"> for example (defined in the index.html) then i want that to serve my spa. Currently it seems that if i hit a route that exists by a existing mvc controller then that it is served by the controller. But if i pass any route which doesn't exist it loads my spa. i.e. /thisdoesnotexist redirects to /memberspa. But /home (which is served by my home controller) correctly is rendered by the home/index method.

If this is possible then i would ideally want to have two separate spa's served from paths off the sites root. The other potentially being /admin .

I'm currently using the following in my startup.cs

   if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseSpaStaticFiles();
            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{action=Index}/{id?}");
            });

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";
             
                if (env.IsDevelopment())
                {
                    spa.UseAngularCliServer(npmScript: "start");
                }
            });
        }
@SteveSandersonMS
Copy link
Member

Please see docs about ASP.NET middleware, as it allows you to set up configurations like this. For example, app.Map lets you branch the middleware pipeline for a specific subpath. You could put the UseSpa call in one of those branches.

@tbertenshaw
Copy link
Author

tbertenshaw commented Feb 7, 2018

@SteveSandersonMS That works great, only thing is when i navigate to the relative path /memberspa the index.html loads as expected but it is unable to load the css and js as it is looking http://localhost:54715/styles.bundle.css for example rather than http://localhost:54715/memberspa/styles.bundle.css if i manually try and load these resources from this relative path they work. I cannot for the life of me workout how i change that.

#startup.cs

       public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseSpaStaticFiles();


            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{action=Index}/{id?}");
            });

            app.Map(new PathString("/memberspa"), appMember =>
             {
                 appMember .UseSpa(spa =>
                 {
                     spa.Options.SourcePath = "ClientApp";

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

        }

@SteveSandersonMS
Copy link
Member

the index.html loads as expected but it is unable to load the css and js as it is looking http://localhost:54715/styles.bundle.css for example rather than http://localhost:54715/memberspa/styles.bundle.css

Getting the client-side code to load the correct files is more of a question about your SPA framework. It looks like you're using Angular, so Angular docs would be the place to go. I think they make use of a <base> tag in the <head> to control this, though you'd need to check Angular docs to be sure. Hope that's OK!

@tbertenshaw
Copy link
Author

Yes its the vanilla angular template. I already modified the base tag prior to the app.Map change <base href="/memberspa"> on the index.html and once the resources are loaded the javascript navigation worked relative to that path. It just doesn't seem to load the initial resources from that relative path.

I'll see if i can find more details.

@tbertenshaw
Copy link
Author

tbertenshaw commented Feb 7, 2018

ok <base href="/memberspa/"> works. Still getting some errors. But the samples seem to be working ok.

image
If i paste http://localhost:54715/renewals/sockjs-node/info?t=1518020722410 into another tab i get a response. So seems that whatever is initiating that request isnt doing it relative to /renewals/

image

@tbertenshaw
Copy link
Author

created a repo to illustrate what i have done and to replicate the errors https://github.com/tbertenshaw/MultiSpa

@k11k2
Copy link

k11k2 commented Feb 14, 2018

Any of you help me to sort it. Like previous version we load angular components from .cshtml
<app-root></app-root>

for new angular 5 template , I'm unable make it possible. How could I achieve it. if not suggest good process.

@kdcllc
Copy link

kdcllc commented Apr 6, 2018

@tbertenshaw I made the path work by simply doing the following:

 app.UseStaticFiles();
            app.UseSpaStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

   spa.Options.SourcePath = "ClientApp";
  if (env.IsDevelopment())
  {
    spa.UseAngularCliServer(npmScript: "start");
   }

AND
Modify .angular-cli.json file:

 "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "baseHref": "/spa/",
      "assets": [
        "assets"
      ],

So my sample is running http://localhost:5000/spa

@tbertenshaw
Copy link
Author

tbertenshaw commented Apr 6, 2018

@kdcllc are you using the example repo? I've just checked and still getting the zonejs errors. Invested a heap of time trying to get this working. Even opened a bounty on stackoverflow and didn't get a resolution.

@Vx2gas
Copy link

Vx2gas commented May 1, 2018

@tbertenshaw Any updates to this? Is this working? Having a typical MVC site with a /spa section that is authorized is awesome.

@tbertenshaw
Copy link
Author

@Vx2gas nah couldn't get the issues i spotted resolved, and think the owners are treating this issue as closed and thus not seeing the comments.

@Vx2gas
Copy link

Vx2gas commented May 1, 2018

Thanks @tbertenshaw. @kdcllc anychance you can fork the repo to show us its working?

@kdcllc
Copy link

kdcllc commented May 2, 2018

@Vx2gas and @tbertenshaw I have created a git repo here https://github.com/kdcllc/aspnetcore-angular-realworld-example-app. Please take a look at the code that makes use of /spa url segment.

@Vx2gas
Copy link

Vx2gas commented May 7, 2018

@tbertenshaw I'm not sure what specific errors you are getting. When I pulled your repo down, everything seems to work just fine.

If I simply add the following, I can protect the members area inside of app.Map(new PathString("/members"), appRenewals =>

app.Use(async (context, next) =>
{
   var httpContext = (context as HttpContext);
   if (httpContext.User.Identity.IsAuthenticated)
   {
      Thread.CurrentPrincipal = httpContext.User;
      await next();
   }
   else
   {
      context.Response.Redirect("/account/login");
   }
});

@Vx2gas
Copy link

Vx2gas commented May 8, 2018

@tbertenshaw I would add, I can "hack" my way to solve the problem by doing the following:
ng serve --deploy-url /members/ --public-host http://localhost:50908/members/sockjs-node

@Vx2gas
Copy link

Vx2gas commented May 9, 2018

@tbertenshaw Here is the repo that I forked with it working. I removed all the testing stuff for simplicity to get to the bare bones. It also been updated to angular 6 etc...
https://github.com/Vx2gas/MultiSpa

@SteveSandersonMS I know you mentioned its an angular-cli issue... can you please double check with my repo and see why I have to do the ng serve --deploy-url /members/ --public-host http://localhost:50908/members/sockjs-node in the package.json

I just don't know enough on what is going on.

@chrisckc
Copy link

@tbertenshaw Did you ever get this working? It's easy enough to get this working in production mode where static files are served from 'ClientApp/dist' but to also get it to work properly in dev mode using SpaProxy requires a bit of trickery.

To resolve the request errors for '/sockjs_node/' you need to capture those requests outside of app.Map("path"...) and forward them to the SpaProxy. (i believe that port 4200 needs to be used for the Angular Spa template in place of 8080 below)

This is what i had to do in the middleware before app.Map("path"...), in my case i have my Spa running under the /app path.

// Captures Requests for the webpack dev server javascript files
// These requests are made outside of /app path so capture here
app.MapWhen(context => webPackDevServerMatcher(context), webpackDevServer => {
    webpackDevServer.UseSpa(spa => {
        spa.UseProxyToSpaDevelopmentServer(baseUri: "http://localhost:8080");
    });
});
        // Captures the requests generated when using webpack dev server in the following ways:
        // via: https://localhost:5001/app/
        // via: https://localhost:5001/webpack-dev-server/app/
        // captures requests like these:
        // https://localhost:5001/webpack_dev_server.js
        // https://localhost:5001/__webpack_dev_server__/live.bundle.js
        // wss://localhost:5001/sockjs-node/978/qhjp11ck/websocket
        private bool webPackDevServerMatcher(HttpContext context) {
            string pathString = context.Request.Path.ToString();
            return pathString.Contains(context.Request.PathBase.Add("/webpack-dev-server")) ||
                context.Request.Path.StartsWithSegments("/__webpack_dev_server__") ||
                context.Request.Path.StartsWithSegments("/sockjs-node");
        }

I will post a link to a working repo soon enough, it will be for an Aurelia app but it should also work fine for Angular as long as Angular is also configured to build and run for a matching sub-path.

@chrisckc
Copy link

chrisckc commented Nov 5, 2018

@tbertenshaw It is possible the change the websockets url which is used by the webpack-dev-server so that requests for /sockjs-node/ are made from a different base url, such as /app/sockjs-node/ but as far as i know this would require the Angular webpack config the be modified. To do that you would need to eject from the Angular CLI by running ng eject. I don't think that option is supported or provided in the latest version of Angular.

Here is my working repo which has Angular running from an /app/ sub-path off root:

https://github.com/chrisckc/DotNetCoreAngularHMR-Spa/tree/spa-served-from-sub-path

Checkout the 'spa-served-from-sub-path' branch.

It works in both Development and Production modes.

Here is the link to the stackoverflow thread referenced previously.
https://stackoverflow.com/questions/48684879/serve-angular-spa-pathed-off-the-root/53065094#53065094

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants