Компилятор Ангуляр в 200 строчек кода

    Привет. Меня зовут Роман, и я не изобретатель велосипедов. Мне нравится фреймворк Angular и экосистема вокруг него, и я разрабатываю с его помощью свои веб-приложения. С моей точки зрения, основное преимущество Angular в долгосрочной перспективе базируется на разделении кода между HTML и TypeScript, что подробно было описано одним из его разработчиков why-angular-renders-components-with.html Это преимущество имеет и обратную сторону: необходимость компиляции в принципе и сложность динамической компиляции компонентов в runtime. А так хочется использовать уже знакомый синтаксис шаблонов Angular, чтобы дать пользователю своих приложений возможность настраивать шаблоны писем, генерировать отчеты и таблицы для печати или задавать формат экспорта xml файлов! Чтобы узнать, как это сделать — добро пожаловать под кат!

    Задача


    В целом, использование шаблонов Angular пользователем может выглядеть следующим образом: у нас есть некий набор данных:

      const data = {
        project: 'MySuperProject',
        userName: 'Roman',
        role: 'admin',
        projectLink: 'https://example.com/my-super-projectproject'
      }
    

    Нужно дать возможность настроить текст письма, который будет отправляться пользователю после редактирования проекта. С помощью шаблона Angular это может выглядеть так:

      <body>
      Добрый день! Проект {{project}} доступен по ссылке <a href="{{projectLink}}">3D проект вашего заказа</a>
        <div *ngIf="role == 'admin'">
         Для редактирования проекта пройдите по ссылке <a href="{{projectLink}}?mode=edit">Редактировать</a>
        </div>
      </body>
    

    Библиотека ng-template


    Эту задачу можно решить использованием компилятора Angular на клиентской (или даже серверной стороне), но это весьма трудоёмко и потребует притащить много мегабайт кода на клиент. Почему же компилятор Angular такой большой? Это связано с тем, что он поддерживает море разнообразного функционала для композиции компонентов и модулей, а также содержит собственный парсер HTML! Поэтому я решил написать минимальный преобразователь шаблонов Angular, который будет использовать встроенный в браузер парсер HTML. Это удалось сделать всего лишь в 200 с небольшим строчек кода за пару часов. Результатом я решил поделиться с общественностью на GitHub

    Использовать библиотеку ng-template довольно просто:

    Устанавливаем зависимость из npm

    npm install --save @quanterion/ng-template

    или через yarn

    yarn add @quanterion/ng-template

    И используем следующим образом:

    import { compileTemplate, htmlToElement } from '@quanterion/ng-template';
    
    async test() {
      let data = { name: 'Roman' };
      let element = htmlToElement(`<div>{{name}}</div>`);
      await compileTemplate(element, data);
      alert(element.outerHTML);
    }
    

    Поддерживаемый синтаксис


    1. Выражения {{expression}} с возможностью доступа к переменным и вызова функций
    2. Шаблоны ng-template
    3. Контейнеры ng-container
    4. Условия *ngIf + *ngIf as
    5. Циклы *ngFor
    6. Стили [style.xxx]=«value» и [style.xxx.px]=«value»
    7. Условные классы [class.xxx]=«value»
    8. Observables {{name$}} c автоматической подпиской на значение (как пайп async)

    Подробнее смотрите в тестах ng-template.spec.ts

    Использование Eval


    Для вычисления выражений в шаблонах используется eval с преферансом и куртизанками. Дело в том, что в шаблонах Angular доступ к переменным используется без привычного для JavaScript префикса this. Поэтому требуется вызвать eval(), у которого в области видимости лежат все переменные из объекта с данными. Сгенерировать такой код для eval() у меня не получилось, т.к. код вида

    const data = { a: 1, b: () => 4 };
    const expression = 'a+b()';
    eval('a =1; b = ??;' + expression);
    

    не позволяет передать функции

    Решение было найдено путем создания функции, у которой параметры имеют имена полей объекта с данными:

    const data = { a: 1, b: () => 4 };
    let entries = []
    for (let property in data ) {
      entries.push([property, data[property]])
    }
    const params = entries.map(e => e[0]);
    const fun = new Function('code', ...params, `return eval(code)`);
    const args = entries.map(e => e[1]);
    const expression = 'a+b()';
    const result = fun.call(undefined, expression , ...args);
    

    P.S.: Я надеюсь в будущем, когда API нового компилятора Ivy стабилизируется, можно будет генерировать набор операторов для Ivy и создавать полноценные компоненты в динамике!

    Ссылка на исходники
    Поделиться публикацией

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

      +1
      чтобы дать пользователю своих приложений возможность настраивать шаблоны писем, генерировать отчеты и таблицы для печати или задавать формат экспорта xml файлов

      А почему не взять тот же mustache, конечно синтаксис далеко не ангуларовский, но он портирован на многие языки и фреймворки.
      Ну уж если хочется «ангулараподобия», есть шаблонизатор Tangular.
      И очень бы хотелось иметь страничку для тестирования шаблонизатора с двумя полями (данные и шаблон) и кнопкой генерации.
        0
        Я, конечно, присматривался к различным шаблонизаторам, но ряд причин побудил написать своё
        1) Хотелось именно Angular синтакс из-за перспективы делать в будущем компиляцию на Ivy, чтобы в случае такого перехода не сломалась обратная совместимость
        2) Размер — ряд шаблонизаторов, которые я смотрел (Blaze, Handlebars) имели размер > 50Kb
        3) Асинхронность — возможность вставлять в шаблон данные из RxJs Observable, например, грузить картинки по HTTP
        +1
        Ivy

        Оффтоп и ИМХО, но у меня на него пока скепсис.
        разработчики уже в который раз рендеринг меняют для angular? 3 или 4. Так по мне, пока лучше пускай переживет несколько версий, и только потом можно будет на него полагаться.
          –1
          Такие вещи уже 15 лет делаются на XSLT, там из коробки и инклюд шаблонов в шаблон есть, и возможность предварительной компиляции шаблонов есть, и одинаково легко как на серверной стороне делаются, так и на клиентской. Что то типа такого
           <data>
            <project>MySuperProject</project>
            <userName>Roman</userName>
            <role>admin</role>
            <projectLink>https://example.com/my-super-projectproject</projectLink>
           </data>
          
           <xsl:template name="letter">
            <body>
             Добрый день! Проект
             <xsl:value-of select="./project"/>
             доступен по ссылке
             <a href="{./projectLink}">3D проект вашего заказа</a>
             <xsl:if test="./role = 'admin'">
              <div>
               Для редактирования проекта пройдите по ссылке
               <a href="{./projectLink}?mode=edit">Редактировать</a>
              </div>
             </xsl:if>
            </body>
           </xsl:template>
          
          

          Само собой, есть богатый инструмент для создания выборок, агрегации, проверок условий и прочая прочая
            0
            Классная вещь! а может она работать не только со данными — объектами, но и вызывать функции, втч асинхронные? К, примеру, мне нужно в отчёт картинку сгенерить или диаграмму и параметры этой картинки или диаграммы я только в шаблоне указываю, возможности предварительно нагенерировать все варианты картинок, сами понимаете, нет.
              0
              Никто не мешает скрестить ее с js :) xslt выполнит свою часть преобразования данных в нужный формат, а JS обеспечит ее нужными данными
                0
                В том, то и прикол, что данные нужны в момент преобразования, а не после. К примеру, условие нужно или нет отображать эту картинку в принципе может браться из настроек по HTTP запросу. Т.е. для гибкости в моём случае нужно, чтобы любая часть шаблона вычислялась произвольным выражением. В этом и плюс Ангуляра, что у него везде в условиях, циклах итп вставляется кусок JS. А что это за кусок — обращение к данным или сложные функции — по барабану!
                  0
                  Все решаемо, используем «и в хвост в и гриву», и с проверкой прав, и
                  помимо графиков
                  image
                  есть и кучу изображений делаем.
                    0
                    А ссылочку на JS библиотеку, где это решено, дадите?
                      –1
                      Не дам, у нас свое родное :)

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

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