request(options)
Описание
Выполняет XHR (aka AJAX) запросы и возвращает промис.
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
Сигнатура
promise = m.request(options)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
options | Object | Да | Объект с параметрами запроса. |
options.method | String | Нет | HTTP-метод для использования. Допустимые значения: GET , POST , PUT , PATCH , DELETE , HEAD или OPTIONS . По умолчанию используется GET . |
options.url | String | Да | Имя пути для отправки запроса, опционально интерполированное значениями из options.params . |
options.params | Object | Нет | Данные для интерполяции в URL и/или сериализации в строку запроса (query string). |
options.body | Object | Нет | Данные для сериализации в тело запроса (для методов, отличных от GET и HEAD ). |
options.async | Boolean | Нет | Определяет, должен ли запрос быть асинхронным. По умолчанию true . |
options.user | String | Нет | Имя пользователя для HTTP-авторизации. По умолчанию undefined . |
options.password | String | Нет | Пароль для HTTP-авторизации. По умолчанию undefined . Эта опция предоставлена для совместимости с XMLHttpRequest , но её использование не рекомендуется, так как она отправляет пароль в виде открытого текста по сети. |
options.withCredentials | Boolean | Нет | Определяет, следует ли отправлять куки в сторонние домены. По умолчанию false . |
options.timeout | Number | Нет | Количество миллисекунд, которое может занять запрос, прежде чем он будет автоматически прерван. По умолчанию undefined . |
options.responseType | String | Нет | Ожидаемый тип ответа. По умолчанию "" , если определен extract , "json" , если не определен. Если responseType: "json" , выполняется JSON.parse(responseText) . |
options.config | xhr = Function(xhr) | Нет | Предоставляет базовый объект XMLHttpRequest для низкоуровневой конфигурации и опциональной замены (путем возврата нового XHR). |
options.headers | Object | Нет | Заголовки, добавляемые к запросу перед отправкой (применяются непосредственно перед options.config ). |
options.type | any = Function(any) | Нет | Конструктор, который будет применен к каждому объекту в ответе. По умолчанию - функция идентичности. |
options.serialize | string = Function(any) | Нет | Метод сериализации, который будет применен к body . По умолчанию JSON.stringify , или, если options.body является экземпляром FormData или URLSearchParams , - функция идентичности (т.е. function(value) {return value} ). |
options.deserialize | any = Function(any) | Нет | Метод десериализации, который будет применен к xhr.response или нормализованному xhr.responseText . По умолчанию - функция идентичности. Если определен extract , deserialize будет пропущен. |
options.extract | any = Function(xhr, options) | Нет | Хук для указания способа чтения ответа XMLHttpRequest. Полезен для обработки данных ответа, чтения заголовков и куки. По умолчанию это функция, которая возвращает options.deserialize(parsedResponse) , вызывая исключение, когда код состояния ответа сервера указывает на ошибку или когда ответ синтаксически недействителен. Если предоставлен пользовательский колбэк extract , параметр xhr является экземпляром XMLHttpRequest, используемым для запроса, а options - это объект, переданный в вызов m.request . Кроме того, deserialize будет пропущен, и значение, возвращаемое из callback extract , будет использовано как есть при разрешении промиса. |
options.background | Boolean | Нет | Если false , перерисовывает смонтированные компоненты по завершении запроса. Если true , не перерисовывает. По умолчанию false . |
returns | Promise | Promise, который разрешается в данные ответа после того, как они были обработаны методами extract , deserialize и type . Если код состояния ответа указывает на ошибку, promise отклоняется, но этого можно избежать, установив опцию extract . |
promise = m.request(url, options)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
url | String | Да | Имя пути для отправки запроса. options.url переопределяет это значение, если присутствует. |
options | Object | Нет | Объект с параметрами запроса. |
returns | Promise | Promise, который разрешается в данные ответа после того, как они были обработаны методами extract , deserialize и type . |
Эта вторая форма в основном эквивалентна m.request(Object.assign({url: url}, options))
, просто она не зависит от глобального ES6 Object.assign
внутри.
Как это работает
Утилита m.request
— это тонкая обертка вокруг XMLHttpRequest
, позволяющая выполнять HTTP-запросы к удаленным серверам для сохранения и/или получения данных из базы данных.
m.request({
method: 'GET',
url: '/api/v1/users',
}).then(function (users) {
console.log(users);
});
Вызов m.request
возвращает promise и запускает перерисовку по завершении цепочки promise.
По умолчанию m.request
предполагает, что ответ находится в формате JSON, и преобразует его в объект JavaScript (или массив).
Если код состояния HTTP-ответа указывает на ошибку, возвращенный Promise будет отклонен. Предоставление callback extract
предотвратит отклонение promise.
Типичное использование
Вот иллюстративный пример компонента, который использует m.request
для получения данных с сервера.
var Data = {
todos: {
list: [],
fetch: function () {
m.request({
method: 'GET',
url: '/api/v1/todos',
}).then(function (items) {
Data.todos.list = items;
});
},
},
};
var Todos = {
oninit: Data.todos.fetch,
view: function (vnode) {
return Data.todos.list.map(function (item) {
return m('div', item.title);
});
},
};
m.route(document.body, '/', {
'/': Todos,
});
Предположим, что запрос к серверу по URL /api/items
возвращает массив объектов в формате JSON.
Когда m.route
вызывается внизу, компонент Todos
инициализируется. Вызывается oninit
, который вызывает m.request
. Это позволяет асинхронно получить массив объектов с сервера. "Асинхронно" означает, что JavaScript продолжает выполнять другой код, пока он ожидает ответа от сервера. В этом случае это означает, что fetch
возвращается, и компонент отображается с использованием исходного пустого массива в качестве Data.todos.list
. После завершения запроса к серверу массив объектов items
присваивается Data.todos.list
, и компонент перерисовывается, отображая список <div>
с заголовками каждого todo
.
Обработка ошибок
Когда запрос (не file:
) возвращается с любым статусом, отличным от 2xx или 304, он отклоняется с ошибкой. Это обычный экземпляр ошибки Error, но с несколькими специальными свойствами.
error.message
устанавливается в необработанный текст ответа.error.code
устанавливается в сам код состояния.error.response
устанавливается в проанализированный ответ с использованиемoptions.extract
иoptions.deserialize
, как это делается с обычными ответами.
Это полезно во многих случаях, когда ошибки сами по себе являются данными, которые вы можете использовать. Если вы хотите определить, истек ли срок действия сеанса, вы можете сделать if (error.code === 401) return promptForAuth().then(retry)
. Если вы столкнулись с механизмом регулирования API, и он вернул ошибку с "timeout": 1000
, вы можете сделать setTimeout(retry, error.response.timeout)
.
Индикаторы загрузки и сообщения об ошибках
Вот расширенная версия приведенного выше примера, которая реализует индикатор загрузки и сообщение об ошибке:
var Data = {
todos: {
list: null,
error: '',
fetch: function () {
m.request({
method: 'GET',
url: '/api/v1/todos',
})
.then(function (items) {
Data.todos.list = items;
})
.catch(function (e) {
Data.todos.error = e.message;
});
},
},
};
var Todos = {
oninit: Data.todos.fetch,
view: function (vnode) {
return Data.todos.error
? [m('.error', Data.todos.error)]
: Data.todos.list
? [
Data.todos.list.map(function (item) {
return m('div', item.title);
}),
]
: m('.loading-icon');
},
};
m.route(document.body, '/', {
'/': Todos,
});
Есть несколько различий между этим примером и предыдущим. Здесь Data.todos.list
изначально имеет значение null
. Кроме того, есть дополнительное поле error
для хранения сообщения об ошибке, и представление компонента Todos
изменено для отображения сообщения об ошибке (если оно есть) или значка загрузки (если Data.todos.list
не является массивом).
Динамические URL
URL запросов могут содержать интерполяции:
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id); // logs 123
});
В приведенном выше коде :id
заполняется данными из объекта params
, и запрос становится GET /api/v1/users/123
.
Интерполяции игнорируются, если в свойстве params
нет соответствующих данных.
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
В приведенном выше коде запрос становится GET /api/v1/users/foo:bar?id=123
Прерывание запросов
Иногда желательно прервать запрос. Например, в виджете автозаполнения/typeahead вы хотите убедиться, что завершается только последний запрос, потому что обычно автозаполнители запускают несколько запросов по мере ввода пользователем, и HTTP-запросы могут завершаться не по порядку из-за непредсказуемой природы сетей. Если другой запрос завершается после последнего запущенного запроса, виджет будет отображать менее релевантные (или потенциально неправильные) данные, чем если бы последний запущенный запрос завершился последним.
m.request()
предоставляет свой базовый объект XMLHttpRequest
через параметр options.config
, который позволяет вам сохранить ссылку на этот объект и вызвать его метод abort
при необходимости:
var searchXHR = null;
function search() {
abortPreviousSearch();
m.request({
method: 'GET',
url: '/api/v1/users',
params: { search: query },
config: function (xhr) {
searchXHR = xhr;
},
});
}
function abortPreviousSearch() {
if (searchXHR !== null) searchXHR.abort();
searchXHR = null;
}
Загрузка файлов
Чтобы загрузить файлы, сначала вам нужно получить ссылку на объект File
. Самый простой способ сделать это - из <input type="file">
.
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
Приведенный выше фрагмент отображает поле выбора файла. Если пользователь выбирает файл, срабатывает событие onchange
, которое вызывает функцию upload
. e.target.files
- это список объектов File
.
Далее вам нужно создать объект FormData
для создания многокомпонентного запроса, который является специально отформатированным HTTP-запросом, который может отправлять данные файла в теле запроса.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
Далее вам нужно вызвать m.request
и установить для options.method
HTTP-метод, который использует тело (например, POST
, PUT
, PATCH
), и использовать объект FormData
в качестве options.body
.
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
Предполагая, что сервер настроен на прием многокомпонентных запросов, информация о файле будет связана с ключом myfile
.
Загрузка нескольких файлов
Можно загрузить несколько файлов в одном запросе. Это сделает пакетную загрузку атомарной, т.е. никакие файлы не будут обработаны, если во время загрузки произойдет ошибка, поэтому невозможно сохранить только часть файлов. Если вы хотите сохранить как можно больше файлов в случае сбоя сети, вам следует рассмотреть возможность загрузки каждого файла в отдельном запросе.
Чтобы загрузить несколько файлов, просто добавьте их все в объект FormData
. При использовании поля выбора файла вы можете получить список файлов, добавив атрибут multiple
к полю:
m.render(document.body, [
m('input[type=file][multiple]', { onchange: upload }),
]);
function upload(e) {
var files = e.target.files;
var body = new FormData();
for (var i = 0; i < files.length; i++) {
body.append('file' + i, files[i]);
}
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
Мониторинг прогресса
Иногда, если запрос по своей сути медленный (например, загрузка большого файла), желательно отображать индикатор прогресса для пользователя, чтобы сигнализировать о том, что приложение все еще работает.
m.request()
предоставляет свой базовый объект XMLHttpRequest
через параметр options.config
, который позволяет вам прикреплять прослушиватели событий к объекту XMLHttpRequest:
var progress = 0;
m.mount(document.body, {
view: function () {
return [
m('input[type=file]', { onchange: upload }),
progress + '% completed',
];
},
});
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
config: function (xhr) {
xhr.upload.addEventListener('progress', function (e) {
progress = e.loaded / e.total;
m.redraw(); // сообщить Mithril.js, что данные изменились и требуется повторная отрисовка
});
},
});
}
В приведенном выше примере отображается поле выбора файла. Если пользователь выбирает файл, инициируется загрузка, и в callback config
регистрируется обработчик событий progress
. Этот обработчик событий срабатывает всякий раз, когда происходит обновление прогресса в XMLHttpRequest. Поскольку событие прогресса XMLHttpRequest не обрабатывается напрямую движком виртуального DOM Mithril.js, необходимо вызвать m.redraw()
, чтобы сигнализировать Mithril.js о том, что данные изменились и требуется перерисовка.
Приведение ответа к типу
В зависимости от общей архитектуры приложения может быть желательно преобразовать данные ответа запроса в определенный класс или тип (например, для единообразной обработки полей даты или для наличия вспомогательных методов).
Вы можете передать конструктор в качестве параметра options.type
, и Mithril.js будет создавать его экземпляр для каждого объекта в HTTP-ответе.
function User(data) {
this.name = data.firstName + ' ' + data.lastName;
}
m.request({
method: 'GET',
url: '/api/v1/users',
type: User,
}).then(function (users) {
console.log(users[0].name); // logs a name
});
В приведенном выше примере, предполагая, что /api/v1/users
возвращает массив объектов, конструктор User
будет создан (т.е. вызван как new User(data)
) для каждого объекта в массиве. Если ответ вернул один объект, этот объект будет использоваться в качестве аргумента body
.
Ответы, отличные от JSON
Иногда конечная точка сервера не возвращает ответ JSON: например, вы можете запросить файл HTML, файл SVG или файл CSV. По умолчанию Mithril.js пытается проанализировать ответ, как если бы это был JSON. Чтобы переопределить это поведение, определите пользовательскую функцию options.deserialize
:
m.request({
method: 'GET',
url: '/files/icon.svg',
deserialize: function (value) {
return value;
},
}).then(function (svg) {
m.render(document.body, m.trust(svg));
});
В приведенном выше примере запрос извлекает файл SVG, ничего не делает для его анализа (потому что deserialize
просто возвращает значение как есть), а затем отображает строку SVG как доверенный HTML.
Конечно, функция deserialize
может быть более сложной:
m.request({
method: 'GET',
url: '/files/data.csv',
deserialize: parseCSV,
}).then(function (data) {
console.log(data);
});
function parseCSV(data) {
// наивная реализация для простоты примера
return data.split('\n').map(function (row) {
return row.split(',');
});
}
Игнорируя тот факт, что приведенная выше функция parseCSV не обрабатывает множество случаев, которые обрабатывал бы правильный анализатор CSV, приведенный выше код регистрирует массив массивов.
Пользовательские заголовки также могут быть полезны в этом отношении. Например, если вы запрашиваете SVG, вы, вероятно, захотите установить соответствующий тип контента. Чтобы переопределить тип запроса JSON по умолчанию, установите для options.headers
объект пар ключ-значение, соответствующих именам и значениям заголовков запроса.
m.request({
method: 'GET',
url: '/files/image.svg',
headers: {
'Content-Type': 'image/svg+xml; charset=utf-8',
Accept: 'image/svg, text/*',
},
deserialize: function (value) {
return value;
},
});
Получение деталей ответа
По умолчанию Mithril.js пытается проанализировать xhr.responseText
как JSON и возвращает проанализированный объект. Может быть полезно более подробно изучить ответ сервера и обработать его вручную. Это можно сделать, передав пользовательскую функцию options.extract
:
m.request({
method: 'GET',
url: '/api/v1/users',
extract: function (xhr) {
return { status: xhr.status, body: xhr.responseText };
},
}).then(function (response) {
console.log(response.status, response.body);
});
Параметром для options.extract
является объект XMLHttpRequest после завершения его операции, но до того, как он был передан в возвращенную цепочку promise, поэтому promise все еще может оказаться в отклоненном состоянии, если обработка вызовет исключение.
Отправка запросов к IP-адресам
Из-за (очень упрощенного) способа обнаружения параметров в URL, сегменты адреса IPv6 ошибочно принимаются за интерполяции параметров пути, и поскольку параметрам пути нужно что-то, чтобы отделить их для правильной интерполяции, это приводит к возникновению ошибки.
// Это не работает
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
Чтобы обойти это, вы должны передать пару адрес IPv6 + порт в качестве параметра.
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
С адресами IPv4 этой проблемы нет, и вы можете использовать их как обычно.
// Это будет работать так, как вы ожидаете
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
Почему JSON вместо HTML
Многие серверные фреймворки предоставляют движок представлений, который интерполирует данные базы данных в шаблон перед обслуживанием HTML (при загрузке страницы или через AJAX), а затем используют jQuery для обработки взаимодействия с пользователем.
В отличие от этого, Mithril.js - это фреймворк, разработанный для приложений с толстым клиентом, которые обычно загружают шаблоны и данные отдельно и объединяют их в браузере с помощью JavaScript. Выполнение тяжелой работы по созданию шаблонов в браузере может принести такие преимущества, как снижение эксплуатационных расходов за счет освобождения серверных ресурсов. Отделение шаблонов от данных также позволяет более эффективно кэшировать код шаблона и обеспечивает лучшую повторное использование кода в различных типах клиентов (например, настольных, мобильных). Еще одно преимущество заключается в том, что Mithril.js обеспечивает парадигму разработки пользовательского интерфейса удержанного режима, что значительно упрощает разработку и обслуживание сложных взаимодействий с пользователем.
По умолчанию m.request
ожидает, что данные ответа будут в формате JSON. В типичном приложении Mithril.js эти данные JSON обычно используются представлением.
Следует избегать попыток отображения динамического HTML, сгенерированного сервером, с помощью Mithril. Если у вас есть существующее приложение, которое использует систему шаблонов на стороне сервера, и вы хотите изменить его архитектуру, сначала решите, возможно ли это вообще. Переход от архитектуры толстого сервера к архитектуре толстого клиента обычно требует значительных усилий и включает в себя рефакторинг логики из шаблонов в логические службы данных (и тестирование, которое с этим связано).
Службы данных могут быть организованы по-разному в зависимости от характера приложения. RESTful архитектуры популярны среди поставщиков API, а сервис-ориентированные архитектуры часто требуются там, где существует множество высокотранзакционных рабочих процессов.
Почему XHR вместо fetch
fetch()
- это более новый Web API для получения ресурсов с серверов, аналогичный XMLHttpRequest
.
m.request
Mithril.js использует XMLHttpRequest
вместо fetch()
по ряду причин:
fetch
еще не полностью стандартизирован и может быть подвержен изменениям спецификации.- Вызовы
XMLHttpRequest
можно прервать до их разрешения (например, чтобы избежать гонок данных в пользовательских интерфейсах мгновенного поиска). XMLHttpRequest
предоставляет хуки для прослушивателей прогресса для длительных запросов (например, загрузка файлов).XMLHttpRequest
поддерживается всеми браузерами, тогда какfetch()
не поддерживается Internet Explorer и более старыми версиями Android (до 5.0 Lollipop).
В настоящее время из-за отсутствия поддержки браузерами fetch()
обычно требует polyfill, который имеет размер более 11 КБ в несжатом виде - почти в три раза больше, чем модуль XHR Mithril.js.
Несмотря на то, что модуль XHR Mithril.js намного меньше, он поддерживает множество важных и нетривиальных в реализации функций, таких как интерполяция URL и сериализация строки запроса, в дополнение к своей способности беспрепятственно интегрироваться с подсистемой автоматической перерисовки Mithril.js. Polyfill fetch
не поддерживает ни одну из этих функций и требует дополнительных библиотек и шаблонов для достижения того же уровня функциональности.
Кроме того, модуль XHR Mithril.js оптимизирован для конечных точек на основе JSON и делает этот наиболее распространенный случай подходящим образом кратким - т.е. m.request(url)
- тогда как fetch
требует дополнительного явного шага для анализа данных ответа как JSON: fetch(url).then(function(response) {return response.json()})
API fetch()
имеет несколько технических преимуществ перед XMLHttpRequest
в нескольких необычных случаях:
- он предоставляет потоковый API (в смысле "потоковой передачи видео", а не в смысле реактивного программирования), который обеспечивает лучшую задержку и потребление памяти для очень больших ответов (за счет сложности кода).
- он интегрируется с Service Worker API, который обеспечивает дополнительный уровень контроля над тем, как и когда происходят сетевые запросы. Этот API также предоставляет доступ к push-уведомлениям и функциям фоновой синхронизации.
В типичных сценариях потоковая передача не обеспечит заметных преимуществ в производительности, потому что, как правило, не рекомендуется загружать мегабайты данных. Кроме того, выигрыш в памяти от многократного повторного использования небольших буферов может быть компенсирован или аннулирован, если они приведут к чрезмерным перерисовкам браузера. По этим причинам выбор потоковой передачи fetch()
вместо m.request
рекомендуется только для приложений, требующих чрезвычайно больших ресурсов.
Избегайте анти-паттернов
Promises - это не данные ответа
Метод m.request
возвращает Promise
, а не сами данные ответа. Он не может вернуть эти данные напрямую, потому что HTTP-запрос может занять много времени (из-за задержки сети), и если бы JavaScript ждал его, он бы заморозил приложение до тех пор, пока данные не станут доступны.
// ИЗБЕГАТЬ
var users = m.request('/api/v1/users');
console.log('list of users:', users);
// `users` - это НЕ список пользователей, это промис
// Предпочтительный вариант
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});