WebOptimizer
WebOptimizer copied to clipboard
Alternate way to get file names
Hi! Thanks for the excellent module! I am using it with .net 2.0 without any problem.
I have a question on alternate ways to access the hashed file name for javascript files. As I understand the only way currently is in the script tag. The module will automatically add the has when rendered. I have three use cases or js files that do not include script tags. Do you have any other ways to access the hashed file name?
-
Web workers How can I include one js file in another as a web worker and still have the hashed file name?
-
Dynamically adding scripts to webpage I am dynamically adding scripts to the page after is loads. Currently, I request an html file that includes the
-
Dynamically requested function files. Maybe I have an "on click" event with a 1000 line function. I go and get the function, if it doesn't already exist, only when the button is first clicked. How can I know what the hash is without requesting through my web app like with point 2?
If you have any ideas of alternate ways to get the hash I'd appreciate it!
Thank you! Christopher
Please let me know what you think -
I made a section where I can request a url (ex: /data/webopt?id=/js/main.min.js) and get the file name w/ hash, or send a request for the latest file contents (ex: /data/webopt?handler=content&id=/js/main.min.js). The response headers include the hash as an etag, hopefully to keep cache up to date. I don't know of another way to get and cache the results as the proper url w/out knowing the url before hand.. Although that probably won't work either as the browser doesn't know the new etag yet..?
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebOptimizer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Microsoft.Extensions.Options;
using System.Text;
namespace Data_Governance_WebApp.Pages.Data
{
public class JsModel : PageModel
{
private IAssetPipeline _pipeline;
private IAssetBuilder _assetBuilder;
private IOptionsSnapshot<WebOptimizerOptions> _options;
public JsModel(IMemoryCache cache, IAssetPipeline pipeline, IAssetBuilder assetBuilder, IOptionsSnapshot<WebOptimizerOptions> options)
{
_pipeline = pipeline;
_assetBuilder = assetBuilder;
_options = options;
}
protected string GenerateHash(IAsset asset)
{
string hash = asset.GenerateCacheKey(HttpContext);
return $"{asset.Route}?v={hash}";
}
public async Task<ActionResult> OnGetContentAsync(string id)
{
_pipeline.TryGetAssetFromRoute(id, out IAsset asset);
var src = GenerateHash(asset);
IAssetResponse response = await _assetBuilder.BuildAsync(asset, HttpContext, _options.Value);
IAssetResponse cachedResponse = response;
string cacheKey = response.CacheKey;
HttpContext.Response.ContentType = asset.ContentType;
foreach (string name in cachedResponse.Headers.Keys)
{
HttpContext.Response.Headers[name] = cachedResponse.Headers[name];
}
HttpContext.Response.Headers[HeaderNames.CacheControl] = $"max-age=31536000"; // 1 year
if (HttpContext.Request.Query.ContainsKey("v"))
{
HttpContext.Response.Headers[HeaderNames.CacheControl] += $",immutable";
}
HttpContext.Response.Headers[HeaderNames.ETag] = src.Split("v=")[1];
return Content(Encoding.UTF8.GetString(cachedResponse.Body, 0, cachedResponse.Body.Length));
}
public ActionResult OnGet(string id)
{
_pipeline.TryGetAssetFromRoute(id, out IAsset asset);
HttpContext.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
HttpContext.Response.Headers.Add("Pragma", "no-cache"); // HTTP 1.0.
HttpContext.Response.Headers.Add("Expires", "0"); // Proxies.
return Content(GenerateHash(asset));
}
}
}
unfortunately the etag doesn't seem to help w/ breaking the cache 😢
Also, my code is missing the piece to rebuild the asset if it changed.. maybe you have an easier way up your sleeve?
Did you manage to find a solution?
Hey @benjamin-stern, I'm doing something similar to #87. But I don't have a way to get the names entirely in another javascript file w/out using some sort of c# helper.
For example, I'm using gulp to build scss into css. In the scss I've included a few fonts.
@import '../../../node_modules/@fontsource/rasa/scss/mixins';
@include fontFace(
$fontName: Rasa,
$weight: 600,
$display: swap,
$unicodeMap: latin,
$fontDir: '/font/rasa/files/'
);
To preload this font in my layout.cshtml
@* at the top of my `layout.cshtml.` *@
@using Helpers
@using Microsoft.Extensions.Caching.Memory
@inject IMemoryCache Cache
<!-- other head stuffus -->
<link rel="preload" href="@Helpers.UrlHelpers.FontHash("/font/rasa/files/rasa-latin-600-normal.woff2", Cache)" as="font" type="font/woff2" crossorigin />
and the FontHash
help function:
// you prob don't need all of these... I have a lot of stuff going on in the helpers.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using WebOptimizer;
namespace Atlas_Web.Helpers
{
public static class PageExtensions
{
//https://github.com/ligershark/WebOptimizer/issues/87
public static string AddBundleVersionToPath(this IRazorPage page, string path)
{
var context = page.ViewContext.HttpContext;
var assetPipeline = context.RequestServices.GetService<WebOptimizer.IAssetPipeline>();
if (assetPipeline.TryGetAssetFromRoute(path, out IAsset asset))
{
return string.Concat(
path,
"?v=",
asset.GenerateCacheKey(context, new WebOptimizerOptions())
);
}
else
{
return path;
}
}
}
}
So in the output html, you can see the version # in there:
... which should be matching the version coming up in the css file :)
I do have a live demo of this on heroku free teir.. but ran out of build minutes until July 1. you can check it out then to see how the hash matches https://demo.atlas.bi
I will note that when running gulp watch and also rebuilding the site for development sometimes the hashes mismatch because of timing etc. But on production site where everything is built and started in order, it works well.
The source code is here: https://github.com/atlas-bi/atlas-bi-library/blob/master/web/Pages/Shared/_Layout.cshtml https://github.com/atlas-bi/atlas-bi-library/blob/master/web/Helpers/Extensions.cs
Hey @christopherpickering, Thanks for getting back to me so promptly.
I was unaware of https://github.com/ligershark/WebOptimizer/issues/87 so thank you for bringing it to my attention. I'm assuming the FontHash
Helper function you referenced calls upon the AddBundleVersionToPath
to retrieve the files' hash.
As I saw no documentation on how to call or embed the functionality prior to your comment, I went ahead and implemented an alternative solution.
I needed to get a reference to a JavaScript file that could be dynamically downloaded and initialized by my own custom script so I used a html link tag to generate the hashed file like it normally would for css
<link href="/*/filename.js" rel="preload" id="filename-script" type="text/javascript" as="script" />
and then retrieved the file path via the link's id filename-script
, as it is a supported attribute for the link tag, and then create a new script tag off the document that is dynamically appended.
In my case I needed to get a handle to initialize code so it may work out better for me, but I am glad for the example you've provided above and I may use it in other instances.
Many Thanks.
@benjamin-stern No problem 😄 you reminded me of something else I've done, but don't like at all... But I suppose anyone using razor pages has done something that they didn't like at all 😆
Since the hash just comes on certain tags (Link, script), I have wrapped tags that I wanted a hash on inside a text area, and then use javascript to get the file names out if I choose to load them later.
<textarea><script src="/js/shared.min.js"></script></textarea>
This gets the hash in there without processing the script in the browser and you can come back to it later.
Now I've updated to use the hash helper. For example like this, to load a script for non IE 11 browsers:
<script>
(function(){
try {
new Function('async () => {}')();
console.log("loading code highlight")
var element = document.createElement("script");
element.type = 'text/javascript';
element.src = "@this.AddBundleVersionToPath("/js/code.min.js")";
document.body.appendChild(element);
} catch (error) {
}
})()
</script>