Skip to content

Commit 4af9efe

Browse files
authored
[Blazor] Generate Link headers based on StaticWebAssets manifest properties (#61166)
1 parent c3a578d commit 4af9efe

File tree

5 files changed

+269
-4
lines changed

5 files changed

+269
-4
lines changed

src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public void ApplyConvention(EndpointBuilder eb)
4949
if (_collection != null && _collectionImportMap != null)
5050
{
5151
eb.Metadata.Add(_collection);
52+
eb.Metadata.Add(new ResourcePreloadCollection(_collection));
5253

5354
if (_collectionUrl != null)
5455
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using System.Text;
6+
using Microsoft.Extensions.Primitives;
7+
8+
namespace Microsoft.AspNetCore.Components.Endpoints;
9+
10+
internal class ResourcePreloadCollection
11+
{
12+
private readonly Dictionary<string, StringValues> _storage = new();
13+
14+
public ResourcePreloadCollection(ResourceAssetCollection assets)
15+
{
16+
var headerBuilder = new StringBuilder();
17+
var headers = new Dictionary<string, List<(int Order, string Value)>>();
18+
foreach (var asset in assets)
19+
{
20+
if (asset.Properties == null)
21+
{
22+
continue;
23+
}
24+
25+
// Use preloadgroup property to identify assets that should be preloaded
26+
string? group = null;
27+
foreach (var property in asset.Properties)
28+
{
29+
if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase))
30+
{
31+
group = property.Value ?? string.Empty;
32+
break;
33+
}
34+
}
35+
36+
if (group == null)
37+
{
38+
continue;
39+
}
40+
41+
var header = CreateHeader(headerBuilder, asset.Url, asset.Properties);
42+
if (!headers.TryGetValue(group, out var groupHeaders))
43+
{
44+
groupHeaders = headers[group] = new List<(int Order, string Value)>();
45+
}
46+
47+
groupHeaders.Add(header);
48+
}
49+
50+
foreach (var group in headers)
51+
{
52+
_storage[group.Key ?? string.Empty] = group.Value.OrderBy(h => h.Order).Select(h => h.Value).ToArray();
53+
}
54+
}
55+
56+
private static (int order, string header) CreateHeader(StringBuilder headerBuilder, string url, IEnumerable<ResourceAssetProperty> properties)
57+
{
58+
headerBuilder.Clear();
59+
headerBuilder.Append('<');
60+
headerBuilder.Append(url);
61+
headerBuilder.Append('>');
62+
63+
int order = 0;
64+
foreach (var property in properties)
65+
{
66+
if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase))
67+
{
68+
headerBuilder.Append("; rel=").Append(property.Value);
69+
}
70+
else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase))
71+
{
72+
headerBuilder.Append("; as=").Append(property.Value);
73+
}
74+
else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase))
75+
{
76+
headerBuilder.Append("; fetchpriority=").Append(property.Value);
77+
}
78+
else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase))
79+
{
80+
headerBuilder.Append("; crossorigin=").Append(property.Value);
81+
}
82+
else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
83+
{
84+
headerBuilder.Append("; integrity=\"").Append(property.Value).Append('"');
85+
}
86+
else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase))
87+
{
88+
if (!int.TryParse(property.Value, out order))
89+
{
90+
order = 0;
91+
}
92+
}
93+
}
94+
95+
return (order, headerBuilder.ToString());
96+
}
97+
98+
public bool TryGetLinkHeaders(string group, out StringValues linkHeaders)
99+
=> _storage.TryGetValue(group, out linkHeaders);
100+
}

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Hosting;
1414
using Microsoft.Extensions.Options;
15+
using Microsoft.Extensions.Primitives;
1516

1617
namespace Microsoft.AspNetCore.Components.Endpoints;
1718

@@ -275,6 +276,12 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
275276
{
276277
if (_httpContext.RequestServices.GetRequiredService<WebAssemblySettingsEmitter>().TryGetSettingsOnce(out var settings))
277278
{
279+
if (marker.Type is ComponentMarker.WebAssemblyMarkerType)
280+
{
281+
// Preload WebAssembly assets when using WebAssembly (not Auto) mode
282+
AppendWebAssemblyPreloadHeaders();
283+
}
284+
278285
var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions);
279286
output.Write($"<!--Blazor-WebAssembly:{settingsJson}-->");
280287
}
@@ -311,6 +318,15 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
311318
}
312319
}
313320

321+
private void AppendWebAssemblyPreloadHeaders()
322+
{
323+
var preloads = _httpContext.GetEndpoint()?.Metadata.GetMetadata<ResourcePreloadCollection>();
324+
if (preloads != null && preloads.TryGetLinkHeaders("webassembly", out var linkHeaders))
325+
{
326+
_httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, linkHeaders);
327+
}
328+
}
329+
314330
private static bool IsProgressivelyEnhancedNavigation(HttpRequest request)
315331
{
316332
// For enhanced nav, the Blazor JS code controls the "accept" header precisely, so we can be very specific about the format

src/Components/Endpoints/test/EndpointHtmlRendererTest.cs

+72-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Net.Http;
54
using System.Text.Encodings.Web;
65
using System.Text.Json;
76
using System.Text.RegularExpressions;
@@ -71,6 +70,78 @@ public async Task CanRender_ParameterlessComponent_ClientMode()
7170
Assert.Empty(httpContext.Items);
7271
}
7372

73+
[Fact]
74+
public async Task CanPreload_WebAssembly_ResourceAssets()
75+
{
76+
// Arrange
77+
var httpContext = GetHttpContext();
78+
var writer = new StringWriter();
79+
80+
httpContext.SetEndpoint(
81+
new Endpoint(
82+
ctx => Task.CompletedTask,
83+
new EndpointMetadataCollection([
84+
new ResourcePreloadCollection(
85+
new ResourceAssetCollection([
86+
new ResourceAsset("second.js", [
87+
new ResourceAssetProperty("preloadrel", "preload"),
88+
new ResourceAssetProperty("preloadas", "script"),
89+
new ResourceAssetProperty("preloadpriority", "high"),
90+
new ResourceAssetProperty("preloadcrossorigin", "anonymous"),
91+
new ResourceAssetProperty("integrity", "abcd"),
92+
new ResourceAssetProperty("preloadorder", "2"),
93+
new ResourceAssetProperty("preloadgroup", "webassembly")
94+
]),
95+
new ResourceAsset("first.js", [
96+
new ResourceAssetProperty("preloadrel", "preload"),
97+
new ResourceAssetProperty("preloadas", "script"),
98+
new ResourceAssetProperty("preloadpriority", "high"),
99+
new ResourceAssetProperty("preloadcrossorigin", "anonymous"),
100+
new ResourceAssetProperty("integrity", "abcd"),
101+
new ResourceAssetProperty("preloadorder", "1"),
102+
new ResourceAssetProperty("preloadgroup", "webassembly")
103+
]),
104+
new ResourceAsset("preload-nowebassembly.js", [
105+
new ResourceAssetProperty("preloadrel", "preload"),
106+
new ResourceAssetProperty("preloadas", "script"),
107+
new ResourceAssetProperty("preloadpriority", "high"),
108+
new ResourceAssetProperty("preloadcrossorigin", "anonymous"),
109+
new ResourceAssetProperty("integrity", "abcd"),
110+
new ResourceAssetProperty("preloadorder", "1"),
111+
new ResourceAssetProperty("preloadgroup", "abcd")
112+
]),
113+
new ResourceAsset("nopreload.js", [
114+
new ResourceAssetProperty("integrity", "abcd")
115+
])
116+
])
117+
)
118+
]),
119+
"TestEndpoint"
120+
)
121+
);
122+
123+
// Act
124+
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty);
125+
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
126+
127+
// Assert
128+
Assert.Equal(2, httpContext.Response.Headers.Link.Count);
129+
130+
var firstPreloadLink = httpContext.Response.Headers.Link[0];
131+
Assert.Contains("<first.js>", firstPreloadLink);
132+
Assert.Contains("rel=preload", firstPreloadLink);
133+
Assert.Contains("as=script", firstPreloadLink);
134+
Assert.Contains("fetchpriority=high", firstPreloadLink);
135+
Assert.Contains("integrity=\"abcd\"", firstPreloadLink);
136+
137+
var secondPreloadLink = httpContext.Response.Headers.Link[1];
138+
Assert.Contains("<second.js>", secondPreloadLink);
139+
Assert.Contains("rel=preload", secondPreloadLink);
140+
Assert.Contains("as=script", secondPreloadLink);
141+
Assert.Contains("fetchpriority=high", secondPreloadLink);
142+
Assert.Contains("integrity=\"abcd\"", secondPreloadLink);
143+
}
144+
74145
[Fact]
75146
public async Task CanPrerender_ParameterlessComponent_ClientMode()
76147
{

src/Shared/Components/ResourceCollectionResolver.cs

+80-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,21 @@ public ResourceAssetCollection ResolveResourceCollection()
4242
#if !MVC_VIEWFEATURES
4343
string? label = null;
4444
string? integrity = null;
45+
string? preloadRel = null;
46+
string? preloadAs = null;
47+
string? preloadPriority = null;
48+
string? preloadCrossorigin = null;
49+
string? preloadOrder = null;
50+
string? preloadGroup = null;
4551
#else
4652
string label = null;
4753
string integrity = null;
54+
string preloadRel = null;
55+
string preloadAs = null;
56+
string preloadPriority = null;
57+
string preloadCrossorigin = null;
58+
string preloadOrder = null;
59+
string preloadGroup = null;
4860
#endif
4961

5062
// If there's a selector this means that this is an alternative representation for a resource, so skip it.
@@ -59,15 +71,44 @@ public ResourceAssetCollection ResolveResourceCollection()
5971
label = property.Value;
6072
foundProperties++;
6173
}
62-
6374
else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
6475
{
6576
integrity = property.Value;
6677
foundProperties++;
6778
}
79+
else if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase))
80+
{
81+
preloadRel = property.Value;
82+
foundProperties++;
83+
}
84+
else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase))
85+
{
86+
preloadAs = property.Value;
87+
foundProperties++;
88+
}
89+
else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase))
90+
{
91+
preloadPriority = property.Value;
92+
foundProperties++;
93+
}
94+
else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase))
95+
{
96+
preloadCrossorigin = property.Value;
97+
foundProperties++;
98+
}
99+
else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase))
100+
{
101+
preloadOrder = property.Value;
102+
foundProperties++;
103+
}
104+
else if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase))
105+
{
106+
preloadGroup = property.Value;
107+
foundProperties++;
108+
}
68109
}
69110

70-
AddResource(resources, descriptor, label, integrity, foundProperties);
111+
AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, preloadOrder, preloadGroup, foundProperties);
71112
}
72113
}
73114

@@ -97,11 +138,23 @@ private static void AddResource(
97138
#if !MVC_VIEWFEATURES
98139
string? label,
99140
string? integrity,
141+
string? preloadRel,
142+
string? preloadAs,
143+
string? preloadPriority,
144+
string? preloadCrossorigin,
145+
string? preloadOrder,
146+
string? preloadGroup,
100147
#else
101148
string label,
102149
string integrity,
150+
string preloadRel,
151+
string preloadAs,
152+
string preloadPriority,
153+
string preloadCrossorigin,
154+
string preloadOrder,
155+
string preloadGroup,
103156
#endif
104-
int foundProperties)
157+
int foundProperties)
105158
{
106159
if (label != null || integrity != null)
107160
{
@@ -115,6 +168,30 @@ private static void AddResource(
115168
{
116169
properties[index++] = new ResourceAssetProperty("integrity", integrity);
117170
}
171+
if (preloadRel != null)
172+
{
173+
properties[index++] = new ResourceAssetProperty("preloadrel", preloadRel);
174+
}
175+
if (preloadAs != null)
176+
{
177+
properties[index++] = new ResourceAssetProperty("preloadas", preloadAs);
178+
}
179+
if (preloadPriority != null)
180+
{
181+
properties[index++] = new ResourceAssetProperty("preloadpriority", preloadPriority);
182+
}
183+
if (preloadCrossorigin != null)
184+
{
185+
properties[index++] = new ResourceAssetProperty("preloadcrossorigin", preloadCrossorigin);
186+
}
187+
if (preloadOrder != null)
188+
{
189+
properties[index++] = new ResourceAssetProperty("preloadorder", preloadOrder);
190+
}
191+
if (preloadGroup != null)
192+
{
193+
properties[index++] = new ResourceAssetProperty("preloadgroup", preloadGroup);
194+
}
118195

119196
resources.Add(new ResourceAsset(descriptor.Route, properties));
120197
}

0 commit comments

Comments
 (0)