Очень маленький фреймверк или как написать собственный Angularjs в 200 строк

Не так давно в одном из уже практически написаных проектов возникла необходимость в использовании подобия вэб-компонентов. Хочу расказать что у нас получилось, постараюсь кратко:

Цель:

Начать использовать компонентный подход в HTML верстке(новую семантику), а именно вэбкомпоненты.

Под компонентом я подразумеваю — “независимый модуль программного кода, предназначенный для повторного использования и развертывания”. К примеру в другом проекте.



Тут хочу сделать небольшое отступление и внести ясность – речь пойдет о компонентах, а не о виджетах(функционально обособленных единицах приложения, объединяющих в себе представление, логику и/или данные, конкретного приложения — javascript+html+css).
Разница, по моему мнению, состоит в том что компонент не подвязывается к конкретной модели данных или логике конкретного приложения – и вы можете его без труда перенести в другое приложение. А виджет – это единица конкретного приложения тесно связанная логикой или данными, и перенести его куда либо без внутреннего изменения самого виджета у вас не получится.
В качестве примеров компонента могу привести селект, аккордеон или табки, а виджетов – панель состояния или окно отображения сообщений в чате.

Зачем:

Реюзабельный код и удобство верстки с помощью возможности расширения HTML синтаксиса.

Как сказано в доке к одному известному фреймверку:
“Directives is a unique and powerful feature available only in Angular. Directives let you invent new HTML syntax, specific to your application.”
— я хочу показать, что это возможно и без angularjs.
И потому, что я хочу использовать компонентный подход(аналог angularjs директив) с виджет-ориентированной архитектурой или с теми шаблонизаторами к которыми мне удобно работать.
Вобщем использовать ту архитектуру приложения или фреймверк которые мне удобны в конкретном случае.

И описанное далее это ни в коем случае не MV* фреймверк, и никогда не задумывался как онный.

Как:

В простейшем случае подобие вэбкомпонента возможно рассмотреть состоящим из двух основных частей: шаблона и скрипта.
Декораторор(css) – пока не будем рассматривать по причине дополнительной логики в нем возможной. Да и сравнивать лучше с уже чем-то существующим, поэтому совсем чуточку потрогаем angularjs.
Вызвать JavaScript функцию я думаю ни у кого не вызовет трудностей, так что мы основной упор сделали на реализации шаблонов для HTML.

В недавно опубликованной статье приводятся два подхода для шаблонизации на клиентской стороне.

Подход 1 — используется в течении уже длительного времени – создание «закадрового» DOM, с его последующим сокрытием.
<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

Достоинства/Недостатки:
+ Использование браузерного DOM – браузер знает что это и умеет с этим работать. При необходимости мы можем легко клонировать его.
+ Ничего лишнего не отображается – hidden предотвращает блок шаблона от показа.
— Инертно – к примеру, даже несмотря на то что наше содержание скрыто, сетевой запрос будет отправлен и выполнен для загрузки изображения.
— дизайн и тематизация – основная страница должна загрузить CSS разметку и для вебкомпонентов. Это делает подход хрупкими т.к. нет никаких гарантий, что мы не столкнемся с конфликтов имен.

Подход 2 — перегрузка
 и манипулирование с ее содержание в виде строки.
Скорее всего В 2008 году Джон Резиг стал первым, кто предложил использовать этот подход. Но в данное время существуют множество других реализаций, например handlebars.js или mustache.js.
<script id="mytemplate" type="text/x-handlebars-template"> <img src="logo.png"> <div class="comment"></div> </script>

Достоинства/Недостатки:
+ Ничего не рендерится – браузер не отображает этот блок, потому что
 имеет “display: none” по умолчанию.
+ Инертность – браузер не парсит и не анализирует содержание скрипта, т.к. Это JS, и тип установлен как "text/javascript".
- Безопасность – подспудно поощряется использование innerHtml. А это может привести к XSS уязвимости.

Кроме того лично мне видятся еще следующие недостатки этих подходов:
  • Подход 1 – Шаблон хотя и скрыт от отображения, все равно находится в основной ветке DOM, и это может оказать влияние на быстродействие выборок (к примеру getElementByID или querySelector) в зависимости от конкретной реализации движка браузера. И самое неприятное – мы можем случайно изменить сам шаблон компонента.
  • Подход 2 – быстродействие- парсинг и шаблонизация проводится JavaScript
  • К тому же оба эти подхода не умеют работать если в шаблонируемый узел уже вставлена часть DOM с динамически подвязанными через addEventListener листенерами

Скажу сразу что предлагаемое нами не является чем-то уникальным и не лишено всех указанных недостатков, просто позволяет еще немного приблизится к цели.

Наш подход - как и первый вариант мы работаем непосредственно с DOM деревом а не строкой. Берем узел DOM который содержит не откомпилированные компоненты – клонируем его и полученным клоном заменяем данный узел в основной ветке DOM. Это позволяет нам далее работать с оригиналом пока отображается клон.

Т.к. мы уже имеем отпарсеную DOM ветвь вне дерева отображения, содержащую наши компоненты, мы можем свободно манипулировать ими и их контентом, как с узлами(например, с помощью querySelectorAll(“component.name”)).

Сам шаблон может содержатся в скрипте(как в первом варианте) или в отдельном html файле, нам абсолютно без разницы. В итоге мы должны получить строку для регистрации компонента в нашем фреймверке. В первый раз мы преобразуем строку шаблона в DOM вне основной ветки с помощью innerHTML, в дальнейшем этот DOM используется как кэш и все манипуляции происходят с помощью нативных методов браузера для работы с DOM.

Как использовать:

  • описать компонент как javascript объект

    component = { name: ..., template: ..., script: function (element, attrs) {….}, onattach: function (element) {….}, instantiate: true/false }

Чуть подробнее:
  • name – имя вэбкомпонента, с помощью которого его можно использовать в html разметке
  • template – строка представляющая собой html шаблон с отмеченным местом вхождения контента().
  • script – логика работы компонента(параметры element – корневой узел компонента, attrs – его атрибуты). Выполняется один раз при компиляции компонента до добавления его в DOM
  • onattach – скрипт который выполняется сразу после аттача откомпилированого компонента в DOM
  • instantiate – создается или нет новый экземпляр компонента для каждого отдельного тега. Если создается то экземпляр компонента становится доступен черейз свойство корневого элемента (element.component).
  • Зарегистрировать компонент в
    TagBuilder.register(component);
  • Использовать компонент в html разметке, например
    <accordion>
                    <accordion-page header=”Header 1”>
                    … 
                    </accordion-page>
                    <accordion-page class="open" header=”Header 2”>
                    … 
                    </accordion-page>
                    ...
    </accordion>

    или через w-component=”accordion” в случае если вы не хотите удалять узел из DOM(К примеру если вам необходимо расширить функциональность ).
    Просто откомпилировать часть DOM содержащую компоненты
    TagBuilder.apply('component css selector or components wrapper');

    Возможно так же компилировать html шаблоны с компонентами как строки, с помощью TagBuilder.compile(string, options); , а потом с помощью TagBuilder.apply('component css selector or components wrapper'); приаттачить скриптовую часть компонентов. В последнем случае для этого на элеменш маркирующие атрибуты(w-component=”name”) .
  • Поделиться публикацией

    Похожие публикации

    Комментарии 13

      0
      Извините, возможно не понял, но где у вас всякие замечательные штуки типа ng-repeat, ng-switch, ng-view и т.д. (видимо, за это отвечает builder.compile)?
      Где dependency injection (да и вообще непонятно, как производится вставка данных, видимо, через builder.compile)?
      200 строк кода — но вы же используете jQuery, не так ли?:)
      И самый главный вопрос — не подскажите, какова была мотивация? Ну то, что постарались разобраться с внутренностями angular и их философией — это вы молодец.
        0
        Прошу прощения но ng-repeat, ng-switch, ng-view нет — их нет, причины:
        не имеют отношения к семантике html. К примеру ng-repeat отвечает за работу с коллекцией данных, а «ngView is a directive that complements the $route service by including the rendered template of the current route into the main layout (index.html) file.» В начале статьи я попытался объяснить что именно в дальнейшем подразумивается под компанентами.

        Речь шла не шла о директивах связаных с данными — нам нужны были именно кастомные компоненты. К примеру у нас в проекте около 8000 вхождений одинаковых 4 строк htmlи коротенького скрипта в 6 строк — теперь вместо этого написано просто <td w-component="acc">, ушло очень много копипаста. Или тот же датапикер которого несколько десятков — <datapicker time="now"/> и все, а внутри и валидатор и разметка и параметризирование.

        А мотивация — «мне нужны были вэб компоненты» в проекте, в котором я не мог использовать ангулар (кстати пример вставки данных есть через underscore, если имеется ввиду шаблонизация). И пока замечательно работает
          0
          а почему тогда angular? для красивого заголовка?;)
            0
            это первая причина, а вторая — “Directives is a unique and powerful feature available only in Angular. Directives let you invent new HTML syntax, specific to your application.” Я хотел показать что директивах нет ничего волшебного и уникального
              0
              И сорри, забыл сказать — зависимостей никаких нет в самом фреймворке не используется ни jQuery ни какая либо другая библиотека. Но примеры да — используют jQuery, просто чтобы показать логику работы
            0
            есть ощущение, что ту задачу, которую вы описали, должны в полной мере решить просто правильно написанные шаблоны. может даже декларативные, типа yate
              0
              Если вы имели ввиду — habrahabr.ru/company/yandex/blog/151700/? То да может, просто то что использует яндекс намного более сложный и нагруженный фреймворк. И кроме того у него немного другой подход — у нас шаблонизация на стороне клиента с использованием DOM машины браузера, у них перд компиляция в JS функцию и потом шаблонизация, тоесть фактически «Подход 2» из статьи + декларативное использование шаблона.
          0
          фреймверк

          Фреймворк
            0
            есть же jquery.widgetFactory, dojo.widgetFactory + любой шаблонизатор и все будет работать на ура. Замысел в чем? В легковесном коде?
              0
              Замысел в новой семантике HTML и ввыделении нового уровня абстракции. Как написано здесть — dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html " describes a method for declaring inert DOM subtrees in HTML and manipulating them to instantiate document fragments with identical contents."

              Ни jquery.widgetFactory ни dojo.widgetFactory + любой шаблонизатор не позволят вам декларативно описать использование шаблона в html с уже существующем контентом + забиндиными кликами, а именно:
              <expandable header="Click">
                  <ul>
                      <li>lkj;lksdf</li>
                      <li>lkj;lksdf</li>
                      <li>lkj;lksdf</li>
                      <li>lkj;lksdf</li>
                      <li>lkj;lksdf</li>
                  </ul>
              </expandable>
              


              фреймворки которые указали вы — это императив, вот тот же яндекс, отказывается от императива, и можете почитать почему habrahabr.ru/company/yandex/blog/151700/. Кроме они, к сожелению, имеют зависимости от других библиотек.

              PS Я совершенно согласен с товарищами с W3C (Dimitri Glazkov, Google, <dglazkov@chromium.org>, Rafael Weinstein, Google, <rafaelw@chromium.org> Tony Ross, Microsoft, <tross@microsoft.com> dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html) что это необходимо
                0
                PSS Могу добавить только то что — у нас в проекте мидл верстальщик — сам начал писать и использовать компоненты уже через пару дней, как только прочувствовал. Тот же датапикер — из jquery, (я уже это упоминал) нам понадобилось его расширить версткой, поведением и валидацией, и как итог: намного удобнее его использовать в html маркапе как /> чем обертывать его еще 4 дивами для отображения таймлана после выбора даты + везде делать связку JS
                    0
                    Спасибо за линку не знал, интерестно — почитаю внимательнее
                    <div data-dojo-type="dijit.Dialog" data-dojo-props='title:"My Dialog",
                        onFocus:function(){ /* a focus event handler */ }'
                        data-dojo-id="myDialog">
                    </div>
                    

                    Но к сожалению, подход очень похож на ангуляровский, т.е. вместо использования методов addEventListener или on в JQuery на уже существующем контенте, я вынужден исползовать: data-dojo-event=«onClick» data-dojo-args=«evt» то есть прописаные ивенты и аргументы во фреймворке.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое