Простой шаблонизатор на чистом JS со связями
Задача минимум
Сделать односторонний (JS → HTML) шаблонизатор, связывающий JS данные с html отображением максимально просто. Решение должно быть быстрым, с минимальным порогом вхождения — тяп ляп и готово. Максимальная допиливаемость под конкретные требования — примитивность залог успеха.
Принцип работы
Со стороны html
Будем писать обычный html код, где для связи с js будут использоваться обычные data-атрибуты:
data-template — содержимое, которое будет отображаться внутри тега
data-namespace — данные, которые привязываются к JS
Со стороны JS
Данные должны записываться и обновляться прозрачно. Т.е. если у нас есть объект object со свойством data, то данный должны обновляться в html сразу после обычного:
object.data = 5;
Без всяких вспомогательных методов типа:
object.setData = function(val){
this.data = val;
document.getElementById("tpl").html = val;
}
object.setData(5);
— это норм подход, но что будете делать, если с сервера приходит большая пачка json данных, которые надо распихать по разным объектам и отобразить изменения в интерфейсе? Писать и вызывать свой сеттер для каждого свойства/набора свойств — мне такая реализация надоела.
Решение простое. Для каждого свойства объекта мы можем задать дефолтные сеттеры/геттеры, срабатывающие как раз при изменении изменении значения обычным присваиванием. Называется это Object.defineProperty (…) — пересказывать статью не буду, перейду сразу к идее.
Нам надо пробежаться по всем свойствам объекта, которые имеют связь с html. Способов это сделать два:
1) Пробежаться по dom, вытащив оттуда все возможные (не повторяющиеся) значения атрибута data-namespace в отдельный массив
2) Для каждого объекта в js вручную будем задавать массив, который говорит, какие данные (только какие, а не с чем) будут использоваться в связях.
Так как код претендует на наглядность реализации и примитивность — выбираем второй вариант.
Чтобы не ходить вокруг да около, сразу приведу пример с пояснениями.
html
тут должно быть значение dataA из объекта со значением свойства this_namespace=test
тут должно быть значение dataB из объекта со значением свойства this_namespace=test
тут должно быть значение dataA из объекта со значением свойства this_namespace="test2"
js
var obj1 = {
this_namespace: 'test', // имя которое будет использоваться для идентификации объекта
this_data: ['dataA', 'dataB'] // имена которое будет использоваться для идентификации свойств объекта
};
var obj2 = {
this_namespace: 'test2', // имя которое будет использоваться для идентификации объекта
this_data: ['dataA'] // имена которое будет использоваться для идентификации свойств объекта
}
Как видим, изначально в объекте нет никаких данных, мы их получим позже — для начала объект надо подготовить к такому.
Magic
Вся магия заключается в том, чтобы внутри объекта пробежаться по массиву this_data и насоздавать в этом же объекте по 2 свойства на каждый элемент массива:
1) __dataA, __dataB… — тут будут храниться значения
2) dataA, dataB… — и тут будут храниться значения =)
Смысл такой манипуляции в том, что если мы зададим например дефолтный сеттер просто для свойства dataA, который будет записывать переданное значение сюда же — получится рекурсивненько. Чтобы этого избежать, создается отдельное свойство с префиксом »__» (можно любой, например fake_ или magic_ — просто я выбрал такой чтобы не засорять namespace объекта).
Получается, что когда мы напишем obj.dataA = 3, это значение запишется в свойство obj.__dataA, а дефолтный геттер при запросе obj.dataA будет отдавать значение obj.__dataA. То есть по факту, в массиве obj вообще нету свойств dataA и dataB в чистом виде — сеттеры и геттеры подменяют их на свойство с префиксом.
Как я и говорил, нужно обеспечить максимально низкий порог вхождения — чтобы начинающим было проще. А кому надо могут переписать как хотят. В связи с этим, весь метод придания объекту нужного вида будет реализован через прототип объекта.
Тут код неудобно читать, так что сразу смотрим сюда (jsfiddle) — там весь код с комментариями и запуск.
Что еще
- Не кроссбраузерно, (ie11+) из за необходимости использования let вместо var — чтобы особо не заморачиваться с замыканиями. Но опять же — можно переписать.
- Вложенные объекты из коробки работать не будут — можно дописать. Я привел лишь максимально сокращенный код, который можно подстроить под себя.
- Следуя концепции того, что в html не должно быть логики, я не стал добавлять возможность использования в шаблонах каких-то даже примитивных операций (например dataA + dataB). Тупо вывод данных. Логика должна быть на js. Хотя когда я только начал это делать, такая возможность была и там пришлось по-быстрому использовать eval.
- В коде нет никаких проверок вообще, это вы делаете сами по необходимости, просто в моем конкретном случае, для которого писался этот метод, все проверки идут через другую прослойку.
- Как утешительный приз: в начале статьи я говорил о том что «воооот, нам приходит куча json дааааных, что же мне с ними дееелать…» — тут все просто: парсим JSON, распихиваем его в нужные объекты через Object.assign — это будет работать.
- В своих проектах я именую все переменные и свойства с префиксом — типом данных. Что-то вроде arr_list = [1,2,3]; int_count = 2; и т.д. Мне это позволяет «без регистрации и смс» настроить дефолтные геттеры отдавать значение, соответствующее типу данных, и если вдруг у нас int_count = 1.4, нам вернется результат 1 и на лету проверять все значения. Это дисциплинирует и реально помогает.
- Для каждой ситуации можно предусмотреть разные set и get, в зависимости от того что хочется получить в итоге — оч удобно.
Спрашивайте, критикуйте, будьте здоровы!
Комментарии (12)
10 января 2017 в 14:26
+1↑
↓
В почившем в бозе rivets.js и в набирающем обороты vue.js используется примерно такой же подход к организации реактивности.
10 января 2017 в 15:02
–1↑
↓
порог вхождения на порядок выше, в моем случае можно не разбираться, никакого api, если нужно только обновлять UI10 января 2017 в 15:19
+2↑
↓
Я не об этом. Про порог вхождения тоже можно поспорить — все-таки наличие многократно вычитанной документации и Q&A снимает множество вопросов.
10 января 2017 в 15:40
0↑
↓
У vue.js порог вхождения ниже чем в вашем примере имхо. Даже если не читать документацию все интуитивно понятно.
10 января 2017 в 15:44
0↑
↓
я о применении, а не разборе исходников, в каком месте это https://vuejs.org/v2/examples/index.html проще чемlet obj1 = { this_namespace: 'test', this_data: ['dataA', 'dataB'] }; obj1._sTpl();
даже документация не нужна
10 января 2017 в 15:59
0↑
↓
Что такое data-template? Ссылка на внешний html шаблон? А неймспейс — это… Зачем он? Да и вызов приватного метода _sTpl вообще непонятно что делает. И откуда он у пустого объекта взялся?
Тут документация нужна для каждой строчки, т.к. понятно всё только вам, как автору.
10 января 2017 в 14:36
+2↑
↓
Вы как-то хитро вывернули наизнанку механизм шаблонизации. Обычно шаблон должен знать, с какими данными он работает, но не наоборот. Было бы логичнее сделать так: в функцию передается объект с данными и настройки, а он возвращает объект-прокси, который выглядит точно так же, но умеет обновлять UI:var obj = { dataA: 1, dataB: 2 }; var proxy = template(obj, 'test', ['dataA', 'dataB']); proxy.dataA = 2; // обновляем UI
P.S. По поводу именования переменных с префиксом — попробуйте Typescript, он позволит больше не заниматься сизифовым трудом.10 января 2017 в 14:46
0↑
↓
шаблон должен знать, с какими данными он работает
— это можно реализовать пробежавшись по DOM, я писал об этом, но не применил, т.к. это уже другая история.Про объект-прокси скажу: как логичнее — решается для конкретной задачи.
До typescript я еще не дорос10 января 2017 в 15:07
0↑
↓
Если мы говорим о хорошей архитектуре приложения, то его компоненты должны быть максимально независимы друг от друга и пересекаться только в определенных местах, которые называются точками композиции. Тогда приложение будет легко разрабатывать и тестировать.Шаблон не может не знать о модели данных. В вашем случае атрибутом
data-template="dataA"
вы описываете контракт: для шаблона необходим объект, содержащий полеdataA
. Это нормально и по-другому никак не сделаешь. А вот ссылка из модели на шаблон явно лишняя.10 января 2017 в 15:14
0↑
↓
А вот ссылка из модели на шаблон явно лишняя.
Так одни и те-же данные можно доставать из разных экземпляров
10 января 2017 в 15:01
+1↑
↓
Хорощий старт, ещё несколько месяцев и вы напишите свой vue.js/marteshka.jsне стал добавлять возможность использования в шаблонах каких-то даже примитивных операций (например dataA + dataB)
«a + b» — это не логика, а вид представления данных, так же как и «firstname + ' ' + lastname», который можно разложить в `{{firstname}} {{lastname}}`такая возможность была и там пришлось по-быстрому использовать eval.
Не так все просто, вам бы тогда пришлось бы делать паресер выражения и отслеживать все зависимости т.е. +10 кб кода, а если прсто eval — то это будет dirtychecking который нужно будет дергать на каждый чих.распихиваем его в нужные объекты через Object.assign — это будет работать.
Это не все, нужно рекурсивно конвертировать объеты ну и с биндингами что-то делать.10 января 2017 в 15:08
0↑
↓
В случае с {{firstname}} {{lastname}} мы можем обернуть это в отдельные теги или дополнительно парсить такие конструкции.
eval в рамках нужной области видимости работал прекрасно, а рекурсивно конвертировать объекты не так сложно, да и если набор данных статичен то в этом нет необходимости.