Ruby on C(ocaine) 💉

Od jakiegoś czasu chodziło mi po głowie stworzenie rozszerzenia w C dla języka Ruby. Po kilku wieczorach pracy stworzyłem mini biblioteczkę – MatrixBoost, która przyspiesza niektóre operacje (aktualnie mnożenie i odwracanie macierzy) klasy Matrix ze standardowej biblioteki Ruby’ego.

Post dostępny tutaj:

https://dev.to/bajena/ruby-on-c-ocaine-intro-to-c-extensions-for-newbies-40ib

Co oznacza błąd „invalid byte sequence in UTF-8” w Ruby i jak go naprawić?

Jeśli trafiłeś na tą stronę to prawdopodobnie zmagasz się z tym samym problemem, co ja. Z tego posta dowiesz się trochę o tym, czym są kodowania znaków, skąd biorą się nieprawidłowe sekwencje bajtów i jak je naprawić.

Post można przeczytać pod tym adresem:

https://dev.to/bajena/solving-invalid-byte-sequence-in-utf-8-errors-in-ruby-1f27

Jak stworzyć własne źródło danych w Google Data Studio?

Google Data Studio jest stosunkowo nową technologią, jednak zyskująca na popularności. Ostatnio zainteresował mnie temat budowania źródeł danych dla GDS i postanowiłem opisać moje doświadczenia związane z tą platformą budując przykładowy connector pobierający dane z API Spotify.

Zacząłem serię postów, którą będę prowadził w serwisie medium.com.

Poniżej lista dotychczas napisanych postów:

Piszemy rozszerzenie do Sublime Text 3

Potrzeba matką wynalazku

Ostatnio pisząc testy jednostkowe zauważyłem, że bardzo często powtarzam schemat „Napisz test” -> „Skopiuj test” -> „Zmień wartość w skopiowanym teście tak, by przetestować warunek odwrotny” -> „Zmień opis na odwrotny”.

W praktyce wygląda to mniej więcej tak:

negate_example.gif

Pomyślałem, że mogę nieco usprawnić ten proces, a przy okazji nauczyć się czegoś nowego – wpadłem więc na pomysł, że stworzę plugin do mojego ulubionego edytora – Sublime Text 3.

Plugin ten ma mieć jedno bardzo proste zadanie, a mianowicie zamienianie angielskich zdań w trzeciej osobie liczby pojedynczej z twierdzących na przeczące i na odwrót.

Ale od czego mam zacząć?

Skoro pomysł już jest, to teraz wystarczy go zrealizować, prawda? Problem jest tylko taki, że nie mam pojęcia, jak pisać pluginy do ST3. Szybkie googlowanko zaprowadziło mnie do tego artykułu. Stamtąd dowiedziałem się, że pluginy są po prostu pythonowymi klasami dziedziczącymi po sublime_plugin.TextCommand i odpalane są przez interpreter Pythona wbudowany w Sublime’a. Nie byłem tym zachwycony, bo nigdy nie pisałem w tym języku, jednak odnalazłem w nim sporo analogii do ruby’ego, a z większością problemów wynikających z nieznajomości API lub składni mogłem się uporać dzięki kochanemu StackOverflow.

Mimo że głównym elementem mojego pluginu są operacje na stringach, potrzebowałem również zapoznać się z podstawami API Sublime’a (Dokumentację można znaleźć tutaj). Interfejs nie jest super intuicyjny, ale do mojego celu wystarczyło zrozumieć kilka konceptów:

  • klasa View – reprezentuje okienko edytora. W momencie, gdy nasz ekran podzielony jest na kilka okien edycji wywołanie self.view w klasie naszego plugina zwróci okienko, w którym aktualnie jest kursor.
  • Klasa Edit – jej instancja przekazywana do każdej komendy (pluginu) w sublimie. Nie posiada ona żadnych metod publicznych i służy grupowaniu zmiar bufora, czyli w praktyce umożliwia użytkownikowi np. cofnięcie lub ponowienie zmian wprowadzonych przez nasz plugin.
  • Klasa Region – reprezentuje wycinek tekstu. Jest to w zasadzie tablica dwóch liczb + kilka pomocniczych metod.

Po zapoznaniu się z tutorialem i podstawami API Sublime’a zacząłem zastanawiać się, jak napisać mój plugin…

Krok 1 – szukamy zdania w linii

Pierwszą rzeczą, którą musi zrobić nasz program będzie znalezienie w linii, w kórej aktualnie znajduje się kursor Regionu obejmującego zdanie w pojedynczych (‚ ‚) lub podwójnych (” „) apostrofach. Aktualna pozycja kursora musi zawierać się w tym regionie. Postanowiłem, że najlepiej do tego sprawdzi się wyrażenie regularne.

Przy budowaniu wyrażeń regularnych bardzo przydatna jest strona https://pythex.org/ – zaoszczędziłem dzięki niej mnóstwo czasu.

Zacząłem od następującego wyrażenia:

(\”.*\”|(\’.*\’))

To wyrażenie jest niezłe, bo pozwala na dopasowanie zarówno do pojedynczych, jak i podwójnych apostrofów ale ma jedną wadę – jest „zachłanne”. Oznacza to, że w wypadku, gdy linia zawiera kilka stringów w apostrofach nasze wyrażenie znajdzie cały ciąg znaków znajdujący się pomiędzy pierwszym i ostatnim apostrofem w danym zdaniu. Czyli zamiast It requires a name” and „It requires a surname wyszuka „It requires a name” and „It requires a surname”. Na szczęście python posiada proste narzędzie, pozwalające to naprawić:

(\”.*?\”|(\’.*?\’))

Teraz wyrażenie działa tak, jakbyśmy tego oczekiwali.

Niestety moja radość nie trwała długo – kolejnym problemem, na który się natknąłem, był fakt, że moje wyrażenie nie radzi sobie z tzw. escaped quotes, czyli z pojedynczym apostrofem poprzedzonym znakiem backslash. Tutaj znowu python daje radę – zgodnie z regular expression cheatsheet ze strony pythex.org aby odfiltrować apostrofy poprzedzone backslashem można wykorzystać negative lookbehind assertion:

(?<!…)

Finalnie wyrażenie wygląda tak:

(\”.*?\”|((?<!\\)\’.*?(?<!\\)\’))

Po zbudowaniu odpowiedniego wyrażenia musiałem już tylko spośród dopasowanych ciągów znaleźć taki, którego region zawiera pozycję kursora:

def find_quotes(self):
quotes_regex = r'(\".*?\"|((?<!\\)\'.*?(?<!\\)\'))'
iterator = re.finditer(quotes_regex, self.current_line())
for match in iterator:
quote_span = match.span()
region = sublime.Region(quote_span[0] + self.current_line_start(), quote_span[1] + self.current_line_start())
if region.contains(self.cursor_position):
return (quote_span[0], quote_span[1])

view raw
find_quotes.py
hosted with ❤ by GitHub

Krok 2 – negujemy zdanie

Po znalezieniu zdania będziemy chcieli zamienić je na przeczące. Metoda, którą wymyśliłem jest dość naiwna, ale jak dobrze wiemy jeżeli coś jest głupie a działa, to nie jest głupie 🙂

Najpierw przeanalizujmy sobie, jakie są możliwe czasowniki w czasie present simple dla podmiotów w 3. osobie i liczbie pojedynczej i w jaki sposób je zanegować:

  • Czasownik nieregularny „is” -> aby zanegować dodajemy „not”
  • Czasowniki modalne –  „should” -> „shouldn’t”, „must” -> „mustn’t”, „has to” -> „does not have to”
  • Pozostałe czasowniki – czasownik+s -> does not czasownik z wyjątkami:
    • Czasowniki kończące się na -ies negujemy poprzez zamiane –ies na -y (np. flies -> does not fly). Specjalnym przypadkiem są słowa, które w formie bezokolicznikowej kończą się -ies (np. lies -> does not lie).
    • Czasowniki kończące się na -es poprzedzone zgłoskami „ss”, „x”, „ch”, „sh”, „o” negujemy poprzez usunięcie całej końcowki -es (np. goes -> does not go, misses -> does not miss)

Kod klasy SentenceNegator wygląda następująco:

class SentenceNegator:
IRREGULAR_ES_VERB_ENDINGS = ["ss", "x", "ch", "sh", "o"]
CONSONANTS = ['a', 'e', 'i', 'o', 'u', 'y']
def negate(self, sentence):
# is
if sentence.find("isn't") > 1:
return sentence.replace("isn't", "is")
if sentence.find("isn\\'t") > 1:
return sentence.replace("isn\\'t", "is")
if sentence.find("is not ") > 1:
return sentence.replace("is not ", "is ")
if sentence.find("is ") > 1:
return sentence.replace("is ", "is not ")
# has
if sentence.find("does not have") > 1:
return sentence.replace("does not have", "has")
if sentence.find("doesn't have") > 1:
return sentence.replace("doesn't have", "has")
if sentence.find("doesn\\'t have") > 1:
return sentence.replace("doesn\\'t have", "has")
if sentence.find("has ") > 1:
return sentence.replace("has ", "does not have ")
# should
if sentence.find("shouldn't") > 1:
return sentence.replace("shouldn't", "should")
if sentence.find("shouldn\\'t") > 1:
return sentence.replace("shouldn\\'t", "should")
if sentence.find("should not") > 1:
return sentence.replace("should not", "should")
if sentence.find("should") > 1:
return sentence.replace("should", "should not")
# must
if sentence.find("mustn't") > 1:
return sentence.replace("mustn't", "must")
if sentence.find("mustn\\'t") > 1:
return sentence.replace("mustn\\'t", "must")
if sentence.find("must not") > 1:
return sentence.replace("must not", "must")
if sentence.find("must ") > 1:
return sentence.replace("must ", "must not ")
# can
if sentence.find("can't") > 1:
return sentence.replace("can't", "can")
if sentence.find("can\\'t") > 1:
return sentence.replace("can\\'t", "can")
if sentence.find("cannot") > 1:
return sentence.replace("cannot", "can")
if sentence.find("can ") > 1:
return sentence.replace("can ", "cannot ")
# doesn't work -> works
doesnt_regex = r'(doesn\'t|doesn\\\'t|does not) (?P<verb>\w+)'
if re.search(doesnt_regex, sentence):
def replace_doesnt(matchobj):
verb = matchobj.group(2)
if verb.endswith("y") and self.__is_consonant(verb[2]):
return "{0}ies".format(verb[0:1])
for ending in self.IRREGULAR_ES_VERB_ENDINGS:
if verb.endswith(ending):
return "{0}es".format(verb)
return "{0}s".format(verb)
return re.sub(doesnt_regex, replace_doesnt, sentence, 1)
verb_regex = r'(It |it |)(?P<verb>\w+)s( |$)'
# works -> does not work
def replace_verb(matchobj):
subject = matchobj.group(1)
verb = matchobj.group(2)
whitespace = matchobj.group(3)
# flies -> fly, but not die -> dy
if verb.endswith("ie") and len(verb) > 3:
verb = "{0}y".format(verb[0:2])
# stresses -> stress
for ending in self.IRREGULAR_ES_VERB_ENDINGS:
if verb.endswith("{0}e".format(ending)):
verb = verb[0:1]
return "{0}does not {1}{2}".format(subject, verb, whitespace)
if re.search(verb_regex, sentence):
return re.sub(verb_regex, replace_verb, sentence, 1)
return sentence
def __is_consonant(self, letter):
return letter not in self.CONSONANTS

view raw
sentence_negator.py
hosted with ❤ by GitHub

Zasada jest prosta – dla wyżej wymienionych typów czasowników należy wykonać takie kroki:

  • Jeśli zdanie zawiera przeczenie czasownika danego typu zaneguj go i zwróć zdanie pozytywne
  • Jeśli zdanie nie zawiera przeczenia, to znajdź niezanegowany czasownik i spróbuj go zanegować
  • Jeśli zdanie nie zawiera czasownika (nie jest zdaniem) – zwróć wejściowy ciąg znaków

Warto wspomnieć, że takie podejście sprawdzi się tylko dla zdań jednokrotnie złożonych, niezawierających conditionali. Czyli np. „It has a value” zostanie poprawnie zmienione w „It does not have a value”, ale już „It has a value if other value is 0” może dać nieoczekiwany rezultat, czyli: „It has a value if other value is not 0”. W takim przypadku nasz program musiałby znać kontekst zdania, żeby móc je poprawnie zanegować.

Krok 3 – piszemy testy jednostkowe

Pierwszą rzeczą jaką zrobiłem zaraz po zapoznaniu się z API Sublime’a było zadanie sobie pytania „jak mam to przetestować?”. Stwierdziłem, że przy mojej niskiej znajomości Pythona szansa na wprowadzenie błędu przy którejś z kolei zmianie jest całkiem duża, a dobre pokrycie testami da mi pewność, że nic po drodze nie zepsuję (poza tym to po prostu dobra praktyka).

Tutaj z pomocą przyszła mi biblioteka https://github.com/randy3k/UnitTesting. Działa ona w bardzo prosty sposób – symuluje realne użycie naszego pluginu poprzez stworzenie nowego okienka edytora i wykonanie naszej komendy. Po odpaleniu komendy możemy sprawdzić czy tekst w oknie testowym jest zgodny z oczekiwanym.

Plik z zestawem testów należy umieścić w folderze tests naszej paczki.

Przykładowy plik z testem jednostkowym wygląda tak:

import sublime
import sys
from unittest import TestCase
class TestNegateSentence(TestCase):
def setUp(self):
self.view = sublime.active_window().new_file()
# make sure we have a window to work with
s = sublime.load_settings("Preferences.sublime-settings")
s.set("close_windows_when_empty", False)
def tearDown(self):
if self.view:
self.view.set_scratch(True)
self.view.window().focus_view(self.view)
self.view.window().run_command("close_file")
def test_negate_is(self):
self.check_substitution('"The dog is black"', '"The dog is not black"')
def check_substitution(self, input, expected):
self.set_text(input)
self.view.run_command("negate_sentence")
self.assertEqual(self.get_text(), expected)
def set_text(self, string):
self.view.run_command("insert", {"characters": string})
def get_text(self):
return self.view.substr(self.view.line(self.view.text_point(0, 0)))
def move_cursor(self, position):
pt = self.view.text_point(0, position)
self.view.sel().clear()
self.view.sel().add(sublime.Region(pt))

view raw
test.py
hosted with ❤ by GitHub

Krok 4 – dodajemy CI i pokrycie testami

Po dodaniu testów do projektu pomyślałem, że fajnym usprawnieniem będzie automatyczne uruchamianie testów w którymś z serwisów umożliwiających CI. Okazało się, że w repozytorium https://github.com/randy3k/UnitTesting można znaleźć gotową konfigurację do systemu Travis CI. Integracja z Travis CI odbyła się bezboleśnie – już pierwszy build zaświecił się na zielono.

Zrzut ekranu 2017-10-23 o 22.07.56.png
Wynik buildu w Travis CI

Poza integracją z CI postanowiłem również zintegrować się z codecov – serwisem pokazującym w jakim stopniu nasz projekt jest pokryty testami. Ponownie okazało się to banalne, ponieważ plik .travis.yml zawiera konfigurację, która automatycznie generuje plik z pokryciem testami. Jedyne co musiałem zrobić, to zalogować się w codecov.io i wskazać odpowiednie repozytorium z GitHuba.

Zrzut ekranu 2017-10-23 o 22.16.02.png
Widok projektu w codecov.io

Dzięki dodaniu ciągłej integracji i pokrycia testami mam pewność, że każda linia jest przetestowana, a mój projekt działa jak należy.

Oprócz tego zyskałem też takie czadowe badge, które dodałem do repozytorium w  GitHubie:

Zrzut ekranu 2017-10-23 o 22.20.05.png

Krok 5 – sprawdzamy plugin w akcji

Czas zobaczyć plugin w akcji. Można go przetestować wykorzystując jeden ze sposobów:

  • Na wybranym zdaniu nacisnąć kombinację klawiszy „^ + ⌘ + ⇧ + n” na Macu lub „CTRL + SHIFT + ALT + n” na windowsie/linuxie.
  • Nacisnąć „⌘ + ⇧ + P” i w okienku poleceń wybrać NegateSentence

Poniżej mała prezentacja:

plugin_in_action

Podsumowanie

Napisanie własnego pluginu do edytora, w którym codziennie spędza się po 8 godzin daje sporą satysfakcję, nawet jeżeli jest to super proste narzędzie.

Poza samą satysfakcją takie proste zadanko idealnie sprawdziło się jako poligon służący do nauki Pythona. Dwie pieczenie na jednym ogniu!

Całość projektu można znaleźć pod adresem https://github.com/Bajena/SublimeNegateSentence

Chrome DevTools part 3 – Breakpoints

Po prawie miesiącu przerwy wracam z kolejną częścią serii o Chrome DevTools. Tym razem zajmę się sprawą debugowania JavaScriptu. Dla niektórych to może być zaskoczeniem, ale debugowanie aplikacji klasyczną metodą alert(‚dupa’) już dawno przestało być szczytem możliwości 🙂

Chrome pozwala na zatrzymywanie i analizę wykonywania skryptów za pomocą breakpointów. Okazuje się, że breakpointy można ustawiać na kilka różnych sposobów. Dzisiejszy post jest przeglądem tych metod.

Na potrzebę tej części przygotowałem prostą aplikację, która losuje sto liczb i wypisuje je na stronie.

<html>
<head>
<meta charset="utf-8">
<title>Breakpoints demo</title>
<script src="http://code.jquery.com/jquery-latest.js"
type="text/javascript"></script>
</head>
<body>
<div id="numbers" />
<script>
(function() {
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max min + 1)) + min;
}
for(var i = 0;i < 100;i++) {
var x = getRandomInt(0, 100);
$('#numbers').append(x).append("<br/>");
if (x === 0) {
throw "ZERO!";
}
}
})();
</script>
</body>
</html>

view raw
breakpoints.html
hosted with ❤ by GitHub

1. Line of code

Najprostsza metoda debugowania – zatrzymuje program zawsze podczas wykonania danej linii. Taki breakpoint można ustawić klikając lewym klawiszem myszy na numer linii.

Ten sam efekt można osiągnąć wpisując debugger w kodzie aplikacji.

Zrzut ekranu 2017-07-04 o 19.48.49

 

 

 

2. Breakpoint warunkowy

Załóżmy, że interesuje nas stan aplikacji jedynie w ostatnim obiegu pętli for. Nie musimy dodawać do naszego kodu dodatkowego warunku rodzaju:

if (i == 99) { debugger; }

Zamiast tego można użyć breakpointu warunkowego. W tym celu należy kliknąć prawym przyciskiem myszy na wybraną linie i podać warunek zatrzymania.

cond
Breakpointy warunkowe

3. DOM breakpoints

Kolejnym sposobem na debugowanie aplikacji są DOM breakpoints. Mogą być one przydatne, gdy np. wiele miejsc w kodzie modyfikuje element drzewa strony. Wtedy zamiast stawiać pojedyncze breakpointy w każdym z tych miejsc możemy dodać DOM breakpoint. 

Zmodyfikujmy nieco kod naszego dema – zamiast pętli użyjemy dwóch timerów dodających liczby do elementu #numbers.

setInterval(function() {
var x = getRandomInt(0, 100);
$('#numbers').append(x).append("<br/>");
}, 1000);
setInterval(function() {
var x = getRandomInt(101, 200);
$('#numbers').prepend("<br/>").prepend(x);
}, 2000);

Załóżmy, że interesuje nas, który z timerów dodał konkretną liczbę. Ustawmy w tym celu DOM breakpoint zatrzymujący program, gdy zawartość #numbers ulegnie zmianie (subtree modifications).

dom.gif
DOM breakpoint

Debugger zatrzyma teraz program, gdy jeden z dwóch timerów spróbuje dodać element do #numbers.

Oprócz subtree modifications mamy również do dyspozycji attribute modifications (zatrzymuje program, gdy atrybut elementu ulegnie zmianie) oraz node removal (gdy element zostanie usunięty).

4. XHR breakpoints

Aplikacje JavaScriptowe bardzo często wykonują setki zapytań do zewnętrznych serwisów. Czasem może się to wymknąć spod kontroli i może zdarzyć się, że nasza aplikacja wywołuje nadmierną liczbę zapytań lub pyta zły adres.

Aby zbadać, które miejsce w naszym kodzie wywołuje dane zapytanie możemy zdefiniować XHR breakpoint, który zatrzyma program w momencie, gdy wywołane zostanie zapytanie, którego adres URL zawiera wybraną przez nas frazę.

Zmodyfikujmy nasz skrypt tak, by jeden z timerów pobierał losową liczbę z serwisu random.org za pomocą funkcji fetch (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch):

setInterval(function() {
var x = getRandomInt(0, 100);
$('#numbers').append(x).append("<br/>");
}, 1000);
setInterval(function() {
fetch('https://www.random.org/integers/?num=1&min=0&max=100&col=1&base=10&format=plain&rnd=new&#39;).then(function(response){
return response.text();
}).then(function(number) {
$('#numbers').prepend("<br/>").prepend(number);
});
}, 2000);
})();

view raw
xhr_breakpoints.js
hosted with ❤ by GitHub

Dodajmy teraz XHR breakpoint, który zatrzyma program, gdy wykonane zostanie zapytanie do random.org:

xhr.gif
XHR breakpoints

5. Event listener breakpoints

Chrome pozwala na dodawanie breakpointów, które reagują na javascriptowe eventy takie, jak kliknięcie, czy wpisywanie tekstu. Więcej o Event Listenerach można przeczytać poprzedniej części serii.

6. Exception breakpoints

Fajną, choć momentami upierdliwą funkcją są breakpointy wyzwalane w momencie wystąpienia wyjątku. Zmieńmy nasz program tak, by losował tylko 0 lub 1. Gdy wylosowane zostanie 0 rzucony zostanie nieobsłużony wyjątek. Gdy wylosowane zostanie 1, rzucamy i łapiemy wyjątek.

Kliknięcie ikonki pauzy spowoduje zatrzymanie programu, gdy wylosowane zostanie 0. Dodatkowo, jeśl włączymy checkbox Pause on caught exceptions, to program zatrzymany zostanie również wtedy, gdy wylosowana zostanie jedynka.

exc.gif
Exception breakpoints

7. Function breakpoints

Fajną opcją Chrome’a, której wcześniej nie znałem są function breakpoints. Takie breakpointy zatrzymują program, gdy wywołana zostanie dana funkcja. Według dokumentacji DevToolsów aby dodać taki breakpoint należy w kodzie naszej aplikacji dodać linię:

debug(funkcja)

Zmienna funkcja jest identyfikatorem funkcji, którą chcemy śledzić.
Niestety, mi nie udało się odpalić tego breakpointa (ReferenceError) z poziomu kodu i zadziałał dopiero gdy ustawiłem zwykły breakpoint na początku programu i wpisałem w konsoli chrome’a debug(myFunction). 

function myFunction() {
console.log('my func');
}
setInterval(function() {
var x = getRandomInt(0, 1);
if (x == 0) {
myFunction();
}
$('#numbers').append(x).append("<br/>");
}, 1000);

func
Function breakpoints

Podsumowanie

Mimo, że najczęściej przy debugowaniu używamy najprostszej metody – zwykłego zatrzymania na danej linijce kodu, to uważam, że warto mieć gdzieś z tyłu głowy pozostałe opisane przeze mnie techniki. Być może pozwolą nam zaoszczędzić kiedyś sporo czasu.

W następnej części zobaczymy, jak za pomocą DevTools analizować wydajność naszej aplikacji.

Chrome DevTools część 2 – Event Listeners

Czas na kolejną funkcję DevToolsów, o której zawsze wiedziałem, że jest, ale nigdy jej nie użyłem, czyli Event Listeners.

Prosty przykład

Zakładka event listeners, jak nietrudno się domyślić, pozwala na przeglądanie javascriptowych eventów zbindowanych do danego elementu.

Zacznijmy klasycznie od prostego przypadku. Przygotowałem w tym celu mały przykład:

<html>
<head>
<meta charset="utf-8">
<title>Event handlers demo</title>
<script src="http://code.jquery.com/jquery-latest.js" type="text/javascript"></script>
</head>
<body>
<div class="wrapper" style="width:300px;height:300px;background:red;">
<button>Click me</button>
</div>
<script>
(function() {
$('.wrapper').click(function wrapperHandler() {
console.log('wrapper');
});
$('button').click(function buttonHandler() {
console.log('button');
});
})();
</script>
</body>
</html>

view raw
index.html
hosted with ❤ by GitHub

Po otwarciu zakładki Event Listeners dla wrappera widzimy event handler dodany przez mój kod:

Zrzut ekranu 2017-06-13 o 21.36.48.png
Listener dla wrappera

Zobaczmy teraz co zostanie wyświetlone dla elementu button:

Zrzut ekranu 2017-06-13 o 21.47.28.png
Listenery dla guzika

Co tu się odjaniepawliło? Skąd listener wrappera w listenerach buttona? Okazuje się, że Chrome domyślnie pokazuje listenery nie tylko dla wybranego elementu, ale również dla jego przodków w drzewie. Odzaczenie checkboxa Ancestors spowoduje odfiltrowanie listenerów przodków.

Czas pobrudzić sobie ręce

Wszystko wydaje się proste, a jednak przy próbie podejrzenia event listenerów dla prostego guzika w Emberowej aplikacji zastałem następujący widok:

Zrzut ekranu 2017-06-11 o 21.12.25
Początkowa lista listenerów

Wklejony_obraz_13_06_2017__23_05
Wybrany guzik

Dziwne, skąd tyle listenerów zbindowanych do prostego przycisku? Ok, zobaczmy co stanie się po odznaczeniu Ancestors:

Zrzut ekranu 2017-06-11 o 21.52.21
Lista listenerów po odznaczeniu opcji „Ancestors”

Odznaczenie opcji Ancestors ograniczyło nieco liczbę listenerów, jednak wciąż jest ona dość duża jak na przycisk. Tutaj z pomocą przychodzi checkbox Framework Listeners. Po odznaczeniu tej opcji odfiltrowane zostały listenery dodane przez bibliotekę Ember.js, dzięki czemu w końcu znalazłem konkretny listener zbindowany przez mój własny kod do przycisku.

Zrzut ekranu 2017-06-11 o 21.56.16
Event listener zdefiniowany przez naszą aplikację

Event listener breakpoints

Ok, fajnie, wiem już, że do mojego przycisku przypisany jest event handler. Spróbujmy teraz wykorzystać Event listener breakpoints w zakładce Sources aby sprawdzić handler w akcji. Na rozwijanej liście zaznaczamy mouse -> click, dzięki czemu kliknięcie w dowolny element ze zdefiniowanym handlerem dla eventu click spowoduje wstrzymanie wykonania kodu.

Zgodnie z oczekiwaniami kliknięcie w przycisk spowodowało odpalenie debuggera. Kod zatrzymał się jednak w niezbyt interesującym miejscu – we wrapperze dodanym przez zewnętrzną bibliotekę (new relic). Można sobie z tym poradzić poprzez kliknięcie prawym przyciskiem w oknie debuggera i wybranie opcji Blackbox script.

Zrzut ekranu 2017-06-13 o 23.12.41.png
Dodawanie skryptu do listy „Blackboxed scripts”

Po dodaniu odpowiednich wpisów do listy blackboxed scripts i ponownym kliknięciu w guzik debugger odpalił się już w miejscu, które mnie interesowało:

Wklejony_obraz_13_06_2017__23_22
Breakpoint w poprawnym miejscu

Monitorowanie zdarzeń

W przypadku, gdy nie interesuje nas jak zostało obsłużone pewne zdarzenie, a jedynie czas jego wywołania przydatna może okazać się funkcja monitorEvents. W inspectorze należy zaznaczyć interesujacy element, a następnie wpisać:

monitorEvents($0)

każde zdarzenie, które wystąpi dla zaznaczonego elementu zostanie zalogowane w konsoli:

Zrzut ekranu 2017-06-13 o 23.34.39
Logowanie zdarzeń przy uzyciu monitorEvents

Monitorowanie można ograniczyć też do konkretnych eventów, podając nazwę zdarzenia w drugim parametrze funkcji:

monitorEvents($0, ‚click’)

Monitorowanie można zakończyć używając polecenia:

unmonitorEvents($0)

Bonus

Jeśli korzystamy z biblioteki jQuery, to szczegóły event handlerów dla aktualnie wybranego elementu możemy podejrzeć wpisując

$._data(($0), ‚events’)

Zrzut ekranu 2017-06-13 o 23.42.40.png
Lista event handlerów w jQuery

Czasami ten sposób może okazać się szybszy niż przekopywanie się przez zakładkę Event listeners.

Podsumowanie

W tym poście zaprezentowałem kilka sposobów na analizę zdarzeń za pomocą Chrome DevTools. Dowiedzieliśmy się, jak przeglądać listę event listenerów, ustawiać automatyczne breakpointy dla event listenerów oraz jak włączyć logowanie zdarzeń dla elementów drzewa.

W kolejnym poście serii przeanalizuję temat DOM breakpoints.

Chrome DevTools część 1 – wstęp

Zgodnie z zapowiedzią – rozpoczynam serię wpisów o Chrome DevTools. Seria ta jest skierowana do osób, dla których Chromowe narzędzia nie są nowością, ale, tak jak ja, czują, że nie wykorzystują w pełni potencjału jaki oferują. Postaram się omówić i przedstawić na przykładach opcje i zakładki, z którymi webdeveloperzy nigdy „nie mają czasu” się zapoznać.

Pierwszy temat, którym się zajmiemy to zakładka Elements. Przez większość osób jest ona używana jedynie do podglądania drzewa dokumentu HTML lub zmieniania/dodawania reguł CSS.

Powiem o kilku rzeczach, które nie były dla mnie oczywiste na pierwszy rzut oka oraz takich, do których bałem się zbliżyć, bo wydawały mi się zbędne 🙂

Na rozgrzewkę wrzucam coś super prostego i większość z Was pewnie mnie w tym momencie wyśmieje, ale nigdy nie zwróciłem na to uwagi. Otóż, pod drzewkiem HTML jest belka (tzw. breadcrumbs), która umożliwia łatwą nawigację pomiędzy rodzicami aktualnie wybranego elementu drzewa. Zamiast tego zawsze mozolnie klikałem w elementy drzewa…

Mind blown.

breadcrumbs

Kolejny post będzie już bardziej dojrzały. Przedstawię jak i kiedy używać zakładki Event listeners.