feat(web): make webui work at any base path

This commit is contained in:
genteure 2022-08-26 18:56:46 +08:00
parent 8428ca286a
commit ca755d5ec5
7 changed files with 131 additions and 44 deletions

View File

@ -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));

View File

@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.17.1" />
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="4.8.0" />
@ -23,6 +24,7 @@
<PackageReference Include="GraphQL.SystemReactive" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.8" />
<PackageReference Include="NUglify" Version="1.20.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<IWebHostEnvironment>().WebRootFileProvider, sp.GetRequiredService<ManifestEmbeddedFileProvider>()));
// 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<ManifestEmbeddedFileProvider>();
var compositeFileProvider = app.ApplicationServices.GetRequiredService<CompositeFileProvider>();
var sharedStaticFiles = new SharedOptions()
{
// 在运行的 exe 旁边新建一个 wwwroot 文件夹,会优先使用里面的内容,然后 fallback 到打包的资源文件
FileProvider = new CompositeFileProvider(env.WebRootFileProvider, manifestEmbeddedFileProvider),
FileProvider = compositeFileProvider,
RedirectToAppendTrailingSlash = true,
};

View File

@ -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();
})()

@ -1 +1 @@
Subproject commit 08eeaf6789ba1e0825c7e5027c2fb8d3c9108207
Subproject commit 517c2a659f26f4fea088036650de502aa7f8621f