Задача минимум
Сделать односторонний (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
<div data-template="dataA" data-namespace="test">тут должно быть значение dataA из объекта со значением свойства this_namespace=test</div>
<div data-template="dataB" data-namespace="test">тут должно быть значение dataB из объекта со значением свойства this_namespace=test</div>
<div data-template="dataA" data-namespace="test2">тут должно быть значение dataA из объекта со значением свойства this_namespace="test2"</div>
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, в зависимости от того что хочется получить в итоге — оч удобно.
Спрашивайте, критикуйте, будьте здоровы!
Комментарии (55)
impwx
10.01.2017 14:36+5Вы как-то хитро вывернули наизнанку механизм шаблонизации. Обычно шаблон должен знать, с какими данными он работает, но не наоборот. Было бы логичнее сделать так: в функцию передается объект с данными и настройки, а он возвращает объект-прокси, который выглядит точно так же, но умеет обновлять UI:
var obj = { dataA: 1, dataB: 2 }; var proxy = template(obj, 'test', ['dataA', 'dataB']); proxy.dataA = 2; // обновляем UI
P.S. По поводу именования переменных с префиксом — попробуйте Typescript, он позволит больше не заниматься сизифовым трудом.axeax
10.01.2017 14:46шаблон должен знать, с какими данными он работает
— это можно реализовать пробежавшись по DOM, я писал об этом, но не применил, т.к. это уже другая история.
Про объект-прокси скажу: как логичнее — решается для конкретной задачи.
До typescript я еще не доросimpwx
10.01.2017 15:07Если мы говорим о хорошей архитектуре приложения, то его компоненты должны быть максимально независимы друг от друга и пересекаться только в определенных местах, которые называются точками композиции. Тогда приложение будет легко разрабатывать и тестировать.
Шаблон не может не знать о модели данных. В вашем случае атрибутомdata-template="dataA"
вы описываете контракт: для шаблона необходим объект, содержащий полеdataA
. Это нормально и по-другому никак не сделаешь. А вот ссылка из модели на шаблон явно лишняя.axeax
10.01.2017 15:14А вот ссылка из модели на шаблон явно лишняя.
Так одни и те-же данные можно доставать из разных экземпляров
lega
10.01.2017 15:01Хорощий старт, ещё несколько месяцев и вы напишите свой vue.js/marteshka.js
не стал добавлять возможность использования в шаблонах каких-то даже примитивных операций (например dataA + dataB)
«a + b» — это не логика, а вид представления данных, так же как и «firstname + ' ' + lastname», который можно разложить в `{{firstname}} {{lastname}}`
такая возможность была и там пришлось по-быстрому использовать eval.
Не так все просто, вам бы тогда пришлось бы делать паресер выражения и отслеживать все зависимости т.е. +10кб кода, а если прсто eval — то это будет dirtychecking который нужно будет дергать на каждый чих.
распихиваем его в нужные объекты через Object.assign — это будет работать.
Это не все, нужно рекурсивно конвертировать объеты ну и с биндингами что-то делать.
axeax
10.01.2017 15:08В случае с {{firstname}} {{lastname}} мы можем обернуть это в отдельные теги или дополнительно парсить такие конструкции.
eval в рамках нужной области видимости работал прекрасно, а рекурсивно конвертировать объекты не так сложно, да и если набор данных статичен то в этом нет необходимости.
misato
10.01.2017 18:13+1Посмотрите ractive.js — порог вхождения минимальный, всё очень просто, и не требует переделки всего легаси под него.
vlasenkofedor
11.01.2017 12:46Расширение прототипа? В 2017? Это ведь не серьезно!?
не стоит все стричь под одну гребенку
если это ваш проект и вы знаете, что вы делаете, то вполне возможно (в решении статьи не поддерживаю)
почему не облегчить себе жизнь
к примеру:
HTMLCollection.prototype.forEach = NodeList.prototype.forEach = Array.prototype.forEach;
justboris
11.01.2017 12:55+1Лучше все же использовать Array.from:
Array.from(document.querySelectorAll('.button')).forEach(...)
То, что сейчас проект "ваш", и вы помните о том, что нахимичили с браузерными прототипами, не значит, что другие разработчики это заметят вовремя. Мы же о коммерческой разработке говорим, а не личных домашних веб-страничках
vlasenkofedor
11.01.2017 13:16-1Мы же о коммерческой разработке говорим, а не личных домашних веб-страничках
Очень плохое разделение. Не стоит халтурить, а то рука собьется и ненароком вы забудете, что пишете
rumkin
11.01.2017 15:06У себя в проекте можете что угодно переопределять, но при публикации код должен быть каноническим.
vlasenkofedor
11.01.2017 13:07Array.from — не кроссбраузерно. Привел решение без полифилов.
justboris
11.01.2017 13:17+1Какое решение? С расширением прототипа — это плохое решение
vlasenkofedor
11.01.2017 13:35С расширением прототипа — это плохое решение
Разработчики языка представили плохую возможность расширение стандартных прототипов?
Вот уж эти разработчики :-)
Все плохо без меры. А когда мера есть, то и пользоваться нужно. Так как для этого вам специально и открыли доступ.justboris
11.01.2017 13:42+1Эта возможность была предоставлена не специально, а просто так вышло, потому что язык динамический, и никакой разницы между пользовательскими прототипами и стандартными нет.
Сейчас это работает ради обратной совместимости, в документации явно написано, что такой подход не рекомендуется.
vlasenkofedor
11.01.2017 14:01в документации явно написано, что такой подход не рекомендуется.
Вчитайтесь внимательно
Единственным оправданием расширения базовых прототипов является лишь эмуляция новых возможностей, таких как Array.forEach, для неподдерживающих их старых версий языка.
Что вам было и приведено в качестве примера
HTMLCollection.prototype.forEach = NodeList.prototype.forEach = Array.prototype.forEach;
raveclassic
11.01.2017 14:05+2А теперь вы внимательно почитайте спеку. HTMLCollection и NodeList — array-like объекты, которые не подразумевают некоторых методов Array.
vlasenkofedor
11.01.2017 14:15не подразумевают некоторых методов Array
Мне не нужны другие свойства и почему по вашему мнению я должен их тянуть.
Давайте подведем итог. В документации написано, что практика плохая, но оправдания есть.
Потому, не стоит категорично утверждать о неправильности подхода или решения с расширением стандартных прототипов.justboris
11.01.2017 14:19+1Есть оправдание только для новых фич, которые в будущем поддержатся нативно. Поэтому обычно и пишут так
if(!Array.prototype.forEach) { Array.prototype.forEach = function () {...} }
А ваша идея с присвоением метода к NodeList — это отсебятина.
И я бы бежал поскорее от работы с проектом, где такое встречу.vlasenkofedor
11.01.2017 15:30А ваша идея с присвоением метода к NodeList — это отсебятина.
И я бы бежал поскорее от работы с проектом, где такое встречу.
Да не приглашал я вас на свой горшок.
Куда вы собрались бежать бегите. Флаг в руки не забудьте с жесткими утверждениями про прототипы, eval, with…
Aingis
11.01.2017 19:26+1Вообще-то NodeList.forEach() определён в стандарте (через
iterable<Node>
).
На почитать: «NodeList object is finally an Iterable».justboris
11.01.2017 19:37+1Как интересно, спасибо за информацию.
Только одного forEach мало, обычно еще нужны filter и map хотя бы.
Так что отArray.from(nodes)
или[...nodes]
никуда не деться
raveclassic
11.01.2017 13:44+1Была бы возможность закрыть, давно бы закрыли. Пол мира легаси библиотек живет на этой «возможности», и это не значит, что это должно поощраться. Разработчики и eval, и with предоставили, но вы же не пользуетесь, где попало.
Есть здравые рекомендации по поводу расширения прототипов, и они касаются только расширения недостающими по стандарту свойствами, т.е. годятся только для полифиллов.
UPD: justboris одновременно указали на мдн :)
k12th
11.01.2017 14:17+1JS был разработан за 10 дней одним человеком, в нем есть и неудачные моменты, и это нормально.
На данный момент консенсус такой, что расширять прототипы кастомными методами считается плохой практикой. Судьба Prototype.js о чем-то да говорит.
Другое дело полифиллы, потому что они future proof.
k12th
В почившем в бозе rivets.js и в набирающем обороты vue.js используется примерно такой же подход к организации реактивности.
axeax
порог вхождения на порядок выше, в моем случае можно не разбираться, никакого api, если нужно только обновлять UI
k12th
Я не об этом. Про порог вхождения тоже можно поспорить — все-таки наличие многократно вычитанной документации и Q&A снимает множество вопросов.
Rastishka
У vue.js порог вхождения ниже чем в вашем примере имхо. Даже если не читать документацию все интуитивно понятно.
axeax
я о применении а не разборе исходников, в каком месте это https://vuejs.org/v2/examples/index.html проще чем
даже документация не нужна
SerafimArts
Что такое data-template? Ссылка на внешний html шаблон? А неймспейс — это… Зачем он? Да и вызов приватного метода _sTpl вообще непонятно что делает. И откуда он у пустого объекта взялся?
Тут документация нужна для каждой строчки, т.к. понятно всё только вам, как автору.
axeax
3 строчки, 3 настройки которые надо менять
Что происходит дальше описывает статья, и в реализации метода каждая строчка документирована. Еще раз повторюсь — тут задача была в показательности и простоте, без приблуд и претендования на звание фреймворка нуждающегося в документации впринципе, так что «Ссылка на внешний html шаблон?» неуместно, весь метод изначально не для этого предназначен
TheShock
И вообще откуда этот метод взялся? Там что — расширен прототип Object?
axeax
ссылка в статье https://jsfiddle.net/axeax/ky9zbc18/
SerafimArts
Вы только что предложили прочитать документацию (разновидность её) по примеру, который сказали, что настолько прост, что не требует документации. Верно?
axeax
Пример — прост, реализация — для обычного использования прочтения не требуется
все верно, ок, не «не нуждается в документации», а «минимальный набор работающий из коробки со стремящимся к 0 времени затрат времени на чтение документации»
SerafimArts
Хорошо, допустим, а вот такой пример сколько времени на чтение документации требует?
+
Или ещё лучше, с помощью другого решения (не Vue):
Неужели больше? Мне кажется что нет, всё очевиднее и, самое главное, прозрачнее на порядки.
Или вы со мной не согласны? Разве возникают хоть какие-то вопросы по этим примерам, аналогичные приведённым мною?
axeax
мне приходит новый message каждые n секунд, и глядя на это я не понимаю как мне его обновлять каждый раз, я не открывал документацию, но поверю что это можно сделать так же просто как message = response.message
Если хотите сказать о велосипедности — соглашусь.
А я хочу сказать другое — это разбор частного случая на вполне понятном примере
SerafimArts
Ну это понятно. Я просто веду к тому, что утверждение, что "порог вхождения в сабжевые фреймы — выше", довольно голословное. Да, там больше плюшек, но понять как работает пользоваться всё же проще, по-моему.
P.S. Во втором случае — нет, при "message = response.message" данные не обновятся, это же просто переменная на чистом JS без какой-либо магии. Для связывания её надо объявить как обсервер (т.е. тупо завернуть внутрь функции ko.observable('izi pizi')), но это не особо важно.
axeax
Важно, т.к. мой метод как раз реализует всего одну функцию — связывает представление с данными в автоматическом режиме (если допилить несколькими строчками), максимально быстро (в моем приложении специфическое требование к производительности, на счету каждые 50-100мкс)
SerafimArts
Да без проблем, могу кодом сдублировать то, что я и так написал словами выше:
raveclassic
Зашел по ссылке, увидел
Object.prototype._sTpl =
, закрыл.axeax
спасибо что пояснили всем суть проблемы, хабр не только понимающие такой сарказм читают
raveclassic
Подсказка: циклы for in
axeax
Hasownproperty?
raveclassic
Прекрасно, вы только что обрекли на это весь мир.
А если серьезно, расширять системные объекты, еще и enumerable свойствами — в лучшем случае дурной тон.
justboris
Расширять стандартные прототипы — плохо.
За всем кодом не уследить, что-то где-то сломается.
Так что по сути это Javascript-версия паттерна
#define TRUE FALSE
axeax
Я написал почему так сделано
Aingis
Как минимум, стоило задействовать
Object.defineProperty
, чтобы сделать его неперечисляемым.xGromMx
Кстати вот презентация Evan You http://www.thedotpost.com/2016/12/evan-you-reactivity-in-frontend-javascript-frameworks