So I have renderings that depend on specific CSS or specifically Javascript. Do you load the same Javascript on all pages even if the Javascript is for a specific page/rendering, or do you pull in the resource dependencies a different way? What if you want the page to bundle and minify all the javascript/css, would you use the same method?
3 Answers
An alternative to @Gatogordo's option is to use a modular approach to JavaScript using something like requirejs. I wrote a post about using requirejs to organize your js with Sitecore a while a go.
The basics are:
Setup RequireJS
Setup require, this uses a pretty standard require config. The main.js
can be:
require.config({
baseUrl: "/assets/js",
paths: {
jquery: "vendor/jquery.min",
"jquery.migrate": "vendor/migrate",
"jquery.mobile.events": "vendor/mobile.events",
"jquery.royalslider": "vendor/jquery.royalslider",
},
shim: {
"jquery.migrate": {
deps: ["jquery"]
},
"jquery.mobile.events": {
deps: ["jquery"]
},
"jquery.royalslider": {
deps: ["jquery"]
},
}
});
// Now run any initialization javascript for the site:
require(["jquery"], function($) {
// Init code here
});
Here I have added some dependencies that were used on my project. You can customize those dependencies as you need too.
Now add the script to the top of your layout. Its important that its at the top. Note this will not cause blocking because of the nature of how requirejs loads the js files async.
<script src="path-to-requirejs-main-file/require.js"></script>
<script src="/assets/js/main.js"></script>
Now you can create your components by creating a require AMD module. The basics for a require module is that you define the dependencies and return the function. E.g:
define(["jquery", "jquery.royalslider", "jquery.mobile.events"], function($) {
var BasicSlider;
return BasicSlider = (function() {
function BasicSlider(options) {
this.options = options;
this.options = $.extend({}, this.defaults, this.options);
this.initSlider();
}
DetailCarousel.prototype.defaults = {
element: "#basicSlider",
};
DetailCarousel.prototype.initSlider = function() {
// TODO: Initialise the slider here
};
return DetailCarousel;
})();
});
Now in your razor script for your Sitecore rendering you can simple require that module in:
<div class="slider"><!-- Slider Html Structure Here --></div>
<pre>
<script>
require(["jquery", "modules/slider"], function($, Slider){
new Slider({
element: '.slider'
});
});
</script>
Pros/Cons of this method
The main benefits of this method are that your js is loaded modularly, just like your Sitecore components are. Only the JS needed for the particular page is downloaded to the client, so it can save some bandwidth and some processing on the browser as it doesn't need to parse anything that is not being used.
In Http 1.1 this method can be problematic. If you have lots of modules, you can get delays while the files are downloaded the first time. With Http/2 this is not so much of an issue.
RequireJS is not the only JavaScript module loader out there. In ES6 modules are becoming part of the language and there are pros/cons to each module loader so you would have to work out what works best for your project.
For more information on why AMD Modules are good, look here Why AMD?.
Should you use it?
TLDR; If you have a small site with minimal JavaScript - webpack it all into a single file and minify that. If you have a large complex site with many JS modules, RequireJS can make maintaining and writing the client side much easier.
This might be an opinion based question.. but I'll give the approach I usually take.
In general I would keep all css and javascript bundled and minified together. This means visitors will load some extra (and maybe unneeded) bytes at the first request, but each following request can use the cached version as it will be the same for all pages.
Why this approach:
- bundling them all will keep the amount of requests low - which is a good thing
- keeping the file identical over all pages makes best use of caching for your visitors
There is a situation where I would create an extra javascript (and maybe css) file though - could be based on a rendering. When you have an application in your website that requires a lot of javascript -which is not needed everywhere- I would put that javascript in another file and have the rendering add it to the page. Again though, bundled and minified to get one extra file only. This way, the visitor still gets the general file and the extra one is added, creating one extra request (which is again cached). Why? Sometimes you have a website that has a lot of pages and just one section with an application that requires a whole lot of javascript. In that case it can be useful not to have everyone load that javascript (or css) immediately.
So, it's an exercise that probably has no exact best solution. Minifying your files is always a must - but finding the best balance between a least amount of request, using the browser caches and not loading too much unnecessary data is no exact science. As said, I tend to lean towards less requests and much browser cache usage.
Sitecore Habitat solution might have what you are looking for.
Pipeline:
For each rendering you specify a js/css asset or a line of script. In the mvc.getPageRendering pipeline it gathers up all the necessary js/cs for all the renderings on the page. https://github.com/Sitecore/Habitat/blob/master/src/Foundation/Assets/code/Pipelines/GetPageRendering/AddAssets.cs
public class AddAssets : GetPageRenderingProcessor
{
private IList<Asset> _siteAssets;
private IList<Asset> SiteAssets => this._siteAssets ?? (this._siteAssets = new List<Asset>());
public void AddAsset(XmlNode node)
{
var asset = AssetRepository.Current.CreateFromConfiguration(node);
if (asset != null)
{
this.SiteAssets.Add(asset);
}
}
}
Config
<sitecore>
<pipelines>
<mvc.getPageRendering>
<processor
patch:before="*[@type='Sitecore.Mvc.Pipelines.Response.GetPageRendering.GetLayoutRendering, Sitecore.Mvc']"
type="Sitecore.Foundation.Assets.Pipelines.GetPageRendering.AddAssets, Sitecore.Foundation.Assets">
<siteAssets hint="raw:AddAsset">
</siteAssets>
</processor>
</mvc.getPageRendering>
</pipelines>
</sitecore>
RenderAssetService:
Then the RenderAssetService puts it all on the page. https://github.com/Sitecore/Habitat/blob/4433c9572e0ec1630ed1c5cc08dbdf2ee023b54e/src/Foundation/Assets/code/Services/RenderAssetsService.cs
RenderAssetService for example.
public class RenderAssetsService
{
private static RenderAssetsService _current;
public static RenderAssetsService Current => _current ?? (_current = new RenderAssetsService());
public HtmlString RenderScript(ScriptLocation location)
{
var sb = new StringBuilder();
var assets = AssetRepository.Current.Items.Where(asset => (asset.Type == AssetType.JavaScript || asset.Type == AssetType.Raw) && asset.Location == location && this.IsForContextSite(asset));
foreach (var item in assets)
{
if (!string.IsNullOrEmpty(item.File))
{
sb.AppendFormat("<script src=\"{0}\"></script>", item.File).AppendLine();
}
else if (!string.IsNullOrEmpty(item.Inline))
{
if (item.Type == AssetType.Raw)
{
sb.AppendLine(HttpUtility.HtmlDecode(item.Inline));
}
else
{
sb.AppendLine("<script>\njQuery(document).ready(function() {");
sb.AppendLine(item.Inline);
sb.AppendLine("});\n</script>");
}
}
}
return new HtmlString(sb.ToString());
}
public HtmlString RenderStyles()
{
var sb = new StringBuilder();
foreach (var item in AssetRepository.Current.Items.Where(asset => asset.Type == AssetType.Css && this.IsForContextSite(asset)))
{
if (!string.IsNullOrEmpty(item.File))
{
sb.AppendFormat("<link href=\"{0}\" rel=\"stylesheet\" />", item.File).AppendLine();
}
else if (!string.IsNullOrEmpty(item.Inline))
{
sb.AppendLine("<style type=\"text/css\">");
sb.AppendLine(item.Inline);
sb.AppendLine("</style>");
}
}
return new HtmlString(sb.ToString());
}
private bool IsForContextSite(Asset asset)
{
if (asset.Site == null)
return true;
foreach (var part in asset.Site.Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries))
{
var siteWildcard = part.Trim().ToLowerInvariant();
if (siteWildcard == "*" || Context.Site.Name.Equals(siteWildcard, StringComparison.InvariantCultureIgnoreCase))
return true;
}
return false;
}
Rendering
In the default.cshtml, using the rendering service, the js/css is added.
In head tag
@RenderAssetsService.Current.RenderScript(ScriptLocation.Head)
@RenderAssetsService.Current.RenderStyles()
End of body tag
@RenderAssetsService.Current.RenderScript(ScriptLocation.Body)
-
This is interesting. I wasn't aware that habit had something like this. It's very similar to what I use in my projects. How does this handle bundling and minification of the scripts? Commented Nov 28, 2016 at 3:31
-
Habitat doesnt have it, but The minification is in gulp with stuff like uglify. But with this process you have to have many js files. And when you allow them to use script tags, that js is unminified of course. Commented Nov 28, 2016 at 4:14
-
@ChrisAuer Using this how can able to add different JS and CSS for different layouts? Could you please help me on this. Commented May 4, 2020 at 14:01
-
Because you can add the css/js assets to a layout and add that css/js to the AssetService on the fly. You would need to find the right pipeline to find the layout and add the assets. Commented May 4, 2020 at 14:04
-
@ChrisAuer Thanks you, that helps. I will create a custom pipe line Commented May 5, 2020 at 6:54