refactor 3-more into separate books

This commit is contained in:
Ilya Kantor 2015-02-27 13:21:58 +03:00
parent bd1d5e4305
commit 87639b2740
423 changed files with 9 additions and 9 deletions

View file

@ -0,0 +1,153 @@
# Протокол JSONP
Если создать тег `<script src>`, то при добавлении в документ запустится процесс загрузки `src`. В ответ сервер может прислать скрипт, содержащий нужные данные.
Таким образом можно запрашивать данные с любого сервера, в любом браузере, без каких-либо разрешений и дополнительных проверок.
Протокол JSONP -- это "надстройка" над таким способом коммуникации. Здесь мы рассмотрим его использование в деталях.
[cut]
## Запрос
Простейший пример запроса:
```js
function addScript(src) {
var elem = document.createElement("script");
elem.src = src;
document.head.appendChild(elem);
}
addScript('user?id=123');
```
Такой вызов добавит в `HEAD` документа тег:
```js
<script src="/user?id=123"></script>
```
Браузер тут же обработает его: запросит `/user?id=123` с заданного URL и выполнит.
## Обработка ответа, JSONP
В примере выше рассмотрено создание запроса, но как получить ответ? Допустим, сервер хочет прислать объект с данными.
Конечно, он может присвоить её в переменную, например так:
```js
// ответ сервера
var user = {name: "Вася", age: 25 };
```
...А браузер по `script.onload` отловит окончание загрузки и прочитает значение `user`.
Но что, если одновременно делается несколько запросов? Получается, нужно присваивать в разные переменные.
Протокол JSONP как раз и призван облегчить эту задачу.
Он очень простой:
<ol>
<li>Вместе с запросом клиент в специальном, заранее оговорённом, параметре передаёт название функции.
Обычно такой параметр называется `callback`. Например :
```js
addScript('user?id=123&*!*callback=onUserData*/!*');
```
</li>
<li>Cервер кодирует данные в JSON и оборачивает их в функцию, название которой получает из параметра `callback`:
```js
// ответ сервера
onUserData({
name: "Вася",
age: 25
});
```
</li>
</ol>
Это и называется JSONP ("JSON with Padding").
[warn header="Аспект безопасности"]
Клиентский код должен доверять серверу при таком запросе. Ведь серверу ничего не стоит добавить в скрипт любые команды.
[/warn]
## Реестр CallbackRegistry
В примере выше функция `onUserData` должна быть глобальной, ведь `<script src>` выполняется в глобальной области видимости.
Хотелось бы не загрязнять глобальное пространство имён, или по крайней мере свести загрязнение к минимуму.
Как правило, для этого создают один глобальный объект "реестр", который мы назовём `CallbackRegistry`. Далее для каждого запроса в нём генерируется временная функция.
Тег будет выглядеть так:
```html
<script src="user?id=123&callback=*!*CallbackRegistry.func12345*/!*"></script>
```
Сервер обернёт ответ в функцию `CallbackRegistry.func12345`, она вызывает нужный обработчик и очищает память, удаляя себя.
Далее мы посмотрим более полный код всего этого, но перед этим -- важный момент! Нужно предусмотреть обработку ошибок.
## Обнаружение ошибок
При запросе данных при помощи `SCRIPT` возможны различные ошибки:
<ol>
<li>Скрипт может не загрузиться: отказ в соединении, разрыв связи...</li>
<li>Ошибка HTTP, например 500.</li>
<li>Скрипт загрузился, но внутри некорректен и не вызывает функцию. Например, на сервере произошла ошибка и в ответе передан её текст, а вовсе не данные.</li>
</ol>
Чтобы отловить их все "одним махом", используем следующий алгоритм:
<ol>
<li>Создаётся `<script>`.</li>
<li>На `<script>` ставятся обработчики `onreadystatechange` (для старых IE) и `onload/onerror` (для остальных браузеров).</li>
<li>При загрузке скрипт выполняет функцию-коллбэк `CallbackRegistry...`. Пусть она при запуске ставит флажок "все ок". А мы в обработчиках проверим -- если флага нет, то функция не вызывалась -- стало быть, ошибка при загрузке или содержимое скрипта некорректно.</li>
</ol>
## Полный пример
Итак, код функции, которая вызывается с `url` и коллбэками.
Он совсем небольшой, а без комментариев был бы ещё меньше:
```js
//+ src="jsonp/scriptRequest.js"
```
Пример использования:
```js
function ok(data) {
alert("Загружен пользователь " + data.name);
}
function fail(url) {
alert('Ошибка при запросе ' + url);
}
// Внимание! Ответы могут приходить в любой последовательности!
scriptRequest("user?id=123", ok, fail); // Загружен
scriptRequest("/badurl.js", ok, fail); // fail, 404
scriptRequest("/", ok, fail); // fail, 200 но некорректный скрипт
```
Демо, по нажатию на кнопке запускаются запросы выше:
[codetabs src="jsonp" height=100]
## COMET
COMET через `SCRIPT` реализуется при помощи длинных опросов, также как мы обсуждали в главе [](/xhr-longpoll).
То есть, создаётся тег `<script>`, браузер запрашивает скрипт у сервера и... Сервер оставляет соединение висеть, пока не появится, что сказать. Когда сервер хочет отправить сообщение -- он отвечает, используя формат JSONP. И, тут же, новый запрос...

View file

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="scriptRequest.js"></script>
</head>
<body>
<script>
function ok(data) {
alert("Загружен пользователь " + data.id + ": " + data.name);
}
function fail(url) {
alert('Ошибка при запросе ' + url);
}
function go() {
// ответы могут приходить в любой последовательности!
scriptRequest("user?id=123", ok, fail); // Загружен
scriptRequest("/badurl.js", ok, fail); // fail, 404
scriptRequest("index.html", ok, fail); // fail, 200 но некорректный скрипт
}
</script>
<button onclick='go()'>Сделать запросы</button>
</body>
</html>

View file

@ -0,0 +1,49 @@
var CallbackRegistry = {}; // реестр
// при успехе вызовет onSuccess, при ошибке onError
function scriptRequest(url, onSuccess, onError) {
var scriptOk = false; // флаг, что вызов прошел успешно
// сгенерировать имя JSONP-функции для запроса
var callbackName = 'cb' + String(Math.random()).slice(-6);
// укажем это имя в URL запроса
url += ~url.indexOf('?') ? '&' : '?';
url += 'callback=CallbackRegistry.'+callbackName;
// ..и создадим саму функцию в реестре
CallbackRegistry[callbackName] = function(data) {
scriptOk = true; // обработчик вызвался, указать что всё ок
delete CallbackRegistry[callbackName]; // можно очистить реестр
onSuccess(data); // и вызвать onSuccess
};
// эта функция сработает при любом результате запроса
// важно: при успешном результате - всегда после JSONP-обработчика
function checkCallback() {
if (scriptOk) return; // сработал обработчик?
delete CallbackRegistry[callbackName];
onError(url); // нет - вызвать onError
}
var script = document.createElement('script');
// в старых IE поддерживается только событие, а не onload/onerror
// в теории 'readyState=loaded' означает "скрипт загрузился",
// а 'readyState=complete' -- "скрипт выполнился", но иногда
// почему-то случается только одно из них, поэтому проверяем оба
script.onreadystatechange = function() {
if (this.readyState == 'complete' || this.readyState == 'loaded'){
this.onreadystatechange = null;
setTimeout(checkCallback, 0); // Вызвать checkCallback - после скрипта
}
}
// события script.onload/onerror срабатывают всегда после выполнения скрипта
script.onload = script.onerror = checkCallback;
script.src = url;
document.body.appendChild(script);
}

View file

@ -0,0 +1,38 @@
var http = require('http');
var url = require('url');
var static = require('node-static');
var file = new static.Server('.', { cache: 0 });
function accept(req, res) {
var urlParsed = url.parse(req.url, true);
if (urlParsed.pathname == '/user') {
var id = urlParsed.query.id;
var callback = urlParsed.query.callback;
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
var user = {
name: "Вася",
id: id
};
res.end(callback + '(' + JSON.stringify(user) + ')');
} else {
file.serve(req, res);
}
}
// ------ запустить сервер -------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}