Компилятор Ангуляр в 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 это может выглядеть так:
Добрый день! Проект {{project}} доступен по ссылке 3D проект вашего заказа
Для редактирования проекта пройдите по ссылке Редактировать
Библиотека 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(`{{name}}`);
await compileTemplate(element, data);
alert(element.outerHTML);
}
Поддерживаемый синтаксис
- Выражения {{expression}} с возможностью доступа к переменным и вызова функций
- Шаблоны ng-template
- Контейнеры ng-container
- Условия *ngIf + *ngIf as
- Циклы *ngFor
- Стили [style.xxx]=«value» и [style.xxx.px]=«value»
- Условные классы [class.xxx]=«value»
- 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 и создавать полноценные компоненты в динамике!
Ссылка на исходники