unosquare / embedio

A tiny, cross-platform, module based web server for .NET
http://unosquare.github.io/embedio
Other
1.46k stars 176 forks source link

Client-side routing support in FileModule #490

Open rdeago opened 3 years ago

rdeago commented 3 years ago

Background and motivation

As reported by @madnik7 on Slack, EmbedIO doesn't have good support for SPAs with client-side routing, i.e. where there's only one HTML "entry point" page mapping paths to views via Javascript code.

If users always visit the home page first, there's no problem, as when they click a link to, say /login, it gets processed on the client side and no request ever reaches the server. There's no actual login.html file to serve; instead, the client-side router just loads and activates a "login" view.

The problems start when a user wants to load a view directly. Say I save a bookmark to my /dashboard page and later open my browser and click it:

As of version 3.4.3, the only workaround is to provide an OnMappingFailed callback that redirects requests to /, so at least the user sees something (namely the home page) they can navigate from. This is clearly not enough: the user explicitly requested the /dashboard page, so that's what they should get.

Other servers (Apache, ASP.NET Core applications, _insert_your_preferencehere) have no problem handling this situation, but it's a show-stopper for EmbedIO.

Proposed enhancement

When the requested URL path maps to a view in a SPA, the "main" HTML file should be served with no redirection occurring whatsoever, so the client-side code can see the path and act accordingly.

Or, to reformulate in a less application-specific fashion: an EmbedIO application should be able to decide which path is actually requested to the Provider of a FileModule, based upon the HTTP context's RequestedPath (plus possibly other factors, e.g. context Items).

Implementation proposals

Add a PreProcessPath callback to FileModule, whose signature is define by the following delegate:

using System.Threading.Tasks;

namespace EmbedIO.Files
{
    /// <summary>
    /// A callback used to obtain the actual path to be served by a <see cref="FileModule"/>, based upon the requested path
    /// (and other context data if desired).
    /// </summary>
    /// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
    /// <returns>The path to serve.</returns>
    public delegate string FilePreProcessPathCallback(IHttpContext context);
}

The default callback should just return context.RequestedPath, thus replicating current behavior.

Usage examples

Let's assume we have a SPA with client-side routing, where paths (if not otherwise served by other modules) map to actual files if they have an extension, or to views if they have no extension. This is a rather common use case; other SPAs may have simpler rules (maybe an array of view names, if they are less than a dozen and not bound to change often) or more complex ones, but all it takes to support them is different code in the PreProcessPath callback.

Let's also assume that the SPA has a /404 view that shows an appropriate "not found" message.

var server = new WebServer(o => o
        .WithUrlPrefix(url)
        .WithMode(HttpListenerMode.EmbedIO))
    .WithLocalSessionManager()
    .WithWebApi("/api", m => m
        .WithController<MyWebApiController>())
    .WithModule(new MyWebSocketModule("/ws"))
    .WithStaticFolder("/", HtmlRootPath, true, m => m
        .WithContentCaching(UseFileCache)
        .WithPreProcessPath(ctx => {
            var path = ctx.RequestedPath;
            return string.IsNullOrEmpty(Path.GetExtension(path)) ? "/index.html" : path;
        }))
    .WithModule(new RedirectModule("/", "/404"));

Risks

maarlo commented 2 years ago

Good solution for SPAs from my point of view.

madnik7 commented 2 years ago

Nice job! Actually, I had to do almost the same by customizing WebModuleBase.

osnoser1 commented 2 years ago

Hi all, question, actually is there a way to handle this case with the current version?