Просмотр исходного кода

Blazor Web can use `BasePath` component instead of `<base href="">` (#64590)

* Proposal of `BasePath` component.

* Unit tests.

* Feedback: give the component more coverage.

* Feedback: update our template.
Ilona Tomkowicz 3 месяцев назад
Родитель
Сommit
c8d47d32ea

+ 2 - 0
src/Components/Endpoints/src/PublicAPI.Unshipped.txt

@@ -1 +1,3 @@
 #nullable enable
+Microsoft.AspNetCore.Components.Endpoints.BasePath
+Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void

+ 46 - 0
src/Components/Endpoints/src/Routing/BasePath.cs

@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Endpoints;
+
+/// <summary>
+/// Renders a &lt;base&gt; element whose <c>href</c> value matches the current request path base.
+/// </summary>
+public sealed class BasePath : IComponent
+{
+    private RenderHandle _renderHandle;
+
+    [Inject]
+    private NavigationManager NavigationManager { get; set; } = default!;
+
+    void IComponent.Attach(RenderHandle renderHandle)
+    {
+        _renderHandle = renderHandle;
+    }
+
+    Task IComponent.SetParametersAsync(ParameterView parameters)
+    {
+        _renderHandle.Render(Render);
+        return Task.CompletedTask;
+    }
+
+    private void Render(RenderTreeBuilder builder)
+    {
+        builder.OpenElement(0, "base");
+        builder.AddAttribute(1, "href", ComputeHref());
+        builder.CloseElement();
+    }
+
+    private string ComputeHref()
+    {
+        var baseUri = NavigationManager.BaseUri;
+        if (Uri.TryCreate(baseUri, UriKind.Absolute, out var absoluteUri))
+        {
+            return absoluteUri.AbsolutePath;
+        }
+
+        return "/";
+    }
+}

+ 1 - 1
src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj

@@ -6,8 +6,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <Compile Include="..\..\Shared\test\AutoRenderComponent.cs" Link="TestComponents\AutoRenderComponent.cs" />
     <Compile Include="$(ComponentsSharedSourceRoot)src\WebRootComponentParameters.cs" Link="Shared\WebRootComponentParameters.cs" />
+    <Compile Include="$(ComponentsSharedSourceRoot)test\**\*.cs" LinkBase="Shared" />
   </ItemGroup>
 
   <ItemGroup>

+ 93 - 0
src/Components/Endpoints/test/Routing/BasePathTest.cs

@@ -0,0 +1,93 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Test.Helpers;
+
+#nullable enable
+
+namespace Microsoft.AspNetCore.Components.Endpoints;
+
+public class BasePathTest
+{
+    [Fact]
+    public void PreservesCasingFromNavigationManagerBaseUri()
+    {
+        _ = CreateServices(out var renderer, "https://example.com/Dashboard/");
+        var componentId = RenderBasePath(renderer);
+
+        Assert.Equal("/Dashboard/", GetHref(renderer, componentId));
+    }
+
+    [Theory]
+    [InlineData("https://example.com/a/b/", "/a/b/")]
+    [InlineData("https://example.com/a/b", "/a/")]
+    public void RendersBaseUriPathExactly(string baseUri, string expected)
+    {
+        _ = CreateServices(out var renderer, baseUri);
+
+        var componentId = RenderBasePath(renderer);
+
+        Assert.Equal(expected, GetHref(renderer, componentId));
+    }
+
+    private static TestServiceProvider CreateServices(out TestRenderer renderer, string baseUri = "https://example.com/app/")
+    {
+        var services = new TestServiceProvider();
+        var uri = baseUri.EndsWith('/') ? baseUri + "dashboard" : baseUri + "/dashboard";
+        var navigationManager = new TestNavigationManager(baseUri, uri);
+        services.AddService<NavigationManager>(navigationManager);
+        services.AddService<IServiceProvider>(services);
+
+        renderer = new TestRenderer(services);
+        return services;
+    }
+
+    private static int RenderBasePath(TestRenderer renderer)
+    {
+        var component = (BasePath)renderer.InstantiateComponent<BasePath>();
+        var componentId = renderer.AssignRootComponentId(component);
+        renderer.RenderRootComponent(componentId);
+        return componentId;
+    }
+
+    private static string? GetHref(TestRenderer renderer, int componentId)
+    {
+        var frames = renderer.GetCurrentRenderTreeFrames(componentId);
+        for (var i = 0; i < frames.Count; i++)
+        {
+            ref readonly var frame = ref frames.Array[i];
+            if (frame.FrameType == RenderTreeFrameType.Element && frame.ElementName == "base")
+            {
+                for (var j = i + 1; j < frames.Count; j++)
+                {
+                    ref readonly var attribute = ref frames.Array[j];
+                    if (attribute.FrameType == RenderTreeFrameType.Attribute && attribute.AttributeName == "href")
+                    {
+                        return attribute.AttributeValue?.ToString();
+                    }
+
+                    if (attribute.FrameType != RenderTreeFrameType.Attribute)
+                    {
+                        break;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private sealed class TestNavigationManager : NavigationManager
+    {
+        public TestNavigationManager(string baseUri, string uri)
+        {
+            Initialize(baseUri, uri);
+        }
+
+        protected override void NavigateToCore(string uri, bool forceLoad)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 1 - 1
src/Components/Samples/BlazorUnitedApp/App.razor

@@ -3,7 +3,7 @@
 <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <base href="/" />
+    <BasePath />
     <link rel="stylesheet" href="@Assets["css/bootstrap/bootstrap.min.css"]" />
     <link rel="stylesheet" href="@Assets["css/bootstrap-icons/bootstrap-icons.min.css"]" />
     <link rel="stylesheet" href="@Assets["css/site.css"]" />

+ 1 - 0
src/Components/Samples/BlazorUnitedApp/_Imports.razor

@@ -3,6 +3,7 @@
 @using Microsoft.AspNetCore.Components.Forms
 @using Microsoft.AspNetCore.Components.Routing
 @using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Endpoints
 @using Microsoft.AspNetCore.Components.Web.Virtualization
 @using Microsoft.JSInterop
 @using BlazorUnitedApp

+ 2 - 1
src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor

@@ -4,6 +4,7 @@
 @using TestContentPackage.NotFound
 @using Components.TestServer.RazorComponents
 @using Microsoft.AspNetCore.Components
+@using Microsoft.AspNetCore.Components.Endpoints
 @using Microsoft.AspNetCore.Components.Routing
 @using Microsoft.AspNetCore.Components.Web
 @using System.Threading.Tasks
@@ -99,7 +100,7 @@
 <html lang="en">
 <head>
     <meta charset="utf-8" />
-    <base href="/subdir/" />
+    <BasePath />
     <HeadOutlet />
 </head>
 <body>

+ 2 - 1
src/Components/test/testassets/Components.TestServer/RazorComponents/NamedFormContextNoFormContextApp.razor

@@ -1,10 +1,11 @@
 @using Components.TestServer.RazorComponents.Pages.Forms
+@using Microsoft.AspNetCore.Components.Endpoints
 
 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="utf-8" />
-    <base href="/subdir/" />
+    <BasePath />
     <HeadOutlet />
 </head>
 <body>

+ 4 - 2
src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor

@@ -1,9 +1,11 @@
-<!DOCTYPE html>
+@using Microsoft.AspNetCore.Components.Endpoints
+
+<!DOCTYPE html>
 <html lang="en">
 
 <head>
     <meta charset="utf-8" />
-    <base href="/subdir/" />
+    <BasePath />
 
     <HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
 </head>

+ 2 - 1
src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor

@@ -1,11 +1,12 @@
 @using Components.TestServer.RazorComponents.Pages.Forms
+@using Microsoft.AspNetCore.Components.Endpoints
 @using Microsoft.AspNetCore.Components.Web
 
 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="utf-8" />
-    <base href="/subdir/" />
+    <BasePath />
     <link rel="stylesheet" href="@Assets["Components.TestServer.styles.css"]" />
     <HeadOutlet />
 </head>

+ 1 - 1
src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/App.razor

@@ -4,7 +4,7 @@
 <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <base href="/" />
+    <BasePath />
     <ResourcePreloader />
     @*#if (SampleContent)
     <link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/_Imports.razor

@@ -1,5 +1,6 @@
 @using System.Net.Http
 @using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Endpoints
 @*#if (IndividualLocalAuth)
 @using Microsoft.AspNetCore.Components.Authorization
 ##endif*@