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



Привет. Меня зовут Роман, и я не изобретатель велосипедов. Мне нравится фреймворк 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. alexs0ff
    /#20027804 / +1

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

    А почему не взять тот же mustache, конечно синтаксис далеко не ангуларовский, но он портирован на многие языки и фреймворки.
    Ну уж если хочется «ангулараподобия», есть шаблонизатор Tangular.
    И очень бы хотелось иметь страничку для тестирования шаблонизатора с двумя полями (данные и шаблон) и кнопкой генерации.

    • x512
      /#20027832

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

  2. alexs0ff
    /#20027898 / +1

    Ivy

    Оффтоп и ИМХО, но у меня на него пока скепсис.
    разработчики уже в который раз рендеринг меняют для angular? 3 или 4. Так по мне, пока лучше пускай переживет несколько версий, и только потом можно будет на него полагаться.

  3. tuxi
    /#20028236

    Такие вещи уже 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>
    
    

    Само собой, есть богатый инструмент для создания выборок, агрегации, проверок условий и прочая прочая

    • x512
      /#20028254

      Классная вещь! а может она работать не только со данными — объектами, но и вызывать функции, втч асинхронные? К, примеру, мне нужно в отчёт картинку сгенерить или диаграмму и параметры этой картинки или диаграммы я только в шаблоне указываю, возможности предварительно нагенерировать все варианты картинок, сами понимаете, нет.

      • tuxi
        /#20028276

        Никто не мешает скрестить ее с js :) xslt выполнит свою часть преобразования данных в нужный формат, а JS обеспечит ее нужными данными

        • x512
          /#20028302

          В том, то и прикол, что данные нужны в момент преобразования, а не после. К примеру, условие нужно или нет отображать эту картинку в принципе может браться из настроек по HTTP запросу. Т.е. для гибкости в моём случае нужно, чтобы любая часть шаблона вычислялась произвольным выражением. В этом и плюс Ангуляра, что у него везде в условиях, циклах итп вставляется кусок JS. А что это за кусок — обращение к данным или сложные функции — по барабану!

          • tuxi
            /#20028316

            Все решаемо, используем «и в хвост в и гриву», и с проверкой прав, и

            помимо графиков
            image

            • x512
              /#20028332

              А ссылочку на JS библиотеку, где это решено, дадите?

              • tuxi
                /#20028346

                Не дам, у нас свое родное :)