embedio
embedio copied to clipboard
Client-side routing support in FileModule
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:
- the URL path doesn't start with either
/api
or any other path served by "functional" modules, so the requests slips path them; - the request finally reaches the
FileModule
serving the app's static files; - neither
/dashboard
nor/dashboard.html
(in case there's aDefaultExtension
set) are found by the file provider; - error 404!
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_preference_here) 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
- If a
PreProcessPath
callback takes some time to complete, it can have a negative performance impact on theFileModule
it is attached to. I defined it as synchronous (it returnsstring
, notTask<string>
) so hopefully people won't get weird ideas, such as querying a database with a table of view names. - If EmbedIO gains more traction because of better support for SPAs, we may have to cope with even more issues being opened about
HttpListener
's shortcomings. OK, just kidding... maybe.
Good solution for SPAs from my point of view.
Nice job! Actually, I had to do almost the same by customizing WebModuleBase.
Hi all, question, actually is there a way to handle this case with the current version?