📜 ⬆️ ⬇️

Minification of Ext JS and Sencha Touch applications using ASP.NET

If you are writing web applications on ExtJS in conjunction with ASP.NET MVC and want to minify the source files, but for some reason you don’t like to use the standard SenchaCmd for this, welcome to Cat. For those who do not have time and already want to try, at the end of the article there are links to the library, but for now we will try to figure out what the problem is and write such a miniature self.

What will be the result
public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add( new SenchaBundle("~/bundles/my-sencha-app") .IncludeDirectory("~/Scripts/my-sencha-app", "*.js", true) ); } } 


Intro


So, you are developing using the ExtJS 4 or SenchaTouch 2 libraries, and your web applications are structured in the way that the library developers themselves recommend. As the application grows, the number of sources increases, which certainly leads to a download delay, or you just want to hide your beautiful source code from others.

The first thing that comes to mind is to use SenchaCmd - a product that the Sencha team recommends. He can feed the index.html file or the URL of the application, he obediently takes the page and tracks the order in which the source code was loaded, then gives it to the minifier, and you get what you want at the output.
')
What is the inconvenience? Opinions can vary here, but IMHO is too heavy for compressing SenchaCmd files. A java application, nodejs and phantomjs are involved in the process. In principle, for such rare operations as minification before uploading to the server, it can be useful, but there are still nuances. For example, Index.cshtml you will not give him: he will not understand the plots with Razor-markup. You can give the URL of the application, but if you use authentication before passing which does not load the entire application, then in the minified file there will not be all sources either. And in the case of Windows authentication, everything is generally bad .

It would be much easier to say: "Here's a folder for you, figure it out for yourself, and give me a compressed file." On the Internet, there are a lot of minifiers, but there are no people among them who could establish dependencies between source files. Let's try to fix it.

Let's get started


The ASP.NET stack already has a tool for concatenation and minification - Bundles . He needs only a little help - namely, suggest the order in which to glue the source code.

BundleConfig.cs
 public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add( new ScriptBundle("~/bundles/my-sencha-app") { Orderer = // ? } .IncludeDirectory("~/Scripts/my-sencha-app", "*.js", true); ); } } 

Exactly what is needed! Look at the Orderer.

IBundleOrderer
 public interface IBundleOrderer { IEnumerable<BundleFile> OrderFiles(BundleContext context, IEnumerable<BundleFile> files); } 

At the entrance a collection of files, at the exit - also, only sorted. Let's think about how to organize them. ExtJS has several ways to define dependencies.

Explicit:

Implicit (configuration properties only):

We will not have any complaints to the first case - in the code it means in the code. The rest is quite amenable to analysis.

We will determine the structure of the program. For a start, we have a JS file. It can have several classes inside, each of which can have dependencies on other classes:

SenchaFile.cs
 public class SenchaFile { /// <summary> ///    /// </summary> public IEnumerable<SenchaClass> Classes { get; set; } /// <summary> ///   /// </summary> public virtual IEnumerable<SenchaFile> Dependencies { get; set; } } 

SenchaClass.cs
 public class SenchaClass { /// <summary> ///   /// </summary> public string ClassName { get; set; } /// <summary> ///   /// </summary> public IEnumerable<string> DependencyClassNames { get; set; } } 

Now you need to somehow determine which classes are described in the files. You can search for regulars, for example, but I would put this skill aside for later. Especially since we have a JSParser from Microsoft.Ajax.Utilities. It returns the contents of the JS file as a tree of blocks, each of which can be, for example, a function call, a property call, and so on. Look for where in the file application instances are created (Ext.application), classes are defined or redefined (Ext.define, Ext.override):

SenchaFile.cs
 public class SenchaFile { // .. /// <summary> ///  ,    /// </summary> protected virtual IEnumerable<SenchaClass> GetClasses() { var extApps = this.RootBlock.OfType<CallNode>() .Where(cn => cn.Children.Any()) .Where(cn => cn.Children.First().Context.Code == "Ext.application") .Select(cn => cn.Arguments.OfType<ObjectLiteral>().First()) .Select(arg => new SenchaClass(arg) { IsApplication = true }); var extDefines = this.RootBlock.OfType<CallNode>() .Where(cn => cn.Arguments.OfType<ConstantWrapper>().Any()) .Where(cn => cn.Arguments.OfType<ObjectLiteral>().Any()) .Where(cn => { var code = cn.Children.First().Context.Code; return code == "Ext.define" || code == "Ext.override"; }) .Select(cn => { var className = cn.Arguments.OfType<ConstantWrapper>().First().Value.ToString(); var config = cn.Arguments.OfType<ObjectLiteral>().First(); return new SenchaClass(config) { ClassName = className }; }); foreach (var cls in extApps.Union(extDefines)) { yield return cls; } } } 

The next step is to determine the dependencies of each class. To do this, take the same JSParser and go through all the cases of dependency definition (explicit and implicit) described above. I will not cite the code in order not to load the article, but the essence is the same: we iterate over the tree of blocks in search of the necessary properties and select the names of the classes used.

Now we have a list of files, each file has the classes described in it, and each class has its dependencies. And you need to somehow arrange them in turn. For this there is a so-called topological sorting . The algorithm is simple and for those interested there is an online demo :



SenchaOrderer.cs
 public class SenchaOrderer { /// <summary> ///     /// </summary> /// <param name="node">,   </param> /// <param name="resolved">    </param> protected virtual void DependencyResolve<TNode>(TNode node, IList<TNode> resolved) where TNode: SenchaFile { //        node.Color = SenchaFile.SortColor.Gray; //     foreach (TNode dependency in node.Dependencies) { //        ( ),   if (dependency.Color == SenchaFile.SortColor.White) { DependencyResolve(dependency, resolved); } //    (),   :    else if (dependency.Color == SenchaFile.SortColor.Gray) { throw new InvalidOperationException(String.Format( "Circular dependency detected: '{0}' -> '{1}'", node.FullName ?? String.Empty, dependency.FullName ?? String.Empty) ); } } //  ,    ... //        ,      . node.Color = SenchaFile.SortColor.Black; resolved.Add(node); } /// <summary> ///      /// </summary> /// <param name="files">  </param> /// <returns>  SenchaFileInfo</returns> public virtual IEnumerable<TNode> OrderFiles<TNode>(IEnumerable<TNode> files) where TNode: SenchaFile { var filelist = files.ToList(); //        IList<TNode> unresolved = filelist; IList<TNode> resolved = new List<TNode>(); TNode startNode = unresolved .Where(ef => ef.Color == SenchaFile.SortColor.White) .FirstOrDefault(); while (startNode != null) { DependencyResolve(startNode, resolved); startNode = unresolved .Where(ef => ef.Color == SenchaFile.SortColor.White) .FirstOrDefault(); } return resolved; } } 


That's all. A couple more service files and you can use:

BundleConfig.cs
 public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add( new SenchaBundle("~/bundles/my-sencha-app") .IncludeDirectory("~/Scripts/my-sencha-app", "*.js", true) ); } } 


Index.cshtml
 ... <script src="@Url.Content("~/bundles/my-sencha-app")" type="text/javascript"></script> 


Total


What are the advantages of such a decision? I think it’s obvious: use the standard functionality provided by the ASP.NET framework. What are the disadvantages? They also have:

We use this miniature in several projects, one of which has a peculiar file structure. Basically, after connecting it, they started up without problems, but in that kind of way I had to tweak the source a little bit in order to remove the spaghetti dependencies.

To try


  1. NuGet. Package SenchaMinify .
  2. Draft on GitHub with demos.

A separate command line exe-file ( SenchaMinify.Cmd ) is also included on the githaba. So anyone can use their favorite automation tools.

I will be glad to constructive, ideas or pull-requests.

Source: https://habr.com/ru/post/224191/


All Articles