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"))
{
// 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);
}

View File

@ -191,16 +191,13 @@
}
else
{
if (dataAttributes == null || !dataAttributes.ContainsKey("data-reload") || dataAttributes["data-reload"] != "false")
if (id == "")
{
if (id == "")
{
count += 1;
id = $"page{PageState.Page.PageId}-script{count}";
}
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
count += 1;
id = $"page{PageState.Page.PageId}-script{count}";
}
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
}
index = content.IndexOf("<script", index + 1);
}

View File

@ -514,7 +514,7 @@
private void AddScript(Resource resource, Alias 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))
{
@ -532,11 +532,27 @@
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 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)
{
foreach (var attribute in resource.DataAttributes)
@ -552,10 +568,6 @@
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>";
}
else
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
}
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) {
if (script.hasAttribute("src") && script.src !== "") {
if (script.src) {
return script.src;
} else if (script.id) {
return script.id;
} else {
return script.innerHTML;
}
}
export function onUpdate() {
let timestamp = Date.now();
let enhancedNavigation = scriptInfoBySrc.size !== 0;
// 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);
}
} else {
// existing script
scriptInfo.timestamp = timestamp;
if (script.hasAttribute("data-reload") && script.getAttribute("data-reload") === "true") {
reloadScript(script);
}
function reloadScript(script) {
try {
if (isValid(script)) {
replaceScript(script);
}
}
// remove scripts that are no longer referenced
for (const [key, scriptInfo] of scriptInfoBySrc) {
if (scriptInfo.timestamp !== timestamp) {
scriptInfoBySrc.delete(key);
} catch (error) {
if (script.src) {
console.error(`Script Reload failed to load external script: ${script.src}`, error);
} else {
console.error(`Script Reload failed to load inline script: ${script.innerHTML}`, error);
}
}
}
function reloadScript(script) {
try {
replaceScript(script);
} catch (error) {
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);
}
function isValid(script) {
if (script.innerHTML.includes('document.write(')) {
console.log(`Script using document.write() not supported by Script Reload: ${script.innerHTML}`);
return false;
}
return true;
}
function replaceScript(script) {
return new Promise((resolve, reject) => {
var newScript = document.createElement("script");
var newScript = document.createElement('script');
// replicate attributes and content
for (let i = 0; i < script.attributes.length; i++) {
newScript.setAttribute(script.attributes[i].name, script.attributes[i].value);
}
newScript.innerHTML = script.innerHTML;
newScript.removeAttribute('data-reload');
// dynamically injected scripts cannot be async or deferred
newScript.async = false;
@ -70,11 +77,10 @@ function replaceScript(script) {
newScript.onload = () => resolve();
newScript.onerror = (error) => reject(error);
// remove existing script
// remove existing script element
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);
});
}

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.Collections.Generic;
using System.Linq;
using Oqtane.Shared;
namespace Oqtane.Models
@ -13,7 +12,7 @@ namespace Oqtane.Models
private string _url;
/// <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>
public ResourceType ResourceType { get; set; }
@ -45,7 +44,7 @@ namespace Oqtane.Models
public string CrossOrigin { get; set; }
/// <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>
public string Bundle { get; set; }
@ -60,7 +59,7 @@ namespace Oqtane.Models
public ResourceLocation Location { get; set; }
/// <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>
public string Content { get; set; }
@ -70,9 +69,9 @@ namespace Oqtane.Models
public string RenderMode { get; set; }
/// <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>
public bool Reload { get; set; }
public ResourceLoadBehavior LoadBehavior { get; set; }
/// <summary>
/// Cusotm data-* attributes for scripts - not applicable to Stylesheets
@ -96,7 +95,7 @@ namespace Oqtane.Models
resource.Location = Location;
resource.Content = Content;
resource.RenderMode = RenderMode;
resource.Reload = Reload;
resource.LoadBehavior = LoadBehavior;
resource.DataAttributes = new Dictionary<string, string>();
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 Oqtane.Shared;
@ -27,6 +28,13 @@ namespace Oqtane.Models
this.Type = Type;
}
public Script(string Content, ResourceLoadBehavior LoadBehavior)
{
SetDefaults();
this.Content = Content;
this.LoadBehavior = LoadBehavior;
}
public Script(string Src, string Integrity, string CrossOrigin)
{
SetDefaults();
@ -35,6 +43,22 @@ namespace Oqtane.Models
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)
{
SetDefaults();
@ -45,9 +69,10 @@ namespace Oqtane.Models
this.Content = Content;
this.Location = Location;
this.Bundle = Bundle;
this.Reload = Reload;
this.LoadBehavior = (Reload) ? ResourceLoadBehavior.BlazorPageScript : ResourceLoadBehavior.Once;
this.DataAttributes = DataAttributes;
this.RenderMode = RenderMode;
}
}
}