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…

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)

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/