JavaScript

Synchroniczna asynchroniczność

Jest pewna rzecz w JavaScript, którą wszyscy kochamy: asynchroniczność. Jest też pewna rzecz w JS, którą wszyscy nienawidzimy: asynchroniczność. Każdy, kto choć raz miał większy kontakt z tym dziwacznym językiem, wie czym jest budząca postrach asynchroniczność. To zmora wszystkich AJAX-owych wyjadaczy i ich młodszych kolegów, hackujących w node.js.

Ale o co chodzi?

Jak niektórym wiadomo, JS jest językiem jednowątkowym. W uproszczeniu mówiąc: może wykonywać tylko jedną rzecz naraz. Oczywiście zrodziło to szereg konsekwencji i zmusiło co tęższe głowy do szukania rozwiązań i obejść tego problemu. O jakich konsekwencjach mówimy? Wyobraźmy sobie już tutaj wspomniane żądanie AJAX-owe. Wysyła ono zapytanie o dane do konkretnej strony internetowej. Jak wiadomo, to trwa – w zależności od wielu różnych czynników (prędkość połączenia, wydajność i obciążenie serwera docelowego itd.) – od kilku sekund do nawet kilku minut. Gdyby JS był zupełnie „normalnym” językiem programowania, takie zapytanie oznaczałoby zawieszenie całego programu, który czekałby na odpowiedź. Zero odpowiedzi na kliknięcia przycisku („Człowieku, nie widzisz, że ja tu czekam?!”), zero padającego śniegu („CZEKAM! DAJ MI SPOKÓJ!”), zero… I w tym miejscu wkracza właśnie na scenę asynchroniczność: a co gdyby takie żądanie wykonywało się w mitycznym tle a cała reszta strony była wciąż w pełni funkcjonalna? Oczywiście od razu kupiono ten pomysł. I tu pojawił się problem.

Mityczne działanie w tle pociąga za sobą jedną, niezwykle ważną konsekwencję, która często pozostaje niezrozumiała dla nowicjuszy i ludzi dotąd korzystających z innych języków programowania. Otóż któż z nas nie próbował zrobić czegoś takiego:

var result = AJAX.get( 'strona.html' );

Jakaż wielka była nasza frustracja, gdy uzmysławialiśmy sobie, że to po prostu nie działa… Jak dla mnie był to największy moment oświecenia w moim obcowaniu z JS-em: zrozumienie, że funkcje asynchroniczne nie zwracają wartości. Rozwiązując problem zawieszania się programu, równocześnie natknęliśmy się na kolejny, równie ważny.

Callbacki

Najprostszym i najczęściej stosowanym rozwiązaniem (choć totalnie nielogicznym dla newbies) jest zastosowanie tzw. callbacków. Nasza funkcja asynchroniczna radośnie wykonuje się w tle i gdy wreszcie uda się jej pobrać pewne dane, przekazuje je do funkcji, zadeklarowanej przez nas, która nam jeszcze radośniej owe dane przerobi. Wygląda to o wiele straszniej niźli zwykłe var result, choć wciąż jest w miarę jasne:

AJAX.get( 'strona.html', function( result ) {
	// Tutaj robimy coś z pobranymi danymi…
} );

Voilà! Dostaliśmy się do tego, co chcieliśmy. Jest tylko jeden problem: nie istnieje sensowny sposób przekazania pobranych danych do reszty skryptu – jedyne miejsce kontaktu z nimi to wnętrze callbacku. Niekoniecznie jest to wada (hej, mamy natywną enkapsulację ;)), lecz warto o tym pamiętać.

Jak widać, callbacki można łatwo sobie przyswoić i są nawet przyjemne, prawda? Błąd! To, że nie istnieje sensowny sposób na wyjęcie potrzebnych nam danych poza callback, oznacza, że wszystkie operacje od nich zależne należy wykonać w jego wnętrzu. I choć zaczyna się niewinnie, bardzo łatwo skończyć z kodem w postaci:

AJAX.get( 'strona.html', function( result ) {
	AJAX.get( 'innastrona.html', function( nextResult ) {
		AJAX.get( 'jeszczeinnastrona.html', function( alsoResult ) {
			weNeedToGoDeeper();
		} );
	} );
} );

Zaczyna się robić bardzo nieprzyjemnie, prawda? W przypadku bardziej skomplikowanych aplikacji grzęźniemy w callbackach powsadzanych w callbacki, które z kolei są odpalane w callbackach innych callbacków. Po kilku godzinach pracy z takim kodem słowo „callback” staje się uosobieniem wszelkich koszmarów a kod nimi usiany wygląda jak pismo szatana. Nic zatem dziwnego, że postanowiono poszukać sensowniejszego rozwiązania problemu asynchroniczności.

Model zdarzeniowy

Jeśli ktoś wie, co to DOM (jeśli wiesz, to pewnie na dźwięk tej nazwy również jeżą Ci się włosy na głowie ;)) albo przynajmniej posługiwał się kiedyś jQuery, zna też model zdarzeniowy. To dzięki niemu przeglądarka jest w stanie reagować na kliknięcia przycisków, pacnięcia headera, wciśnięcia klawiszy oraz wszystkie te dziwne rzeczy, które user z nią próbuje zrobić. Zasada tutaj jest prosta: mamy pewien obiekt (np. przycisk), któremu może się coś stać (np. zostanie zmolestowany kliknięciem). W chwili, gdy to coś się stanie, nasz obiekt po prostu o tym informuje. I w sumie tyle. W teorii brzmi to prosto (aż za prosto), w praktyce też nie jest szczególnie skomplikowane. Posłużę się tutaj prostym przykładem z jQuery:

$( 'button' ).on( 'click', function( e ) {
	alert( 'Molestują mnie! D:' );
} );

Co bardziej wyczuleni zauważą, że w sumie tutaj też mamy pewien rodzaj callbacku: w końcu po dostaniu informacji od przycisku, że ktoś go kliknął, wywołujemy funkcję. Gdzie zatem jakaś zmiana? A otóż w tym, że w przypadku modelu zdarzeniowego nie jesteśmy uzależnieni od jednego, monolitycznego callbacku. Obsługę danego zdarzenia możemy rozrzucić po wielu fragmentach naszego kodu (oczywiście sensownych) i podzielić na mniejsze, bardziej wyspecjalizowane fragmenty:

$( 'button' ).on( 'click', function( e ) {
	alert( 'Molestują mnie! D:' );
} );

// Tutaj tona innego kodu JS.

$( 'button' ).on( 'click', function( e ) {
	alert( 'Zawiadamiam policję!' );
	callThePolice();
} );

Takie rozwiązanie jest o wiele elastyczniejsze od normalnego callbacku. Co więcej, praktycznie każdy natywny moduł node.js wspiera ten model. To samo dotyczy de facto każdego API dostępnego w przeglądarce (obiekt XMLHttpRequest, odpowiedzialny za AJAX-a, również go wspiera). Model zdarzeniowy jest standardowym sposobem reagowania na wszelkie zmiany i interakcje z użytkownikiem. Pozwala wielu niezależnym od siebie częściom programu reagować ze sobą (np. moduł B może nasłuchiwać czy przypadkiem moduł A nie rzuca błędu), co pozwoliło Zakasowi na stworzenie skalowalnej architektury aplikacji w JS.

Promises (Futures)

Choć model zdarzeniowy jest bardzo potężny, nie zawsze się go wykorzystuje. Wyobraźmy sobie prostą funkcję do pobierania danych z bazy danych. Owszem, może ona emitować zdarzenie completed w chwili pobrania danych i można tego zdarzenia nasłuchiwać, ale wydaje się to pewnym przerostem formy nad treścią. Co więcej, w niektórych przypadkach potrzebujemy całości obsługi w jednym miejscu. Nie chcemy równocześnie babrać się z surowymi callbackami, bo są po prostu brzydkie. Na pomoc przychodzą nam promises (zwane też czasami futures).

Czym jest ten dziwny mechanizm, który zdobył tak wielką popularność i serca programistów JS na całym świecie? Powstał on jako jedna z propozycji tzw. CommonJS APIs (ma nawet swojąspecyfikację). Pomysł okazał się na tyle dobry a zarazem prosty i przyjemny, że wkrótce opanuje całe DOM API (co w gruncie rzeczy jest powodem do świętowania :D). Całość w gruncie rzeczy opiera się na jednej metodzie – then, służącej do łączenia w łańcuchy poszczególnych funkcji asynchronicznych. Pozwala to pisać o wiele przejrzystszy kod niźli w przypadku zastosowania surowych callbacków. Załóżmy, że nasza metoda AJAX.get korzysta z promises – jej najprostsze wywołanie zatem wyglądałoby następująco:

AJAX.get( 'strona.html' ).then( function( result ) {
	// Wszystko się udało i pobraliśmy.
	console.log( result );
}, function( error ) {
	// Pojawił się błąd.
	console.error( error );
} );

Jak widać, nie przekazujemy już do AJAX.get callbacku – pojawia się natomiast then, które… Tak, bierze dwa callbacki ;) Spokojnie, ma to swój sens. Pierwszy z nich odpala się jedynie w przypadku, gdy wszystko poszło dobrze i otrzymaliśmy potrzebne zdanie. Drugi służy do obsługi błędów. Co prawda na tak prostym przykładzie nie widać zbyt dobrze przewagi promises nad normalnymi callbackami, jednak wystarczy przyjrzeć się naszemu nieprzyjemnemu przykładowi z callbackami, by dostrzec rzeczywistą przewagę obiecanek:

AJAX.get( 'strona.html' ).then( function( result ) {
	return AJAX.get( 'innastrona.html' )
} ).then( function( nextResult ) {
	return AJAX.get( 'jeszczeinnastrona.html' );
} ).then( function( alsoResult ) {
	weNeedToGoDeeper();
} );

Żegnajcie, okropne poziomy zagłębień i tryliony następujących po sobie nawiasów! Witajcie, pięknie i czytelne promises! Wygląda pięknie, prawda? Oczywiście coś za coś ;) Jeśli już raz wejdzie się w promises, to de facto cały kod powinien ich używać (inaczej nie będzie można utworzyć łańcuszka z then). Mimo wszystko nie ma raczej konkretnych przeciwwskazań do stosowania promises (chyba, że musimy mieć maksymalną wydajność – wówczas lepiej rozglądnąć się za innym rozwiązaniem) a obecnie są najczytelniejszym mechanizmem obsługi asynchronicznego kodu w przeglądarce.

Co się dzieje za kulisami? Wyobraźmy sobie prościutką funkcję asynchroniczną – jej jedynym zadaniem jest… bycie asynchroniczną i zwrócenie losowo albo poprawnego wyniku, albo błędu. Wymusić asynchroniczność jest bardzo łatwo – wystarczy zastosować timer (setTimeout/setInterval; w przyszłości prawdopodobnie będzie można także skorzystać z genialnego setImmediate, prawda Firefox i Chrome? W node.js jest już dostępne od wersji 0.9). Natomiast co do losowania wyniku można posłużyć się np. ładną funkcją rand. W ostateczności otrzymujemy coś takiego:

function simpleAsync() {
	setTimeout( function() {
		console.log( Math.floor( Math.random() * 2 + 1) === 2 ? 'działa' : new Error() );
	}, 1000 );
}

Jak nie trudno się domyślić, po 1 sekundzie zobaczymy albo napis ‚Działa’, albo ładny błąd. Teraz zastanówmy się jak zrobić z tego obiecankę. Najnowsze przeglądarki udostępniają globalną funkcję Promise, której oczywiście nie należy jeszcze używać (jest tzw. eksperymentalną funkcjonalnością i trzeba grzebać w ustawieniach, żeby się w ogóle do niej dobrać). Na szczęście istnieją setki implementacji promises, wśród których jest także używana przeze mnie, when.js. Również jQuery udostępnia niby-promise (jQuery.Deferred), jednak nie jest to do końca zgodna ze standardem implementacja. W tym artykule użyjemy when.js.

W przypadku when.js, promise jest tak naprawdę częścią większego obiektu, zwracanego przez metodę when.defer. Ma on 2 metody: resolve (zwracającą wartość i uruchamiającą 1. callback w then) oraz reject (rzucającą błędem i uruchamiającą 2. callback w then) oraz 1 własność: promise (tak, to nasz skubaniec z then). Po zmianach nasza funkcja może wyglądać następująco:

function simpleAsync() {
	var defer = when.defer();

	setTimeout( function() {
		Math.floor( Math.random() * 2 + 1 ) === 2 ? defer.resolve( 'działa' ) : defer.reject( new Error() );
	}, 1000 );
	
	return defer.promise;
}

Jak widać, zmiany nie są szczególnie drastyczne – dają się znieść. Dzięki nim dostajemy w pełni działającą obiecankę, która pozwala nam w znaczny sposób uczytelnić nasz kod.

Obsługa błędów

Dla programistów, zwłaszcza przychodzących do JS z innych języków, najbardziej naturalną formą obsługi błędów jest mechanizm wyjątków. W JS jest również obecny i działa tak, jakbyśmy tego oczekiwali. Ale nie do końca…

function simpleAsync() {
	setTimeout( function() {
		throw new Error( 'HA!' );
	}, 50 );
}

try {
	simpleAsync();
} catch( e ) {
	console.log( e );
}

Powyższy kod, niezgodnie z intuicją, wyrzuci nam błąd:

Uncaught Error: HA!

„Konsola”

Jakkolwiek byśmy nie próbowali, obsługa wyjątków w przypadku funkcji asynchronicznych nie zadziała. Trzeba znaleźć inne sposoby obsługi błędów. Jeden już znamy – jest nim drugi callback w then. Ale co choćby w przypadku zwykłych, surowych callbacków? Otóż w ich przypadku powstał niepisany standard (spotykany zwłaszcza w czasie pracy z node.js): 1. parametr callbacku oznacza błąd (lub null/undefined jeśli błąd nie wystąpił), natomiast 2. parametr to otrzymane dane. Tutaj warto także od razu zaznaczyć, że drugim niepisanym standardem jest umiejscawianie callbacku jako ostatniego parametru. Zobaczmy to na przykładzie pobierania danych z pliku:

var fs = require( 'fs' );

fs.readFile( './file.txt', { encoding: 'utf8' }, function( err, data ) {
	if( err ) {
		return; // Albo logowanie błędu, wywalenie systemu, spalenie żarówki, whatever
	}
	
	console.log( data );
} );

W node.js bardzo popularnym rozwiązaniem jest także wywołanie zdarzenia error (de facto większość natywnych modułów w node.js zawiera metody, które takie zdarzenie emitują):

var fs = require( 'fs' );

fs.createReadStream( './noexistent' ).on( 'error', function( err ) {
	console.log( err );
} );

W przeglądarce również wiele różnych API zwraca błędy w ten sposób (słynne image.onerror). Chyba najuniwersalniejszym użyciem tego w tym środowisku jest window.onerror, łapiące większość błędów, które zdarzają się na stronie. Korzysta z tego m.in. Errorception. Niemniej, jakkolwiek by na sprawę nie patrzeć, taki sposób obsługi błędów nie jest aż tak intuicyjny, jak stare, sprawdzone wyjątki.

Synchroniczna asynchroniczność?

Największą bolączką asynchronicznośći jest zaburzenie kolejności czytania kodu. W JS nigdy nie możemy mieć pewności, że wszystko wykonuje się w porządku, w którym jest wypisane na ekranie. Stąd tak usilna potrzeba rozwiązań takich jak promises – pozwalają one choć trochę uporządkować callbackowe piekło. Jednak wciąż nie są tak wygodne jak zwykły, synchroniczny kod. Czy zatem jesteśmy skazani na takie rozwiązania pośrednie? I tak, i nie.

Promises były wielkim skokiem naprzód, lecz programiści wciąż szukali coraz dziwniejszych sposobów, żeby kod asynchroniczny przypominał synchroniczny. Ich poszukiwania doprowadziły do powstania node-fibers. Jak sama nazwa wskazuje, jest to rozwiązanie napisane z myślą o node.js. Wykorzystuje ono potęgę… C++, dodając kilka ciekawostek do silniczka V8 (silnik JS, wyjęty z Chromium, na którym zbudowano całe node.js). Jest to rozwiązanie bardzo potężne, ale równocześnie zbyt inwazyjne i nigdy nie znajdzie się w przeglądarkach.

Na ratunek przychodzi nam jednak ECMAScript 6 a dokładniej jedna z jego części, zwąca się enigmatycznie generatorami. I choć specyfikacja jest po prostu całkowicie niestrawna, generatory można bardzo prosto ogarnąć. Zacznijmy od przykładu:

function* simpleGenerator() {
 var i = 0;

	while ( true ) {
		yield ++i;
	}
}

var iterator = simpleGenerator();

console.log( iterator.next() ); // 1
console.log( iterator.next() ); // 2
console.log( iterator.next() ); // 3 itd.

W sumie widzimy, że generator to tak naprawdę funkcja, choć jakoś tak dziwnie zadeklarowana (pojawia się * przy słówku kluczowym function) oraz nowe słówko kluczowe yield (można w dużym uproszczeniu potraktować je jako generatorowy odpowiednik return). Widzimy też inifinite loop (while(true)), co powinno od razu zapalić czerwoną lampkę w głowach wszystkich programistów (wszak nie ma nigdzie żadnego break) – w teorii po odpaleniu tej funkcji pętla powinna zablokować cały program i lecieć tak długo, aż nie skończy się nam pamięć. Tak się jednak nie dzieje. Dlaczego? Przypatrzmy się jak „tradycyjnie” można by zapisać powyższy generator:

function simpleGenerator() {
	var i = 0;

	return {
		next: function() {
			return {
				value: ++i,
				done: false
			};
		}
	};
}

var iterator = simpleGenerator();

console.log( iterator.next() ); // 1
console.log( iterator.next() ); // 2
console.log( iterator.next() ); // 3 itd.

Jak widać, stworzenie prostego pseudogeneratora jest bardzo proste (jest tak proste, że TC39 – grupa zajmująca się ES6 – stwierdziła wręcz, że nie ma potrzeby wprowadzać mechanizmu rozróżniającego generator od zwykłej funkcji). Po głębszej analizie i porównaniu obydwóch przykładów można dojść do wniosku, że yield w automagiczny sposób sprawia, że działanie generatora jest przerywane aż do czasu kolejnego wywołania iterator.next, po czym zaczyna wykonywać jego kod od kolejnej po yield komendzie (w naszym wypadku to kolejne yield z powodu pętli ;)) – po prostu „skacze” między kolejnymi wystąpieniami yield. „I co z tego?” – zapyta się co bardziej rozgarnięty czytelnik. A to, że można przerwać wykonywanie generatora na funkcji asynchronicznej, pozwolić się jej wykonać, po czym wrócić do generatora! Brzmi obiecująco? Oczywiście od razu pojawi się problem w jaki sposób wykorzystać ten mechanizm, żeby funkcja asynchroniczna powróciła do generatora (czyli wywołała iterator.next) a równocześnie przekazała mu wartość (czyli wywołała iterator.next z parametrem ;)). Okazuje się, że i tutaj niezbędny będzie callback. Jednak całość kodu staje się o wiele przyjemniejsza (przykład z node.js):

var fs = require( 'fs' );

function* simpleGenerator( callback ) {
	var file = yield fs.readFile( 'test.txt', { encoding: 'utf8' }, callback );

	console.log( file ); // Zwróci nam zawartość pliku.
}

var callback = function( err, data ) {
	iterator.next( data ); // Przekazanie zawartości pliku z powrotem do generatora.
},
iterator = simpleGenerator( callback );

console.log( iterator.next() ); // Wywołanie 1. yield.

Można zakrzyknąć: nareszcie! Mamy asynchroniczny kod, w którym działa var – jedynymi niedogodnościami są słówko yield oraz potrzeba przekazywania callbacku. Choć pierwszej zlikwidować się nie da, druga jest możliwa do usunięcia (o czym później). Widać zatem, że możliwe jest stworzenie kodu asynchronicznego, który wygląda de facto jak synchroniczny (i tak też się zachowuje, wykonując się od góry do samego dołu). Oczywiście tutaj też istnieją pewne niemożliwe do przeskoczenia ograniczenia asynchroniczności (wciąż nie da się zwrócić normalnie wartości z wnętrza generatora do reszty kodu – choć w tym wypadku raczej nikt nie ma ochoty wychodzić poza generator, jeśli to nie jest absolutnie konieczne ;)), niemniej kod staje się maksymalnie czytelny, bez porzucania korzyści płynących z asynchroniczności. A co z obsługą błędów? Dobra wiadomość jest taka, że wyjątki wewnątrz generatora w końcu działają tak, jak się tego spodziewamy! To wszystko dzięki metodzie iterator.throw:

var fs = require( 'fs' );

function* simpleGenerator( callback ) {
	try {
		var file = yield fs.readFile( 'nie istnieje', { encoding: 'utf8' }, callback );
		console.log(file);
	} catch( e ) {
		console.error( 'Wystąpił błąd:', e ); // Wyświetli nam błąd.
	}
}

var callback = function( err, data ) {
	if( err ) {
		return iterator.throw( err );
	}

	iterator.next( data ); // Przekazanie zawartości pliku z powrotem do generatora.
},
iterator = simpleGenerator( callback );

console.log( iterator.next() ); // Wywołanie 1. yield.

Jak widać dodaliśmy tylko jedną linijkę do naszego callbacka i już możemy wewnątrz generatora korzystać z dobrodziejstw wyjątków.

Trzeba przyznać, że kod napisany przy użyciu generatorów wygląda po prostu ładnie – nawet promises nie mogą się z nim równać. Ale zanim polecisz przepisywać swój kod na generatory, czas na kubeł zimnej wody: generatory to ficzer z ES6, które oficjalnie nie jest zaimplementowane w żadnej przeglądarce. Zarówno w Firefoxie, jak i Chrome, żeby skorzystać z nich, należy grzebać w ustawieniach. W przypadku node.js również trzeba wykonać dodatkowy kroki, czyli zainstalowanie wersji >=11.6.x i uruchomienie node z parametrem --harmony. Nie zraża to jednak entuzjastów do eksperymentowania z tym niezwykle użytecznym mechanizmem.

W Sieci pojawiło się wiele bibliotek, które służą za wrapper dla generatorów. Ich jedynym zadaniem jest dbanie o to, żeby wywoływać za nas iterator.next (lub rzucać błędy przez iterator.throw) i dostarczać odpowiednich callbacków. Takimi biblioteki są m.in. waitfor (która potrzebuje po prostu yield [funkcja,parametry] a sama zajmuje się resztą) czy też task.js. Zobaczmy jak ładnie może to wyglądać:

spawn( function*() {
	var status=$('#status'),
		data;

	data = yield $.ajax( url );

	$( '#result' ).html( data );
	status.html( 'Download complete.' );

	yield status.fadeIn().promise();
	yield sleep( 2000 );

	status.fadeOut();
} );

Generatory + promises – brzmi jeszcze lepiej, prawda? Można zapomnieć w zupełności o callbackach – po prostu wywołujemy funkcję a cała magia dzieje się za kulisami. To akurat przykład zaczerpnięty ze strony task.js. Jak widać, cały kod jest umieszczony wewnątrz funkcji spawn a jedynym naszym zadaniem jest umieszczenie yield w odpowiednich miejscach. Czyż nie jest to piękne?

Jak widać generatory zdają się być jak dotąd najdoskonalszym rozwiązaniem problemu asynchroniczności. Kod stworzony dzięki nim jest czysty, przejrzysty i o wiele krótszy. Jedynymi prawdziwymi zmartwieniami w sprawie generatorów są obecnie małe wsparcie i wydajność (co oczywiście z czasem powinno się zmienić na lepsze). Natomiast łatwość użycia już podziałała na środowisko JS-owców, którzy coraz agresywniej z tym eksperymentują (nie muszę chyba zaznaczać, że mnie również generatory kupiły od razu ;)). Zostaje tylko czekać i… samemu eksperymentować. Tym bardziej, że stworzenie własnego wrappera na generatory wcale nie jest trudne i można to zrobić w zasadzie w ciągu kilkunastu minut. Co też zrobimy. Ale nie dziś ;)

Tagi:asynchronicznośćJavaScript

komentarzy 17

  • Awatar
    Mr.Mr

    24 grudnia 2013 00:11

    TL;DR – chociaż pewnie był ciekawy :P

    A tak na serio to bardzo interesujące. Ja jestem chory jak słyszę AJAX, więc te wszystkie sprawy są mi bliskie sercu.

    Czekam na następną część!

    Odpowiedz
    • Awatar
      Comandeer

      24 grudnia 2013 15:03

      Niestety, druga część skupi się na node.js ;)

      Odpowiedz
  • Awatar
    Michał Załęcki

    24 grudnia 2013 10:29

    > hej, mamy natywną enkapsulację
    Z tą hermetyzacją, a już w ogóle natywną, w JavaScript bym uważał. Chyba najlepszym rozwiązanie, będzie i tak zwracanie „api”, powiedzmy… przez funkcję-klasę, ale nadal brakuję dużo do modyfikatorów dostępu.
    > W przypadku bardziej skomplikowanych aplikacji grzęźniemy w callbackach
    powsadzanych w callbacki, które z kolei są odpalane w callbackach innych
    callbacków.
    W JS nie jest jeszcze tak źle, szczególnie jeżeli masz doświadczenie z WordPressem. Tam oddzwanianie jest na porządku dziennym, czasami nawet całą klasą, nie tylko pojedynczą funkcją.

    Do JS mam mieszane uczucia. Jest świetnie puki pracujesz offline czy używasz AJAXa, robisz jakieś animacje, dodajesz wodotryski, których robienie w CSS jest niemożliwe, albo niewarte zachodu. Jednak jak chcesz zrobić coś większego musisz wspomagać się rozwiązaniami osób trzecich. Szkoda, że JS nie dorobił się API, które pozwalało by na efektywne łączenie się z zewnętrznymi bazami danych (bez duetu np. PHP+JSON). Utrzymanie większych partii kodu też do najłatwiejszych nie należy. Jakieś dobre pomysły na utrzymywanie kodu? „Przestrzenie nazw” czy coś jeszcze?

    Miło przywitać nowego redaktora :)

    Odpowiedz
    • Awatar
      Comandeer

      24 grudnia 2013 15:19

      > Z tą hermetyzacją, a już w ogóle natywną, w JavaScript bym uważał.
      Bardzo fajnie wygląda specyfikacja modułów w ES 6, wyraźnie wzorowana na tej znanej z CommonJS.
      >nadal brakuję dużo do modyfikatorów dostępu
      nie byłbym tego taki pewien ;) od dawna szuka się sposobu na protected a w ES 6 nadchodzi Object.observe, które powinno wreszcie problem rozwiązać.
      >W JS nie jest jeszcze tak źle, szczególnie jeżeli masz doświadczenie z WordPressem.
      to coś w WP jest tak niemożliwie kalekie… to niby jest system eventowy, ale… no nie da się tego używać ;)
      >Jednak jak chcesz zrobić coś większego musisz wspomagać się rozwiązaniami osób trzecich.
      tzn? nie bardzo wiem o czym mówisz
      >Szkoda, że JS nie dorobił się API, które pozwalało by na efektywne łączenie się z zewnętrznymi bazami danych (bez duetu np. PHP+JSON).
      przecież powstały pure-JS rozwiązania do pracy z wieloma bazami, m.in. MySQL
      >Jakieś dobre pomysły na utrzymywanie kodu?
      choćby już wspomniany wcześniej system modułów z ES6, który obecnie jest „profillowany” przez AMD i moduły CommonJS.
      mam wrażenie, że dla Ciebie JS to wciąż głównie język browserowy. dla mnie już od dawna nie
      >Miło przywitać nowego redaktora :)
      nie chwalmy dnia… ;) to dopiero jeden artykuł.

      Odpowiedz
      • Awatar
        Michał Załęcki

        24 grudnia 2013 15:40

        Z WP, aż tak źle nie jest, wystarczy dokumentacja, pisana z resztą bardzo skrupulatnie, wiem, używam. Poza tym, jedna z najlepszych społeczności w ogóle. Nie jestem przekonany do wyższości noda.js nad ASP.NET czy PHP5 więc, tak, dla mnie pozostanie jeszcze długo (a może krócej niż mi się wydaje) „tylko” browserowy, chodź takie projekty jak Ghost są ciekawymi alternatywami i pokazują, że node.js warto się interesować. Co do rozwiązań osób trzecich. Np. jak chcesz mieć między bazowy mechanizm, dzięki któremu połączysz się z różnymi bazami danych to sięgasz po JugglingDB – rozwijany przez prywatną firmę, nie jak PDO, rozwijany przez samych twórców PHP. Bardzo ciekawy jestem kolejnych artykułów o node.js. Może coś „from scratch”?

        Odpowiedz
      • Awatar
        Comandeer

        24 grudnia 2013 15:51

        >nie są wbudowane
        jak dla mnie PHP ma zbyt rozbudowaną bibliotekę standardową. takie PDO powinno być właśnie „doinstalowalne” osobno, w razie potrzeby.
        >Brakuje np. odpowiednika PDO
        w tym, co zalinkowałeś, jest obsługa prepared statements, więc czego chcieć więcej? ;) wystarczy trochę poszperać, żeby znaleźć ORM-y, np. https://npmjs.org/package/anydb-sql
        >Nie jestem przekonany do wyższości noda.js nad ASP.NET czy PHP5
        ale warto go poznać ;) choćby z powodu takich narzędzi jak LESS, Stylus, Grunt, Bower, Yeoman… nawet przecież PHP-owy composer jest wzorowany na menedżerze pakietów node’a

        Odpowiedz
      • Awatar
        Michał Załęcki

        24 grudnia 2013 16:41

        PDO jest poza SPL, no i jest najwydajniejszym sterownikiem baz danych, wręcz podstawową funkcjonalnością, w końcu to PHP Data Objects, a że kapitalnie działa to tylko oklaski dla autorów. Samo SPL jest małe, głównie iteratory , ArrayObject i wyjątki. No cały czas jesteśmy przy nodzie, a ja bym chciał to z klienta :P, wiec na razie bez mostków się nie obejdę. Szczególnie mnie to boli jeżeli chodzi o urządzenia mobilne. Np. jeżeli mam serwis to nie mogę zrobić appki w HTML5 + JS bez przygotowania backendu. Czyli muszę dodać kontroler specjalnie do obsługi aplikacji mobilnej.

        Odpowiedz
      • Awatar
        Comandeer

        24 grudnia 2013 16:45

        >a ja bym chciał to z browsera :P
        nie przesadzajmy… poza tym masz ;) WebSQL w webkitach i IndexedDB w reszcie :D
        >Np. jeżeli mam serwis to nie mogę zrobić appki w HTML5 + JS bez przygotowania backendu.
        obecnie można już robić appki, gdzie backend służy tylko do inicjalnego wysłania pliku i już. a jeśli musimy mieć dane w bazie na serwie, to są specjalne biblioteki do synchronizacji (de facto takie mini systemy kontroli wersji, cholernie potężne ustrojstwo).

        Odpowiedz
      • Awatar
        Michał Załęcki

        24 grudnia 2013 16:48

        No mam WebSQL, ale ono działa lokalnie. Jeżeli napiszę app w C# na WP to mogę z MySQL się łączyć. Nie widzę więc przeciwwskazań. Nie wiem dlaczego w JS tego brakuję. Liczę, że się w niedalekiej przyszłości taka funkcjonalność pojawi.

        Odpowiedz
      • Awatar
        Comandeer

        24 grudnia 2013 16:56

        Nie sądzę, żeby się pojawiło. przeglądarka jest zamkniętym środowiskiem, które jedynie komunikuje się z serwerem. poza tym bardzo mnie ciekawi jakbyś taką bazę zabezpieczył w JS ;) przecież w kodzie będą leżeć surowe dane dostępu do bazy a ze specyficzności przeglądarki dostęp musiałby być z każdego możliwego miejsca. bezpieczeństwo jest zerowe.
        jeśli chodzi o JS z poziomu przeglądarki, ono powinno się komunikować z serwerem tylko przy pomocy API (tu i tak leci autoryzacja choćby przy pomocy OAuth 2.0). REST + JSON są na tyle przyjemne, że dodawanie danych do bazy nie jest szczególnie udziwnione. a na pewno o wiele bezpieczniejsze
        a jeśli mamy WebSQL lokalnie, to przecież można zmapować odpowiednie fragmenty bazy serwera na kliencie i synchronizować (o czym już wspominałem).
        zresztą łączenie się bezpośrednio z klienta do bazy danych na serwie moim zdaniem narusza ACID. w przypadku mobilnych jest to tym bardziej widoczne, bo mamy często słabą Sieć, w dodatku często się rozłączającą. de facto bez dodatkowych mechanizmów sprawdzania spójności danych itd (czyli system kontroli wersji) takie rozwiązanie byłoby po prostu głupotą (cały paradygmat offline first, czyli PE 2.1)
        jak dla mnie bardzo słaby pomysł – zwłaszcza, że istniejące rozwiązania sobie z tym bez problemu radzą, są łatwe w obsłudze i dodatkowo zapewniają większą spójność danych.

        Odpowiedz
      • Awatar
        Michał Załęcki

        24 grudnia 2013 17:20

        Jak zabezpieczyć? Uprawnieniami. Co za problem, by użytkownik aplikacji mobilnej miał swoje konto i tabelę? Dla aplikacji np. firmy jak znalazł. Zawsze lepiej mieć możliwość i z niej nie skorzystać niż być jej pozbawionym.

        Odpowiedz
      • Awatar
        Comandeer

        24 grudnia 2013 17:27

        >Zawsze lepiej mieć możliwość i z niej nie skorzystać niż być jej pozbawionym.
        rozumiem, że dlatego lubisz goto w PHP? ;)
        >Jak zabezpieczyć? Uprawnieniami.
        fajnie, tylko, że nawet zwykłe uprawnienie do odczytu może posłużyć co poniektórym do wyciągnięcia nie tylko swoich danych

        W3C pracuje nad raw sockets, więc jeśli jesteś na tyle szalony, żeby napisać driver dla MySQL, to droga wolna ;) ja jednak z tego na pewno nie skorzystam

        Odpowiedz
      • Awatar
        Michał Załęcki

        24 grudnia 2013 17:38

        No jak każdy ma swoją tabelę i tylko do niej uprawnienia to nie widzę problemu. goto w PHP 5.3 nie jest tym samym goto z Basica, ważny jest kontekst. Doceniam możliwości. Nie jestem przyzwyczajony do używania goto gdzie indziej niż w plikach wsadowych, wiec potraktowałem to jako ciekawostkę. Jednak goto ma już lepsze alternatywy, a bezpośredniego połączenia z bazą danych nie ma :(

        > jeśli jesteś na tyle szalony, żeby napisać driver dla MySQL, to droga wolna ;)

        To ja już wolę pisać nowy kontroler :P

        Odpowiedz
      • Awatar
        Comandeer

        24 grudnia 2013 17:43

        >No jak każdy ma swoją tabelę i tylko do niej uprawnienia to nie widzę problemu.
        ale czy to na pewno wówczas jest wciąż relacyjna DB? bo to już mi przypomina model obiektowy/dokumentowy znany np. z MongoDB
        poza tym – 1k userów = 1k tabel w bazie. nie wygląda to najciekawiej ;)

        zresztą nie rozumiem co tak bardzo odrzuca Cię w połączeniu się z API serwera?
        POST /user/id/2
        GET /user/id/2
        PUT /user/id/2
        DELETE /user/id/2
        cholernie czytelne i IMO wygodniejsze niźli bezpośrednie babranie się z bazą danych. + uniwersalne – można transparentnie zmieniać leżącą poniżej bazę danych (ba, całą implementację serwera) a przeglądarka dalej po prostu wysyła żądania pod te same adresy.

        Odpowiedz
      • Awatar
        Michał Załęcki

        24 grudnia 2013 22:58

        No jeżeli zależy nam na relacji to szukamy innego rozwiązania. API serwera mnie nie odrzuca oczywiście, w sumie tylko tak rozwiązuje zapytania związane z AJAXem. Mimo wszystko parę ciekawych zastosowań bym znalazł.

        Odpowiedz

Zostaw odpowiedź