Improvements to the DoT.js template engine

    The time for the template zoo has passed, now MVC dinosaurs are running around, and they use built-in template engines and component builders. But to replace the old less convenient templating engines in Knockout and Backbone, they are sometimes needed, basically, they stopped in development at the level around 2014.

    So it was with DoT.js . Initially abandoned by the authors for about a year in 2013, he received their attention briefly, rising from version 1.0.1 to 1.1.1, and was again abandoned (or stabilized, depending on how you reasoned). In this regard, back in 2013, it was necessary ( to make a clone of DoT.js ), and now - and to upgrade it.

    It is as fast as the built-in_.template()in Underscore / Lodash, but with improved syntax, in which the need to write JS in templates is not common, but in Underscor, it is always needed. They even came up with a special term for these brackets with scripts: javascript encapulated sections (JES), and they basically got rid of them.

    What do we get additionally?


    1. The structure of the template engine has been redesigned (in 2013, a link from there) to make it easier to read and reduce the number of decoding functions;
    2. Tests showed that the average speed did not change (fluctuations -3% - + 10% depending on the parameters);
    3. A team of work on structure has been added, similar to work on an array;
    4. 4th parameter - filter of the elements of the structure or array;
    5. In some places, slowdowns due to bypass bugs are compensated by optimization of the code and regexp;
    6. The global name "doT" is able to change to another in the settings (the original - no);
    7. The order in version numbering and versioning of global functions encodeHTML () - instances adopted here for optimization has been put in place.

    For reference on version numbering


    In npm, a copy of version 1.1.1 one-to-one in package.json is named as 1.1.2, but in the file - the number 1.1.1 remains; in branch 2.0 in the repo - the same thing with non-update of number 1.1.1 and there is only 1 difference in var rw = unescape(...). In general, everything is done for confusion. Therefore, we believe that the newest version is 1.1.1, in which we take into account the difference from the 2.0 branch. Branch 2.0 does not deserve its title.


    On occasion, it’s convenient to make documentation (there is from the authors ), with a tool for checking it. (What is readable on the Web is the links below.) In short:

    • it retained the “original” Underscore syntax _.template(), in which {{ ... }}we can write any JS code inside the brackets “ ”, including open curly and operator braces, and HTML fragments outside the brackets text.
    • parentheses can be redefined by redefining all regexps with them in the settings (usually not necessary);
    • the name of the senior structure element 'it'can also be redefined, as well as 4 logical behavior settings;
    • supported by AMD, commonJS and just its global name ( 'doT');
    • in addition to the basic universal syntax, it has a number of commands similar to the Mustache / Handelbars style;
    • like them, it _.template()has 2 stages of template (currying of parameters) - into a function and then into HTML (or other) code;
    • not sharpened strictly under HTML, but tied to JS, so its scope is browsers and NodeJS;
    • not much larger in volume than the source code _.template() - 3.3 K in a compressed, non-zipped form.

    (For experiments with the code of the new or old template engine, you can use the old example at http://jsfiddle.net/6KU9Y/2/ (but then there is a better js-feed). A convenient page for testing 2 engines on a common template and data is also built in the repository - c test/index.html. By default, it compares the same results in deployed and minified versions of files. And the second engine can only work with a clone, because it may have a global name that is different 'doT'. Instead of a minified one, you can put a clone, and in the first place, for example , the original DoT.js 1.1.1 and see s differences in parsing.)

    image

    In the file of the second engine, you need to set a name before opening the page globalName:'doTmin'.

    However, online the same thing is not difficult: https://jsfiddle.net/spmbt/v3yvpbsu/26/embedded/#Result or with frames and code editing, who have large screens: https://jsfiddle.net/spmbt/ v3yvpbsu / 26 / . You don’t need to connect files, just copy-paste the contents of 2 versions of DoT.js, and in the second, it’s more convenient to do this in a clone - correct the name doTto doTmin(even if not minimized). By default, DoT.js 1.1.1 is set to the original and DoT12.js 1.2.1 is a clone.

    The benchmark (3rd button) is designed for a short period of time, therefore it depends on parallel processes, and the speed of synchronous algorithms can be judged by averaging over a large number of measurements (the average accumulates and allows you to slightly judge the speeds). There is a benchmark built into the project, but it is complex, via Node, and it does the same (for a group of test files).

    The auto-update checkbox and error trapping are integrated in the test page, which allows you to run tests according to the version that remains healthy. So, it's easy to compare team speeds '~'and'@'over the array. The second one can also work, but much more slowly - by 10-15% (checked by the Bench button). This is due to the need to use a slower for-in loop in the second case. Nevertheless, one cannot do without for-in for structures, so as not to have to prepare arrays from structures for the original version that does not have a command '@'.

    Immediately traditionally, we note that the 2nd calculation (calculation by a compiled template ) is a hundred times faster (for short expressions) than a full compilation every time (1st number, 100 times fewer number of measurements). In the screenshot, " Comp1e3: 99.22ms" means: "1000 full compilations were done in 99.22ms." "Run5e5: 69.08 ms“in the 5th line means:“ 50 thousand fast HTML generations per template were performed in an average time of 69.08ms for every 10 thousand generations. ”

    A drop-down list of examples has been added , from which you can extract typical examples of DoT.js commands and test them in 2 script versions: Changes to examples are temporarily saved and you can return to them by re-selecting the previous paragraph in the example if the browser page has not been reloaded.

    About teams - more info


    At the beginning of the paragraph, the name in quotes will be the name of the DoT12.jsregular expression in the code that is responsible for this function (command) of standardization.

    Command " valEncEval "


    One regexp under this name combines the 3 previous teams that have similar syntax.

    {{ часть выражения и операторов JS }}

    The universal heritage of Underscore. Nothing more is needed, it is self-sufficient to describe everything (spaces between brackets are optional). But to read ... Read more than 40 lines is strictly not recommended. JS open brackets are interspersed with the same template braces closed by the same parenthesis. Behind them are unclosed HTML tags. This is normal practice, it works fine, the machine understands. At the same time, Mustache / Handlebars are already readable.

    {{= выражение JS }}

    The value of the expression is laid out in the surrounding HTML stream, while retaining the HTML tags and despite the unclosed tag brackets. So, met in the value of the expression, if it is laid out in a browser page, it will behave not as text, but as a tag - will lead to line breaks according to HTML rules."
    "


    {{! выражение JS }}

    The same, but returning the text in the output with the "security" of the HTML code: html-tags and html-encoded ( &...;) characters turn into text;

    Command " conditional "


    {{? if-выражение }} then-шаблон {{?}}

    Conditional inclusion of a template (spaces between brackets are optional);

    {{? if-выражение }} then-шаблон {{?? [if-else-выражение]}} [if-]else-шаблон {{?}}

    Branching and conditional text chains of templates.

    An expression is any one containing global variables or a structure with a name 'it'into which the parameter is passed during template moderation.

    if-else is easy to get by inverting the if-expression. The syntax did not become complicated, apparently, for the sake of speed.

    The command " use "


    {{# выражение, выдающее строку }}

    An analogue of a preprocessor (macros) - in the string, you can insert at first any fragment of text, including unpaired template brackets. The result will be compiled. For example, through {{# ...}}you can enter the text of another template into the template through a variable. But the team was conceived for simpler and more specific cases, paired with the define team.

    Team " the define "


    Appeared from version 1.0. At first, it was decided not in the text of the templates, but in the list of parameters after the settings (3rd parameter in doT.template (template, settings, parameters)).

    The format for defining a variable for "use" commands in the "define" command is:
    {{## def.defin1 :что_угодно_до_скобки#}}
    или {{## def.defin1 =что_угодно_до_скобки#}} - имеет другой смысл (функция)

    Variables can be with dots and $, any line is defined in them. There are a number of tricks.

    Dots in the name (someone’s style was copied).

    If the first 4 characters are 'def.', They are deleted.

    If through a colon, pairs def [code] = {arg: param, text: v} are written;

    Through equality, the function 'def' is defined (it is convenient to look at the test page in the list of examples).

    You can define a macro in one place to use 2 or more times. Like all macros - it is doubtful in terms of code quality. If you need macros - this means - it took someone to shut themselves off, reducing the size of the texts in a dirty way, because it wasn’t thought in time how to reduce it from the point of view of the project. And the resources of the script are spent on storing variables.

    A certain plus is that the environments and places of the script are shared, as with a couple of use-useParams commands.

    And there is a semantic difference in the fact that scripts are executed on the fly, at the moment of linking the template with data, and define and use - one step earlier, when linking the template function with the template. In a word, these are macros.

    Command " defineParams "


    {{##foo="bar"#}}

    So the parameters are determined, accumulating in the internal structure, and then used many times after.

    Command " useParams "

    Using previously defined parameters.

    {{#def.foo}}

    Nothing prevents you from taking and defining parameters in JS:

    {{ операторы JS }}

    except for the unwillingness to mix the template environment and the rest of JS. Here, syntax highlighting and code analysis in IDEs and editors will be lost. Therefore, there are more reasons not to write JS in template texts.

    Team " iterate "


    Combining 2 teams with different speeds and capabilities. A while pattern {{~...}}is a run through an array with a while loop. (Obviously, we chose from everything and selected the fastest in browsers at that time.) It works 10-15% faster than its alternative {{@...}}on the for-in-template, which can run through an array or structure. 4th parameter - filtering elements by expression. In the original version, only an array is supported and without filtering. It doesn’t suit you - there is always a " {{ ... }}" (writing is convenient, reading is not, like Perl or machine code).

    {{~ it : value : index : filter-expression }} while-шаблон {{~}}

    where itis the word 'it'(or another) meaning the first argument, or the global name of the array, or the expression that returns the array; value- any name, for example, 'v'or 'value'without quotes, which will be used in the for-template in place of the substituted value of the array element, for example, in the expression {{= value+1}}; index - likewise, any name that defines the index of an array element.

    Yes, the parameters are indicated "over the top" (first value, then index), but it so happened with them, we will not change here and in the next similar command. The logic is that the last (and generally the last) indexcan be omitted, if not needed in the template, along with a colon.

    You can omit other parameters in the clone, leaving colons. In the original - it’s impossible: at least a letter, but you must write. The first default parameter is 'it' (or rather - templateSettings.varname), the rest also have defaults, but they are very technical, unrecorded.

    In sum, everything is done so that compilation and execution of templates are as fast as possible, without unnecessary actions and beautiful things. Most likely, you can still improve something, and some performance improvements will worsen compilation time, so there is a reasonable balance in everything.

    {{@ it : value : index : expression }} for-in-шаблон {{@}}

    Running through the structure, at its first level. It runs slower in the browser (% by 10-15) than in the array, it is necessary to take into account the particularities of the order in which keys-numbers and other keys are issued (numbers go first, then all other keys, except for older versions of IE such as 8th and lower, Opera and old Fx of the same past tense. Where the numbers went in a row with others.). Moreover, no standard guarantees the order, but it is. Using it or checking, or relying on arrays, is up to the developer. The most reputable will say that you can’t trust 100% and will be right. The same story in both Python and JSON.

    At the same time, using the order in which JSON came or the structure was defined reduces the amount of procedure code. The 4th parameter adds an expression - a filter condition. If false, the element is not displayed. (If there is no condition, the parasitic condition persists "if(1)"? This is a payment for the balance.)

    Examples of parameter abbreviations


    {{@::i}} for-in-шаблон {{@}}

    it is just a passage through all the elements of the structure.

    Example:

    {{@::i}} {{=it[i]}} {{@}}

    anyway that

    {{@:v}} {{=v}} {{@}}

    Instead of patterns - you can use arrays in "@"-commands. But on the contrary, in "~"-commands (in arrays) - structures - it is impossible, for the sake of compatibility with the original.

    ... After 7 years, all this will disappear and die under the weight of new magnificent frameworks, where issues will be resolved in their own way and in a different way. forEach, filter, reduce, JSX - template killers are already on the doorstep and with one foot each on this side of the door. And even chimpanzees can manage screen layouts. In the meantime, hurry up to dig into crutches until they finally become stories like the K155LA3.

    What kind of settings are there and why are they needed


    varname:	'it',

    This setting is pretty straightforward. In template expressions, the name is used instead of arguments [0] (the first parameter of the intermediate compiled function). arguments [0] are far from always applicable due to nested functions, and the name is almost always if there are no conflicts. Now, if there are conflicts, the name can be changed, moreover, not only directly in the code of the library template engine, which is bad, but also in the 2nd parameter doT.template(), in the local settings of the current command. (There is also a way to “statically” change settings by reaching them at window.doT.templateSettings.)

    There are other specific names for this template engine that can cause conflicts. They are easy to see on the test page by clicking the “Show function” button on the test page test/index.html. These are the names:out, arr1, arr2, ..., ll (two small L), v, i (when passing through arrays. They screen the same external names. But it- on a special account, without it - nowhere, therefore it is taken out in the settings.

    strip: true,

    Throwing out extra spaces, tabs and line breaks. If the formatting of the output text is not important, use true. The function and template will be shorter, and the compilation speed, strange as it may seem, is slightly lower (1-2%); performance - indistinguishable by speed. Those. to speed up compilation, you need to set strip: false.

    append: true,

    The style of adding pieces of HTML code and data to the out variable is by summing in a chain or by assignment operators. The latter lengthens the text of the function, therefore, if you do not particularly need it, select true. (Apparently, this was once a question - what to choose and what works faster.)

    log: true,

    Not used. Either forgot to delete, or you need somewhere in neighboring scripts such as NodeJS - express.

    selfcontained: false,

    Here's a little optimization story. A function doT.template(...)can be prepared in one common environment ( _globals) and then executed as doT.template(...)(...), or maybe separately (come by ajax or from a file). In the latter case, you need true (extends the function doT.template (...)), and usually, in the first case, false. Then it is not necessary to generate excess in it, and the calculated is stored in _globals._encodeHTML, generated from _globals.doT.encodeHTMLSource(), but not always, but only if there are commands {{! выражение}}in the templates.

    In other words, selfcontained = true- means that the template function doT.template()will be used separately from doT.js, so it must contain everything for template execution. Everything - this means only a special case of encoding HTML characters with commands{{!}}. If they are, the function must include the definition of the encoding function — the line doT.encHtmlStrwhen it is created (this is done in clone 1.2.1, and in the original the function is encodeHTMLSourceconverted to a string).

    There is a flaw in version 1.1.1 of the original - the algorithm always “sticks” the function code into the template, without compression, even if selfcontained = falseit had to be fixed. This function also deals with parameter binding doNotSkipEncodedconstantly, although this is only necessary when creating a template function.

    Then, the original engine has a version conflict problem because they use a global object (window, globals) to optimize the use of the HTML encoding function. It was solved in clone 1.2.1 by the fact that the global name of the encoding function was chosen depending on the name of the engine and version. It turned out something like this:

    var encHt = '_'+dS.globalName + doT.version.replace(/\./g,'').
    ...
    encHtmlStr:'var encodeHTML=typeof '+ encHt +'!="undefined"?'+ encHt +':function(c){return((c||"")+"").replace('
      + (dS.doNotSkipEncoded ?'/[&<>"\'\\/]/g':'/&(?!#?\\w+;)|[<>"\'/]/g')
      +',function(s){return{"&":"&","<":"<",">":">",\'"\':""","\'":"'","/":"/"}[s]||s})};'

    We get the string to be inserted into the template function, but if selfcontained = falsethere is one {{! выражение}}, then we restrict ourselves to executing it in the global object in order to use it encodeHTML().

    doNotSkipEncoded: false,

    Argument doT.encodeHTMLSource(). Works for functions {{! выражение}}- issuing safe (without executable tags) HTML code. If they are in any environment template, the function of _globals._encodeHTMLgenerating safe characters is determined for the first time to save repeated calls. Made to solve such bugs: github.com/olado/doT/issues/106 . If true, then all view codes are not encoded "&....;", and the main result is non-coding of the ampersand in '&'in such expressions.

    Conclusion


    To compile speed obviously need such parameters that require less action: if possible, selfcontained = false, strip: false, append: true. The remaining parts of doT are well optimized, with the fastest solutions selected on average. The speed of versions depends on the specific type of templates, so the statement about speed can only be averaged over the range of tasks.

    doNotSkipEncodedaffects the result in commands {{!...}}: when truecoded characters of the form remain unchanged &...;.

    In general, the clone compiles the template into a function somewhat slower due to the increased amount of code analysis that needs to be done to solve some bugs. For example, the unescape () function is extended. If you remove the last 2 replace from it, the speed will increase by 3% (Chrome v.61 Canary), but there will be some bugs.

    If you do not pay attention to units of percent, then the doT.js template engine is one of the fastest and, at the same time, compact. ES6 and even does not use new methods of arrays - it was written in the wrong era. This gives a plus in that it is supported by all browsers (should work in IE8 as well). In IE11 tested. On the test page, test.index.html is performance.now()polyfilled.

    • Version comparison, tests and cumulative benchmarks for DoT - DoT12: https://jsfiddle.net/spmbt/v3yvpbsu/26/embedded/#Result • Version comparison, tests and cumulative benchmarks for DoT - DoT12: https: // jsfiddle. net / spmbt / v3yvpbsu / 26 / embedded / # Result or with frames and code editing: https://jsfiddle.net/spmbt/v3yvpbsu/26/

    Github DoT12.js(a clone of the original), DoT.js .
    JSFiddle for experiments with the template engine (2013 code) and templates (the Dot clone was originally introduced; in other neighboring feed numbers, readers could leave the results of their experiments, which they can document and leave a link in the comment; check the operation of their version without saving - click the button “Run”).
    An article on the 2013 DoT.js clone with performance tests.

    * Closure Compiler - compresses a little better in Advanced mode than Uglify;
    * Using doT.js (a good selection of examples, 2012)
    * doT.js: chained if-else if in dot.js Ways to write if-else chains;
    * Documentation with sandboxes, from the authors, in detail, with explanatory examples on the links.

    Also popular now: