Jak każdy zapewne wie, obiekt XHR (AJAX) języka JavaScript nie umożliwia odwoływania się do zasobów spoza domeny, na której jest umieszczony dany skrypt (Same origin policy).
Oznacza to tyle, że chcąc wywołać jakąś usługę sieciową spoza naszej domeny, nie zrobimy tego bezpośrednio przy pomocy obiektu XMLHttpRequest i musimy kombinować z różnego rodzaju obejściami tego problemu – o czym szerzej w tym wpisie.
Nie możemy wywołać następującej operacji:
1 2 3 | $.getJSON('http://flickr.com/api/test.json', function(data) { alert(data); }); |
Jak w takim razie poradzić sobie z tym problemem? Rozwiązań jest kilka, o czym poniżej.
Komunikacja jednokierunkowa
Najprostszym z obejść jest wysłanie jednokierunkowego requesta do zasobu, którego potrzebujemy „powiadomić” o zaistniałej sytuacji. W praktyce wygląda to mniej więcej tak:
1 2 | var image = new Image(); image.src = 'http://blog.kamilbrenk.pl/files/counter.php'; |
Co robi powyższy kod? Otóż tworzony jest nowy obrazek, a następnie do adresu obrazka przypinamy adres usługi, do której chcemy się odwołać. W rozwiązaniu tym nie musimy nawet osadzać tak stworzonego obrazka w dokumencie, aby request został wysłany.
Pod adresem http://blog.kamilbrenk.pl/files/counter.php jest dostępny następujący kod licznika:
1 2 | $counter = file_get_contents("counter.txt"); $counter++; file_put_contents("counter.txt", $counter); |
Tak więc user wchodzący na naszą stronę wysyła żądanie do licznika, ten zwiększa stan o jeden i wszyscy są zadowoleni – rozwiązanie spisuje się idealnie bez wykorzystania obiektu XHR.
Komunikacja dwukierunkowa
A co jeśli potrzebujemy rozwiązania, w którym wysyłane zapytanie zwraca odpowiedź od zewnętrznej usługi sieciowej?
Rozwiązanie to jest już trochę trudniejsze, jednak dla chcącego nic trudnego. Posłużymy się tutaj metodą zwaną JSONP.
1 2 3 4 5 | var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'http://blog.kamilbrenk.pl/files/json.php?callback=myFunction'; $('body').append(script); |
Co tutaj się dzieje? Tworzymy nowy skrypt w JavaScript, do adresu skryptu dajemy adres usługi sieciowej. W parametrach adresu dodajemy callback=myFunction. Oznacza to tyle, że funkcja zwrotna (callback) to myFunction. Zdefiniujmy więc tą funkcję:
1 2 3 | function myFunction(data) { alert(data.firstname + ' ' + data.lastname); } |
Aby wszystko działało prawidłowo, musimy jeszcze skonfigurować naszą usługę zwracającą odpowiednie dane. Robimy to następująco (przykładowo):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php if (isset($_GET['callback'])): ?> var response = { firstname: 'Kamil', lastname: 'Brenk' }; <?= $_GET['callback']; ?>(response); <? endif; ?> |
Wszystko jasne? Jak widać też nie jest najciężej, choć metoda ta wymaga definiowania funkcji zwrotnej w naszej zewnętrznej usłudze, co czasem może być kłopotliwe.
Na szczęście wiele zewnętrznych API popularnych usług domyślnie udostępnia callbacki, więc nie ma większych problemów.
Kolejnym problemem z wykorzystaniem tej metody jest konieczność wysyłania wszystkich żądań metodą GET, co nie zawsze jest pożądane. Ponadto nie mamy możliwości kontrolowania przesyłanych czy otrzymywanych nagłówków HTTP.
Inne sposoby komunikacji typu cross-domain
Bridge Design Pattern
Sposobów jest kilka, a najpopularniejszym z nich jest wykorzystanie wzorca Most (Bridge) w JavaScript/PHP (czy innym języku server-side).
Metoda ta polega na wysłaniu requesta do naszego serwera, a następnie po stronie serwera wykonujemy jakąś usługę (zazwyczaj po stronie stronie serwera nie obowiązuje koncepcja Same origin policy, chyba że sobie tego zażyczymy -> X-Content-Security-Policy) i do skryptu zwracamy już obrobione dane (JSON/XML).
Więcej o tym wzorcu napisał Tomasz Kowalczyk, którego wpis chciałem tutaj co nieco rozszerzyć o nowe sposoby wysyłania żądań do odrębnych domen.
<iframe> – wysyłanie danych metodą POST
Kolejnym rozwiązaniem (i jednym z pierwszych, jeszcze sprzed czasów AJAX-a) jest umieszczanie pływającej ramki IFRAME.
Sposób ten polega na utworzeniu w JavaScript ramki IFRAME. Następnie w ramce tej tworzymy formularz GET/POST, dołączamy kilka pól z wartościami, które chcemy przesłać. Na koniec wysyłamy formularz, po czym usuwamy pływającą ramkę. Prawda, że proste?
Nie będę tutaj pisał przykładowego kodu, bo zostało to już zrobione przez Frontend.pl, gdzie też odsyłam zainteresowanych tematem.
Cross-domain requests with jQuery
Na sam koniec zostawiłem coś najlepszego – świetną wtyczkę usuwającą blokadę i niwecząc koncepcję Same origin policy. Jest to wtyczka do jQuery (Cross-Domain Ajax mod), która powoduje, iż możemy spokojnie wykonywać żądania do innych stron, zwracać od nich odpowiedzi – możemy robić co tylko zechcemy.
Możemy więc wykonać następujący kod:
1 2 3 4 5 6 7 8 | $.ajax({ url: 'http://google.pl/', type: 'GET', success: function(res) { var headline = $(res.responseText).text(); alert(headline); } }); |
I nie żartuję tutaj, wszystko wspaniale się wykona – zostanie zwrócony prawidłowy wynik. O co więc chodzi?
Aby móc pobrać odpowiedź od zewnętrznego serwera musimy wykorzystać wyżej opisaną metodę JSONP. Proces przebiega następująco:
- Tworzymy skrypt JavaScript, przypisujemy do niego adres zewnętrznej usługi z funkcją callback,
- Usługa do której się odwołujemy to Yahoo YQL, o czym może wkrótce napiszę więcej,
- Do naszej usługi sieciowej dołączamy adres strony wraz z poleceniem YQL, które chcemy wykonać na pobieranej stronie; polecenie takie jest połączeniem SQL i xPath:
1SELECT * FROM html WHERE url="http://stackoverflow.com" AND xpath='//div/h3/a'
W wyniku czego otrzymamy plik XML z wynikiem:
1
2
3<results>
<a class="question-hyperlink" href="/questions/661184/filling-the-text-area-with-the-text-when-a-button-is-clicked" title="In ASP.net, I need the code to fill the text area (in the form) when a button is clicked. Can you help me through by showing a simple .aspx code containing the script tag? ">Filling the text area with the text when a button is clicked</a>...
</results> - Odbieramy przesyłkę (dokument XML), obrabiamy go i zwracamy do funkcji, która wywołała żądanie AJAX-a.
To wszystko. Wadą takiego rozwiązania jest konieczność wywoływania osobnej usługi sieciowej, zanim dostaniemy się do naszej usługi. Wynika z tego, że zamiast jedno połączenie, musimy wykonać dwa połączenia (jedno na naszym serwerze, jedno na serwerze Yahoo).
Więcej wad się nie dopatrzyłem, dlatego szczerze polecam to rozwiązanie na bolączki większości programistów. Prosto i przyjemnie!
Podsumowanie
Na pewno rozwiązań problemu jest dużo więcej – w końcu ile głów, tyle pomysłów. Niemniej jednak powyższe zestawienie powinno wystarczyć w przypadku 90% problemów.
Poza tym dochodzi pytanie, czy na pewno potrzebujemy odwoływać się do zewnętrznych usług przy pomocy JavaScript? W końcu nie bez powodu nałożono taką blokadę – jest to zabezpieczenie przed XSS, CSRF i pochodne.
Wyobraźmy sobie taki przykład: Kowalski wchodzi na Twoją witrynę. Jednocześnie jest zalogowany na stronie swojego banku (w osobnej zakładce).
No więc możesz na swojej stronie utworzyć pływającą ramkę IFRAME i od razu ją ukryć. Następnie wczytujesz ze strony banku formularz do wykonywania przelewów. Przy pomocy JavaScript wypełniasz formularz, wprowadzając jakieś dane i swój numer konta bankowego. Zatwierdzasz i przesyłasz formularz. Potem jeszcze tylko zdobyć kod potwierdzający operację w e-banku i masz kasę na swoim koncie :D
Podsumowując, staraj się unikać wykonywania żądań do innych serwerów przy pomocy JavaScript. Inaczej może to się obrócić przeciwko Tobie, jeśli komuś uda się zastosować atak CSRF (Cross-site request forgery) na Twojej stronie. Konsekwencji może być naprawdę sporo.
Widzę, że przygotowałeś o wiele obszerniejsze podsumowanie tematu niż ja, pogratulować. ;]
O iinnych metodach pobierania danych ze zdalnego serwera można przeczytać także tu:
http://www.yarpo.pl/2011/05/06/odczytywanie-danych-ze-zdalnego-serwera/
Szczególnie ciekawym wydaje mi się stosowanie `XMLHttpIframeRequest’ (obiekt oparty o pływającą ramkę posiadający spójny z `XMLHttpRequest’ interfejs)
@Patryk: zgadza się, całkiem fajnie zrobiona ta klasa XMLHttpIframeRequest :) Mógłbyś jeszcze dostosować do IE i byłoby git.
W sumie dostosowanie nie jest trudne.
W IE po prostu pliki JS dodawane z poziomu kodu nie posiadają zdarzenia onload. Zamiast tego posiadają onreadystatechange (tak jak Ajax). Ma to swoje minusy – wymaga osobnego kodu, ale i tez plusy. Tworząc obiekt i ustawiając jego src plik jest zaczytywany. W momencie dodania obiektu do struktury DOM zostaje wykonany.
Były nawet głosy, aby takie podejście zastosować jako obowiązujący standard:
http://www.nczonline.net/blog/2011/02/14/separating-javascript-download-and-execution/
A skrypt wyżej linkowany w niedługim czasie mam zamiar poprawić :)