script reload improvements

This commit is contained in:
sbwalker 2025-01-16 14:06:13 -05:00
parent 4630ee6e93
commit 0204ff8dd5
7 changed files with 129 additions and 67 deletions

View File

@ -70,7 +70,7 @@
if (!script.Contains("><") && !script.Contains("data-reload")) if (!script.Contains("><") && !script.Contains("data-reload"))
{ {
// add data-reload attribute to inline script // add data-reload attribute to inline script
headcontent = headcontent.Replace(script, script.Replace("<script", "<script data-reload=\"true\"")); headcontent = headcontent.Replace(script, script.Replace("<script", "<script data-reload=\"always\""));
} }
index = headcontent.IndexOf("<script", index + 1); index = headcontent.IndexOf("<script", index + 1);
} }

View File

@ -190,8 +190,6 @@
scripts.Add(new { href = src, type = type, bundle = "", integrity = integrity, crossorigin = crossorigin, location = location.ToString().ToLower(), dataAttributes = dataAttributes }); scripts.Add(new { href = src, type = type, bundle = "", integrity = integrity, crossorigin = crossorigin, location = location.ToString().ToLower(), dataAttributes = dataAttributes });
} }
else else
{
if (dataAttributes == null || !dataAttributes.ContainsKey("data-reload") || dataAttributes["data-reload"] != "false")
{ {
if (id == "") if (id == "")
{ {
@ -201,7 +199,6 @@
var pos = script.IndexOf(">") + 1; var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes); await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
} }
}
index = content.IndexOf("<script", index + 1); index = content.IndexOf("<script", index + 1);
} }
if (scripts.Any()) if (scripts.Any())

View File

@ -514,7 +514,7 @@
private void AddScript(Resource resource, Alias alias) private void AddScript(Resource resource, Alias alias)
{ {
var script = CreateScript(resource, alias); var script = CreateScript(resource, alias);
if (resource.Location == Shared.ResourceLocation.Head && !resource.Reload) if (resource.Location == Shared.ResourceLocation.Head && resource.LoadBehavior != ResourceLoadBehavior.BlazorPageScript)
{ {
if (!_headResources.Contains(script)) if (!_headResources.Contains(script))
{ {
@ -532,11 +532,27 @@
private string CreateScript(Resource resource, Alias alias) private string CreateScript(Resource resource, Alias alias)
{ {
if (!resource.Reload) if (resource.LoadBehavior == ResourceLoadBehavior.BlazorPageScript)
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
else
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
var dataAttributes = ""; var dataAttributes = "";
if (!resource.DataAttributes.ContainsKey("data-reload"))
{
switch (resource.LoadBehavior)
{
case ResourceLoadBehavior.Once:
dataAttributes += " data-reload=\"once\"";
break;
case ResourceLoadBehavior.Always:
dataAttributes += " data-reload=\"always\"";
break;
}
}
if (resource.DataAttributes != null && resource.DataAttributes.Count > 0) if (resource.DataAttributes != null && resource.DataAttributes.Count > 0)
{ {
foreach (var attribute in resource.DataAttributes) foreach (var attribute in resource.DataAttributes)
@ -552,10 +568,6 @@
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") + ((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>"; "></script>";
} }
else
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
} }
private void SetLocalizationCookie(string cookieValue) private void SetLocalizationCookie(string cookieValue)

View File

@ -1,67 +1,74 @@
const scriptInfoBySrc = new Map(); const scriptKeys = new Set();
export function onUpdate() {
// determine if this is an enhanced navigation
let enhancedNavigation = scriptKeys.size !== 0;
// iterate over all script elements in document
const scripts = document.getElementsByTagName('script');
for (const script of Array.from(scripts)) {
// only process scripts that include a data-reload attribute
if (script.hasAttribute('data-reload')) {
let key = getKey(script);
if (enhancedNavigation) {
// reload the script if data-reload is always or if the script has not been loaded previously and data-reload is once
let dataReload = script.getAttribute('data-reload');
if (dataReload === 'always' || (!scriptKeys.has(key) && dataReload == 'once')) {
reloadScript(script);
}
}
// save the script key
if (!scriptKeys.has(key)) {
scriptKeys.add(key);
}
}
}
}
function getKey(script) { function getKey(script) {
if (script.hasAttribute("src") && script.src !== "") { if (script.src) {
return script.src; return script.src;
} else if (script.id) {
return script.id;
} else { } else {
return script.innerHTML; return script.innerHTML;
} }
} }
export function onUpdate() { function reloadScript(script) {
let timestamp = Date.now(); try {
let enhancedNavigation = scriptInfoBySrc.size !== 0; if (isValid(script)) {
replaceScript(script);
// iterate over all script elements in page
const scripts = document.getElementsByTagName("script");
for (const script of Array.from(scripts)) {
let key = getKey(script);
let scriptInfo = scriptInfoBySrc.get(key);
if (!scriptInfo) {
// new script added
scriptInfo = { timestamp: timestamp };
scriptInfoBySrc.set(key, scriptInfo);
if (enhancedNavigation) {
reloadScript(script);
} }
} catch (error) {
if (script.src) {
console.error(`Script Reload failed to load external script: ${script.src}`, error);
} else { } else {
// existing script console.error(`Script Reload failed to load inline script: ${script.innerHTML}`, error);
scriptInfo.timestamp = timestamp;
if (script.hasAttribute("data-reload") && script.getAttribute("data-reload") === "true") {
reloadScript(script);
}
}
}
// remove scripts that are no longer referenced
for (const [key, scriptInfo] of scriptInfoBySrc) {
if (scriptInfo.timestamp !== timestamp) {
scriptInfoBySrc.delete(key);
} }
} }
} }
function reloadScript(script) { function isValid(script) {
try { if (script.innerHTML.includes('document.write(')) {
replaceScript(script); console.log(`Script using document.write() not supported by Script Reload: ${script.innerHTML}`);
} catch (error) { return false;
if (script.hasAttribute("src") && script.src !== "") {
console.error("Failed to load external script: ${script.src}", error);
} else {
console.error("Failed to load inline script: ${script.innerHtml}", error);
}
} }
return true;
} }
function replaceScript(script) { function replaceScript(script) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var newScript = document.createElement("script"); var newScript = document.createElement('script');
// replicate attributes and content // replicate attributes and content
for (let i = 0; i < script.attributes.length; i++) { for (let i = 0; i < script.attributes.length; i++) {
newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); newScript.setAttribute(script.attributes[i].name, script.attributes[i].value);
} }
newScript.innerHTML = script.innerHTML; newScript.innerHTML = script.innerHTML;
newScript.removeAttribute('data-reload');
// dynamically injected scripts cannot be async or deferred // dynamically injected scripts cannot be async or deferred
newScript.async = false; newScript.async = false;
@ -70,11 +77,10 @@ function replaceScript(script) {
newScript.onload = () => resolve(); newScript.onload = () => resolve();
newScript.onerror = (error) => reject(error); newScript.onerror = (error) => reject(error);
// remove existing script // remove existing script element
script.remove(); script.remove();
// replace with new script to force reload in Blazor // replace with new script element to force reload in Blazor
document.head.appendChild(newScript); document.head.appendChild(newScript);
}); });
} }

View File

@ -0,0 +1,10 @@
namespace Oqtane.Shared
{
public enum ResourceLoadBehavior
{
Once,
Always,
Never,
BlazorPageScript
}
}

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Models namespace Oqtane.Models
@ -13,7 +12,7 @@ namespace Oqtane.Models
private string _url; private string _url;
/// <summary> /// <summary>
/// A <see cref="ResourceType"/> so the Interop can properly create `script` or `link` tags /// A <see cref="ResourceType"/> to define the type of resource ie. Script or Stylesheet
/// </summary> /// </summary>
public ResourceType ResourceType { get; set; } public ResourceType ResourceType { get; set; }
@ -45,7 +44,7 @@ namespace Oqtane.Models
public string CrossOrigin { get; set; } public string CrossOrigin { get; set; }
/// <summary> /// <summary>
/// For Scripts a Bundle can be used to identify dependencies and ordering in the script loading process /// For Scripts a Bundle can be used to identify dependencies and ordering in the script loading process (for Interactive rendering only)
/// </summary> /// </summary>
public string Bundle { get; set; } public string Bundle { get; set; }
@ -60,7 +59,7 @@ namespace Oqtane.Models
public ResourceLocation Location { get; set; } public ResourceLocation Location { get; set; }
/// <summary> /// <summary>
/// Allows specification of inline script - not applicable to Stylesheets /// For Scripts this allows for the specification of inline script - not applicable to Stylesheets
/// </summary> /// </summary>
public string Content { get; set; } public string Content { get; set; }
@ -70,9 +69,9 @@ namespace Oqtane.Models
public string RenderMode { get; set; } public string RenderMode { get; set; }
/// <summary> /// <summary>
/// Indicates that a script should be reloaded on every page transition - not applicable to Stylesheets /// Specifies how a script should be loaded in Static rendering - not applicable to Stylesheets
/// </summary> /// </summary>
public bool Reload { get; set; } public ResourceLoadBehavior LoadBehavior { get; set; }
/// <summary> /// <summary>
/// Cusotm data-* attributes for scripts - not applicable to Stylesheets /// Cusotm data-* attributes for scripts - not applicable to Stylesheets
@ -96,7 +95,7 @@ namespace Oqtane.Models
resource.Location = Location; resource.Location = Location;
resource.Content = Content; resource.Content = Content;
resource.RenderMode = RenderMode; resource.RenderMode = RenderMode;
resource.Reload = Reload; resource.LoadBehavior = LoadBehavior;
resource.DataAttributes = new Dictionary<string, string>(); resource.DataAttributes = new Dictionary<string, string>();
if (DataAttributes != null && DataAttributes.Count > 0) if (DataAttributes != null && DataAttributes.Count > 0)
{ {
@ -125,5 +124,18 @@ namespace Oqtane.Models
}; };
} }
} }
[Obsolete("Reload is deprecated. Use LoadBehavior property instead for scripts.", false)]
public bool Reload
{
get => (LoadBehavior == ResourceLoadBehavior.BlazorPageScript);
set
{
if (value)
{
LoadBehavior = ResourceLoadBehavior.BlazorPageScript;
};
}
}
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Shared; using Oqtane.Shared;
@ -27,6 +28,13 @@ namespace Oqtane.Models
this.Type = Type; this.Type = Type;
} }
public Script(string Content, ResourceLoadBehavior LoadBehavior)
{
SetDefaults();
this.Content = Content;
this.LoadBehavior = LoadBehavior;
}
public Script(string Src, string Integrity, string CrossOrigin) public Script(string Src, string Integrity, string CrossOrigin)
{ {
SetDefaults(); SetDefaults();
@ -35,6 +43,22 @@ namespace Oqtane.Models
this.CrossOrigin = CrossOrigin; this.CrossOrigin = CrossOrigin;
} }
public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, ResourceLoadBehavior LoadBehavior, Dictionary<string, string> DataAttributes, string RenderMode)
{
SetDefaults();
this.Url = Src;
this.Integrity = Integrity;
this.CrossOrigin = CrossOrigin;
this.Type = Type;
this.Content = Content;
this.Location = Location;
this.Bundle = Bundle;
this.LoadBehavior = LoadBehavior;
this.DataAttributes = DataAttributes;
this.RenderMode = RenderMode;
}
[Obsolete("This constructor is deprecated. Use constructor with LoadBehavior parameter instead.", false)]
public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, bool Reload, Dictionary<string, string> DataAttributes, string RenderMode) public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, bool Reload, Dictionary<string, string> DataAttributes, string RenderMode)
{ {
SetDefaults(); SetDefaults();
@ -45,9 +69,10 @@ namespace Oqtane.Models
this.Content = Content; this.Content = Content;
this.Location = Location; this.Location = Location;
this.Bundle = Bundle; this.Bundle = Bundle;
this.Reload = Reload; this.LoadBehavior = (Reload) ? ResourceLoadBehavior.BlazorPageScript : ResourceLoadBehavior.Once;
this.DataAttributes = DataAttributes; this.DataAttributes = DataAttributes;
this.RenderMode = RenderMode; this.RenderMode = RenderMode;
} }
} }
} }