Една от отговорностите на екипа за преобразуване е уеб приложение, което следва „модела за проектиране на съветника“. Шаблонът за проектиране на съветника предполага, че потребителят трябва да попълни много данни в няколко стъпки, за да стигне до края на фунията. Това, което потребителят прави веднъж или два пъти, членовете на нашия екип изпълняват десетки или стотици пъти. Изпълнението на предни задачи може да бъде доста бавно поради нуждите от често въвеждане на данни във фунията.

В интерес на истината си струва да се отбележи, че има начин да се заредят данни във фуния за една държава, но нашето приложение е за няколко държави. Такова зареждане работи по доста лесен начин - трябва само да добавите параметър /?id=[funnel_id] в низа на заявката и данните ще бъдат заредени в приложението.

Въпреки факта, че този подход опростява работата, все още има недостатъци:

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

Имайки положителен опит в писането на разширения за опростяване на ежедневните задачи, се запитах „как мога да избегна или автоматизирам въвеждането на данни във фунията?“

Постановка на проблема

За да разбера обхвата на задачата, първо изброих най-болезнените точки:

  • попълване на данните за фунията за всяка държава
  • поддръжка на различни сценарии; има два възможни резултата в края на фунията: положителен или отрицателен. За всеки от тях е необходим различен набор от данни
  • бърз преход към края на фунията, за да избегнете преминаването през всяка стъпка

Внедряването на точките по-горе би ускорило значително разработването на предни задачи. Освен запълването на фунията, друга задача, която може да бъде опростена, идва на ум - A/B тестове.

A/B тестовете се използват широко в нашето приложение, а като двигател използваме Kameleoon. Трудността с Kameleoon е, че не е ясно кои тестове и техните вариации са активни на определена страница. Разбира се, това не означава, че е невъзможно да се открие. Можете да отворите редактора на Kameleoon (той зарежда скрипта на страницата) и да видите всички възможни тестове или да погледнете конзолата на браузъра (Kameleoon има API, който ви позволява да видите тестовете, стартирани на страница). И двете опции могат да се използват, но те далеч не са удобни или бързи начини за преглед на активните тестове на страница. Поради това реших да добавя поддръжка на Kameleoon към моя списък със задачи.

Търсене на решение

За мен беше важно да не променям нищо в изходния код на приложението за фуния. Моето решение трябваше да бъде външно и независимо. Тъй като Google Chrome е нашият основен инструмент за разработка (разбира се, след VS Code), започнах да гледам към разширенията на Chrome. Възможни са всякакви типове разширения, от добавяне на маркиране към страница до персонализиран панел в прозореца с инструменти за разработчици. Добавянето на HTML маркиране към страницата ми прозвуча добре.

След като решихте инструмент за изпълнение на задача, изскочи нов проблем - как да напълня фунията с данни? За да съхраняваме данни в предния край, използваме ngrx store, така че започнах да търся опции за вмъкване на данни в store от външната страна на основното приложение.

Прегледах документацията на ngrx, но не намерих отговор там. По това време ми хрумна разширението Redux DevTools. От самото начало използвах това разширение, докато работех по този проект, за да проследя промените на store. Стана очевидно, че има начин да се стигне до store.

Здравейте на Redux DevTools

След като изтеглих неговия изходен код, започнах да отстранявам грешки в разширението Redux DevTools; след известно време разбрах с какво ще трябва да се справя. Ето диаграма, описваща основната идея зад Redux DevTools:

Нека да разгледаме по-отблизо тази диаграма. В началото Redux DevTools създава глобална променлива в обекта window, наречена __REDUX_DEVTOOLS_EXTENSION__. Ето от какво се състои тази променлива:

export interface ReduxDevtoolsExtension {
 connect(
 options: ReduxDevtoolsExtensionConfig
 ): ReduxDevtoolsExtensionConnection;
 send(action: any, state: any, options: ReduxDevtoolsExtensionConfig): void;
}

Когато се извика функцията connect, се връща обект със следния API:

export interface ReduxDevtoolsExtensionConnection {
  subscribe(listener: (change: any) => void): void;
  unsubscribe(): void;
  send(action: any, state: any): void;
  init(state?: any): void;
  error(anyErr: any): void;
}

В този пример с код трябва да се вземат предвид две функции: subscribe и send. Функцията subscribe се използва от ngrx за абониране за външни действия, докато функцията send се използва за уведомяване за промени, случващи се в store.

От диаграмата следва, че няма начин да се свържете директно към ngrx store. Това, което можете да направите, е да предоставите API, който store ще използва, за да се свърже с вашето разширение; този подход се използва в Redux DevTools.

Ограничение на този подход е, че store може да се свърже само с един API. Това ми пречи да използвам същия подход, тъй като би довело до несъвместимост между моето разширение и Redux DevTools (те няма да работят едновременно). Това не е това, което исках.

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

Както е показано, кодът за разширение може да бъде изпълнен в два различни контекста:

  • контекст на страницата: има достъп до window обект и DOM елементи, докато няма достъп до API на Chrome;
  • контекст на разширението: има независим window обект, който няма нищо общо с window обекта от текущата страница. Той няма достъп до страницата javascript, но има право да манипулира DOM и да извиква API на Chrome.

За обмен на данни между контексти се използва обмен на съобщения. Може да се постигне с помощта на функцията window.sendMessage. Мисля, че този подход се използва от съображения за сигурност, тъй като двигателят на Chrome може да проверява съдържанието на съобщенията за заразен код.

И така, знаем, че има два контекста и вие, като разработчик, решавате кои скриптове в кой контекст да се изпълняват. Някои от скриптовете на Redux DevTools се изпълняват в контекста на страницата, което позволява на store да се свърже с предоставения API; други скриптове се изпълняват в контекста на разширението, това включва панела с инструменти за програмисти. Помежду си тези скриптове комуникират чрез съобщения. Тъй като има много съобщения и не всички са ваши, е необходимо да филтрирате излишните. Ще се потопим по-задълбочено в това по-късно в статията.

Идеята ми беше да се представя за разширение Redux DevTools и да изпращам съобщения до store от негово име. Предимство на този подход е възможността да използвам Redux DevTools с мое собствено разширение; от друга страна, това води до зависимост с Redux DevTools. Реших, че това е приемлив компромис.

Писане на разширението

Преди да започнете да пишете кода, беше необходимо да решите още един проблем: как да покажете наличните опции (сценарии) за попълване на фунията на страницата? Исках страницата да има малка джаджа, която може да се мести из страницата и която да стои над всички останали елементи. Намерих решението в нашия собствен проект: при решаването на един от проблемите, Bilel Msekni използва ъглови елементи, които могат да бъдат използвани като самостоятелно ъглово приложение. Този подход е много удобен, тъй като ще трябва да добавя само един потребителски елемент към страницата, а Angular ще свърши останалото вместо мен.

Разработката на Chrome Extension започна с дефиниране на скелета, в който файлът manifest.json играе важна роля:

В горния код content_scripts съдържа скриптове, които ще бъдат качени на страницата. За да се определи в кой момент от време трябва да се зареди определен скрипт, флагът run_at се използва с опциите document_start или document_end (използвани по подразбиране). Също така си струва да обърнете внимание на параметъра matches, той има информация за кои сайтове или домейни ще бъде заредено разширението. Ако вашето разширение трябва да се изпълнява само на определени сайтове, би било по-добре да ги посочите в опцията matches.

След като скелетът на разширението е дефиниран, е време да създадете скриптови файлове и да ги конфигурирате да се зареждат на страницата. Например, решете кои файлове да заредите в контекста на страницата и кои в контекста на разширението. Дефинирах следните файлове за бъдещо разширение:

pageScriptWrap.js зарежда файла pageScript.js в контекста на страницата, като добавя маркера <script /> към head на страницата (по този начин кодът ще бъде изпълнен в контекста на страницата и ще има достъп до обекта window):

pageScript.js съдържа кода за изпращане на съобщения от името на Redux DevTools до store.

webPanelScriptWrap.js зарежда скрипта webPanel.js в контекста на страницата по същия начин, както го прави pageScriptWrap.js. Скриптът webPanel.js е изградено приложение с ъглови елементи, той също трябва да се изпълнява в контекста на страницата, за да има достъп до store.

contentScript.js файлът съдържа логиката за работа с Chrome API и следователно работи в контекста на разширението.

След като дефинирах основните скриптове и начините за зареждането им, започнах да пиша кода.

Ъглови елементи

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

ngDoBootstrap() се извиква за инициализиране на Angular-Elements, където createCustomElement създава обвивката на приложението и я поставя във функцията customElements.define(), която дефинира новия персонализиран етикет към браузъра.

customElements.define()е част от стандарта ECMAScript и се поддържа от всички основни браузъри.

След като персонализираният елемент бъде деклариран, етикетът<rettoua-web-panel /> може да бъде добавен към страницата. Браузърите ще открият персонализирания елемент и ще инициализират Angular приложение в него. Това е основната характеристика на Angular-elements — възможността да се изпълнява пълноценно приложение в един таг.

Приложението Angular е доста просто и е представено като джаджа на страницата:

Widget е отговорен за показването на подготвен сценарий, който може да бъде изпратен до store. Може също да се използва за преминаване към последната стъпка на фунията (застрахователна връзка). Джаджата е много проста и не изисква допълнително описание. Това, което е наистина интересно, е начинът за взаимодействие с store.

Имитиране на Redux DevTools

Както вече споменах, реших да използвам разширението Redux DevTools, а именно да изпращам съобщения от негово име, за да изпращам данни към store. Първото нещо, което трябва да направите, е да създадете съобщение в правилния формат, второто е да изпратите съобщение от името на Redux DevTools. За тази цел създадох малък клас:

В примерния код полето source съдържа идентификатора Redux DevTools, с който това разширение определя, че съобщението му принадлежи и го обработва, като изпраща съобщение до store.

Функцията sendMessage създава съобщение и го разширява със свойство source, което действа като идентификатор.

Функцията sendAction създава ACTION съобщение, което съответства на Action в ngrx.

Функцията sendGo създава съобщение от тип [Router] Go, за да извика действието за промяна на местоположението, посочено в url.

За да се определи необходимия формат на съобщението, беше необходимо да се отстранят грешки в разширението Redux DevTools. На този етап бих искал да изразя своята благодарност към разширението Redux DevTools, тъй като ме спаси от необходимостта да пиша голямо количество код.

Използване на API на Chrome

Уиджетът съдържа подготвени сценарии, които могат да бъдат изпратени до store от потребителя. С цел разширяване на възможностите на джаджата е добавена възможност за редактиране на сценарий, което е много удобно, ако трябва да покриете конкретен случай. API на Chrome се използва за запазване на сценарии в локално хранилище:

Функцията persist записва данни в хранилището чрез ключ, а функцията get се използва за извличане на данни от хранилището.

Тъй като API на Chrome е достъпен само в контекста на разширението, приспособлението не може да го използва, тъй като е част от страницата и се изпълнява в нейния контекст. За да реша този проблем, използвах съобщения и rxjs потоци:

Във функцията sendMessage параметърът source в обекта payload е важен — благодарение на него съобщенията могат да бъдат идентифицирани като принадлежащи към моето разширение. За получаване на съобщения се използва window.addEventListener с първия параметър, обозначен като message, което позволява получаването само на съобщения; след това проверява полето source, ако съвпада, тогава съобщението трябва да бъде обработено. Всички съобщения се изпращат към потока subject за по-нататъшна обработка.

Следващата стъпка е да изпратите съобщения, когато сценарият се промени; effect е подходящ за това:

Когато сценарият се промени, се изпраща ново състояние за запазване в локално хранилище, където SAVE_SCENARIO_STATE се използва като ключ. Чрез същия ключ запазеното състояние може да бъде извлечено от локално хранилище. Остава само да получите желаното съобщение и да запишете действителната стойност в хранилището:

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

След като написах всичко, което първоначално беше планирано за работа със сценарии, преминах към последната, но не по-малко важна част - Kameleoon.

Справете се с A/B тестовете на Kameleoon

Използването на A/B тестове често означава промяна на потребителския интерфейс или поведението на страницата в зависимост от активния вариант. По време на разработката и тестването често се налага превключване между активна вариация на A/B тест, което е доста неудобно поради липсата на лесен и бърз начин за това.

A/B тестовете се зареждат на страницата чрез специален скрипт и се съхраняват в обекта window.Kameleoon.API.experiments. Този обект съдържа списък с всички заредени тестове и техните варианти. Всеки експеримент съдържа важна информация като:

  • активен тест или не
  • списък с възможни варианти
  • активна вариация

Наличните A/B тестове са доста лесни за показване в изпълнимия модул с помощта на обекта experiments:

След показване на наличните A/B тестове е възможно да добавите начин за промяна на активния вариант, като щракнете върху него. Следният код показва как да го направите:

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

Като цяло джаджата предоставя следните функции:

  • изпращане на избрания сценарий до магазина
  • поддържа редактиране на сценарии
  • възможност за директно преминаване към последната стъпка
  • показване на заредени A/B тестове
  • промяна на активната вариация на A/B теста

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

Източниците могат да бъдат намерени тук.