How to minify JavaScript inside a view page's script block with minimal effort?
I have some page specific scripts that would like to put on specific view pages. But the ASP.NET MVC4 bundling and minification only works with script files, not script code inside a view page.
UPDATE
I took Sohnee's advice to extract the scripts into files. But I need to use them on specific pages so what I end up doing is:
on layout page, i created an optional section for page specific javascript block:
#RenderSection("js", required: false)
</body>
then in the view page, let's say Index.cshtml, i render the script section like such:
#section js{
#Scripts.Render("~/bundles/js/" + Path.GetFileNameWithoutExtension(this.VirtualPath))
}
as you can see, it assumes the javascript filename (index.js) is the same as the view page name (index.cshtml). then in the bundle config, i have:
var jsFiles = Directory.GetFiles(HttpContext.Current.Server.MapPath("Scripts/Pages"), "*.js");
foreach (var jsFile in jsFiles)
{
var bundleName = Path.GetFileNameWithoutExtension(jsFile);
bundles.Add(new ScriptBundle("~/bundles/js/" + bundleName).Include(
"~/Scripts/pages/" + Path.GetFileName(jsFile)));
}
then, if you are on index page, the HTML output will be:
<script src="/bundles/js/Index?v=ydlmxiUb9gTRm508o0SaIcc8LJwGpVk-V9iUQwxZGCg1"></script>
</body>
and if you are on products page, the HTML output will be:
<script src="/bundles/js/Products?v=ydlmxiUb9gTRm508o0SaIcc8LJwGpVk-V9iUQwxZGCg1"></script>
</body>
You can minify inline scripts using this HTML helper
using Microsoft.Ajax.Utilities;
using System;
namespace System.Web.Mvc
{
public class HtmlHelperExtensions
{
public static MvcHtmlString JsMinify(
this HtmlHelper helper, Func<object, object> markup)
{
string notMinifiedJs =
markup.Invoke(helper.ViewContext)?.ToString() ?? "";
var minifier = new Minifier();
var minifiedJs = minifier.MinifyJavaScript(notMinifiedJs, new CodeSettings
{
EvalTreatment = EvalTreatment.MakeImmediateSafe,
PreserveImportantComments = false
});
return new MvcHtmlString(minifiedJs);
}
}
}
And inside your Razor View use it like this
<script type="text/javascript">
#Html.JsMinify(#<text>
window.Yk = window.Yk || {};
Yk.__load = [];
window.$ = function (f) {
Yk.__load.push(f);
}
</text>)
</script>
If you use System.Web.Optimization than all necessary dlls are already referenced otherwise you can install WebGrease NuGet package.
Some additional details available here: http://www.cleansoft.lv/minify-inline-javascript-in-asp-net-mvc-with-webgrease/
EDIT:
Replaced DynamicInvoke() with Invoke(). No need for runtime checks here, Invoke is much faster than DynamicInvoke. Added .? to check for possible null.
The way to do this with minimal effort is to extract it into a script file. Then you can use bundling and minification just as you want.
If you want to minify it inline, it will be a much greater effort than simply moving the script off-page.
Based on #samfromlv's answer, I created an extension to handle CSS as well. It also takes BundleTable.EnableOptimizations into consideration.
OptimizationExtensions.cs
Adding in an answer for ASP.NET MVC Core. The solution I used to minify inline JS and razor generated html was WebMarkupMin.
It ultimately boiled down to adding these two minuscule changes to my project:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();
//added
app.UseWebMarkupMin();
app.UseMvc(.....
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//added
services.AddWebMarkupMin(
options =>
{
//i comment these two lines out after testing locally
options.AllowMinificationInDevelopmentEnvironment = true;
options.AllowCompressionInDevelopmentEnvironment = true;
})
.AddHttpCompression();
}
There's a great blog post by Andrew Lock (author of ASP.NET Core in Action) about using WebMarkupMin https://andrewlock.net/html-minification-using-webmarkupmin-in-asp-net-core/ WebMarkupMin is highly configurable and Andrew's post goes way more indepth, highly recommended reading it intently before just copying and pasting.
A little late for the party, but for .NET Core you could use a TagHelper to minify the content of a script tag like this:
[HtmlTargetElement("script", Attributes = MinifyAttributeName)]
public class ScriptTagHelper : TagHelper
{
private const string MinifyAttributeName = "minify";
[HtmlAttributeName(MinifyAttributeName)]
public bool ShouldMinify { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (!ShouldMinify)
{
await base.ProcessAsync(context, output);
return;
}
var textChildContent = await output.GetChildContentAsync();
var scriptContent = textChildContent.GetContent();
// or use any other minifier here
var minifiedContent = NUglify.Uglify.Js(scriptContent).Code;
output.Content.SetHtmlContent(minifiedContent);
}
}
and then use it in your views:
<script minify="true">
...
</script>
Fenton had a great answer about this: "rather than minify inline JavaScript code, externalize the inline JavaScript code and then you can minify with any standard JavaScript minifiers / bundlers."
Here is how you externalize the JavaScript: https://webdesign.tutsplus.com/tutorials/how-to-externalize-and-minify-javascript--cms-30718
Here is my direct answer to minify the inline JavaScript code (require a bit of manual work).
Copy the inline JavaScript code snippet and paste them into a separate JavaScript file and save it, e.g. inline.js
Use esbuild to minify the inline code snippet in inline.js, see more details about minification here
esbuild --minify < inline.js > inline-minified.js
Copy the minified JavaScript code snippet in inline-minified.js and paste it back into the original HTML to replace the original code inside of the tag.
Done.
Related
My environment: ASP.NET Core 5 with RazorPages, Webpack 5.
In razor pages (.cshtml) that reference svg files, I want to inline them. This is something Webpack can do (via a plugin), but I'm not sure how to integrate these two tech stacks.
I could write templatised cshtml files, and populate them via webpack:
ContactUs.cshtml.cs
ContactUs.cshtml <------ read by webpack
ContactUs.generated.cshtml <------ generated by webpack
But then how do I force msbuild / aspnet to use the generated file (ContactUs.generated.cshtml) instead of the template file (ContactUs.cshtml) when building?
I suspect the answer is to use IPageRouteModelConvention but I'm unsure how.
(A dirty workaround is to instead use the filenames ContactUs.template.cshtml and ContactUs.cshtml but I prefer something like the above, as "generated" is clearer.)
UPDATE
To simplify the problem:
The compiler looks for Foo.cshtml.cs and Foo.cshtml.
How do I tell it to instead look for Foo.cshtml.cs and Foo.generated.cshtml?
When loading the app, the framework loads for you a set of PageRouteModels which is auto-generated from the razor page folders (by convention). Each such model contains a set of SelectorModel each one of which has an AttributeRouteModel. What you need to do is just modify that AttributeRouteModel.Template by removing the suffixed part from the auto-generated value.
You can create a custom IPageRouteModelConvention to target each PageRouteModel. However that way you cannot ensure the routes from being duplicated (because after modifying the AttributeRouteModel.Template, it may become duplicate with some other existing route). Unless you have to manage a shared set of route templates. Instead you can create a custom IPageRouteModelProvider. It provides all the PageRouteModels in one place so that you can modify & add or remove any. This way it's so convenient that you can support 2 razor pages in which one page is more prioritized over the other (e.g: you have Index.cshtml and Index.generated.cshtml and you want it to pick Index.generated.cshtml. If that generated view is not existed, the default Index.cshtml will be used).
So here is the detailed code:
public class SuffixedNamePageRouteModelProvider : IPageRouteModelProvider
{
public SuffixedNamePageRouteModelProvider(string pageNameSuffix, int order = 0)
{
_pageNameSuffixPattern = string.IsNullOrEmpty(pageNameSuffix) ? "" : $"\\.{Regex.Escape(pageNameSuffix)}$";
Order = order;
}
readonly string _pageNameSuffixPattern;
public int Order { get; }
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
}
public void OnProvidersExecuting(PageRouteModelProviderContext context)
{
if(_pageNameSuffixPattern == "") return;
var suffixedRoutes = context.RouteModels.Where(e => Regex.IsMatch(e.ViewEnginePath, _pageNameSuffixPattern)).ToList();
var overriddenRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var route in suffixedRoutes)
{
//NOTE: this is not required to help it pick the right page we want.
//But it's necessary for other related code to work properly (e.g: link generation, ...)
//we need to update the "page" route data as well
route.RouteValues["page"] = Regex.Replace(route.RouteValues["page"], _pageNameSuffixPattern, "");
var overriddenRoute = Regex.Replace(route.ViewEnginePath, _pageNameSuffixPattern, "");
var isIndexRoute = overriddenRoute.EndsWith("/index", StringComparison.OrdinalIgnoreCase);
foreach (var selector in route.Selectors.Where(e => e.AttributeRouteModel?.Template != null))
{
var template = Regex.Replace(selector.AttributeRouteModel.Template, _pageNameSuffixPattern, "");
if (template != selector.AttributeRouteModel.Template)
{
selector.AttributeRouteModel.Template = template;
overriddenRoutes.Add($"/{template.TrimStart('/')}");
selector.AttributeRouteModel.SuppressLinkGeneration = isIndexRoute;
}
}
//Add another selector for routing to the same page from another path.
//Here we add the root path to select the index page
if (isIndexRoute)
{
var defaultTemplate = Regex.Replace(overriddenRoute, "/index$", "", RegexOptions.IgnoreCase);
route.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel() { Template = defaultTemplate }
});
}
}
//remove the overridden routes to avoid exception of duplicate routes
foreach (var route in context.RouteModels.Where(e => overriddenRoutes.Contains(e.ViewEnginePath)).ToList())
{
context.RouteModels.Remove(route);
}
}
}
Register the IPageRouteModelProvider in Startup.ConfigureServices:
services.AddSingleton<IPageRouteModelProvider>(new SuffixedNamePageRouteModelProvider("generated"));
Identity URLs are all of the form : /Identity/Account/Login etc
How can I change them (all) to be of the form /myapp/Identity/Account/Login etc ?
Is there a single "base" property or setter ?
(using latest .NET Core 3 preview 8)
The default UI uses Razor Pages, and by convention, the URLs are based on the filesystem path, similar to how Web Forms used to work back in the day. In other words, that's the URL because the page is literally located at /Areas/Identity/Pages/Account/Login.cshtml (the Areas and Pages portions of the path are logical, and removed from the URL by convention, leaving just /Identity/Account/Login.
If you want to modify this, you'll need to specify custom routes, via something like:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/Identity/Account/Login", "Login");
});
You can also change the route on the actual page via the #page directive in the cshtml file:
`#page "Login"`
However, for the Identity UI, that approach would require you to scaffold the page into your project, obviously, in order to be able to change that.
For chaning Razor Page route, you could try Use a parameter transformer to customize page routes
Detail steps below:
IdentityParameterTransformer
public class IdentityParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
// Slugify value
if (value.ToString().StartsWith("Identity"))
{
return $"/MyApp/{ value.ToString() }";
}
return value.ToString();
}
}
Register
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.Add(
new PageRouteTransformerConvention(
new IdentityParameterTransformer()));
});
I maintain a large legacy ASP.NET MVC application, which was recently converted to .Net Core.
I need to introduce cache busting for our JavaScript and CSS files. I appreciate this can easily be done using the asp-append-version="true" attribute on the new .Net Core script tag helper.
However, my application has script tags in over a 100 places. Adding the attribute in all those places will touch large numbers of pages, which means a lot of regression testing.
Is there a way to create a new script tag helper that inherits from the .Net Core script tag helper, and that always has the asp-append-version="true" attribute? That will give me cache busting without having to update lots of files.
...create a new script tag helper that inherits from the .Net Core script tag helper, and that always has the asp-append-version="true" attribute?
Code (View on GitHub)
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
namespace AspNetCoreScriptTagHelperOverride
{
[HtmlTargetElement("script")] // A
public class MyScriptTagHelper : ScriptTagHelper
{
public MyScriptTagHelper(
IHostingEnvironment env,
IMemoryCache cache,
HtmlEncoder html,
JavaScriptEncoder js,
IUrlHelperFactory url) : base(env, cache, html, js, url) { } // B
public override void Process(TagHelperContext context, TagHelperOutput output)
{
const string appendVersion = "asp-append-version";
if (!context.AllAttributes.Any(a => a.Name == appendVersion))
{
var attributes = new TagHelperAttributeList(context.AllAttributes);
attributes.Add(appendVersion, true);
context = new TagHelperContext(attributes, context.Items, context.UniqueId);
} // E
base.AppendVersion = true; // C
base.Process(context, output); // D
}
}
}
Explanation
A: Set the TagName to "script".
B: Implement the base constructor.
C: Hard code AppendVersion to true.
D: Call the base class's Process.
E: Overcome AttributeMatcher.TryDetermineMode
Usage
In _ViewImports.cshtml remove the existing tag helper and add your override.
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
#addTagHelper *, AspNetCoreScriptTagHelperOverride
Be sure to use the name of your assembly.
Once that is done, your code will execute wherever there is a script tag helper. For instance, both of the following will have AppendVersion set to true.
<script src="~/js/site.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/js/site.js" asp-append-version="false"></script>
This will be the the resultant HTML:
<script src="/js/site.js?v=4q1jwFhaPaZgr8WAUSrux6hAuh0XDg9kPS3xIVq36I0"></script>
See Also
https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs
asp-append-version="true" this should append version to script. With .net core 1.1 it worked just fine. Recently upgraded to version 2.0 and it no longer works. Any ideas why?
https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues/471
Basically you now need
#addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
And to make it easier, you can have a global _ViewImports.cshtml file under the Views folder, and just throw that line in there and it will apply the line to all View pages.
In MVC, we can do versioning by maintaining config value then used public class to append the version in JS and CSS reference.
CS:
public static class StaticFileHelper
{
static string staticVersion;
static StaticFileHelper()
{
staticVersion = System.Configuration.ConfigurationManager.AppSettings["JSVersioning"];
}
public static string StaticFile(this UrlHelper html, string filename)
{
var virtualPath = ReleaseVirtualPath(filename);
var root = html.RequestContext.HttpContext.Request.ApplicationPath;
if (root.Length > 1)
{
virtualPath = root + virtualPath;
}
return virtualPath;
}
}
In case of this I made this solution
<link rel="stylesheet" type="text/css" href="\css\custom.css?v=#Generator.RandomStringGenerator(9)">
So my random generator is generating different strings for version then I never face with this situation again. Also I am sharing random string generator for interested people.
private static Random random = new Random();
public static string RandomStringGenerator(int length)
{
const string chars = "abcdefghijklmnopqrstuwxyz0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
Is there any way I can do this?
Some of the benefits of bundling are:
Minimization
Gzip compression
The request has a token parameter for handling files versiones (cache).
In my site I use a lot of bundles, but in some pages I only have 1 script and I don't think I should create a bundle only for 1 script. Is there any way I can use this three benefits with Url.Content method.
My utopic solution would be to set up something (maybe in the web.config) and whenever Url.Content is called it adds this functionality. Using it in either of this ways:
<script type="text/javascript" src="#Url.Content("~/Scripts/...")"></script>
<script type="text/javascript" src="~/Scripts/..."></script>
(The second one is because I'm using Razor 2)
If that is not possible I can make an extension method to UrlHelper to add this functionality.
Thanks!
There's nothing really wrong with creating a bundle with one file to get the benefits of minification and versioning. You would have to use the Scripts.Render helper as well, there's no support for this in the UrlHelper currently, but as you mentioned already you could write an extension method to call into the Scripts helper.
Update (by OP)
Here are my extension method for anyone who want to use it:
public static IHtmlString DynamicScriptsBundle(this HtmlHelper htmlHelper, string nombre, params string[] urls)
{
string path = string.Format("~/{0}", nombre);
if (BundleTable.Bundles.GetBundleFor(path) == null)
BundleTable.Bundles.Add(new ScriptBundle(path).Include(urls));
return Scripts.Render(path);
}
public static IHtmlString DynamicStylesBundle(this HtmlHelper htmlHelper, string nombre, params string[] urls)
{
string path = string.Format("~/{0}", nombre);
if (BundleTable.Bundles.GetBundleFor(path) == null)
BundleTable.Bundles.Add(new StyleBundle(path).Include(urls));
return Styles.Render(path);
}