UserJS: Allegro Seller Info - lista aukcji
• tech • 1748 słów • 9 minut czytania
Nie sądziłem, że skrypty użytkownika (UserJS) tak bardzo popularne w poprzedniej dekadzie wciąż się dobrze trzymają i nadal często są wykorzystywane. Choć pewnie większość wypierana jest przez proste, małe dedykowane rozszerzenia. Mimo faktu, że idealnie nadawałby się w tym miejscu typowy skrypt poprawiający lub modyfikujący to i owo na stronie…
Pamiętam, że w zamierzchłych czasach standardy tutaj wyznaczał Firefoksowy Greasemonkey. A skryptowanie było dostępne natywnie, bądź za pomocą wtyczki, w każdej ówczesnej przeglądarce, nawet IE miało swoje rozszerzenia.
Tak ostatnio babrając się w JavaScripcie przy próbie okiełzania mojego prostego, eksperymentalnego kodu wtyczki do Chrome-a (o którym może kiedyś opowiem), zacząłem na nowo odkrywać ten język. I to pomimo tego, że kiedyś nawet coś większego w JavaScripcie udało mi się zbudować. Ale tak bywa kiedy rzadko się używa danej technologii, czy języka. Co jakiś czas trzeba na nowo poznawać te wszystkie stare/nowe idiomy, sztuczki i kruczki, które kiedyś były czymś naturalnym. Nie było inaczej także tutaj, ale nie o tym teraz tutaj…
Gdy raz na kilka lat zdarza mi się napisać coś większego w #JavaScript to zawsze na nowo muszę odkrywać zapomniane tricki i idiomy #JS-a... zamiast walnąć prowizorkę, którą i tak tylko ja będę używał ;)
— Marcin Malich (@malcompl) February 17, 2019
Przy tej zabawie z rozszerzeniem przypomniałem sobie, że może poprawiłbym coś na Allegro, szczególnie te irytujące mnie braki w użyteczności, które poprawiłyby trochę komfort korzystania z tego serwisu. Bo nic tak nie wkurza jak psucie i wywalanie używanych funkcjonalności lub wprowadzanie zmian, które nie chcą współgrać z naszymi przyzwyczajeniami.
Tą wielce irytującą mnie niedogodnością jest brak kluczowych informacji (nazwa i lokalizacja sprzedającego) na listach aukcji. I to pomimo tego, że po prawej stronie mamy tyle wolnego miejsca, gdzie można byłoby te dane wyświetlić…
Co prawda po najechaniu myszką na zdjęcie pojawia się nieco rozszerzona karta z informacjami, ale to jednak nie to samo… więc postanowiłem sam nieco ulepszyć użyteczność serwisu.
I wtedy babrając się ze wspomnianym rozszerzeniem, przeleciała mi paskudna myśl o jakieś wtyczce do przeglądarki. Na szczęście szybo się opamiętałem, bo to przecież jak strzelanie z armaty do muchy. Zdecydowanie lepiej byłoby wykorzystać stare poczciwe skrypty użytkownika. Oczywiście o ile w ogóle coś takiego jeszcze istnieje w aktualnie istniejących modern przeglądarkach.
Szybko zorientowałem się, że pod googlową przeglądarką prym wiedzie rozszerzenie Tampermonkey i takowe sobie zainstalowałem. O dziwo kiedyś w odległych czasach, chyba nawet takie skrypty w Chromie działały natywnie, a przynajmniej tak gdzieś w sieci ludzie pisali.
Na początek upewniłem się czy czasem te dane do karty wyświetlanej przy zdarzeniu hover nie są zasysane z sieci na żądanie przez XHR and stuff. Teraz panuje taka moda, że wszystko na żywo dociągane jest z serwera. I gdyby tutaj tak zrobiono to i dupa, i kamieni kupa…, bo nic nie zrobimy. Przecież nie ma sensu wysyłać dziesiątek zapytań dla każdej aukcji na liście.
Na szczęście zakładka Network w DevTools-ach nie pokazuje żadnej aktywności dla tej strony przy pokazywaniu karty. To oznacza, że dane są już gdzieś załadowane na stronie. Pogrzebałem w DOM-ie strony, sądząc, że może gdzieś jakieś ukryte elementy się znajdują i modyfikowana jest tylko ich widoczność, ale nic z tego. Chociaż wydaje się, że jednak ma miejsce takie manipulowanie wyświetlaniem ukrytych elementów drzewa, ale dopiero po dodaniu tych elementów do DOM-a, które następuje przy pierwszym wyświetleniu karty.
Rzuciłem okiem na źródło strony, a tam moim oczom ukazał się ciekawy widok. Większa wstawka kodu JS opakowująca dane wszystkich aukcji ze strony w obiekt __listing_StoreState_base
;)
A w nim znalazłem dokładnie te informacje jakie są mi są tutaj potrzebne.
Dodatkowo istnienie tego obiektu w kontekście strony jest idealnym znacznikiem pozwalającym stwierdzić, czy aktualna strona zawiera listę aukcji, produktów i należy ja “poprawić”, czy może to jakaś inna strona serwisu. Bo tak się składa, że po url-ach nie da się tego w łatwy sposób określić.
Wszystkie elementy listy z aukcjami są też ładnie opakowane w HTML5-owe <article/>
, w którego atrybutach udało mi się zlokalizować identyfikator aukcji…
W oczy może koleć te napaćkanie różnych atrybutów w prawie każdym elemencie strony. Pakują pełno tych skryptów śledzących i zbierających dane o akcjach użytkownika, że pewnie dowolne kliknięcie na stronie jest przetwarzane i analizowane, a na końcu zamieniane na pieniądze…
Fajnie byłoby sobie zrobić mapę indeksującą dane aukcji z __listing_StoreState_base
po identyfikatorze aukcji, w celu szybkiego wyszukiwania obiektu.
var map = {};
__listing_StoreState_base.items.itemsGroups.forEach(g => {
g.items.forEach(o => { map[o.id] = o; });
});
A wtedy prostym selektorem łatwo pobrać listę aukcji i jednoznacznie stwierdzić, czy to rzeczywiście aukcja, czy może jakiś innych fragment strony opakowany tagiem <article/>
.
document.querySelectorAll('article[data-analytics-view-value]').forEach(e => {
var item = map[e.getAttribute('data-analytics-view-value')];
if (!item)
return;
// dodawanie wstawki do strony...
});
Pora na dorzucenie danych do strony… Po krótkiej analizie idealnym miejscem wydaje się ten sam kontener, gdzie znajduje się nagłówek aukcji z tytułem w h2
. Dodanie mojego elementu z pozycjonowaniem absolutnym względem rodzica skutkuje zamierzonym efektem.
Teraz trzeba jakoś zlokalizować przyszłego parenta dla mojego diva
-a. Niestety mało jest na stronie elementów identyfikowanych po id, a nazwy klas też jakby generowane losowo i na różnych stronach mają zupełnie inne nazwy, więc na nic się mi one nie przydadzą.
Miałem pomysł na określenie rodzica za pomocą nagłówka aukcji:
var parent = node.getElementsByTagName('h2')[0].parentElement;
Ale gdy po najechaniu na zdjęcie zostały już wstrzyknięte elementy karty, to pierwszy h2
na jaki natrafię to będzie właśnie ten z tej karty… zdecydowałem się zrobić to paskudnie po chamsku:
var parent = node.children[0].children[0].children[1].children[0];
I tak skrypt jest dostosowany do bieżącej wersji i jak cokolwiek zostanie zmienione na stronie to będzie wymagał poprawek, więc nie ma sensu szukać jakiś genialnych rozwiązań ;)
A mając już parenta wystarczy ustawić odpowiednie pozycjonowanie, aby stał się on elementem zawierającym dla naszego wstrzykiwanego na koniec dziecka.
parent.style.position = 'relative';
parent.insertAdjacentHTML('beforeend', `
<div class="${cssName}">
<span class="seller"><a href="${item.seller.userListingUrl}">${item.seller.login}</a></span>
<span class="location">${item.location}</span>
</div>
`);
Oczywiście nie obyło się też bez wstrzyknięcia na początku kawałka kodu z własnymi stylami CSS:
const cssName = 'mX6tzke5';
document.body.insertAdjacentHTML('beforeend', `
<style type="text/css">
div.${cssName} { position: absolute; top: 0; right: 0; }
div.${cssName} > span { display: block; text-align: right; }
div.${cssName} > span.seller a { color: #00a790; text-decoration: none; }
div.${cssName} > span.seller a: link { color: #00a790; }
div.${cssName} > span.seller a: visited { color: #006456; }
div.${cssName} > span.location { margin-bottom: 10px; }
</style>
`);
Bo niestety podpięcie się lub wykorzystanie istniejących klas, których nazwy się zmieniają jest dosyć ryzykowne. Próba potencjalnego programowego znalezienia pasującej klasy też nie wydaje się prosta. A po chamsku najprościej, a co ;)
No i wszystko gotowe!
Po małym testowaniu na różnych stronach, okazuje się, że jednak czasem te puste miejsce na liście po lewej stronie jest wykorzystywane. Przedmioty z kont firmowych lub oficjalnych sklepów, mające ustawione jakieś własne loga właśnie tam je wyświetlają i wszystko psują.
Ale i na to jest sposób. Kawałek htmla z logiem wstawiany jest jako pierwszy potomek w naszym parencie, więc łatwo wykryć ten fakt i coś z tym zrobić.
// jesli jest logo sklepu, to przenies je do naszego kontenera,
// zeby wyswietlalo sie pod nasza wstawka...
if (parent.firstElementChild.nodeName != 'H2')
parent.lastElementChild.appendChild(parent.firstElementChild);
Można pomyśleć, że pierwsza działająca wersja jest już gotowa, ale nie tak prędko!
Zmiana sortowania listy aukcji lub przejście do kolejnych podstron z produktami dokonywane jest asynchronicznie AJAX-em. Cała zawartość listy jest przebudowywana, a nasz globalny obiekt z aukcjami, jak sama nazwa wskazuje, nie będzie zawierał aktualnie doładowanych danych, bo on służy tylko do tej bazowej zawartości dostarczanej wraz ze statyczną stroną początkową.
Argh… czyli debugowanie JS-a i szukanie jakiegoś punktu zaczepienia.
A debugowanie zaciemnionego i skompresowanego kodu nie jest przyjemne, nawet po jego przeformatowaniu. Co prawda nie powinienem narzekać, bo jednak przypomina jakiś pseudokod, co i tak teoretycznie powinno być łatwiejsze niż babranie się w binarkach i asemblerach… Ale w praktyce to już jakoś łatwiej mi poruszać się po dezasemblowanym kodzie binarnym.
Cały wieczór męczyłem się z JavaScript-owym debugerem i tak naprawdę nie znalazłem żadnego ciekawego eventu lub kodu, pod który mógłbym się łatwo podpiąć i na przykład ponownie przebudować zawartość listy aukcji.
Na szczęście śledzenie prawie całej ścieżki wykonywania kodu - upierdliwe jest debugowania asynchronicznych shitów w JS-sie - począwszy od wysłania requesta przez XMLHttpRequest
do budowania elementu nie poszło na marne. Rzuciło trochę światła na bebechy…
Okazało się, że wykorzystywana jest biblioteka React.js, a w jej wewnętrznych strukturach doklejanych do zbudowanych przez nią elementów DOM (__reactEventHandlers$[jakiś-hash]
, __reactInternalInstance$[jakiś-hash]
) dorzucane są też odebrane z sieci interesujące mnie dane.
Referencje do danych o aukcji z elementu DOM można łatwo wyłuskać za pomocą prostego kodu:
var item;
for (var i in node) {
if (i.startsWith('__reactEventHandlers')) {
item = node[i].children.props.item;
break;
}
}
Co ciekawe, jeśli dany article
nie posiada tego szukanego elementu (atrybutu) to widocznie nie jest to aukcja, a jakiś inny element article
na stronie. Co ostatecznie umożliwia skorzystanie z wydajniejszej tutaj metody getElementsByTagName
zamiast selektora do wyłapania wszystkich wystąpień aukcji.
Jak się można domyśleć, na pierwszej stronie bazowej też można dobrać się do tych bebechów reacta w elementach DOM-owych. A to wszytko ostatecznie ładnie upraszcza i unifikuje kod ;)
Teraz tylko znaleźć jakieś zdarzenie, w którym mógłbym odświeżyć przebudowaną listę aukcji.
Próbowałem wykorzystać różne tricki z nasłuchiwaniem zmian w DOM-ie włącznie, wychwytując dodawanie i usuwanie elementów <article/>
, ale nie otrzymałem zadowalających rezultatów.
Pojawiła się kolejna ciekawa idea. Na czas ładowania i budowania listy, jak to ma miejsce w tego typu sytuacjach, często wyblurowany jest fragment strony czy interfejsu aplikacji z danymi, prezentując jakiś progress-bar lub innego tego typu gówienko. No i …
No i tak jak się spodziewałem, jakiś tam przodek elementu zawierającego listę aukcji na czas “odświeżania” dostaje drugą dodatkową klasę ze stylami. Co skutkuje tym, że atrybut class
tego elementu zmienia się w takt występowania zmian listy. Obserwując jego modyfikacje otrzymam precyzyjne informacje o rozpoczęciu i zakończeniu budowania nowej listy.
var mutationObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
if (m.attributeName == 'class' && m.target.className.indexOf(' ') == -1)
UpdateList();
});
});
mutationObserver.observe(listNode, { attributes: true });
Usuniecie dodatkowej klasy będzie równoznaczne przypisaniu nowej wartości bez spacji. Będzie to dokładne zdarzenie informujące o zakończeniu przebudowania listy, które powinno “odpalić” mój kod wzbogacający nową listę o wstawki z informacjami o sprzedającym ;)
Ostateczne testy potwierdziły idealnie działanie skryptu. Także można korzystać, do czasu aż Allegro coś zmieni i popsuje, ale wtedy naprawimy i tak w kółko…
Pełny kod mojego UserJS-a do Allegro można będzie tymczasowo znaleźć na githubie w gits-ach lub w mal-code-repo z przykładowymi kawałkami kodu. Docelowo pewnie kiedyś trafi w jakieś inne, dedykowane miejsce.
A tymczasem pojawił się kolejny pomysł, aby rozbudować skrypt i na stronach aukcji, gdzieś w nagłówku, także wyświetlać informacje o lokalizacji sprzedającego. Ale o tym w kolejnym odcinku, bo teraz już wiem, że wkrótce powstanie :)
Komentarze (0)