From ca755d5ec5b85447858a0496632efd1ae3db66fe Mon Sep 17 00:00:00 2001 From: genteure Date: Fri, 26 Aug 2022 18:56:46 +0800 Subject: [PATCH] feat(web): make webui work at any base path --- BililiveRecorder.Web/BasicAuthMiddleware.cs | 4 +- .../BililiveRecorder.Web.csproj | 2 + BililiveRecorder.Web/DynamicHtmlController.cs | 92 +++++++++++++++++++ BililiveRecorder.Web/IndexController.cs | 38 -------- BililiveRecorder.Web/Startup.cs | 9 +- .../embeded/.webui_injection.js | 28 ++++++ webui/source | 2 +- 7 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 BililiveRecorder.Web/DynamicHtmlController.cs delete mode 100644 BililiveRecorder.Web/IndexController.cs create mode 100644 BililiveRecorder.Web/embeded/.webui_injection.js diff --git a/BililiveRecorder.Web/BasicAuthMiddleware.cs b/BililiveRecorder.Web/BasicAuthMiddleware.cs index fe723fa..c9b6857 100644 --- a/BililiveRecorder.Web/BasicAuthMiddleware.cs +++ b/BililiveRecorder.Web/BasicAuthMiddleware.cs @@ -11,12 +11,12 @@ namespace BililiveRecorder.Web public class BasicAuthMiddleware { private readonly RequestDelegate next; - private readonly ManifestEmbeddedFileProvider fileProvider; + private readonly CompositeFileProvider fileProvider; private const string BasicAndSpace = "Basic "; private static string? Html401Page; - public BasicAuthMiddleware(RequestDelegate next, ManifestEmbeddedFileProvider fileProvider) + public BasicAuthMiddleware(RequestDelegate next, CompositeFileProvider fileProvider) { this.next = next ?? throw new ArgumentNullException(nameof(next)); this.fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); diff --git a/BililiveRecorder.Web/BililiveRecorder.Web.csproj b/BililiveRecorder.Web/BililiveRecorder.Web.csproj index ffb08de..1480ad3 100644 --- a/BililiveRecorder.Web/BililiveRecorder.Web.csproj +++ b/BililiveRecorder.Web/BililiveRecorder.Web.csproj @@ -10,6 +10,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/BililiveRecorder.Web/DynamicHtmlController.cs b/BililiveRecorder.Web/DynamicHtmlController.cs new file mode 100644 index 0000000..d12a583 --- /dev/null +++ b/BililiveRecorder.Web/DynamicHtmlController.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AngleSharp.Html; +using AngleSharp.Html.Parser; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.FileProviders; +using NUglify; + +namespace BililiveRecorder.Web +{ + [Controller] + [ApiExplorerSettings(IgnoreApi = true)] + public sealed class DynamicHtmlController : Controller + { + private static string? cachedIndexHtml; + private readonly CompositeFileProvider fileProvider; + + public DynamicHtmlController(CompositeFileProvider fileProvider) + { + this.fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); + } + + [Route("/", Name = "Home Page"), HttpGet] + public ActionResult GetHomePage() + { + if (cachedIndexHtml is null) + { + using var file = this.fileProvider.GetFileInfo("/index.html").CreateReadStream(); + using var reader = new StreamReader(file, Encoding.UTF8); + var html = reader.ReadToEnd(); + cachedIndexHtml = html + .Replace("__VERSION__", GitVersionInformation.FullSemVer) + .Replace("__FULL_VERSION__", GitVersionInformation.InformationalVersion) + ; + } + + return this.Content(cachedIndexHtml, "text/html", Encoding.UTF8); + } + + [Route("/ui/", Name = "WebUI Html"), HttpGet] + public async Task GetWebUIAsync() + { + var parser = new HtmlParser(); + var fileInfo = this.fileProvider.GetFileInfo("/ui/index.html"); + + using var injectionScriptStream = new StreamReader(this.fileProvider.GetFileInfo(".webui_injection.js").CreateReadStream()); + var scriptContent = await injectionScriptStream.ReadToEndAsync(); + + using var stream = fileInfo.CreateReadStream(); + using var document = await parser.ParseDocumentAsync(stream).ConfigureAwait(false); + + var spaPath = this.HttpContext.Items.ContainsKey("webui-spa-path") ? ((PathString)this.HttpContext.Items["webui-spa-path"]!) : this.Request.Path; + + spaPath.StartsWithSegments("/ui", out var remaining); + + var head = document.Head!; + var template = document.CreateElement("template"); + template.Id = "delayed-init"; + head.AppendChild(template); + + var scripts = document.QuerySelectorAll("script[type='module']"); + var css = document.QuerySelectorAll("link[rel='stylesheet']"); + + foreach (var script in scripts) + { + script.Remove(); + template.AppendChild(script); + } + + foreach (var node in css) + { + node.Remove(); + template.AppendChild(node); + } + + var initScript = document.CreateElement("script"); + initScript.TextContent = Uglify.Js(scriptContent).Code; + initScript.SetAttribute("data-href", remaining); + + head.AppendChild(initScript); + + this.Response.StatusCode = 200; + this.Response.ContentType = "text/html; encoding=utf-8"; + + using var writer = new StreamWriter(this.Response.Body); + document.ToHtml(writer, new MinifyMarkupFormatter()); + } + } +} diff --git a/BililiveRecorder.Web/IndexController.cs b/BililiveRecorder.Web/IndexController.cs deleted file mode 100644 index 27507b6..0000000 --- a/BililiveRecorder.Web/IndexController.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.IO; -using System.Text; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.FileProviders; - -namespace BililiveRecorder.Web -{ - [Controller, Route("/", Name = "Home Page")] - [ApiExplorerSettings(IgnoreApi = true)] - public sealed class IndexController : Controller - { - private static string? result; - private readonly ManifestEmbeddedFileProvider fileProvider; - - public IndexController(ManifestEmbeddedFileProvider fileProvider) - { - this.fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); - } - - [HttpGet] - public ActionResult Get() - { - if (result is null) - { - using var file = this.fileProvider.GetFileInfo("/index.html").CreateReadStream(); - using var reader = new StreamReader(file, Encoding.UTF8); - var html = reader.ReadToEnd(); - result = html - .Replace("__VERSION__", GitVersionInformation.FullSemVer) - .Replace("__FULL_VERSION__", GitVersionInformation.InformationalVersion) - ; - } - - return this.Content(result, "text/html", Encoding.UTF8); - } - } -} diff --git a/BililiveRecorder.Web/Startup.cs b/BililiveRecorder.Web/Startup.cs index 384806d..85ab159 100644 --- a/BililiveRecorder.Web/Startup.cs +++ b/BililiveRecorder.Web/Startup.cs @@ -74,7 +74,9 @@ namespace BililiveRecorder.Web ; })); - services.AddSingleton(new ManifestEmbeddedFileProvider(typeof(Startup).Assembly)); + services + .AddSingleton(new ManifestEmbeddedFileProvider(typeof(Startup).Assembly)) + .AddSingleton(sp => new CompositeFileProvider(sp.GetRequiredService().WebRootFileProvider, sp.GetRequiredService())); // Graphql API GraphQL.MicrosoftDI.GraphQLBuilderExtensions.AddGraphQL(services) @@ -152,6 +154,7 @@ namespace BililiveRecorder.Web { if (originalPath.StartsWithSegments("/ui")) { + context.Items["webui-spa-path"] = originalPath; context.Request.Path = "/ui/"; } else @@ -193,11 +196,11 @@ namespace BililiveRecorder.Web ctp.Mappings[".mjs"] = "text/javascript; charset=utf-8"; ctp.Mappings[".json"] = "application/json; charset=utf-8"; - var manifestEmbeddedFileProvider = app.ApplicationServices.GetRequiredService(); + var compositeFileProvider = app.ApplicationServices.GetRequiredService(); var sharedStaticFiles = new SharedOptions() { // 在运行的 exe 旁边新建一个 wwwroot 文件夹,会优先使用里面的内容,然后 fallback 到打包的资源文件 - FileProvider = new CompositeFileProvider(env.WebRootFileProvider, manifestEmbeddedFileProvider), + FileProvider = compositeFileProvider, RedirectToAppendTrailingSlash = true, }; diff --git a/BililiveRecorder.Web/embeded/.webui_injection.js b/BililiveRecorder.Web/embeded/.webui_injection.js new file mode 100644 index 0000000..8a439c0 --- /dev/null +++ b/BililiveRecorder.Web/embeded/.webui_injection.js @@ -0,0 +1,28 @@ +(function () { + const currentScript = document.currentScript; + if (currentScript && "string" === typeof currentScript.dataset.href) { + const SERVER_PATH = currentScript.dataset.href; + const baseTag = document.getElementsByTagName('base')[0]; + console.log("SERVER_PATH: " + SERVER_PATH); + const pathname = location.pathname; + console.log("location.pathname: " + pathname); + if (SERVER_PATH.length === 0) { + let BASE = pathname + '/'; + baseTag.href = BASE; + console.log("base path: " + BASE); + } else if (pathname.endsWith(SERVER_PATH)) { + var i = pathname.lastIndexOf(SERVER_PATH); + if (i > -1) { + let BASE = pathname.slice(0, i) + '/'; + baseTag.href = BASE; + console.log("base path: " + BASE); + } + } else { + console.log('????'); + } + } + const init = document.getElementById('delayed-init'); + document.head.append(init.content.cloneNode(true)); + init.remove(); + currentScript.remove(); +})() diff --git a/webui/source b/webui/source index 08eeaf6..517c2a6 160000 --- a/webui/source +++ b/webui/source @@ -1 +1 @@ -Subproject commit 08eeaf6789ba1e0825c7e5027c2fb8d3c9108207 +Subproject commit 517c2a659f26f4fea088036650de502aa7f8621f