Глава 17
HTTP

Мечтата зад Уеб е за общо информационно пространство, в което ние общуваме чрез споделяне на информация. Неговата универсалност е от съществено значение: факта, че хипер-връзката може да сочи към нещо било то индивидуално, местно или глобално е да го проектира или изтъкне.”

Tim Berners-Lee, The World Wide Web: Много кратка лична история

Протоколът Hypertext Transfer Protocol, споменат в Глава 12, е механизмът, чрез който се изисква информация и в условие на Wrold Wide Web. Тази глава описва протокола по-подробно и обяснява начина, по който браузъра на JavaScript има достъп до него.

Протоколът

Ако изпишете eloquentjavascript.net/17_http.html в адресната лента на браузъра си, той първо поглежда адреса на сървъра, свързан с eloquentjavascript.net и се опитва да отвори TCP връзка към него на порт 80, който по подразбиране е порт за HTTP трафик. Ако сървърът съществува и приеме връзката, браузъра изпраща нещо такова:

GET /17_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name

После сървърът отговаря (responds), през същата тази връзка.

HTTP/1.1 200 OK
Content-Length: 65585
Content-Type: text/html
Last-Modified: Wed, 09 Apr 2014 10:48:09 GMT

<!doctype html>
... the rest of the document

Браузърът след това взема част от отговора и след празен ред го показва, като HTML документ.

Информацията изпратена от клиента, се нарича искане (request). То започва с този ред:

GET /17_http.html HTTP/1.1

Първата дума е методът на искането GET, което означава, че искаме да получим посочения ресурс. Други общи методи са DELETE за изтриване на даден ресурс, PUT да го замени и POST да изпрати информация за него. Имайте в предвид, че сървърът не е длъжен да изпълнява всяко искане, което получава. Ако отидете на произволен сайт и му кажете DELETE на главната страница, той най-вероятно ще откаже.

Частта след името на метода е пътя на ресурса на искането, към което се отнася. В най-простия случай, ресурс е просто един файл на сървъра, но протокола не изисква това да бъде файл. Ресурсът може да бъде всичко, което може да се прехвърля. Много сървъри генерират отговорите, като ги произвеждат в движение. Например ако отворим twitter.com/marijnjh сървърът поглежда в своята база данни за потребителското име marijnjh и ако установи такова ще генерира страница с профила на този потребител.

След пътя на ресурсите, на първо място в линията на искането е HTTP/1.1 за да покаже, коя версия на HTTP протокола използва.

Отговорът на сървъра започва с версията следвана от статуса на отговора, първо като трицифрен код на състоянието и после, като string четим от човек.

HTTP/1.1 200 OK

Статус кодове започващи с 2 показват, че искането е успешно. Кодове започващи с 4 означават, че нещо не е наред с искането. 404 е може би най-известният код за HTTP статуса, който означава, че поисканият ресурс не може да бъде намерен. Кодове, които започват с 5 означават, че грешката се е случила на сървъра и искането не е виновно.

Първият ред на искането или отговора може да бъде последвано от произволен брой заглавия. Това са редове във формата: “име: стойност”, които уточняват допълнителната информация за искането или отговора. Тези заглавия са част от примерен отговор:

Content-Length: 65585
Content-Type: text/html
Last-Modified: Wed, 09 Apr 2014 10:48:09 GMT

Това ни казва размера и вида на документа за отговор. В този случай това е HTML документ с 65,585 байта. Той също така ни казва последната промяна на този документ.

В по-голямата си част, клиента или сървъра решават , кои заглавия да включат в рамките на искане или отговор, въпреки че няколко заглавия са необходими. Например, Host заглавието, посочва името на хоста, което трябва да бъде включено в искането, защото сървъра може да обслужва множество хостове на един IP адрес и без това заглавие, сървърът няма да знае с кой хост клиент се опитва да се свърже.

След заглавието, искането и отговора могат да бъдат включени в един празен ред, последван от тяло, което съдържа данните, които се изпращат. GET и DELETE не изпращат всички данни, но PUT и POST исканията го правят. По същия начин, някои видове отговори, като отговори за грешки не се нуждаят от тяло.

Браузъри и HTTP

Както видяхме в примера, браузъра ще направи искане, когато въведем URL адреса в бара му. Когато получената HTML страница направи препратка към други файлове, като изображения и JavaScript файлове, те също така ще бъдат извлечени.

Умерено сложен сайт може лесно да включва някъде от 10 до 200 ресурса. За да бъде в състояние да ги донесе бързо, браузъра прави няколко искания едновременно, вместо да чака отговорите един по един. Такива документи са винаги извлечени с помощта GET искане.

HTML страници могат да включват forms (форми), които позволяват на потребителя да попълни информация и да я изпрати на сървъра. Това е пример на форма:

<form method="GET" action="example/message.html">
  <p>Name: <input type="text" name="name"></p>
  <p>Message:<br><textarea name="message"></textarea></p>
  <p><button type="submit">Send</button></p>
</form>

Този код описва форма с две полета: малко с въпрос за име и по-голямо за писане на съобщение. Когато натиснете бутона Send, информацията в тези две полета ще бъде кодирана в query string искане. Когато <form> елемента съдържа метод атрибут GET (или е пропуснат), тази string заявка се закача върху actionURL адреса и браузъра прави GET искане в този URL.

GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

Началото на тази query string заявка се обозначава с въпросителен знак. След това следват двойки имена и стойности, съответстващи на name атрибута на полето на формата с елементи и съдържанието на тези елементи, съответно. Характера амперсанд (&) се използва за разделяне на двойките.

Действителното съобщение, кодирано в предишния URL адрес е “Yes?” въпреки, че въпросителния знак се заменя със странен код. Някои характери в query string исканията трябва да се ескейпнат. Маркировката за въпрос представена със %3F е един от тях. Има едно неписано правило, че всеки формат се нуждае от свой собствен начин за ескейпинг на характерите. Този начин се нарича URL encoding (кодиране), който използва знака за процент %, последван от две шестнадестични цифри, които кодират кода на характера. В този случай 3F, което е 63 в десетична бройна система, е кодът на характера за въпросителен знак. JavaScript осигурява encodeURIComponent и decodeURIComponent функции за кодиране и декодиране на този формат.

console.log(encodeURIComponent("Hello & goodbye"));
// → Hello%20%26%20goodbye
console.log(decodeURIComponent("Hello%20%26%20goodbye"));
// → Hello & goodbye

Ако променим method атрибута на HTML формата, в примера, който видяхме по-рано с POST, HTTP искането направено за представяне на формата ще използва метода POST и поставяне на query string запитване в тялото на искането, вместо да го добави към URL адреса.

POST /example/message.html HTTP/1.1
Content-length: 24
Content-type: application/x-www-form-urlencoded

name=Jean&message=Yes%3F

По конвенция, GET метода се използва за искания, които нямат странични ефекти, като направата на търсене. Исканията, които променят нещо в сървъра, като например създаване на нов акаунт или публикуване на съобщение, следва да бъдат изразени с други методи, като например POST. Софтуера от страна на клиента, като браузър, знае че не трябва сляпо да изпълнява POST искания, но често мълчаливо прави GET искания - например prefetch ресурса вярва, че потребителя се нуждае от него.

В следващата глава ще се върнем към форми и ще поговорим за това как можем да ги пишем с JavaScript.

XMLHttpRequest

Интерфейса, чрез който браузъра JavaScript може да направи HTTP искания се нарича XMLHttpRequest (обърнете внимание на непоследователната капитализация). Той е проектиран от Microsoft за неговия браузър Internet Explorer в края на 1990г. през това време XML файлов формат е много популярна в света на бизнес софтуера - един свят, където Microsoft винаги е бил у дома си. Всъщност, той е толкова популярен, че съкращението XML е било залепено върху предната част на името на интерфейса за HTTP, който по никакъв начин не е обвързан с XML.

Името не е напълно безсмислено все пак. Интерфейса позволява да прави разбор на документи за отговор, като XML ако искате. Смесването на две различни понятия (правене на искане и разбор на отговора) в едно нещо е ужасен дизайн, разбира се , но така стоят нещата.

Когато интерфейса XMLHttpRequest е добавен в Internet Explorer, той позволява на хората да правят неща с JavaScript, които са били много трудни преди. Така например, уеб сайтовете започнали да показват списъци с предложения, когато потребителя пише нещо в текстовото поле. Скрипта ще изпрати текста към сървъра през HTTP, който потребителя въведе. Сървърът, който има някаква база данни за възможни входове ще съпостави записите в базата данни с входящите и ще изпрати обратно възможно изпълнение, което да покаже на потребителя. Това се счита за грандиозен замисъл - хората бяха свикнали да чакат пълното зареждане на страницата за някакво взаимодействие с уеб сайта.

Друг значителен браузър по това време е Morzilla (Firefox по-късно), който не искаше да бъде забравен. За да даде възможност на хората да правят подобни неща на своя браузър, Morzilla копират интерфейса включително и сбърканото име. Следващото поколение браузъри последвали този пример и днес XMLHttpRequest е дефакто стандартен интерфейс.

Изпращане на искане (request)

За да се направи просто искане, ние създаваме обект-искане с XMLHttpRequest конструктора и извикваме неговите open и send методи.

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", false);
req.send(null);
console.log(req.responseText);
// → This is the content of data.txt

Метода open конфигурира искането. В този случай избираме да направим GET искане за файла example/data.txt. URL адреси, които не започват с име на протокол (като http:) са относителни, което означава, че те се тълкуват спрямо текущия документ. Когато започват с наклонена черта / , те заменят текущия път, който е частта след името на сървъра. Когато това не е така, частта на текущия път до и включително характера за последната черта се поставят в предната част на относителния URL адрес.

След отваряне на искането, можем да го изпратим със send метода. Аргументът за изпращане е тялото на запитването. За GET заявки може да подадем null. Ако третият аргумент за open е false, send ще се върне само, след като е получен отговор на искането. Ние можем да прочетем искането в свойството на обекта responseText, от тялото на получения отговор.

Друга информация включена в отговора, също може да бъде извлечена от този обект. Кода на статуса е достъпен чрез status свойството, както и статуса на текста разбираем за човек е достъпен чрез statusText. Заглавията могат да се прочетат с getResponseHeader.

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", false);
req.send(null);
console.log(req.status, req.statusText);
// → 200 OK
console.log(req.getResponseHeader("content-type"));
// → text/plain

Заглавията са чуствителни към регистъра. Те обикновено се пишат с главна буква в началото на всяка дума, като “Content-Type” , но “content-type” и “Content-Type” се отнасят за една и съща заглавна част.

Браузърът автоматично ще добави заглавие към искането, като “Host” (домакин), както и тези необходими за сървъра, за да разбере размера на тялото. Но вие можете да добавите свои собствени заглавия със setRequestHeader метода. Това е необходимо само за модерни приложения, които изискват сътрудничеството на сървъра, с когото говорят - сървърът е свободен да игнорира заглавия, с които не знае как да се справи.

Асинхронни заявки

В примерите, които видяхме искането е приключило, когато send се е върнал. Това е удобно, тъй като свойства, като responseText вече са на разположение. Но също така означава, че нашата програма е на изчакване, докато браузъра и сървъра комуникират. Когато връзката е лоша, сървърът е бавен или файла е голям, това може да отнеме доста време. Още по-лошо е, че манипулаторите на събития не могат да работят, докато нашата програма изчаква и целият документ да стане нечуствителен.

Ако подадем true, като трети аргумент на open, искането ще стане асинхронно. Това означава, че когато извикаме send, единственото нещо, което се случва веднага, е че искането е планирано да бъде изпратено. Нашата програма може да продължи и браузърът ще се погрижи за изпращане и получаване на данни във фонов режим.

Но докато искането се изпълнява ние нямаме достъп до отговора. Имаме нужда от механизъм, който да ни уведоми, когато данните са на разположение.

За тази цел трябва да слушаме за "load" събитие в обекта на искането.

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", true);
req.addEventListener("load", function() {
  console.log("Done:", req.status);
});
req.send(null);

Точно както използването на requestAnimationFrame в Глава 15 нуждата от използване на асинхронно програмиране, опакова нещата, които трябва да се направят след подаването на искането във функция и осигуряването им, когато бъдат поискани в подходящо време. Ще се върнем към това по-късно.

Извличане на XML данни

Когато ресурсът, извлечен от обекта на XMLHttpRequest е XML документ, свойството на обекта responseXML ще анализира представянето на този документ. Работата на това представяне много прилича на DOM обсъдено в Глава 13 с изключение на това, че не разполага с HTML - специфична функционалност, като style свойството. Обекта на responseXML съответства на document обект. Неговото свойство documentElement се отнася до външният етикет на XML документа. В следващият документ (example/fruit.xml) това ще бъде <fruits> тага:

<fruits>
  <fruit name="banana" color="yellow"/>
  <fruit name="lemon" color="yellow"/>
  <fruit name="cherry" color="red"/>
</fruits>

Ние можем да извлечем такъв файл, подобен на този:

var req = new XMLHttpRequest();
req.open("GET", "example/fruit.xml", false);
req.send(null);
console.log(req.responseXML.querySelectorAll("fruit").length);
// → 3

XML документите могат да се използват за обмен на структурирана информация със сървъра. Тяхната форма, тагове вложени в други тагове, се поддава добре за съхраняване на повечето видове данни или поне по-добре отколкото плоски текстови файлове. Интерфейсът на DOM е по-скоро тромав за извличане на информация и XML документите са склонни да бъдат многословни. Често по-добра идея е да общуват с помощта на JSON данни, които са по-лесни за четене и писане, както за програми така и за хора.

var req = new XMLHttpRequest();
req.open("GET", "example/fruit.json", false);
req.send(null);
console.log(JSON.parse(req.responseText));
// → {banana: "yellow", lemon: "yellow", cherry: "red"}

Тестова среда на HTTP

Осъществяването на HTTP заявки в уеб страници отново поражда опасения за сигурността. Човекът, който контролира скриптовете може да няма същите интереси, като лицето на чиито компютър се изпълняват. По конкретно, ако посетя themafia.org, аз не искам неговите скриптове да бъдат в състояние да направят искане до mybank.com, използвайки идентифициращата информация на моя браузър с указания за прехвърляне на всички пари в някоя чужда сметка.

Възможно е уеб сайтовете да се защитят срещу такива атаки, но това изисква усилия и много сайтове не успяват да го направят. Поради тази причина браузърите ни предпазват забранявайки скриптове, които правят искания към други домейни (като themafia.org и mybank.com).

Това може да бъде досаден проблем при изграждането на системи, които искат достъп до няколко домейна с основателна причина. За щастие, сървърите могат да включват глава в случай на изрично указание към браузъра, за одобряване на искания, които идват от други домейни.

Access-Control-Allow-Origin: *

Абстрактни заявки

В Глава 10, в изпълнението на модулна система AMD, ние използвахме една хипотетична функция наречена backgroundReadFile. Тя взема името на файла и функцията и извиква тази функция със съдържанието на файла, когато свърши изтеглянето му. Ето едно просто изпълнение на тази функция:

function backgroundReadFile(url, callback) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.addEventListener("load", function() {
    if (req.status < 400)
      callback(req.responseText);
  });
  req.send(null);
}

Тази абстракция прави по-лесно използването на XMLHttpRequest за прости GET искания. Ако пишем програма, която трябва да направи HTTP искане е добра идея да се използва помощна функция, така че да не се повтаря грозния XMLHttpRequest модел през целия код.

Функцията callbackс имената на аргументите е термин, който често се използва за описване на функции, като тази. Функцията callback се дава на друг код, за да сме сигурни, че този код е начин за обратно извикване по-късно.

Не е трудно да се напише удобна функция за HTTP, съобразена с това, което и налагаме да прави. Предишната прави само GET искания и не ни дава контрол върху заглавията и тялото на искането. Можем да напишем друг вариант на POST искания или общо един, който поддържа различни видове искания. Много JavaScript библиотеки също предоставят опаковки за XMLHttpRequest.

Основният проблем на предишната обвивка е манипулирането на грешка. Когато искането връща статус на код, който показва грешка (404 и на горе), тя не прави нищо. Това може да се оправи при някои обстоятелства, но представете си да сложим показател “зареждане” на страницата, за да покаже, че не сме приключили с извличането на информация. Ако искането не успее, понеже сървърът се счупил или връзката прекъсва, страницата просто ще седи там, подвеждащо изглеждаща, че прави нещо там. Потребителя ще изчака известно време, нетърпелив да разгледа безполезно незавършения сайт.

Ние също трябва да имаме възможност да бъдем уведомени, когато искането не успее, така че да можем да предприемем съответните действия. Например, да отстраним съобщението за "зареждане" и да информираме потребителя, че нещо се е объркало.

Обработката на грешки в асинхронен код са още по-сложни, отколкото в синхронен код. Защото често се налага да се отложи част от работата, поставяйки я във функция за обратно извикване, като обхвата на try блока става безсмислен. В сления код, изключението няма да се прихване, защото извикването на backgroundReadFile се връща веднага. Control след това напуска try блока и функцията, която му е дадена, няма да бъде извикана по-късно.

try {
  backgroundReadFile("example/data.txt", function(text) {
    if (text != "expected")
      throw new Error("That was unexpected");
  });
} catch (e) {
  console.log("Hello from the catch block");
}

За да се справим с липсата на искания, трябва да позволим на допълнителна функция да бъде подадена в нашата обвивка и да я извикаме, когато искането не е наред. Като алтернатива, можем да използваме конвенцията, че ако искането не успее, допълнителен аргумент описващ проблема се предава на редовна функция за обратно извикване. Ето един пример:

function getURL(url, callback) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.addEventListener("load", function() {
    if (req.status < 400)
      callback(req.responseText);
    else
      callback(null, new Error("Request failed: " +
                               req.statusText));
  });
  req.addEventListener("error", function() {
    callback(null, new Error("Network error"));
  });
  req.send(null);
}

Ние добавихме манипулатор за "error" събитие, което ще сигнализира, когато искането не успяло напълно. Също извикваме функция за обратно извикване с аргумент за грешка, когато искането завърши със статус на код, който показва грешка.

Код използващ getURL после трябва да се провери дали е дал грешка и ако установи една да я манипулира.

getURL("data/nonsense.txt", function(content, error) {
  if (error != null)
    console.log("Failed to fetch nonsense.txt: " + error);
  else
    console.log("nonsense.txt: " + content);
});

Това не помага, когато става дума за изключения. Когато оковем няколко асинхронни действия заедно във верига, изключението при всяка точка на веригата ще продължи (освен ако не увием всяка обработка на функцията в собствен try/catch блок) приземявайки се на първото ниво и прекратим действието на веригата.

Promises

За сложни проекти, писането на асинхронен код в обикновен callback стил е трудно за правене правилно. Лесно е да се забрави проверката за грешка или да се даде възможност на неочаквано изключение да скъси програмата по кратък суров начин. Освен това, организирането на правилното управление на грешки, когато грешката трябва да премине през множество функции за обратно извикване и catch блокове, е досадно.

Има много опити за решаване на това с допълнителни абстракции. Един от най-успешните се нарича promises. Promise увива асинхронните действия в обект, който може да бъде разпространен и казва да се направят някои неща, когато завърши действието или се провали. Този интерфейс е настроен да стане част от следващата версия на езика JavaScript, но вече може да се ползва, като библиотека.

Интерфейса за promises не е съвсем интуитивен, но е мощен. Тази глава само грубо ще го опише. Можете да намерите по задълбочена тема на www.promisejs.org.

За да създадем един promise обект, ние извикваме Promise конструктора, давайки му функция, която инициализира асинхронно действие. Конструктора призовава тази функция подавайки и два аргумента, които сами по себе си са функции. Първият се извиква, когато действието приключи успешно, а вторият се извиква, когато се провали.

За пореден път, тука е нашата обвивка за GET искане, като този път връща promise. Ние просто ще извикаме get този път.

function get(url) {
  return new Promise(function(succeed, fail) {
    var req = new XMLHttpRequest();
    req.open("GET", url, true);
    req.addEventListener("load", function() {
      if (req.status < 400)
        succeed(req.responseText);
      else
        fail(new Error("Request failed: " + req.statusText));
    });
    req.addEventListener("error", function() {
      fail(new Error("Network error"));
    });
    req.send(null);
  });
}

Имайте предвид, че интерфейса на самата функция е много по-прост. Можете да го дадете на URL и той да върне promise. Това promise действа, като изход на искането. То има then метод, който може да се извика с две функции: едната да обработва успех, а другата провал.

get("example/data.txt").then(function(text) {
  console.log("data.txt: " + text);
}, function(error) {
  console.log("Failed to fetch data.txt: " + error);
});

Досега това е още един начин да се изрази същото нещо, което вече изразихме. Но това е само, когато ни трябват верижни действия заедно, като promises правят значителна разлика.

Извиквайки then произвежда ново promise , чийто резултат (стойността подадена към success манипулатора) зависи от стойността върната от първата функция подадена към then. Тази функция може да върне друго promise за да покаже, че се върши повече асинхронна работа. В този случай, promise върнато от then, само ще чака promise върнато от функцията манипулатор, успяващо или провалящо се със същата стойност, когато е разрешено. Когато функцията манипулатор връща nonpromise стойност, promise върнато от then веднага успява с тази стойност, като свой резултат.

Това означава, че можем да използваме then да се превърне в резултат на promise. Например, това връща promise, чийто резултат е съдържанието на даден URL адрес, анализиран с JSON:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

Това последното извикване на then не определя манипулатор за провал. Това е позволено. Грешката ще бъде приета от promise върнато от then, което е точно това, което искаме - getJSON не знае какво да прави ако нещо се обърка, но се надяваме, че извикващия го прави.

Като пример, който показва използването на promises, ние ще изградим една програма, която извлича редица JSON файлове от сървъра и докато го прави показва думата loading. Файловете с JSON съдържат информация за хората с линкове към файлове, които представляват други хора в свойства, като father, mother или spouse.

Искаме да получим името на майката на съпруга от example/bert.json. И ако нещо се обърка, искаме да се премахне текста loading и вместо него да се покаже съобщение за грешка. Ето как, това може да бъде направено с promises:

<script>
  function showMessage(msg) {
    var elt = document.createElement("div");
    elt.textContent = msg;
    return document.body.appendChild(elt);
  }

  var loading = showMessage("Loading...");
  getJSON("example/bert.json").then(function(bert) {
    return getJSON(bert.spouse);
  }).then(function(spouse) {
    return getJSON(spouse.mother);
  }).then(function(mother) {
    showMessage("The name is " + mother.name);
  }).catch(function(error) {
    showMessage(String(error));
  }).then(function() {
    document.body.removeChild(loading);
  });
</script>

Получената програма е сравнително компактна и разбираема. Методът catch е подобен на then с изключение на това, че очаква само манипулатор за провал и ще премине през резултата непроменен в случай на успех. Много прилича на catch клаузата за try отчета, контролът ще продължи нормално, след като провалът е прихванат. По този начин, финалния then, който премахва loading съобщението, винаги ще се изпълнява дори ако нещо се обърка.

Можете да мислите за интерфейса на promise, като изпълнение на собствения му език за асинхронен контрол на потока. Допълнителният метод извиква и функционални изрази (expressions) необходими за постигане на това, което прави кода да изглежда малко неудобен, но малко по-неудобен от това да изглежда, че се грижи за всичките грешки на работата си.

Оценяване на HTTP

При изграждането на системата, която изисква комуникация между програмата на JavaScript работеща в браузъра (от страна на клиента) и програмата на сървъра (от страна на сървъра) има няколко различни начина, с които да се моделира тази комуникация.

Най-често използваният модел е този на remote procedure calls (отдалеченото извикване на процедури). В този модел, комуникацията следва моделите на нормалните извиквания на функции, само че функцията работи на друга машина. Извикването съдържа подаване на искане до сървъра, която включва името и аргументите на функциите. Отговора на това искане съдържа върнатата стойност.

Когато мислим от гледна точка на отдалеченото извикване на процедури, HTTP е просто средство за комуникация и най-вероятно ще напишете слой на абстракция, който да го скрива изцяло.

Друг подход е да изградим наша комуникация около концепциите за ресурсите и HTTP методите. Вместо отдалечена процедура, наречена addUser, ще използваме PUT искане до /users/larry. Вместо кодирани потребителски свойства в аргументите на функцията, ще дефинираме формат на документ или ще използваме съществуващ формат, който представлява потребител. Тялото на PUT искането за създаване на нов ресурс е просто такъв документ. Ресурсът е пресилен, като се прави GET искане до URL ресурса (например, /user/larry), който връща документ представляващ ресурса.

В този втори подход е по-лесно да използвате някои от функциите, които осигурява HTTP, като подкрепа за кеширане (складиране) на ресурси (запазване на копие от страна на клиента). Той може да помогне също за съгласуваноста на вашия интерфейс, тъй като ресурсите са по-лесни за разбиране в сравнение с плетеница от функции.

Сигурност и HTTP

Данните пътуващи по Интернет имат тенденцията да следват дълъг и опасен път. За да стигнат местоназначението си, те трябва да се качат през някоя от свободните мрежи и да преминат през мрежи контролирани от различни компании и държави. Във всеки един момент по своя маршрут те могат да бъдат инспектирани или дори модифицирани.

Ако това са важни неща, които трябва да останат в тайна, като паролата за вашия емайл акаунт или да стигнат местоназначението си непроменени, като например номера на сметката ви за превод на пари в уеб-сайта на банката, обикновен HTTP не е достатъчно добър.

Защитен HTTP протокол, чийто URL адреси започват с https:// опаковайки по този начин HTTP трафика го прави по-труден за четене и фалшифициране. Първо клиента потвърждава, че е сървърът за който се твърди и изисква от този сървър да докаже, че разполага с криптографски сертификат издаден от орган за удостоверения, който браузърът разпознава. На следващо място, всички данни преминават през криптираната връзка по начин, който да предотврати подслушване и подправяне.

По този начин, когато работи добре HTTPS пречи на подмяната на сайта с друг, както и подслушването на вашата комуникация. Той не е съвършен, има различни инциденти, където HTTPS се е провалял заради фалшиви или откраднати удостоверения и счупен софтуер. И все пак, обикновен HTTP е тривиална бъркотия, докато HTTPS за да се счупи изисква усилия, които само държави или сложни престъпни организации могат да направят.

Резюме

В тази глава видяхме, че HTTP е протокол за достъп до ресурси в Интернет. Клиентът изпраща искане, което съдържа метод (обикновено GET) и път, който индентифицира ресурс. Сървърът решава, какво да прави с искането и реагира със статус код и отговор с тяло. Исканията и отговорите могат да съдържат заглавия, които предоставят допълнителна информация.

Браузърите правят GET искания за получаване на ресурсите необходими за показването на уеб страницата. Уеб страницата може да съдържа форми, които позволяват информация въведена от потребителя да бъде изпратена заедно с искането отправено в момента на подаване на формуляра. Ще научите за това повече в следващата глава.

Интерфейса, чрез който браузъра на JavaScript може да направи HTTP искане се нарича XMLHttpRequest. Обикновено можем да игнорираме XML частта на това име (но то все още трябва да се пише). Има два начина, по които може да се използва - синхронен, който блокира всичко докато искането завърши и асинхронен, който изисква боравене със събития за да разберем, че отговорът е дошъл. В почти всички случаи асинхронния начин е за предпочитане. Осъществяването на искане изглежда така:

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", true);
req.addEventListener("load", function() {
  console.log(req.status);
});
req.send(null);

Асинхронното програмиране е сложно. Promises са един интерфейс, който го прави малко по-лесно, като помага маршрута на условията за грешки и изключения да мине през правилния манипулатор и абстрахира далеч на някои от най-повтарящите се грешки и предразположените към грешки елементи в този стил на програмиране.

Упражнения

Content преговори

Едно от нещата, които могат да направят HTTP, но не сме обсъдили в тази глава се нарича многоезично съдържание. Заглавието Accept за искане може да се използва за да каже на сървъра, какъв тип документ би искал да получи клиента. Много сървъри игнорират това заглавие, но когато сървърът знае различни начини за кодиране на ресурси, той може да погледне това заглавие и да изпрати този ресурс, който клиента предпочита.

URL eloquentjavascript.net/author e конфигуриран да отговори в прав текст, HTML или JSON в зависимост от това, което клиента иска. Тези формати са идентифицирани чрез стандартизирането на типовете входове text/plain, text/html и application/json.

Изпратете искания за получаване на трите формата на този ресурс. Използвайте метода setRequestHeader във вашия XMLHttpRequest обект, за да настроите заглавието Accept към един от видовете входове дадени по-рано. Уверете се, че сте задали заглавието след извикването на open , но преди да извикате send.

И накрая, опитайте да попитате приложението с application/rainbows+unicorns и вижте какво ще стане.

// Your code here.

Вижте различните примери за използване на XMLHttpRequest в тази глава, участващи в изграждането на искане. Можете да използвате синхронно искане (чрез определяне на трети параметър на open към false), ако искате.

Питането за фалшив тип носител ще върне отговор с код 406 "Не се приема.", което е кода на сървъра, който трябва да върне, когато не може да изпълни заглавието Accept.

Изчакване на множество promises

Конструктора Promise има метод all, който при даден масив от promises, връща promise, което изчаква всички promises в масива за да завършат. След като успее, дава масив със стойностите на резултатите. Ако някое promise в масива се провали, върнатото promise от all се проваля също (със стойността на неизпълнение на проваленото promise).

Опитайте да приложите нещо подобно, като ваша редовна функция, наречена all.

Имайте в предвид, че след като promise е решено (с успех или провал), то не може да бъде успешно и неуспешно отново и по-нататъшни разговори с функциите, които го решават се игнорират. Това може да опрости начина, по който да се справите с провал на promises.

function all(promises) {
  return new Promise(function(success, fail) {
    // Your code here.
  });
}

// Test code.
all([]).then(function(array) {
  console.log("This should be []:", array);
});
function soon(val) {
  return new Promise(function(success) {
    setTimeout(function() { success(val); },
               Math.random() * 500);
  });
}
all([soon(1), soon(2), soon(3)]).then(function(array) {
  console.log("This should be [1, 2, 3]:", array);
});
function fail() {
  return new Promise(function(success, fail) {
    fail(new Error("boom"));
  });
}
all([soon(1), fail(), soon(3)]).then(function(array) {
  console.log("We should not get here");
}, function(error) {
  if (error.message != "boom")
    console.log("Unexpected failure:", error);
});

Функцията подадена към Promise конструктора ще трябва да се извика then върху всички promises в дадения масив. Когато едно от тях успее, трябва да се случат две неща. Получената стойност трябва да се съхранява в правилната позиция в масива на резултата и ние трябва да проверим дали това е последното очаквано promise, за да завършим нашето собствено promise, ако е така.

Последното може да се направи с брояч, който се инициализира по дължината на входящия масив и от който се изважда едно всеки път, когато promise успява. Когато той достигне 0, ние сме се справили. Уверете се, че сте взели под внимание ситуация, в която входящия масив е празен (и по този начин, promise никога няма да се разреши).

Обработката на провала изисква малко размисъл, но се оказва изключително проста. Просто подайте функцията на провала към promise опаковащо всички promises в масива, така че провала на едно от тях води до провал на цялата обвивка.