
Since September last year, when the
MSIE JavaScript Engine for .NET library was replaced by the
JavaScript Engine Switcher library and the
BundleTransformer.CleanCss module was created, there were almost no revolutionary changes in the Bundle Transformer. The changes were mainly evolutionary: support for new versions of minimizers and translators was added (the most routine and difficult part of the project), minor bugs were fixed, and work went on to increase productivity.
But this summer everything changed: from the end of May to July, a huge number of recommendations for improving the project were received from users of Bundle Transformer. Most of them were implemented in version 1.9.0 and subsequent summer updates. In this article we consider the most significant of them:
Classes StyleTransformer and ScriptTransformer
Initially, not entirely successful names were chosen for the
CssTransformer and
JsTransformer classes, because the choice of names was made by analogy with the
CssMinify and
JsMinify from
System.Web.Optimization . When
StyleBundle and
ScriptBundle classes appeared in System.Web.Optimization, it became clear that these names are no good.
')
For a long time I postponed the renaming of these classes for later, but when working on version 1.9.0 I decided to rename them all the same. Now they are called
StyleTransformer and
ScriptTransformer . The old
CssTransformer and
JsTransformer classes are still available in the kernel (implemented as wrappers over new classes), but they are considered obsolete (marked with the
Obsolete attribute) and will be removed in version 2.0.0.
Postprocessors
In late May, I received a pull request
"Support for Autoprefixer" from Vegard Larsen (an employee of the Norwegian company
Digital Creations ). When viewing the code, it immediately became clear that the current architecture of the Bundle Transformer is not suitable for implementing such modules. Vegard implemented the functionality of this module as a style translator, which should have been launched after all other translators (LESS, Sass, etc.). This whole implementation looked like a hack, so I decided to reject this pull request. As a result, Vegard published an unofficial version of the module -
BundleTransformer.Autoprefixer.Unofficial , in NuGet, and I began work on a new architecture of the Bundle Transformer.
A new type of modules was required, which should have been launched after translators and before minimizers, and the number and order of calling such modules should be determined by the developer. As a name for a new type of module, I decided to use the term “postprocessors”, which was invented by Andrei Sitnik (if you don’t know who Andrei Sitnik is or what postprocessors are, I recommend you to listen to
the 6th edition of the Frontflip podcast ).
A postprocessor in a Bundle Transformer can be any class that implements the
IPostProcessor interface or inherits the
PostProcessorBase base class from the
BundleTransformer.Core.PostProcessors namespace. Like other types of modules (adapters), postprocessors must be registered in the
Web.config file. Consider the registration process on the example of CSS postprocessors:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core …> <css defaultPostProcessors="UrlRewritingCssPostProcessor,AutoprefixCssPostProcessor" …> <postProcessors> <add name="UrlRewritingCssPostProcessor" type="BundleTransformer.Core.PostProcessors.UrlRewritingCssPostProcessor, BundleTransformer.Core" useInDebugMode="false" /> <add name="AutoprefixCssPostProcessor" type="BundleTransformer.Autoprefixer.PostProcessors.AutoprefixCssPostProcessor, BundleTransformer.Autoprefixer" useInDebugMode="true" /> </postProcessors> </css> … </core> … </bundleTransformer> … </configuration>
Two postprocessors are registered in the
/configuration/bundleTransformer/core/css/postProcessors :
- UrlRewritingCssPostProcessor. Converts relative paths to absolute paths (the standard Bundle Transformer feature, implemented as a postprocessor).
- AutoprefixCssPostProcessor. Adds Autoprefix support to Bundle Transformer.
At first glance, this is very similar to the registration of minimizers, but there is one small difference: if we can specify only one minimizer in the
defaultMinifier attribute of the
/configuration/bundleTransformer/core/css element, then we can specify any number of postprocessors in the
defaultPostProcessors attribute (even zero). ). Moreover, the order in which we specify the names of postprocessors in this attribute determines the order of their execution. If the attribute is missing, then the default
UrlRewritingCssPostProcessor used as the postprocessor.
The code also shows that the value of the
useInDebugMode attribute of postprocessors differs: in
UrlRewritingCssPostProcessor it is
false (conversion of relative paths to absolute is needed only in release mode, when all files are merged into one), and in
AutoprefixCssPostProcessor -
true (updating vendor prefixes is necessary and in debugging mode and release mode).
Registering JavaScript postprocessors is almost the same as registering CSS postprocessors, except that it must be done in the configuration element
/configuration/bundleTransformer/core/js .
Advanced HTTP debug handlers
As a rule, most users of Bundle Transformer configure modules in a declarative way (via
Web.config ), but in some cases it is necessary to use the imperative approach. For example, when working with LESS variables:
using System.Collections.Generic; using System.Web.Optimization; using BundleTransformer.Core.Builders; using BundleTransformer.Core.Orderers; using BundleTransformer.Core.Transformers; using BundleTransformer.Core.Translators; using BundleTransformer.Less.Translators; public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { var nullBuilder = new NullBuilder(); var nullOrderer = new NullOrderer(); var lessTranslator = new LessTranslator { GlobalVariables = "my-variable='Hurrah!'", ModifyVariables = "font-family-base='Comic Sans MS';body-bg=lime;font-size-h1=50px" }; var styleTransformer = new StyleTransformer( new List<ITranslator> { lessTranslator }); var commonStylesBundle = new Bundle("~/Bundles/BootstrapStyles"); commonStylesBundle.Include("~/Content/bootstrap/bootstrap.less"); commonStylesBundle.Builder = nullBuilder; commonStylesBundle.Transforms.Add(styleTransformer); commonStylesBundle.Orderer = nullOrderer; bundles.Add(commonStylesBundle); } }
In the above code, we explicitly create an instance of the
LessTranslator class and use the
GlobalVariables and
ModifyVariables to configure LESS variables. With this approach, we can transmit to the translator the values ​​of LESS variables obtained from an external source (for example, a database).
There is a second way to work with LESS variables. First you need to create a custom element transformation:
using System.Text; using System.Web.Optimization; public sealed class InjectContentItemTransform : IItemTransform { private readonly string _beforeContent; private readonly string _afterContent; public InjectContentItemTransform(string beforeContent, string afterContent) { _beforeContent = beforeContent ?? string.Empty; _afterContent = afterContent ?? string.Empty; } public string Process(string includedVirtualPath, string input) { if (_beforeContent.Length == 0 && _afterContent.Length == 0) { return input; } var contentBuilder = new StringBuilder(); if (_beforeContent.Length > 0) { contentBuilder.AppendLine(_beforeContent); } contentBuilder.AppendLine(input); if (_afterContent.Length > 0) { contentBuilder.AppendLine(_afterContent); } return contentBuilder.ToString(); } }
And then register it when adding a file to the bundle:
using System.Web.Optimization; using BundleTransformer.Core.Bundles; using BundleTransformer.Core.Orderers; public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { var nullOrderer = new NullOrderer(); const string beforeLessCodeToInject = @"@my-variable: 'Hurrah!';"; const string afterLessCodeToInject = @"@font-family-base: 'Comic Sans MS'; @body-bg: lime; @font-size-h1: 50px;"; var commonStylesBundle = new CustomStyleBundle("~/Bundles/BootstrapStyles"); commonStylesBundle.Include( "~/Content/bootstrap/bootstrap.less", new InjectContentItemTransform(beforeLessCodeToInject, afterLessCodeToInject)); commonStylesBundle.Orderer = nullOrderer; bundles.Add(commonStylesBundle); } }
Unfortunately, the above code examples previously only worked in release mode. This was due to the fact that the HTTP debug handlers "did not know anything" about the settings of the bundles and simply translated the code of the requested files.
To solve this problem, it was necessary, first of all, to find a way to transfer debug HTTP handlers to the URL of the bundle, which includes the requested file. After examining the
System.Web.Optimization.dll assembly code using the decompiler, the solution was found: you need to write your own version of the
BundleResolver class and register it in the appropriate class. I will not go into the implementation details, but just show you how to use the created class:
… using BundleTransformer.Core.Resolvers; public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { BundleResolver.Current = new CustomBundleResolver(); … } }
After that, in debug mode, links to the following files will be generated:
<link href="/Content/bootstrap/bootstrap.less?bundleVirtualPath=%7e%2fBundles%2fBootstrapStyles" rel="stylesheet">
where the query string parameter
bundleVirtualPath contains the URL of the bundle.
Thus, with the bandl URL, I added to the basic debug HTTP-handler the ability to apply custom element transformations and transformations set at the bundle level (translators and postprocessors) to the requested file.
In addition, two additional HTTP handlers were created:
- CssAssetHandler. To handle CSS files.
- JsAssetHandler. To handle javascript files.
They allow you to apply custom element transformations and postprocessors to static files. If the requested static file is not included in any of the bundles, then these HTTP handlers pass the request to an instance of the
System.Web.StaticFileHandler class. In contrast to the HTTP debugger henders bundled with translators, these HTTP handlers are not registered in the
Web.config file automatically (during the installation of NuGet packages), they must be registered manually:
<configuration> … <system.webServer> … <handlers> … <add name="CssAssetHandler" path="*.css" verb="GET" type="BundleTransformer.Core.HttpHandlers.CssAssetHandler, BundleTransformer.Core" resourceType="File" preCondition="" /> <add name="JsAssetHandler" path="*.js" verb="GET" type="BundleTransformer.Core.HttpHandlers.JsAssetHandler, BundleTransformer.Core" resourceType="File" preCondition="" /> … </handlers> … </system.webServer> … </configuration>
Mapping File Extensions and Resource Types in Web.config
Previously, the mapping of file extensions and resource types was hard-wired in the
Asset class code. Now the configuration elements
fileExtensions from the
Web.config file are used for this:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core …> <css …> … <fileExtensions> <add fileExtension=".css" assetTypeCode="Css" /> <add fileExtension=".less" assetTypeCode="Less" /> <add fileExtension=".sass" assetTypeCode="Sass" /> <add fileExtension=".scss" assetTypeCode="Scss" /> </fileExtensions> </css> <js …> … <fileExtensions> <add fileExtension=".js" assetTypeCode="JavaScript" /> <add fileExtension=".coffee" assetTypeCode="CoffeeScript" /> <add fileExtension=".litcoffee" assetTypeCode="LiterateCoffeeScript" /> <add fileExtension=".coffee.md" assetTypeCode="LiterateCoffeeScript" /> <add fileExtension=".ts" assetTypeCode="TypeScript" /> <add fileExtension=".mustache" assetTypeCode="Mustache" /> <add fileExtension=".handlebars" assetTypeCode="Handlebars" /> <add fileExtension=".hbs" assetTypeCode="Handlebars" /> </fileExtensions> </js> … </core> … </bundleTransformer> … </configuration>
The example above shows the situation when all official Bundle Transformer modules are installed (comparisons for the
.css and
.js extensions are added during the kernel installation, and the rest when the corresponding translator modules are installed). This architecture gives us the following advantages:
- No need to store unused mappings. As a rule, in real projects it is not necessary to install all types of translators (for example, the simultaneous use of LESS and Sass is very rare), thus fewer comparisons will be stored in the project.
- The ability to create informal modules-translators. Since now there is no dependence on the kernel code, users of Bundle Transformer have the opportunity to create their own module-translators. An example of such a module is the AngularBundle NuGet package, which , when installed, adds the following mapping to the
Web.config file:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core> … <js …> … <fileExtensions> … <add fileExtension=".html" assetTypeCode="AngularTemplate" /> … </fileExtensions> </js> … </core> … </bundleTransformer> … </configuration>
- Binding of new file extensions to existing modules-translators. For example, if we want the BundleTransformer.Hogan module to start processing files with the
.html extension, we just need to add the following code to the Web.config file:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core …> … <js …> … <fileExtensions> … <add fileExtension=".html" assetTypeCode="Mustache" /> … </fileExtensions> </js> … </core> … </bundleTransformer> … <system.webServer> … <handlers> … <add name="HtmlAssetHandler" path="*.html" verb="GET" type="BundleTransformer.Hogan.HttpHandlers.HoganAssetHandler, BundleTransformer.Hogan" resourceType="File" preCondition="" /> … </handlers> … </system.webServer> … </configuration>
Merging code files before minimizing
The Bundle Transformer, unlike System.Web.Optimization, processes each file separately and this approach offers several advantages:
- It becomes possible to combine different types of resources into one bundle (for example, CSS-, LESS- and Sass-files).
- The minimization of previously minimized files (files with the extensions
.min.css and .min.js ) is not .min.js , which in most cases increases the speed of minimization during the initial access to the bundle.
But some users of Bundle Transformer did not like this approach, because they wanted to take full advantage of the structural minimization features that modern minimizers provide (for example,
CSSO from Yandex).
Therefore, in the new version, the
css and
js configuration elements have the
combineFilesBeforeMinification attribute (the default value is
false ), which allows you to enable the merging of the file code before minimizing:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core <css … combineFilesBeforeMinification="true"> … </css> <js … combineFilesBeforeMinification="true"> … </js> … </core> … </bundleTransformer> … </configuration>
New modules
During this time, three official modules were created for Bundle Transformer at once:
- Postprocessor BundleTransformer.Autoprefixer
- Translator BundleTransformer.Handlebars
- Translator BundleTransformer.Hogan
Since all three modules are based on the code of JavaScript libraries, immediately after installation you need to select your own JavaScript engine for each of them (for more information, see the
readme.txt files of the corresponding NuGet packages).
Consider each of them separately:
Bundle Transformer: Autoprefixer
The
BundleTransformer.Autoprefixer module contains the
AutoprefixCssPostProcessor postprocessor
AutoprefixCssPostProcessor , which updates the vendor prefixes in the CSS code.
AutoprefixCssPostProcessor is based on the popular CSS postprocessor -
Autoprefixer (version 3.1 is currently supported). I will not explain why Autoprefixer is needed, because you can emphasize all the basic information about this product from Andrei Sitnik
’s article
“Autoprefixer - the final solution to the problem of prefixes in CSS” .
In this section, I will discuss how to properly configure BundleTransformer.Autoprefixer. If you have not read the Postprocessors and the Advanced HTTP Debug Handlers sections of this article, be sure to read them, since they address many important points related to the work of BundleTransformer.Autoprefixer.
After installing BundleTransformer.Autoprefixer and selecting the JavaScript engine, you need to perform the following steps:
- Add the
AutoprefixCssPostProcessor postprocessor to the end of the list of active CSS postprocessors, which is specified in the defaultPostProcessors attribute of the configuration element /configuration/bundleTransformer/core/css . - Register the
CssAssetHandler HTTP debug handler in the Web.config file (required for debug mode). - Register an instance of the
CustomBundleResolver class as the current BundleResolver `(required for debug mode).
Then, in the configuration section
/configuration/bundleTransformer/autoprefixer the
Web.config file, you can make the optional settings of the Autoprefixer algorithm:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> … <autoprefixer cascade="true" safe="false"> <browsers> <add conditionalExpression="> 1%" /> <add conditionalExpression="last 2 versions" /> <add conditionalExpression="Firefox ESR" /> <add conditionalExpression="Opera 12.1" /> </browsers> … </autoprefixer> … </bundleTransformer> … </configuration>
Consider in detail all the properties of the configuration section
autoprefixer :
| Property | Data type | Default value | Description |
|---|
browsers | List of conditional expressions | 1%
last 2 versions ,
Firefox ESR
Opera 12.1 | Contains a list of conditional expressions to define a subset of supported browsers. The syntax of conditional expressions is described in detail in the official documentation of Autoprefixer . If the browsers element is not specified or is empty, the default value is used. To completely disable the addition of vendor prefixes, you need to leave only one conditional expression equal to none in the browsers element. |
cascade | Boolean | true | Creates a visual cascade of the following prefixes:
-webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; |
safe | Boolean | false | Includes special safe mode for parsing broken CSS code. |
Bundle Transformer: Handlebars
The
BundleTransformer.Handlebars module contains the
HandlebarsTranslator adapter-translator, which precompiles Handlebars templates in JavaScript.
HandlebarsTranslator is based on the popular template engine -
Handlebars.js (version 2.0.0 is currently supported). Despite the fact that this translator is based on a template engine, it is not much different from other translators that produce JavaScript code as output. Files with template code (by default, the translator processes files with the
.handlebars and
.hbs ) must be registered in the script bundles:
… using Core.Bundles; using Core.Orderers; … public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { … var commonTemplatesBundle = new CustomScriptBundle("~/Bundles/CommonTemplates"); commonTemplatesBundle.Include( … "~/Scripts/handlebars/handlebars.runtime.js", "~/Scripts/handlebars/HandlebarsHelpers.js", "~/Scripts/handlebars/HandlebarsTranslatorBadge.handlebars", …); commonTemplatesBundle.Orderer = nullOrderer; bundles.Add(commonTemplatesBundle); … } }
Unlike CoffeeScript and TypeScript, compiled Handlebars templates require the file
handlebars.runtime.js (a stripped-down version of the library
handlebars.js , from which the code necessary for compiling templates is excluded). This file can be placed in a shared library bundle or in a bundle with Handlebars templates. The main thing that his ad went before the announcement of templates.
In the configuration section
/configuration/bundleTransformer/handlebars the
Web.config file, you can configure the precompilation of templates:
<configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> … <handlebars namespace="Handlebars.templates" rootPath="" knownHelpers="link" knownHelpersOnly="true" data="false"> … </handlebars> … </bundleTransformer> … </configuration>
Consider in detail all the properties of the configuration section of
handlebars :
| Property | Data type | Default value | Description |
|---|
namespace | Line | Handlebars.templates | Sets the namespace for templates. |
rootPath | Line | Empty line | Sets the path to the template root directory. Suppose that we have the following template URL, /Scripts/handlebars/discussions/index.hbs . By default, the name of such a template is extracted from the file name - index , but if we assign this property a value of /Scripts/handlebars/ , then we get the following template name - discussions/index . |
knownHelpers | Line | Empty line | Contains a comma-separated list of known helpers. Adding helper names to this list allows you to optimize the calls to them, which reduces the size of the compiled template. |
knownHelpersOnly | Boolean | false | Allows the use of only known helpers. If the value of this property is true and the template code contains helper calls that are not embedded or not declared in the knownHelpers property, an exception will be thrown. |
data | Boolean | true | Allows compilation to include in the data template for @ data-variables (for example, @index ). If there are iterative blocks in your templates, but @ data variables are not used, it is better to set this property a value equal to false - this will increase performance. |
It is also worth noting that if the name of the file containing the template code begins with an underscore, the template will be compiled as a global partial representation (the initial underscore will be removed from the template name).
Bundle Transformer: Hogan
The
BundleTransformer.Hogan module contains the
HoganTranslator adapter-translator, which precompiles Mustache templates in JavaScript.
HoganTranslator is based on the popular Mustache template compiler -
Hogan.js (version 3.0.2 is currently supported). The principles of BundleTransformer.Hogan work in many ways similar to the principles of BundleTransformer.Handlebars, so we consider only the key differences. Files with template code (by default, the translator processes files with the
.mustache extension) must be registered in the script bundles:
… using Core.Bundles; using Core.Orderers; … public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { … var commonTemplatesBundle = new CustomScriptBundle("~/Bundles/CommonTemplates"); commonTemplatesBundle.Include( "~/Scripts/hogan/template-{version}.js", "~/Scripts/hogan/HoganTranslatorBadge.mustache", …); commonTemplatesBundle.Orderer = nullOrderer; bundles.Add(commonTemplatesBundle); … } }
As in Handlebars, compiled templates require a special JavaScript library - template-3.0.2.js(in future releases, the version number is likely to change).In the configuration section of the /configuration/bundleTransformer/hoganfile, Web.configyou can configure precompiling templates: <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> … <hogan useNativeMinification="false" variable="templates" namespace="" delimiters=""> <sectionTags> <add sectionName="newWindow" openingTagName="_newWindow" closingTagName="newWindow" /> </sectionTags> … </hogan> … </bundleTransformer> … </configuration>
Consider in detail all the properties of the configuration section hogan:| Property | Data type | Default value | Description |
|---|
useNativeMinification | Boolean | false | If the value of this property is equal true, then the minimization of the code of the compiled template will be performed by means of the translator. |
variable | Line | templates | Specifies the name of the JavaScript variable in which the templates will be stored. |
namespace | Line | Empty line | Specifies the namespace that is prefixed to the name of the template. |
sectionTags | Custom Tag List | Blank list | , . , _newWindow newWindow , {{_newWindow}} target="_blank"{{/newWindow}} . |
delimiters | Line | Empty line | , . , ASP, – <% %> ( Web.config — <% %> ). |