Примечание: Это статья — конспект моей лекции. В ней описываются ключевые понятия.
Обновление: В примерах ниже обновились url, в результате запросы по http не работают. Поэтому я внес небольшие корректировки и при получении url картинок заменяю http на https. Работающий код можно посмотреть тут https://codepen.io/denisfl/pen/xxeQzpp.
Чтобы взаимодействовать с сервером (запрашивать или отправлять данные), нам нужны методы. Каждый запрос, который мы отправляем на сервер, включает в себя endpoint и тип отправляемого запроса. Endpoint — это, своего рода, шлюз между клиентом и сервером. В зависимости от валидности запроса сервер отправляет ответ. Если запрос успешен, сервер возвращает данные, например, в формате JSON. В случае ошибки, сервер возвращает сообщение об ошибки. Ответы сервера обычно сопровождаются кодами состояния (status codes), которые помогают понять, что сервер пытается сказать при получении запроса.
Например: 200–299 — успешный запрос, 400–499 — ошибка клиента, 500–599 — ошибка сервера. Вот тут можно посмотреть полный список кодов: HTTP response status codes.
Методы для отправки HTTP-запросов
GET
. С помощью него клиент запрашивает у сервера содержимое ресурса.
HEAD
. Это метод для получения заголовков ресурса. Обычно применяется для получения метаданных и проверки менялся ли ресурс с момента последнего посещения и существует ли он.
POST
. Через этот метод клиент может передать данные в теле сообщения. Это могут быть какие-то данные с заполненной формы. POST
является неидемпотентным методом, то есть при его отправке результат может отличаться. Кроме того, ответы на него не будут кэшироваться.
OPTIONS
. С помощью него можно запросить список методов, которые он или его ресурс поддерживает. Также OPTIONS
можно использовать для того, чтобы «пропинговать» сервер — протестировать его работоспособность.
PUT
. Метод создаёт новый ресурс или заменяет существующий данными, которые указаны в теле запроса.
PATCH
. Работает таким же образом, как и PUT
, только по отношению к части ресурса.
DELETE
. Клиент сообщает о том, что хотел бы удалить некий ресурс.
TRACE
. С помощью него можно проверить, изменяют ли промежуточные узлы в сети запрос клиента.
CONNECT
. Запускает туннель между клиентом и сервером.
Как запросить данные
XMLHttpRequest. Это довольно старый метод, тем не менее он все еще используется.
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
Axios
. Это клиент созданный на Промисах, был создан для удобства работы с HTTP-запросами.
axios.get('https://api.example.com/data')
.then(response => console.log(response.data))
.catch(error => console.error('Ошибка:', error));
Fetch API. Это функция JavaScript, которую можно использовать для отправки запроса на любой URL-адрес веб-API и получения ответа.
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Ошибка:', error));
Fetch и Axios очень похожи по функциональности. Axios создавался в тот момент, когда Fetch API не поддерживалось в разных браузерах и нужно было обеспечить совместимость с Internet Explorer. Сейчас можно использовать и то и другое, в зависимости от того, с чем комфортнее работать.
Запросили, получили, показали
Обычно если мы что-то запросили с сервера через GET, мы хотим это показать. В самом простом случае, мы вставляем полученные данные в DOM. Тут мы не говорим о современных решениях для работы с Virtual DOM.
Возьмем данные с http://pets-v2.dev-apis.com/pets и отрендерим их на страницы.
Для запроса используем fetch api:
async function fetchData() {
try {
const response = await fetch('http://pets-v2.dev-apis.com/pets');
if (!response.ok) {
throw new Error('Ошибка при загрузке данных');
}
const data = await response.json();
// Допустим, у вас есть элемент в DOM для вставки данных
const container = document.getElementById('pets-container');
// Очищаем контейнер перед вставкой новых данных
container.innerHTML = '';
// Вставляем данные в DOM
data.forEach(pet => {
const petElement = document.createElement('div');
petElement.textContent = `Имя: ${pet.name}, Вид: ${pet.animal}`;
container.appendChild(petElement);
});
} catch (error) {
console.error('Произошла ошибка:', error);
}
}
// Вызываем функцию для загрузки данных
fetchData();
И применим этот код к html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pets Data</title>
<style>
.pet {
margin-bottom: 20px;
border: 1px solid #ccc;
padding: 10px;
}
.pet img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<div id="loading-message">Загрузка данных...</div>
<div id="pets-container"></div>
<script>
async function fetchData() {
const loadingMessage = document.getElementById('loading-message');
const container = document.getElementById('pets-container');
try {
const response = await fetch('http://pets-v2.dev-apis.com/pets');
if (!response.ok) {
throw new Error('Ошибка при загрузке данных');
}
const data = await response.json();
loadingMessage.style.display = 'none'; // Скрываем сообщение о загрузке
// Очищаем контейнер перед вставкой новых данных
container.innerHTML = '';
// Вставляем данные каждого питомца в DOM
data.forEach(pet => {
const petElement = document.createElement('div');
petElement.classList.add('pet');
// Вставляем изображения питомца
pet.images.forEach(imageUrl => {
const img = document.createElement('img');
img.src = imageUrl;
petElement.appendChild(img);
});
// Вставляем информацию о питомце
const info = document.createElement('div');
info.innerHTML = `
<p><strong>Имя:</strong> ${pet.name}</p>
<p><strong>Вид:</strong> ${pet.animal}</p>
<p><strong>Город:</strong> ${pet.city}</p>
<p><strong>Штат:</strong> ${pet.state}</p>
<p><strong>Описание:</strong> ${pet.description}</p>
<p><strong>Порода:</strong> ${pet.breed}</p>
`;
petElement.appendChild(info);
container.appendChild(petElement);
});
} catch (error) {
console.error('Произошла ошибка:', error);
loadingMessage.textContent = 'Ошибка загрузки данных';
}
}
// Вызываем функцию для загрузки данных
fetchData();
</script>
</body>
</html>
В JavaScript функции являются объектами, и они могут иметь доступ к своему лексическому окружению, включая переменные и другие функции, определенные в том же контексте. В нашем случае, функция fetchData – это асинхронная функция, которая создает свое собственное лексическое окружение, когда она вызывается. Это лексическое окружение включает в себя все переменные и функции, определенные внутри нее, а также доступ к внешнему контексту, где она была объявлена.
Внутри функции fetchData мы обращаемся к внешним переменным, таким как loadingMessage и container. Эти переменные были определены в том же контексте, где и функция fetchData, и, следовательно, они доступны ей как внешние переменные.
Функция fetchData использует доступ к DOM-элементам loadingMessage и container, чтобы изменять их содержимое и стили. Это возможно благодаря замыканию, которое сохраняет ссылку на лексическое окружение, в котором была определена функция fetchData.
Внутри цикла forEach создается замыкание для каждой итерации. Это происходит потому, что каждый раз, когда вызывается метод appendChild() или устанавливается свойство innerHTML, создается новая функция, которая сохраняет ссылку на лексическое окружение функции fetchData.
Таким образом, замыкания в JavaScript позволяют функциям сохранять доступ к внешним переменным и контексту, в котором они были определены, даже когда эти функции вызываются в других контекстах или в асинхронном коде.
В примере нижу функция fetchData и все переменные, которые она использует, организованы внутри объекта fetchDataObject. Все внутренние переменные функции, такие как loadingMessage и container, теперь доступны через ключевое слово this. Таким образом, объект fetchDataObject является замыканием, которое сохраняет доступ к своему лексическому окружению, включая переменные и методы.
const fetchDataObject = {
loadingMessage: document.getElementById('loading-message'),
container: document.getElementById('pets-container'),
fetchData: async function() {
const _self = this; // Сохраняем ссылку на текущий объект в переменной _self
try {
const response = await fetch('http://pets-v2.dev-apis.com/pets');
if (!response.ok) {
throw new Error('Ошибка при загрузке данных');
}
const data = await response.json();
_self.loadingMessage.style.display = 'none'; // Используем _self для доступа к свойству loadingMessage
_self.container.innerHTML = ''; // Используем _self для доступа к свойству container
data.forEach(pet => {
const petElement = document.createElement('div');
petElement.classList.add('pet');
pet.images.forEach(imageUrl => {
const img = document.createElement('img');
img.src = imageUrl;
petElement.appendChild(img);
});
const info = document.createElement('div');
info.innerHTML = `
<p><strong>Имя:</strong> ${pet.name}</p>
<p><strong>Вид:</strong> ${pet.animal}</p>
<p><strong>Город:</strong> ${pet.city}</p>
<p><strong>Штат:</strong> ${pet.state}</p>
<p><strong>Описание:</strong> ${pet.description}</p>
<p><strong>Порода:</strong> ${pet.breed}</p>
`;
petElement.appendChild(info);
_self.container.appendChild(petElement);
});
} catch (error) {
console.error('Произошла ошибка:', error);
_self.loadingMessage.textContent = 'Ошибка загрузки данных'; // Используем _self для доступа к свойству loadingMessage
}
}
};
fetchDataObject.fetchData();
// Вызываем метод fetchData объекта fetchDataObject для загрузки данных
fetchDataObject.fetchData();
В примере с объектом fetchDataObject, мы используем this внутри объекта для доступа к его свойствам (this.loadingMessage, this.container) и методам. Здесь this указывает на сам объект fetchDataObject, поэтому мы можем обращаться к его свойствам и методам через this.
В то же время, в асинхронной функции fetchData, когда мы создаем переменные loadingMessage и container, мы получаем доступ к этим переменным, используя this, потому что в момент выполнения асинхронной функции контекст может измениться и this может потерять своё значение. Таким образом, мы сохраняем доступ к нужным нам переменным, используя их как свойства объекта this.
А теперь перепишем объект fetchDataObject, используя стрелочные функции.
const fetchDataObject = {
loadingMessage: document.getElementById('loading-message'),
container: document.getElementById('pets-container'),
fetchData: async () => {
const { loadingMessage, container } = fetchDataObject;
try {
const response = await fetch('http://pets-v2.dev-apis.com/pets');
if (!response.ok) {
throw new Error('Ошибка при загрузке данных');
}
const data = await response.json();
loadingMessage.style.display = 'none';
container.innerHTML = '';
data.forEach(pet => {
const petElement = document.createElement('div');
petElement.classList.add('pet');
pet.images.forEach(imageUrl => {
const img = document.createElement('img');
img.src = imageUrl;
petElement.appendChild(img);
});
const info = document.createElement('div');
info.innerHTML = `
<p><strong>Имя:</strong> ${pet.name}</p>
<p><strong>Вид:</strong> ${pet.animal}</p>
<p><strong>Город:</strong> ${pet.city}</p>
<p><strong>Штат:</strong> ${pet.state}</p>
<p><strong>Описание:</strong> ${pet.description}</p>
<p><strong>Порода:</strong> ${pet.breed}</p>
`;
petElement.appendChild(info);
container.appendChild(petElement);
});
} catch (error) {
console.error('Произошла ошибка:', error);
loadingMessage.textContent = 'Ошибка загрузки данных';
}
}
};
fetchDataObject.fetchData();
Теперь функция fetchData
не создает собственный контекст this
, а вместо этого использует контекст объекта fetchDataObject
. Мы также использовали деструктуризацию объекта внутри функции для получения доступа к свойствам loadingMessage
и container
.