Как стать автором
Обновить

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

Время на прочтение3 мин
Количество просмотров4.9K
Привет. Меня зовут Роман, и я не изобретатель велосипедов. Мне нравится фреймворк 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 и создавать полноценные компоненты в динамике!

Ссылка на исходники
Теги:
Хабы:
Всего голосов 16: ↑16 и ↓0+16
Комментарии10

Публикации

Истории

Работа

Ближайшие события