В ерата на динамичното уеб съдържание, появата на уеб 2.0 доведе до изобилие от техники, всички насочени към това да направят изобразяването на уеб съдържание не само по-бързо, но и по-ефективно и по-лесно за автоматизиране. Нивото на абстракция, използвано за постигане на това обаче, понякога може да скрие важните основи, които правят тези подвизи възможни. Тази статия е опит да се отлепят слоевете и да се изследват основните операции, които са в основата на тези по-сложни абстракции.

Целият код, използван тук, е в моето Github repo за ваше удобство

На основно ниво рендирането на DOM се свежда до добавяне и премахване на DOM възли и избор на най-ефективния начин за това. Има и много подобрения, които са изградени около DOM, за да осигурят алтернативни и все по-ефективни начини за изобразяване. Нека започнем от баребон, статична html таблица.

Това е проста таблица, която показва само един ред от данни. Таблицата обаче може да показва множество редове с данни, ако има повече налични данни. Тези данни са налични предимно от външни източници като REST API и в различни формати като json, csv или xml. Таблицата също в момента показва само четири колони и имената на колоните са статични. Таблицата обаче може да показва различен брой колони и колоните могат да имат различни имена. За да постигнете това ниво на гъвкавост, една статична html таблица определено няма да ви отведе далеч.

Представете си, че имате статична таблица за всеки набор от данни, който евентуално бихте могли да имате. Просто не е възможно да опитате да използвате статично маркиране.

Като отправна точка създайте файл „dynamic.js“ в същата директория като вашия файл index.html. Извлечете всички статични данни от тази таблица и използвайте променлива на Javascript, за да я представите.

Като премахнем това, можем да изобразим тази маркировка на таблица, като използваме низови шаблони на Javascript. Заменете основното съдържание с фрагмента по-долу;

Сега създайте променлива, която да съдържа маркирането на таблицата, и използвайте интерполация на низове, за да замените статичните стойности с тези от променливата tableData.

С тези промени виждаме, че маркирането на таблицата вече не съдържа статични данни и вместо това данните се инжектират чрез заместване на стойностите на шаблона с тези от tableData. Това е голям скок, но все още силно неефективен, защото може да показва само един ред.

Освен това таблицата все още не се изобразява на страницата, което трябва да се направи. Има няколко начина, по които това може да се постигне и всеки различен начин има своите предимства и недостатъци. Нека да направим бърза обиколка и да разгледаме няколко от тези начини.

Ако все още не сте го направили, би било полезно да завъртите сървър за статично съдържание, за да изобразите страницата си. Можете да следвате тези стъпки, ако решите

  • npm init -y (инициализиране на проект на възел в текущата папка)
  • npx http-сървър. -p 8080 -d false (това ще изтегли и стартира http-сървъра, без да го добавя към вашите зависимости)

Като алтернатива можете да изтеглите зависимостта на http-сървъра и да използвате скрипт, за да го стартирате

  • npm i -D http-сървър
  • Добавете скрипта ”start”: “npx http-server . -p 8080 -d false” към вашия package.json
  • стартирайте npm run start, за да стартирате сървъра

Отворете браузъра си и отворете http://127.0.0.1:8080.

  • Използвайте твърдо опресняване в браузъра, за да видите промените, направени в изходните файлове
  • С тази стъпка таблицата се изобразява на страницата, но за съжаление не можете да добавяте повече редове с данни към нея в момента.
  1. Използване на innerHTML

Добавете това към файла _dynamic.js_. Използвайки този подход, innerHTML на даден елемент се заменя с шаблона за низ karkup

Заслуги

  • Супер лесен за използване

Недостатъци

  • Маркирането на низа не е валидирано и браузърът ще направи всичко възможно, за да изобрази каквото може.
  • Полезно само за малки демонстрации без обработка на събития

2. Използване на document.createElement()

Заменете option1 във файла _dynamic.js_, за да създадете дъщерен възел. Използвайки този подход, методът createElement на документа се използва за създаване на нова таблица

Можете алтернативно да създадете DOM елементи за всеки от елементите на таблицата и да ги вложите заедно в желаната форма. Но тъй като размерът на маркирането нараства, това много бързо би станало досадно, опитвайки се да съставите ръчно извикванията createElement. Този подход обаче ви дава много повече контрол върху добавянето на необработено маркиране към дървото на вашия документ. Използването на рекурсивни извиквания за преминаване по възлите на дървото би направило този подход значително устойчив. Някои рамки като React използват някакъв специализиран синтаксис (JSX), за да абстрахират createElement от ниско ниво, като в същото време ви предоставят алтернативен начин за конструиране на DOM дървото.

Заслуги

  • Маркирането се проверява, преди да се добави към елемента на шаблона. Опитайте да напишете грешно таг body като например boby —ще забележите, че таблицата няма да бъде изобразена на страницата

Недостатъци

  • Въпреки че все още е лесен за използване, има допълнителни стъпки, необходими за успешното създаване на нов възел на таблица.
  • Използването на рекурсия за разглеждане на дървото на документа значително ще повиши нивото на сложност

3. Използване на DOMParser

Заменете option2 в _dynamic.js_за да използвате DOMParser. Интерфейсът DOMParser предоставя възможност за анализиране на XML или HTML изходен код от низ в DOM документ.

const doc = domparser.parseFromString(низ, mimeType)

Добавете това към файла dynamic.js

Заслуги

  • Точно както при подхода createElement(), маркирането се валидира, преди да се добави към шаблонния елемент. Опитайте да напишете грешно таг body като например boby —ще забележите, че таблицата няма да бъде изобразена на страницата

Недостатъци

  • Въпреки че все още е лесен за използване, има допълнителни стъпки, необходими за успешното създаване на нов възел на таблица.

4. Използване на DocumentFragment

Заменете опция 3 в _dynamic.js_, за да използвате опцията за фрагмент. Интерфейсът DocumentFragment представлява минимален обект на документ, който няма родител. Използва се като олекотена версия на Document, която съхранява сегмент от структура на документ, съставен от възли точно като стандартен документ.

DocumentFragment() Създава и връща нов обект DocumentFragment.

Добавете това към файла dynamic.js

Заслуги

  • Има всички предимства на използването на createElement или DOMParser.
  • Промените, направени във фрагмента, не засягат документа (дори при преформатиране) или оказват влияние върху производителността, когато се правят промени.

Недостатъци

  • Необходими са значително повече операции, за да се манипулират успешно документните възли, но подходът все още е доста ясен.

Надявам се, че това отклонение е помогнало за хвърлянето на светлина върху няколко техники, които можете да използвате, за да изобразите таблицата. Ще използвам който и да е от тези подходи взаимозаменяемо, за да продължа напред, без да имам категорични мнения защо съм направил този избор, така че не се колебайте да ги смесвате и съпоставяте.

А сега обратно към таблицата — изобразяване на повече от един ред

Таблицата, която имаме, може да показва само един ред. Нека променим това и го накараме да показва променлив брой редове, в зависимост от размера на наличните данни. Така че вместо полето _rows_ да има списък с примитивни стойности, нека вместо това използваме списък със стойности на обекти.

Можем да създадем ред в tbody ръчно, но това би било много неефективно. Ще използваме функция за карта, за да направим това по-уместно. Не забравяйте да съедините нанесените редове заедно, за да образувате низ

Ако колоните на таблицата съвпадат с ключовете за данни в обектите на реда, можете по желание да преработите създаването на елемент _thead_. Например, можете просто да преобразувате ключовете за данни в имена на колони, като изпишете стойността на ключа с главни букви.

Можете също така по желание да използвате трансформацията на филтъра в списък, за да изпуснете някои редове въз основа на критериите, които измислите, преди да приложите трансформацията на картата към резултата. Така че само с филтър и карта вече можете да започнете да виждате как можете да композирате по-сложни трансформации на данните, преди най-накрая да ги изобразите в DOM.

Едно подобрение, което можем да направим в този момент, е да капсулираме функционалността, която в момента имаме, плаваща в скрипта вътре в клас.

Реактивност — динамично актуализиране на DOM

Това е напълно различно измерение, що се отнася до изобразяването на DOM, и това е мястото, където различните рамки започват да се разграничават - това е светият граал, крайната цел, която винаги леко се променя в зависимост от естеството на уеб приложението и на целите, които се стреми да постигне. Реактивността е концепцията, при която съдържанието на DOM се променя динамично въз основа на състоянието на данните, които показва.

Реактивност при използване на JavaScript прокси

Обектът Proxy ви позволява да създадете прокси за друг обект, който може да прихваща и предефинира основни операции за този обект.

Създавате прокси с два параметъра:

- цел: оригиналният обект, който искате да прокси

- манипулатор: обект, който определя кои операции ще бъдат прихванати и как да се предефинират прихванатите операции.

Един много елементарен пример би изглеждал така

В този случай по-горе манипулаторът е празен и така този прокси се държи точно като оригиналната цел. Обаче манипулаторът може да насочва различни методи към обекти. Тези методи за прихващане се наричат ​​Traps.

Следователно вече можете да модифицирате манипулатора, за да прихванете например операцията get и да инжектирате ново поведение, когато има достъп до свойство в целевия обект. Например всяка операция get на целта просто ще върне ’world’ вместо действителната стойност

С това поведение вече можете да накарате таблицата да се рендерира отново, когато данните се трансформират.

Проксиране на данните от таблицата

Нека създадем прокси от данните от таблицата. За целите на илюстрацията запазете табличните данни на най-горното ниво на скрипта, така че да останат в глобалния обхват на прозореца на браузъра.

Като запазим данните в глобалния обхват, ние ще можем да ги модифицираме чрез Инструменти за програмисти

В раздела Конзола на инструментите за програмисти променете възрастта на първия потребител в данните в таблицата

Когато преглеждаме данните от таблицата сега, можем да видим, че стойността на възрастта на първия ред е променена, но таблицата, изобразена в DOM, няма представа, че това се е случило — тя не се изобразява повторно.

Проксито ни дава възможност да реагираме, когато данните в таблицата се променят.

Реагиране на промяна на данните

Нека сега да направим още една стъпка и да разгледаме как таблицата може да бъде повторно изобразена. Това ще изисква малко преработване на класа UserTable

1. Създайте частни полета за редове и колони и изложете съответните им сетери и гетери. По този начин таблицата може да бъде създадена, без да е необходимо да имате готови данни за предаване в конструктора

2. Добавете нов метод към класа, който реагира на промяната в данните.

Повторното изобразяване е доста неефективно в сегашния си вид - то заменя целия компонент при всяка промяна на данните. В идеалния случай бихме искали да намалим DOM отпечатъка, засегнат от повторното изобразяване. Стратегията за целенасочено повторно изобразяване би била по-оптимална в това отношение. Това е тема, която ще разгледаме отново по-късно, когато преработим този код, но засега нека продължим с текущото неоптимално изобразяване

Като начало, нека модифицираме proxyHandler да бъде затваряне, което ще приеме слушател и което на свой ред ще бъде уведомено, когато данните се променят в модела

Методът proxify също ще се нуждае от известна модификация, за да приспособи нов параметър — listener — който ще бъде предаден рекурсивно първо в дълбочина, както и към вложените обекти

Конструкторът на таблица вече не трябва да изисква параметър, така че колоната и редовете да са празни по подразбиране и промяната им трябва да задейства актуализация на таблица.

С тези промени на места, нека сега променим и начина, по който компонентът на таблицата е създаден и изобразен първоначално.

Методът onDataChanged на таблицата се предава на функцията proxify за получаване на известия при промяна на данните. Не забравяйте да използвате bind — table.onDataChanged.bind(table) — когато предавате този метод, така че препратката this винаги да сочи към компонента на таблицата.

Сега обратно в Инструменти за разработчици, отидете в раздела Конзола и променете възрастта на данните от първия ред

И точно така, таблицата се изобразява отново с нови стойности.

Но какво да кажем, ако се добави нов ред? Върнете се в раздела _Console_ и добавете нов ред

И както бихме очаквали, таблицата се изобразява отново с новия ред

Но какво да кажем, ако съществуващ ред бъде изтрит? Върнете се в раздела Конзола и изтрийте втория ред

И както бихме очаквали, таблицата се изобразява отново без изтрития ред

И двете операции за снаждане и натискане се улавят от набора trap в proxyHandler. За да използвате операция като delete, трябва да зададете прихващането deleteProperty в proxyHandlerсъщо

Насочено повторно изобразяване

Бях посочил по-рано, че ще посетим отново метода onDataChanged на компонента UserTable, тъй като той беше твърде агресивен в своята стратегия за актуализиране — той заменя целия компонент с новоизобразено копие. Чрез подмяната на съществуващия компонент, всички съществуващи обвързвания на събития също се губят и ще трябва да се направят отначало. Ясно е, че това не е оптимално решение. Можем ли да се справим по-добре?

The Shadow DOM

Една стратегия би била да се използва shadow DOM за сравняване на съществуващия елемент с новия елемент, преди да се актуализират само различните части. DOM в сянка ви позволява да изобразявате извън действителния DOM, но все още да имате същото точно дърво от възли, което би имал действителният DOM.

В този пример използваният компонент е доста прост, както и алгоритъмът за сравнение. Но както бихте си представили, по-сложен компонент би имал и по-сложен алгоритъм за сравнение

Като първа стъпка, нека актуализираме метода render(), за да върне фрагмент от документ

За следващата стъпка нека въведем нов метод compareAndUpdate в компонента UserTable. Този метод ще премине надолу по възлите на дървото и ще сравни nodeValue на възлите Node.TEXT_NODE за равенство и ако не са еднакви, стойността на съществуващия възел ще бъде актуализирана до тази в компонента, изобразен в shadowDOM. onDataChanged също трябва да използва метода compareAndReplace вместо функцията document.replaceChild()

И разбира се, когато актуализираме данните в таблицата, само засегнатият TextNode се актуализира и абсолютно нищо не се заменя.

Това е далеч по-ефективен подход за актуализиране на DOM в сравнение с последния метод, който заместваше целия компонент, но не е непременно пълен или изчерпателен — това е просто илюстрация на това как даден компонент може да се актуализира прозрачно.

Изобразяване на множество вложени компоненти

Колко сложност ще бъде въведена, ако хвърлим още няколко компонента в сместа? Мисля, че първата логична стъпка, която трябва да предприемете, е да помислите за използване на модули, които да помогнат за разделянето на кода на модулни единици. Първо, във файла index.html декларирайте, че скриптът е от тип module.

index.js е типична стандартна входна точка в графиката на модула, така че нека спазваме тази традиция. Оттук нататък нека да видим как можем да изобразим същата таблица.

Разделих оригиналното съдържание на три файла, за да мога да го използвам повторно. Първият е файлът с данни. Това е абстракция на хранилището на данни, както е обичайно в библиотеки като Redux.

Създайте нов файл data.js

Вторият файл —библиотеката — се състои от кода, който може да се използва повторно и не е специфичен за конкретен компонент

Създайте нов файл library.js

Третият файл — userTable.js — е кодът, който е специфичен за определен компонент и който разширява класа Component на библиотеката

Заменете файла dynamic.js в index.html с новия файл index.js.

И със сигурност, когато обновим прозореца, всички неща трябва да се рендират точно както преди. Една фина разлика е, че глобалните променливи, които манипулирахме преди от раздела Конзола в Инструменти за програмисти, вече не са налични

И така, с този първоначален рефакторинг, нека се опитаме да обвием компонента userTable cвътре в родителски компонент App вindex.js.

Ако сега се опитате да опресните страницата, нещата се разпадат. Това, което се изобразява е:

[object DocumentFragment]

Това е така, защото this.userTable().render() създава фрагмент и той се преобразува в низов литерал в метода template(). Очевидно това не е желаният резултат. Трябва да въведем начин за разграничаване на компоненти от маркиране.

Един правдоподобен начин за постигане на тази цел би бил да се въведе друг метод в класа Component за улавяне на дъщерен компонент, когато такъв бъде добавен, и вместо това да се върне контейнер, който по-късно ще бъде заменен с действителния дъщерен възел.

Класът Component трябва да поддържа списък на своите деца. Добавете конструктор и инициализирайте нова променлива на екземпляр в library.js

Актуализирайте метода render() в класа Component, за да замените контейнерите с действителния дъщерен възел.

И сега нещата трябва отново да изглеждат както преди.

Нека добавим още няколко компонента на таблицата, използвайки различни данни. Започнете с добавяне на друг набор от данни в data.js

Сега актуализирайте компонента Приложение в index.js

Така че с тези няколко добавки, трите компонента на таблицата се изобразяват точно както бихте очаквали.

Добавяне на манипулатори на събития

Досега се занимавахме само с DOM елементи, които не се считат за DOM събития. Как трябва да се третират тези? За да изпробвате това, нека добавим нов файл userForm.js и да създадем компонент на формуляр за актуализиране на данните в таблицата.

Нека изобразим това като дете на компонента App в index.js

Когато страницата се изобрази, тя не е нищо, което очаквахме да видим. Обработчиците на събития се третират като низови литерали в метода template.

Това също подчертава проблем, който беше посочен по-рано като недостатък за задаване на innerHTML стойността с помощта на шаблони за низове — браузърът се опитва да изобрази възможно най-добре, ако не може да анализира правилно съдържанието на низа.

Правдоподобно решение може да бъде създаването на базови компоненти за контроли на формуляри, като в този случай TextInput и ButtonInput

Създайте нов файл textInput.js

Създайте нов файл buttonInput.js

Двата компонента са много основни, но те илюстрират как можете да капсулирате атрибутите на контролите на формуляра, които представляват. С тази промяна още няколко файла трябва да бъдат актуализирани, за да отразяват новите контроли на формуляра

Добавете поле formData към файлаdata.js

Актуализирайте компонента userForm в userForm.js, за да използвате proxify и също така да използвате новите контроли, които току-що създадохме

Актуализирайте App.js, за да се съобразите с промените, направени в компонента userForm

Сега формулярът се изобразява без проблеми

Все още обаче не сме определили как да прикрепим манипулаторите на събития към съответните им контроли. Е, това може да се направи във функцията render(), която ще провери дали Component съдържа манипулиращи функции

Добавете нов член — this.element — към конструктора на клас компонент

Актуализирайте функцията render() и докато правите това, присвоете на tновото свойство на елемента изобразената стойност на компонента

Актуализирайте функцията onDataChanged, за да използвате новия елемент

В този момент формулярът се изобразява на страницата и събитията се задействат според очакванията.

Тъй като елементът се изобразява само веднъж и не се заменя по-късно, манипулаторите на четните ще останат прикрепени към този елемент за целия им живот.

Актуализиране на таблични данни с помощта на формуляра

Сега е време да добавите връзка на компонента на таблицата с компонента на формуляра и да видите как могат да си взаимодействат. Но първо преработете както userTable.js, така и userForm.jsза да експортирате само класа. Ще свържем компонентите с данните, от които се нуждаят в компонентаПриложение.

Актуализираният index.js за отразяване на промените, направени в компонентите

Страницата трябва да продължи да се визуализира и да се държи точно както преди стъпката на рефакторинг.

Мисля, че това е хубаво място за пауза. Можем да продължим да се задълбочаваме и да проучваме различни възможности, но мисля, че основната точка тук е отбелязана. Беше доста дълго пътуване, изследвайки различния начин, по който можете да използвате ванилен JavaScript за внедряване на реактивност. В друга статия ще разопаковам това, което разгледахме тук, и ще го приложа в контекста на уеб компонентите.

Има много страхотни и тествани в битки рамки, които вече правят това и които имат много повече функции (както количествено, така и качествено). Надявам се, че това ви вдъхновява винаги да сте любопитни.