Создаем подобие шаблонизатора на TypeScript

  • Tutorial
Совсем недавно Microsoft представила новый язык программирования TypeScript. Наверняка многим понравилось наличие типизации и плагины для Microsoft Visual Studio и других редакторов. Чтобы оценить насколько полезен язык в разработке, я решил поиграться с ним, написав небольшой код, который поможет в разработке приложений.

Каждый с этим сталкивался


Любой, кто разрабатывал приложения с использованием технологий HTML+js, знает, что очень часто приходится решать задачу шаблонизации данных на представлении. Существуют множество решений: как с использованием фреймворков (раз, два, три, четыре и так далее), так и простые методики, вроде таких:

var StringTemplate =
        "<div class=\"{Flag}\" >" +
            "<p class=\"name\" >{Name}</p>" +
            "<p class=\"message\" >{Text}</p>" +
            "<p class=\"date\" >{Date}</p>" +
        "</div>",
    html = "",
    Container = document.getElementById('container2');

    for (var i = 0; i < TestDataLength; i++) {
        
        html += StringTemplate
                .replace(/\{Flag\}/g, TestData[i].Out ? "message out" : "message")
                .replace(/\{Name\}/g, TestData[i].Name || "")
                .replace(/\{Text\}/g, TestData[i].Text || "")
                .replace(/\{Date\}/g, TestData[i].Date.toLocaleTimeString());
        
    }
    Container.innerHTML = html;

Последний способ, на мой взгляд, является самым очевидным и простым, однако, он не учитывает необходимость модификации представления после его формирования. Можно добавлять id или data-id для каждого элемента, который собираемся модифицировать, а потом с помощью jQuery или querySelector выделять необходимый элемент, но поиск по DOM весьма — ресурсоемкий процесс.

Замыкания — любимые друзья js-разработчика!


Самый быстрый способ шаблонизации в случае необходимости частой модификации представления является ручная сборка DOM. Методов для этого нужно аж целых три: document.createElement(), document.createTextNode() и (но не обязательно) document.createAttr(). Работа с этими методами в различных браузерах может быть даже медленнее, чем с innerHTML, но на выходе мы получаем указатель на элемент и код страницы не захламляется идентификаторами.

Немного сахара

Прежде всего, создадим модуль Templater и три функции, которые позволят увеличить производительность и сделают код более приятным для чтения:
module Templater {

    export function Element(TagName: string, Attributes: Object) : HTMLElement {
        var item:HTMLElement = document.createElement(TagName);

        for (var p in Attributes)
            if (typeof(Attributes[p]) == "string")
                item.setAttribute(p, Attributes[p]);
            else if (Attributes[p] instanceof Attr)
                item.setAttributeNode(Attributes[p]);

        return item;
    }

    export function Text(Text: string): Text {
        return document.createTextNode(Text);
    }

    export function Nest(Element: HTMLElement, Items: any[]): HTMLElement {
        var l = Items.length;
        for (var i = 0; i < l; i++)
            if (Items[i])
                Element.appendChild(Items[i]);
        return Element;
    }

   // ...


Первая функция создает тег с указанным именем и задает необходимые атрибуты, Nest — создает иерархическую структуру элементов и Text — просто синоним для document.createTextNode();

Структуры данных

Поскольку в TypeScript есть такое благо цивилизации, как ненавязчивая типизация, то ей стоит воспользоваться. Итак, у нас есть элемент на форме и связанные с ним данные. Сделаем очевидный поступок:
    export class Block {
        constructor (public Element: HTMLElement, public Binding) {
        }
    }

Ну и вспомогательную структуру, которая будет совершать действия над элементом:
       export class Template {

        constructor (public Create: {
                                (Element: { (TagName: string, Attributes: Object): HTMLElement; },
                                    Nest: { (Element: HTMLElement, Items: any[]): HTMLElement; },
                                    Text: { (Text: string): Text; }) : Block; },
                     public Update: { (Element: Block, Data); }) {
        }

        public Make(Data) {
            var item = this.Create(Element, Nest, Text);
            this.Update(item, Data);
            return item;
        }
    }


Внимательный читатель заметит, что в качестве аргументов мы указываем типизированные анонимные функции. Это очень удобно: и разработчик компонента и разработчик, использующий компонент, будут знать, чего библиотека от него хочет.

Пример использования

Шаблон создается таким образом:
import T = Templater;

var myItem = new T.Template(
    (Element, Nest, Text) => {
        var Name, Date, Text, Flag, Container;

        Nest(Container = Element('div', 
                { 'class': Flag = document.createAttribute("class") } ), [
            Name = Element('p', { 'class': 'name' }),
            Text = Element('p', { 'class': 'text' }),
            Date = Element('p', { 'class': 'date' })
        ]);

        return new T.Block(Container, {
            Name: Name,
            Date: Date,
            Text: Text,
            Flag: Flag,
            Container: Container
        });
    },
    (Element: T.Block, Data) => {
        var b = Element.Binding;
        b.Name.innerText = Data.Name || "";
        b.Date.innerText = Data.Date.toLocaleTimeString();
        b.Text.innerText = Data.Text || "";
        b.Flag.nodeValue = "message " + (Data.Out == true ? "out" : "");
    });

Появляется еще одно преимущество TypeScript — укороченная запись анонимной функции ( аргументы ) => { код }. Она делает код намного более понятным к чтению.
Аргументы (Element, Nest, Text) я передаю в первой функции из соображений производительности: все таки прямая ссылка на функцию работает быстрее, чем указатель на статическую функцию модуля. Таковы особенности Javascript и Typescript исправить ситуацию в таком случае просто не способен.
Что же делает этот код?
С помощью первой функции (Create) мы создаем пустой элемент документа, а возвращаем пару (элемент, binding), где последний представляет некое подобие ViewBag, содержащей ссылки на нужные шаблону элементы управления. QuerySelector для такой задачи уже не понадобится.
Вторая же функция (Update) связывает данные из Data на элементы из Binding

Набор тестовых данных


Код, отвечающий за тестовые данные можно вынести в отдельный модуль, как в старом-добром .NET

module Example {
    export class ExampleData {

        constructor (public Name: string,
                    public Date: Date,
                    public Text: string,
                    public Out: bool) {
        }

        public static GetExampleData(count: number): any[] {
            var result = [],
                ExampleText = "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...",
                NameA = "John Doe", NameB = "Jane Doe",
                StartDate = new Date(),
                Interval = 300;

            for (var i = 0; i < count; i++) {
                result.push(new ExampleData(
                    i % 2 == 1 ? NameA : NameB,
                    StartDate,
                    ExampleText,
                    i % 2 == 1));
                StartDate = new Date(StartDate.getTime() + Interval);
            }

            return result;
        }
    }
}

Работая с TypeScript не перестаешь радоваться тому, что ключевое слово this в этом языке обозначает то же, что и C-подобных языках программирования — ссылку на текущий экземпляр объекта класса, а не «что-то-там в зависимости от контекста вызова функции».

Запускаем


Для того, чтобы проверить работоспособность кода, осталось главную функцию:
window.onload = () => {

    var TestDataLength = 1500,
        TestData = Example.ExampleData.GetExampleData(TestDataLength),
        Container = document.getElementById('container1');

    var TestStart = new Date();
    for (var i = 0; i < TestDataLength; i++)
        Container.appendChild(myItem.Make(TestData[i]).Element);
    var TestEnd = new Date();

    console.log("Test completed", {
        time: TestEnd.getTime() - TestStart.getTime(),
        count: TestDataLength
    });

    var StringTemplate =
        "<div class=\"{Flag}\" >" +
            "<p class=\"name\" >{Name}</p>" +
            "<p class=\"message\" >{Text}</p>" +
            "<p class=\"date\" >{Date}</p>" +
        "</div>";
    var html = "";
    Container = document.getElementById('container2');

    TestStart = new Date();
    for (var i = 0; i < TestDataLength; i++) {
        
        html += StringTemplate
                .replace(/\{Flag\}/g, TestData[i].Out ? "message out" : "message")
                .replace(/\{Name\}/g, TestData[i].Name || "")
                .replace(/\{Text\}/g, TestData[i].Text || "")
                .replace(/\{Date\}/g, TestData[i].Date.toLocaleTimeString());
        
    }
    Container.innerHTML = html;
    TestEnd = new Date();
    console.log("Test with string templater completed", {
        time: TestEnd.getTime() - TestStart.getTime(),
        count: TestDataLength
    });
};

И выполнить простую команду:
tsc templater.ts
Немного подождав, компилятор выдает скомпилированный в js файл, который практически не отличается по размеру и читаемости от исходного. Спасибо, Microsoft!

Ссылка на исходный код

Комментарии 16

  • НЛО прилетело и опубликовало эту надпись здесь
      +1
      На хабре, к сожалению, пока подсветки синтаксиса для TypeScript я не нашел. Да и Typescript — надмножество Javascript
      • НЛО прилетело и опубликовало эту надпись здесь
          +4
          Уж поверьте: тайпскрипту я только рад. Хотя бы за типизацию и () => { blablabla }.
          Но это не означает, что я больше никогда не буду писать код на чистом js
          • НЛО прилетело и опубликовало эту надпись здесь
              +3
              Складывается впечатление, что Вы про TypeScript совем ничего не читали/знаете.
            +6
            TypeScript — это не новый язык. Это своего рода управление вашим любимым IDE, позволяющая писать код более грамотно. Вы также можете писать код на Javascript, в коде TypeScript — в любом случае все будет переведено в первое. Это своего рода сахар на этапе разработки — не более.

            Кстати, свежий пост от Nicholas C. Zakas — Does JavaScript need classes?
              0
              Согласен. Уже указывал в статье про TypeScript на Хабре, что его использование (когда речь идет о статической типизации), на мой взгляд, мало отличается от использования jsdoc, при условии, что jsdoc поддерживается IDE.
        +1
               constructor (public _Element: HTMLElement, public _Binding: Object) {
                    this.Element = _Element;
                    this.Binding = _Binding;
                }
        


        public _Element: HTMLElement в сигнатуре конструктора — уже объявляет публичное поле класса, объявление поля и присваивание в теле конструктора избыточно
          0
          Спасибо за поправку. Вот что происходит, когда пытаешься перенести опыт с одного ЯП на другой не прочитав досконально Language Specification
            –4
            Я думаю стоит ещё раз всё таки повторить, что TS это не язык программирования, это надстройка, это JS.

            Далее, я конечно всё понимаю, что вы пришли из .NET (видно по первым строкам кода), что вы любите Microsoft и так далее. Но поверьте, вот эти ваши названия переменных, свойств, методов с большой буквы — просто ужасно режут глаза! Пытаешь понять, ну сколько там конструкторов, когда смотришь на певый пример кода.
            Да и не все, да даже большинство фронт-енд разработчиков не разделают вашего радостного мнения о Microsoft, ибо то, что оно делал на протяжении эН лет, никогда не забут, хоть тому у них и были какие то причины.
              +1
              позволю себе с Вами не согласится. все-таки TypeScript — язык.
              по той же логике точно также можно сказать, что EcmaScript 4 (он же ActionScript) — лишь надстройка на ES3 (JavaScript).
              если существует обратная совместимость (!!!) и компиляция в JS, это не является доказательством обратного.
              Можно вообще сразу компилировать TypeScript в CIL, ASM и т.д. при наличии соответствующего компилятора.
                –2
                Что изменится, если взять Java, сделать компилятор в него из препроцессора с похожим синтаксисом. Вот мы из этого препроцессора получаем Java исходник, а потом создаёт бинарник штатными методами. Что поменяется, это будет новый язык?

                Новый язык, это когда у него есть своя виртуальная машина, спецификация и так далее. В TS это даже толком не спицификация. Да и он просто транслируется в JS, и исполняется потом JS.

                Вы ведь не называется less и sass новыми языками таблиц стилей, который придумали такие то люди/компании?
                  +1
                  т.е. все Си-подобные языки идут лесом — ибо (по этой логике) — они лишь надстройка над Си.
                  смешно :)

                  более того, если мы говорим про языки именно, то нельзя включать виртуальную машину в спецификацию.

                  >Новый язык, это когда у него есть своя виртуальная машина, спецификация и так далее

                  это платформа.
          0
          Короткое объявление функций, классы, модульная система из ECMAScript 6 радуют. Чего уж там, лучше бы сразу организовали транслятор из es6 в es5 да поддержку в ie и было бы всем счастье.
            0
            Работа с этими методами в различных браузерах может быть даже медленнее, чем с innerHTML…

            А не надо создавать поэлементно, создайте структуру и клонируйте ее, получится гораздо быстрее, особенно на сложных структурах.
            Здорово что хоть кто-то посмотрел в сторону dom-шаблонов. Предполагаю статья была больше про typescript нежели про шаблоны, но все таки дайте представить дальнейшее развитие событий ;)
            Дальше вам будет не удобно создавать структуру поэлементно ручками, захочется получать ее из описания в виде xml/html — нужен будет парсер и конструктор. Потом захочется проще получать ссылки на нужные узлы и атрибуты. Нужно чтото будет делать с событиями, как то их снимать с шаблона. Потом захочется упростить работу с dom-структурой чтобы проще «ложить» в нее данные и их синхронизировать. Затем захочется хранить шаблоны отдельных файлах и автоматическое подключение нужного css относящегося к шаблону. Потом включение одого шаблона в другой, динамическое обновление, локализация, темы и т.д. В общем придете к тому что сейчас есть в шаблонах basis.js :)
            А начилось все примерно с того что описано у вас в статье…

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое