Протокол JSONP

Если создать тег <script src>, то при добавлении в документ запустится процесс загрузки src. В ответ сервер может прислать скрипт, содержащий нужные данные.

Таким образом можно запрашивать данные с любого сервера, в любом браузере, без каких-либо разрешений и дополнительных проверок.

Протокол JSONP – это «надстройка» над таким способом коммуникации. Здесь мы рассмотрим его использование в деталях.

Запрос

Простейший пример запроса:

function addScript(src) {
  var elem = document.createElement("script");
  elem.src = src;
  document.head.appendChild(elem);
}

addScript('user?id=123');

Такой вызов добавит в <head> документа тег:

<script src="/user?id=123"></script>

При добавлении тега <script> с внешним src в документ браузер тут же начинает его скачивать, а затем – выполняет.

В данном случае браузер запросит скрипт с URL /user?id=123 и выполнит.

Обработка ответа, JSONP

В примере выше рассмотрено создание запроса, но как получить ответ? Допустим, сервер хочет прислать объект с данными.

Конечно, он может присвоить её в переменную, например так:

// ответ сервера
var user = {name: "Вася", age: 25 };

…А браузер по script.onload отловит окончание загрузки и прочитает значение user.

Но что, если одновременно делается несколько запросов? Получается, нужно присваивать в разные переменные.

Протокол JSONP как раз и призван облегчить эту задачу.

Он очень простой:

  1. Вместе с запросом клиент в специальном, заранее оговорённом, параметре передаёт название функции.

    Обычно такой параметр называется callback. Например :

    addScript('user?id=123&callback=onUserData');
  2. Сервер кодирует данные в JSON и оборачивает их в вызов функции, название которой получает из параметра callback:

    // ответ сервера
    onUserData({
      name: "Вася",
      age: 25
    });

Это и называется JSONP («JSON with Padding»).

Аспект безопасности

Клиентский код должен доверять серверу при таком запросе. Ведь серверу ничего не стоит добавить в скрипт любые команды.

Реестр CallbackRegistry

В примере выше функция onUserData должна быть глобальной, ведь <script src> выполняется в глобальной области видимости.

Хотелось бы не загрязнять глобальное пространство имён, или по крайней мере свести загрязнение к минимуму.

Как правило, для этого создают один глобальный объект «реестр», который мы назовём CallbackRegistry. Далее для каждого запроса в нём генерируется временная функция.

Тег будет выглядеть так:

<script src="user?id=123&callback=CallbackRegistry.func12345"></script>

Сервер обернёт ответ в функцию CallbackRegistry.func12345, она вызывает нужный обработчик и очищает память, удаляя себя.

Далее мы посмотрим более полный код всего этого, но перед этим – важный момент! Нужно предусмотреть обработку ошибок.

Обнаружение ошибок

При запросе данных при помощи SCRIPT возможны различные ошибки:

  1. Скрипт может не загрузиться: отказ в соединении, разрыв связи…
  2. Ошибка HTTP, например 500.
  3. Скрипт загрузился, но внутри некорректен и не вызывает функцию. Например, на сервере произошла ошибка и в ответе передан её текст, а вовсе не данные.

Чтобы отловить их все «одним махом», используем следующий алгоритм:

  1. Создаётся <script>.
  2. На <script> ставятся обработчики onreadystatechange (для старых IE) и onload/onerror (для остальных браузеров).
  3. При загрузке скрипт выполняет функцию-коллбэк CallbackRegistry.... Пусть она при запуске ставит флажок «все ок». А мы в обработчиках проверим – если флага нет, то функция не вызывалась – стало быть, ошибка при загрузке или содержимое скрипта некорректно.

Полный пример

Итак, код функции, которая вызывается с url и коллбэками.

Он совсем небольшой, а без комментариев был бы ещё меньше:

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);
}

Пример использования:

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 но некорректный скрипт

Демо, по нажатию на кнопке запускаются запросы выше:

Результат
scriptRequest.js
server.js
index.html
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);
}
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;
}
<!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>

COMET

COMET через SCRIPT реализуется при помощи длинных опросов, также как мы обсуждали в главе COMET с XMLHttpRequest: длинные опросы.

То есть, создаётся тег <script>, браузер запрашивает скрипт у сервера и… Сервер оставляет соединение висеть, пока не появится, что сказать. Когда сервер хочет отправить сообщение – он отвечает, используя формат JSONP. И, тут же, новый запрос…

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.