AJAX

Представляю Вам третью статью из серии jQuery для начинающих. В этот раз я постараюсь рассказать о реализации AJAX запросов...
Что такое AJAX я думаю рассказывать не стоит, ибо с приходом веб-два-нуля большинство пользователей уже воротят носом от перезагрузок страниц целиком, а с появлением jQuery реализация упростилась в разы...
Примечание: Во всех примерах используется сокращенный вариант вызова jQuery методов, используя функцию $(знак доллара)

jQuery(..).load

Начнем с самого простого — загрузка HTML кода в необходимый нам DOM элемент на странице. Для этой цели нам подойдет метод load. Данный метод может принимать следующие параметры:
  1. url запрашиваемой страницы
  2. передаваемые данные (необязательный параметр)
  3. функция которой будет скормлен результат (необязательный параметр)
Приведу пример JavaScript кода:
// по окончанию загрузки страницы
$(document).ready(function(){
// вешаем на клик по элементу с id = example-1
$('#example-1').click(function(){
// загрузку HTML кода из файла example.html
$(this).load('ajax/example.html');
})
});
Пример подгружаемых данных (содержимое файла example.html):
Example<br/>
Data Loaded By AJAX<br/>
Bye-Bye

jQuery.ajax

Это самый основной метод, а все последующие методы лишь обертки для метода jQuery.ajax. У данного метода лишь один входной параметр — объект включающий в себя все настройки (выделены параметры которые стоит запомнить):
  • async — асинхронность запроса, по умолчанию true
  • cache — вкл/выкл кэширование данных браузером, по умолчанию true
  • contentType — по умолчанию «application/x-www-form-urlencoded»
  • data— передаваемые данные — строка иль объект
  • dataFilter — фильтр для входных данных
  • dataType— тип данных возвращаемых в callback функцию (xml, html, script, json, text, _default)
  • global — тригер — отвечает за использование глобальных AJAX Event'ов, по умолчанию true
  • ifModified — тригер — проверяет были ли изменения в ответе сервера, дабы не слать еще запрос, по умолчанию false
  • jsonp — переустановить имя callback функции для работы с JSONP (по умолчанию генерируется на лету)
  • processData — по умолчанию отправляемые данный заворачиваются в объект, и отправляются как «application/x-www-form-urlencoded», если надо иначе — отключаем
  • scriptCharset — кодировочка — актуально для JSONP и подгрузки JavaScript'ов
  • timeout — время таймаут в миллисекундах
  • type— GET либо POST
  • url— url запрашиваемой страницы
Локальные AJAX Event'ы:
  • beforeSend — срабатывает перед отправкой запроса
  • error — если произошла ошибка
  • success — если ошибок не возникло
  • complete — срабатывает по окончанию запроса
Для организации HTTP авторизации (О_о):
  • username — логин
  • password — пароль
Пример javaScript'а:
$.ajax({
url: '/ajax/example.html', // указываем URL и
dataType : "json", // тип загружаемых данных
success: function (data, textStatus) { // вешаем свой обработчик на функцию success
$.each(data, function(i, val) { // обрабатываем полученные данные
/* ... */
});
}
});

jQuery.get

Загружает страницу, используя для передачи данных GET запрос. Может принимать следующие параметры:
  1. url запрашиваемой страницы
  2. передаваемые данные (необязательный параметр)
  3. callback функция, которой будет скормлен результат (необязательный параметр)
  4. тип данных возвращаемых в callback функцию (xml, html, script, json, text, _default)

jQuery.post

Данный метод аналогичен предыдущему, лишь передаваемые данные уйдут на сервер посредством POST'а. Может принимать следующие параметры:
  1. url запрашиваемой страницы
  2. передаваемые данные (необязательный параметр)
  3. callback функция, которой будет скормлен результат (необязательный параметр)
  4. тип данных возвращаемых в callback функцию (xml, html, script, json, text, _default)
JavaScript:
$(document).ready(function(){ // по завершению загрузки страницы
$('#example-3').click(function(){ // вешаем на клик по элементу с id = example-3
$.post('ajax/example.xml', {}, function(xml){ // загрузку XML из файла example.xml
$('#example-3').html('');
$(xml).find('note').each(function(){ // заполняем DOM элемент данными из XML
$('#example-3').append('To: ' + $(this).find('to').text() + '<br/>')
.append('From: ' + $(this).find('from').text() + '<br/>')
.append('<b>' + $(this).find('heading').text() + '</b><br/>')
.append( $(this).find('body').text() + '<br/>');
});
}, 'xml'); // указываем явно тип данных
})
});
Файл example.xml:
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>

jQuery.getJSON

Загружает данные в формате JSON (удобней и быстрее нежели XML). Может принимать следующие параметры:
  1. url запрашиваемой страницы
  2. передаваемые данные (необязательный параметр)
  3. callback функция, которой будет скормлен результат (необязательный параметр)
JavaScript:
$(document).ready(function(){ // по завершению загрузки страницы
$('#example-4').click(function(){ // вешаем на клик по элементу с id = example-4
$.getJSON('ajax/example.json', {}, function(json){ // загрузку JSON данных из файла example.json
$('#example-4').html('');
// заполняем DOM элемент данными из JSON объекта
$('#example-4').append('To: ' + json.note.to + '<br/>')
.append('From: ' + json.note.from + '<br/>')
.append('<b>' + json.note.heading + '</b><br/>')
.append( json.note.body + '<br/>');
});
})
});
Файл example.json:
{
note:{
to:'Tove',
from:'Jani',
heading:'Reminder',
body:'Don\'t forget me this weekend!'
}
}

jQuery.getScript

данная функция загружает и выполняет локальный JavaScript. Может принимать следующие параметры:
  1. url запрашиваемого скрипта
  2. callback функция, которой будет скормлен результат (необязательный параметр)
JavaScript:
$(document).ready(function(){ // по завершению загрузки страницы
$('#example-5').click(function(){ // вешаем на клик по элементу с id = example-5
$.getScript('ajax/example.js', function(){ // загрузку JavaScript'а из файла example.js
testAjax(); // выполняем загруженный JavaScript
});
})
});
Файл example.js:
function testAjax() {
$('#example-5').html('Test completed'); // изменяем элемент с id = example-5
}

Отправка Формы

Для отправки формы посредством jQuery можно использовать любой из перечисленных способов, а вот для удобства «сбора» данных из формы лучше использовать плагин jQuery Form

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

Отдельно стоит отметить использование JSONP— ибо это один из способов осуществления кросс-доменной загрузки данных. Если немного утрировать — то это подключение удаленного JavaScript'a, содержащего необходимую нам информациию в формате JSON, а так же вызов нашей локальной функции, имя которой мы указываем при обращении к удаленному серверу (обычно это параметр callback). Чуть более наглядно это можно продемонстрировать следующая диаграмма (кликабельно):
При работе с jQuery имя callback функции генерируется автоматически для каждого обращения к удаленному серверу, для этого достаточно использовать GET запрос ввида:
http://api.domain.com/?type=jsonp&query=test&callback=?
Вместо последнего знака вопроса (?) будет подставлено имя callback функции. Если же Вы не хотите использовать данный способ, то Вам необходимо будет явно указать имя callback функции, используя опцию jsonp при вызове метода jQuery.ajax().

События

Для удобства разработки, на AJAX запросах висит несколько event'ов, их можно задавать для каждого AJAX запроса в отдельности, либо глобально. На все event'ы можно повесить свою функцию.
Пример для отображения элемента с id=«loading» во время выполнения любого AJAX запроса:
$("#loading").bind("ajaxSend", function(){
$(this).show(); // показываем элемент
}).bind("ajaxComplete", function(){
$(this).hide(); // скрываем элемент
});
Для локальных событий — вносим изменения в опции метода ajax():
$.ajax({
beforeSend: function(){
// Handle the beforeSend event
},
complete: function(){
// Handle the complete event
}
// ...
});
Для большей наглядности, приведу следующую диаграмму (кликабельно):
Ну и собственно список всех event'ов:
  • ajaxStart— Данный метод вызывается в случае когда побежал AJAX запрос, и при этом других запросов нету
  • beforeSend— Срабатывает до отправки запроса, позволяет редактировать XMLHttpRequest. Локальное событие
  • ajaxSend— Срабатывает до отправки запроса, аналогично beforeSend
  • success— Срабатывает по возвращению ответа, когда нет ошибок ни сервера, ни вернувшихся данных. Локальное событие
  • ajaxSuccess— Срабатывает по возвращению ответа, аналогично success
  • error— Срабатывает в случае ошибки. Локальное событие
  • ajaxError— Срабатывает в случае ошибки
  • complete— Срабатывает по завершению текущего AJAX запроса (с ошибкои или без — срабатывает всегда).Локальное событие
  • ajaxComplete— Глобальное событие, аналогичное complete
  • ajaxStop— Данный метод вызывается в случае когда больше нету активных запросов

Javascript -  appendChild(), removeChild()

Важно: событие не сработает, если количество элементов в NodeList было изменено.
<div id="raz10"><input type="button" value="Нажать"></div>


<script>
var div = document.querySelector('#raz10');
input = div.childNodes;
input[0].onclick = function() {
alert('Не работает');
}
div.innerHTML += '<input type="button" value="Я вновь созданный">';
</script>


<div id="raz10"><input type="button" value="Нажать"></div>


<script>
var div = document.querySelector('#raz10');
input = div.childNodes;
div.innerHTML += '<input type="button" value="Я вновь созданный">';
input[0].onclick = function() {
alert('Работает');
}
</script>

Как удалить элемент на JavaScript

При клике на нём же: 
<button onclick="this.parentNode.removeChild(this);">Вариант 1</button>
<button onclick="this.remove();">Вариант 2</button>
<button onclick="this.outerHTML='';">Вариант 3</button>

Как добавить тег HTML

<style>
#raz div {
transition: 3s;
opacity: 0;
}
</style>


<div id="raz">
<input type="button" value="Добавить">
</div>

appendChild() в конец

<script>
document.querySelector('#raz input').onclick = function() {
var theDiv = document.createElement('div'); // создать новый тег div
theDiv.innerHTML = 'HTML-содержимое'; // его содержимое
this.parentNode.appendChild(theDiv); // поместить новый тег последним в div с id="raz" (вместо this.parentNode может быть любой другой элемент DOM)
getComputedStyle(theDiv).opacity;
theDiv.style.opacity = '1';
}
</script>

insertBefore() в начало

<script>
document.querySelector('#raz input').onclick = function() {
var theDiv = document.createElement('div');
theDiv.innerHTML = 'HTML-содержимое';
this.parentNode.insertBefore(theDiv, this.parentNode.firstChild); // поместить новый тег первым в div с id="raz"
getComputedStyle(theDiv).opacity;
theDiv.style.opacity = '1';
}
</script>

insertBefore() после конкретного элемента

<script>
document.querySelector('#raz input').onclick = function() {
var theDiv = document.createElement('div');
theDiv.innerHTML = 'HTML-содержимое';
this.parentNode.insertBefore(theDiv, this.nextSibling);
getComputedStyle(theDiv).opacity;
theDiv.style.opacity = '1';
}
</script>

insertBefore() до указанного элемента

<script>
document.querySelector('#raz input').onclick = function() {
var theDiv = document.createElement('div');
theDiv.innerHTML = 'HTML-содержимое';
this.parentNode.insertBefore(theDiv, this.previousSibling);
getComputedStyle(theDiv).opacity;
theDiv.style.opacity = '1';
}
</script>

innerHTML внутри элемента (взять содержимое в новый тег)

<style>
#raz8 span {
display: inline-block;
padding: 3px;
border: 1px solid;
}
</style>

<button type="button" id="raz8">новый тег внутри</button>

<script>
(function(){
var i = 1;
document.querySelector('#raz8').onclick = function() {
this.innerHTML = '<span style="border-color: rgba(255,0,0,'+i+');">'+this.innerHTML+'</span>';
i = i - 0.1;
}
})()
</script>
Если есть события JS на тегах внутри элемента, то можно как тут
  1. создать нового потомка,
  2. переместить во вновь созданного потомка всех остальных потомков.

replaceChild(newChild, oldChild) снаружи элемента (окружить новым тегом)

<style>
.raz9 {
display: inline-block;
padding: 3px;
border: 1px solid rgba(255,0,0,0);
transition: 3s;
}
</style>

<button type="button" id="raz9">новый тег снаружи</button>

<script>
document.querySelector('#raz9').onclick = function() {
var theSpan = document.createElement('span');
theSpan.setAttribute('class', 'raz9');
this.parentNode.replaceChild(theSpan, this); // заменить текущий элемент на созданный
theSpan.appendChild(this); // содержимое созданного элемента будет текущий элемент
getComputedStyle(theSpan).borderTopColor;
theSpan.style.borderColor = 'rgba(255,0,0,1)';
}
</script>

Как добавить текст

Вместо document.createElement(tagName) использовать document.createTextNode(text). Так же и с insertBefore().
<div id="raz7">
<input type="button" value="Добавить текст">
</div>

<script>
(function(){
var i = 1;
document.querySelector('#raz7 input').onclick = function() {
var theText = document.createTextNode(' текст'+i);
this.parentNode.appendChild(theText);
i++;
}
})()
</script>

Как переместить элемент

Отличие в том, что элемент уже существует. Так же и с insertBefore().
<style>
#raz5, #raz6 {
height: 50px;
padding: 5px;
}
#raz5 {
background: green;
}
#raz6 {
background: blue;
}
</style>

<div id="raz5">
<input type="button" value="Перенести в синий прямоугольник">
</div>
<div id="raz6"></div>

<script>
document.querySelector('#raz5 input').onclick = function() {
document.querySelector('#raz6').appendChild(this);
}
</script>

Как скопировать (клонировать) элемент

Сделать дубликат кода с помощью cloneNode(true|false)

<button type="button" class="raz1">1</button><button type="button" class="raz1">2</button>

<script>
Array.prototype.slice.call(document.querySelectorAll('.raz1')).forEach(function(a, index, all) {
a.addEventListener('click', function() {
var theI = a.cloneNode(true);
a.parentNode.insertBefore(theI, a.nextSibling);
alert(index+1+' из '+all.length);
}, false);
})
</script>

Как запретить повторный onclick

<input type="button" value="Добавить после input" id="dobavit">

<script>
document.querySelector('#dobavit').onclick = function() {
theDiv = document.createElement('div');
theDiv.innerHTML = 'содержащий текст';
this.parentNode.insertBefore(theDiv, this.nextSibling);
this.onclick = null;
}
</script>

Как создать элемент, а потом его же удалить

<input type="button" value="Новый CSS" onclick="include('on')">
<input type="button" value="Старый CSS" onclick="include('off')">

<script>
var include = (function(){
return function (status){
if(status == 'on'){ // если include('on')
theStyle = document.createElement('style'); // создать новый тег style
theStyle.innerHTML = 'body {background: DarkSalmon;}';
document.querySelector('head').appendChild(theStyle); // тег style поместить в конец head
}else{
theStyle.parentNode.removeChild(theStyle); // удалить созданный тег style
}
}
})();
</script>

В чём отличие innerHTML от appendChild(), removeChild()

innerHTML меняет всё содержимое тега.
<input type="button" value="Новый CSS" onclick="include1('on')">
<input type="button" value="Попытка вернуть старый CSS" onclick="include1('off')">
<input type="button" value="Перезагрузить страницу" onclick="location.reload();">

<script>
var include1 = (function(){
return function (status){
if(status == 'on'){ // если include1('on')
document.querySelector('head').innerHTML = '<style>body {background: DarkSalmon;}</style>'; // сменить содержимое тега head на прописанное в кавычках
}else{ // иначе
document.querySelector('head').innerHTML = '';
}
}
})();
</script>
Можно с помощью .replace и регулярных выражений добиться нужного результата:
<input type="button" value="Новый CSS" onclick="include2('on')">
<input type="button" value="Старый CSS" onclick="include2('off')">

<script>
var include2 = (function(){
return function (status){
if(status == 'on'){
document.querySelector('head').innerHTML = document.querySelector('head').innerHTML.replace(/$/g, '<style class="newStyle">body {background: DarkSalmon;}</style>');
}else{
document.querySelector('head').innerHTML = document.querySelector('head').innerHTML.replace(/<style class="newStyle">[\s\S]*?<\/style>/g, '');
}
}
})();
</script>
Или чтобы изначально в HTML был пустой тег
<style class="nStyle"></style>

<input type="button" value="Новый CSS" onclick="include3('on')">
<input type="button" value="Старый CSS" onclick="include3('off')">

<script>
var include3 = (function(){
return function (status){
if(status == 'on'){
document.querySelector('style.nStyle').innerHTML = 'body {background: DarkSalmon;}';
}else{
document.querySelector('style.nStyle').innerHTML = '';
}
}
})();
</script>
Кто медленнее? innerHTML быстрее работает в Firefox, W3C DOM - в Хроме. IE везде отстающий, но с W3C DOM у него намного хуже.
Кстати, есть ещё самый резвый document.write. Он выводит содержимое после тега script во время загрузки страницы. То есть никаких onclick применить не удастся.

JavaScript: Советы и рекомендации

Оператор !!

Чтобы проверить, является ли значение истинным или !! operatorложным, вы можете вызвать этот оператор double not или not not.
console.log(!! 0) // output: false
console.log(!! 1) // output: true

console.log(Boolean(1)) // output: true
console.log(Boolean(0)) // output: false

Конвертировать string → number

Преобразование строки в число
const string = '101'

console.log(+string) // output: 101
console.log(Number(string)) // output: 101

Обратный метод reverse()

Используйте обратный метод для изменения порядка элементов массива обратите внимание, что обратный метод изменяет исходный массив.
const numbers = ['1', '2', '3']

console.log(numbers.reverse()) // output: [ "3", "2", "1" ]

Математические функции Math.min & max

Найдите минимальные или максимальные значения из массива с помощью функции Math.min & Math.max.
const numbers = [1, 2 ,3, 4, 5]

console.log(Math.min(...numbers)) // output: 1

console.log(Math.max(...numbers)) // output: 5

Объединить массивы

Используйте оператор spread для объединения массивов.
const fruits = ['🍎', '🍌']

const vegetables = ['🥔', '🥕']

const food = [...fruits, ...vegetables]

console.log(food) // output: [ "🍎", "🍌" , "🥔", "🥕" ]

Ложные значения

В javascript есть девять ложных значений.
undefined , null , NaN , 0 , 0n (BigInt 0), -0 ""(empty string),false,document.all

Тернарный оператор

Тернарный оператор позволяет писать оператор if...else более компактно.
let number = 1

if (number == 1) {
console.log('number is one')
} else {
console.log('number is not one')
}

// Syntax: condition ? exprIfTrue : exprIfFalse (MDN)


console.log(number === 1 ? "number is one" : "number is not one");

Удалить дубли в массиве

const fruits = ['🍎', '🍊', '🍎', '🍊']

// Method 1:
const filteredFruits = Array.from(new Set(fruits))
console.log(filteredFruits) // output: Array [ "🍎", "🍊" ]

// Method 2:
const filteredFruits = [...new Set(fruits)]
console.log(filteredFruits) // output: Array [ "🍎", "🍊" ]

Метод map()

Попробуйте использовать метод map если вы хотите манипулировать элементами массива метод map выполняет данную функцию для каждого элемента массива и возвращает новый массив на основе исходного массива
const numbers = [1, 2, 3, 4, 5]

const mapedNumbers = numbers.map(element => element + 1)

console.log(mapedNumbers) // output: [2, 3, 4, 5, 6]

Метод includes()

Чтобы проверить, содержит ли массив определенное значение или нет, используйте метод includes.
const hearts = ['🧡', '💙', '🤍']

console.log(hearts.includes('🧡')) // output: true

console.log(hearts.includes('❤️')) // output: false

Метод filter()

фильтр массивов на основе условий Метод фильтра принимает функцию в качестве аргумента и выполняет эту функцию для каждого элемента массива и возвращает новый массив.
const numbers = [1, 5, 6, 7, 4]

const filteredArray = numbers.filter(element => element > 4)

console.log(filteredArray) // output: [ 5, 6, 7 ]

Кнопка прокрутки вверх

const button = document.querySelector('button')

button.addEventListener('click', function () {
window.scrollTo(0,0)
})

outerWidth

outerWidth( [margin] )

Возвращает значение внешней ширины (включая по умолчанию border и padding) для первого совпавшего элемента в наборе.
Этот метод работает с видимыми и скрытыми элементами. Но нет возможности работы с элементами, которые были косвенно скрыты. К примеру, в следствие скрытия родительского объекта. Пожалуйста, используйте .width() для объектов window и document.
Аргументы:
margin (необязательно)
Логическое значение
Если указано значение true, то значение margin берется во внимание при расчете высоты.
Примеры:

Получает значение outerWidth
var p = $("p:first");
$("p:last").text( "outerWidth:" + p.outerWidth()+ " , outerWidth(true):" + p.outerWidth(true) );
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
$(document).ready(function(){
var p = $("p:first");
$("p:last").text( "outerWidth:" + p.outerWidth()+ " , outerWidth(true):" + p.outerWidth(true) );
});
</script>
<style>
p { margin:10px;padding:5px;border:2px solid #666; }
</style>
</head>
<body>
<p>Hello</p><p></p>
</body>
</html>

window.history

History API даёт доступ к управлению историей браузера в рамках текущей сессии. Браузер создаёт новую сессию, когда пользователь открывает новую вкладку или новое окно браузера.

С помощью History API можно переходить по истории вперёд, назад и управлять содержимым истории. Доступ к API осуществляется с помощью объекта window.history.

Основные методы:

  • back() перемещает пользователя по истории на страницу назад;
  • forward() перемещает пользователя по истории на страницу вперёд;
  • go() универсальный метод для перемещения по истории вперёд или назад;
  • pushState() добавляет новую запись в истории сессии;
  • replaceState() изменяет текущую запись в истории сессии.


Как пишется

Сгруппируем методы и свойства History API на две части:

  • используемые для перемещения по истории в текущей сессии.
  • используемые для управления историей.

Перемещение по истории браузера

Метод back() перемещает пользователя по истории назад. Это работает аналогично нажатию кнопки «Назад» в браузере.

Метод back() не принимает аргументов и не возвращает результата.

window.history.back()

Чтобы переместиться по истории вперёд, используется метод forward(). Он работает аналогично кнопке «Вперёд» в браузере.

Метод forward() не принимает аргументов и не возвращает результата.

window.history.forward()

Метод go() является универсальным и позволяет переместиться по истории вперёд или назад относительно текущей страницы.

Метод принимает один аргумент — число, которое определяет, на сколько шагов по истории вперёд или назад нужно перейти. Отрицательное значение определяет сколько шагов назад, а положительные — вперёд.

window.history.go(2)
// переместит на две позиции вперёд по истории

window.history.go(-1)
// переместит назад на одну страницу

Нулевая позиция 0 означает текущую страницу и вызов с нулём обновляет текущую страницу.

window.history.go(0)

Метод back() аналогичен вызову window.history.go(-1), а метод forward() — вызову window.history.go(1).

Все методы для перемещения по истории браузера back(), forward() и go() являются асинхронными.

О том для чего вообще может понадобиться передавать данные во время навигации смотрите раздел «На практике».
Свойство length хранит количество записей в истории браузера в текущей сессии, включая текущую страницу. То есть новая история всегда будет начинаться с 1.

length работает только для чтения, при изменении значения ничего не произойдёт.

Управление историей

Для создания новой записи в истории используется метод pushState(), а для модификации текущей записи — replaceState().

Оба метода похожи с точки зрения использования и оба принимают три аргумента:

  • объект состояния, в который можно добавить любые данные, необходимые для навигации;
  • неиспользуемый параметр, который существует по историческим причинам;
  • новый URL-адрес, этот параметр опциональный.

У передаваемых аргументов есть ограничения, о которых стоит помнить:

  • В первый аргумент-объект можно записать любой объект с любыми данными, главное чтобы объект был сериализуемым. Браузеры могут накладывать ограничения на размер такого объекта.
  • Второй аргумент игнорируется всеми браузерами кроме Safari. Во избежание ошибок рекомендуют передавать пустую строку.
  • Третий аргумент, URL-адрес, необязательный. Если этот аргумент не был задан, то будет использован текущий URL. Новый URL-адрес должен использовать тот же протокол, домен и порт, иначе будет выброшена ошибка. Если новая запись ведёт на новый относительный адрес, то можно не передавать адрес полностью вместе с доменом, а записать только относительную часть через слэш. Например, /profile.

    О том для чего вообще может понадобиться передавать данные во время навигации смотрите раздел «На практике».
    Рассмотрим работу window.history на примере. В этом примере у нас получится замкнутый круг переходов по страницам. Вы можете поиграться с примером, ниже я разберу код, который используется в демке.

Предположим, что мы зашли на главную страницу сайта с новой вкладки. С помощью кнопки добавим новую запись в историю:

window.history.pushState(
  {},
  '',
  'about.html'
)

Адрес в браузерной сроке location.path изменился, а так же обновилось количество записей в истории history.length. Теперь если нажать кнопку перезагрузки страницы, то мы увидим новую страницу.

Теперь можем изменить текущую запись в истории, нажав на кнопку. Ниже приведён фрагмент код, который делает изменение истории:

window.history.replaceState(
  {},
  '',
  'index.html'
)

Адрес в браузерной строке снова изменился, но количество записей осталось прежним, так как мы ничего не добавили. Далее продолжаем путь на следующую страницу.

Страница цен является последним пунктом в примере, с него можно только вернуться по истории назад на предыдущую страницу.

window.history.back()
// Или можно сделать window.history.go(-1)


Помните, как на предыдущей странице видели страницу «О нас» по ссылке about.html? Мы же попали на главную страницу. Это произошло потому что мы изменили текущую запись в истории, изменив её URL-адрес. В результате, когда мы возвращаемся назад по истории, то используем изменённый URL.

Как понять

Классическая навигация

Основным способом в вебе навигации являются ссылки <a>. С помощью ссылок страницы соединяются друг с другом.

Особенность этого способа навигации в том, что при переходе на новый адрес страница перезагружается. Каждый такой переход сохраняется в истории браузера. История может выглядеть так:


https://example.com/buy
https://example.com/prices
https://example.com/about
https://example.com/

Верхние пункты списка — это недавно посещённые страницы, а последний пункт — страница, с которой началась сессия.

Навигация в одностраничных приложениях

С другой стороны, есть одностраничные приложения, которые работают без перезагрузки страницы. Все содержимое они отрисовывают с помощью JavaScript. Приложение может перерисовать всю страницу, но на истории браузера это никак не отобразится.

История в браузере будет выглядеть как единственный пункт:

https://example.com/

Это ведёт к плохому пользовательскому опыту, потому что пользователю сложно понять по каким страницам он перемещался.

Отсутствие истории ломает нативное поведение браузерных кнопок вперёд и назад, что очень важно, так как эти кнопки часто привязаны к жестам или системным кнопкам в смартфонах.

Сначала эту проблему решали добавлением хэша в адрес сайта с помощью поля window.location.hash. Присвоив новое значение в location.hash, адресная строка сразу же обновлялась и в историю добавлялась новая запись. Но с помощью хэша нельзя строить читаемые многоуровневые урлы, например https://example.com/category/cars/item/12345.

С появлением History API появилась возможность напрямую добавлять записи в историю просмотров. History API так же расширяет возможности для программного перемещения по истории браузера. Это позволяет создавать полноценную навигацию в одностраничных приложениях, менять адрес в браузерной строке и все это будет происходить без перезагрузки страницы.

Работа плагина jQuery validate изнутри

Как валидировать форму

Есть 2 основных способа, чтобы валидировать форму.
1. Использовать имена классов как правила
Как это работает
Мы добавляем к полю, которое нужно провалидировать, html атрибут «class», и это подключит валидацию.
Итак, если нам нужно, чтобы текстовое поле было обязательным мы добавляем в элемент input значение атрибута class = «required»
Html
<form action="/" method="post">
  <input id="Name" type="text" name="Name" value="" class ="required"  />
  <input type="submit" value="Submit" />
</form>
Так вы можете добавить к определенным классам несколько правил.
Плюсы и минусы такого подхода:
Работает только с правилами, которые не принимают аргументов.
Мы используем html атрибут «class» для того, для чего он не предназначен.
Но его легко установить.
Использование метода «addClassRules»
Использование функции «addClassRules» дает нам возможность использовать составное правило для одного класса.
JavaScript
 $.validator.addClassRules({
  name: {
    required: true,
    minlength: 2
  },
  zip: {
    required: true,
    digits: true,
    minlength: 5,
    maxlength: 5
  }
});
Этот код добавляет 2 новых правила для класса «name» и «zip», и, если у нас есть элемент «input», у которого класс «zip», то к нему применяются правила: его значение является обязательным, пользователь может вводить только цифры и длина должна быть точно 5 символов.
Html
<input class="zip" type="text" name="zipCode" />
Информация: Чтобы использовать собственное сообщение для определенного правила requires в составном правиле, нам нужно придумать псевдоним для правила «required», создать новое правило с этим псевдонимом и установить для него сообщение по умолчанию.
JavaScript
$.validator.addMethod("newrequired", $.validator.methods.required, "new name is required");
Или мы можем использовать html атрибут «title», его значение будет сообщением об ошибке для составного правила.
Заметка: Валидация по имени класса работает только для правил валидации, не принимающих никаких аргументов.
2. Добавление правил как JSON объекта к методу validate()
По названию, вы должны были догадаться, что этот метод валидации принимает объект json, поэтому мы можем определить поля, которые нам нужно валидировать и правила валидации для них.
Html
<form>
  <input id="something" type="text" name="userEmail" />
  <input id="submit" type="submit" value="submit" />
</form>
JavaScript
$('form').validate({
  rules: {
    userEmail: {
      email: true,
      required: true
    }
  }
});
Заметка: Когда мы передаем объект «rules» функции «validate» ключом должно быть значение атрибута «name», а не значение «id». Как можно увидеть в примере: ключ -это «userEmail», значение атрибута «name», а у атрибута «id» другое значение.
Плюсы и минусы этого подхода:
  • Этот подход дает нам возможность использовать больше правил валидации, которые принимают аргументы, такие как minlength, remote, equalTo и т.д.
  • Отличный и настраиваемый вручную контроль над всем.
  • Но пользователь должен делать отдельную функцию «validate» с разными опциями для каждой формы.
Добавление или удаление динамических правил.
Добавление правил
Чтобы добавить правило мы должны использовать метод «rules» для элементов jQuery после того как форма провалидирована и передавать как первый параметр строку «add» и как второй параметр — объект правил, которые мы хотим добавить к этому элементу (также мы можем передавать объект «сообщение» для правил, которые мы добавили).
JavaScript
$('.input').rules('add', {
  required: true,
  messages: {
    required: true
  }
})
Удаление правил
Если мы хотим удалить правило или набор правил, мы передаем строку «remove», как первый параметр для метода «rules», а вторым параметром будет строка, которая содержит правила, которые мы хотим удалить, отделенные пробелом.
JavaScript
$('.input').rules('remove', 'min max');
Подход настройки вручную
JavaScript
var validator = $('form').data('validator');
validator.settings.rules.objectName = {
required: true
}
Этот подход очень полезен, если у вас есть созданные правила и объекты сообщений, вы можете расширить правила валидатора своими собственными:
JavaScript
$.extend(validator.settings, { rules: rules, messages: messages });

Сообщения валидации и как они работают

Есть три способа настроить сообщение валидации
1. Передать объект «messages» методу «validate». Объект «messages» состоит из пар ключ\значение. Ключ — это значение атрибута «name» элемента. Значение — объект, содержащий каждое правило и его сообщение.
JavaScript
$('form').validate({
  rules: {
    userEmail: {
    email: true,
    required: true
    }
  },
  messages: {
    userEmail: {
    email: "Please enter your email",
    required: "*"
    }
  }
});
2. Определить значение атрибута «title» элемента
Html
<input id="Email" title="you have to enter a value" type="text" name="Email" />
3. Использовать сообщение по умолчанию. Когда определяется правило валидации, есть встроенные сообщения по умолчанию для встроенных правил.
Заметка: Эти три способа переопределяют друг друга, основываясь на приоритетности, самый приоритетный — переданный объект «messages», а наименее приоритетный — сообщение по умолчанию.

Добавление собственных правил валидации

Когда мы хотим добавить больше правил валидации, чем определены по умолчанию, мы используем метод
JavaScript
$.validator.addMethod
Этот метод принимает как параметры следующее:
  • имя правила;
  • функцию, которая осуществляет валидацию;
  • сообщение по умолчанию.
Функция, которая производит валидацию, может быть с двумя или тремя параметрами
JavaScript
function validationMethod (value, element)
// OR
function validationMethod (value, element, params)
Давайте объясню эти параметры.
Значение: значение DOM элемента, который будет валидироваться
Элемент: сам элемент DOM
Параметры: то, что мы передаем как значение. Для этого примера правила валидации — это то, чему должен равняться params.
JavaScript
$('form').validate({
    rules: {
        firstname: {
            compare: {
                type: "notequal",
               otherprop: "lastname"
            }
       }
    }
});
в этом примере params будет равен {type:«notequal», otherprop: «lastname»}
Пример добавления собственного правила:
JavaScript
$.validator.addMethod("notnumbers", function(value, element) {
    return !/[0-9]*/.test(value);
    },
   "Please don't insert numbers.")

Что именно происходит, когда мы вызываем метод «validate»

Когда мы вызваем метод validate на форме, за кулисами происходит много разных вещей:
Создается объект «validator» со всеми правилами и опциями, присоединенными к форме.
Метод «validate» присоединяет «validator» используя "$.data". Мы можем получить его выбрав форму и вызвав функцию jQuery "$.data" и передать ее «validator». Объект «vaidator» — это все метаданные для валидации, которые дают нам возможность доступа к опциям валидации в любое время жизненного цикла страницы.
Используя этот объект, мы можем изменить во время выполнения опции, которые мы передали методу валидации, такие как добавление или удаление правил, изменение поведения, если поле валидное или невалидное, или даже введение селектора игнорирования.
JavaScript
//getting the validator
var validator = $("selector").data("validator")
Заметка: Когда вы вызываете метод «validate» на форме, которая уже провалидирована, он вернет только объект «validator», используется также $.data, и все предыдущие опции, переданные методом «validate», будут стерты.
JavaScript
var validator = $(".selector").validate(/* rules will be omitted */)
Присоединение событий формы
Что произойдет, когда мы нажмем submit(отправить форму), и в форме будет введено неверное значение для поля, к которому мы присоединили валидацию. Если одно из полей невалидное, то плагин валидации будет присматриваться к нему более пристально, чтобы проверять, валидное оно или нет, по событиям на этом поле.
Сообытия формы, на которые подписывается плагин — это «click», «focusin», «focusout», «keyup», «submit».
Заметка: Вы можете отключить валидацию по определенным событиям, передав их как ключи в методе validate, и false в качестве значений.
JavaScript
$(".selector").validate({
    onfocusout: false,
    onkeyup: false,
    onclick: false,
    onsubmit: false
});

Регулярные выражения для валидации распространенных видов данных

Для проверки текстовых полей на валидность обычно используют регулярные выражения. Существует несколько наиболе распространенных видов таких даных, как например номер кредитки, дата в определенном формате и т. д. 
  • Номер кредитки:
    [0-9]{13,16}
  • ICQ:
    ([1-9])+(?:-?\d){4,}
  • Набор из букв и цифр (латиница):
    ^[a-zA-Z0-9]+$
  • Набор из букв и цифр (латиница + кириллица):
    ^[а-яА-ЯёЁa-zA-Z0-9]+$
  • Домен (например abcd.com):
    ^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$
  • IPv4:
    ((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)
  • IPv6:
    ((^|:)([0-9a-fA-F]{0,4})){1,8}$
  • Имя пользователя (с ограничением 2-20 символов, которыми могут быть буквы и цифры, первый символ обязательно буква):
    ^[a-zA-Z][a-zA-Z0-9-_\.]{1,20}$
  • Пароль (Строчные и прописные латинские буквы, цифры):
    ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).*$
  • Пароль (Строчные и прописные латинские буквы, цифры, спецсимволы. Минимум 8 символов):
    (?=^.{8,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$
  • Дата в формате YYYY-MM-DD:
    [0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01]) 
    Более строгая проверка:
    (19|20)\d\d-((0[1-9]|1[012])-(0[1-9]|[12]\d)|(0[13-9]|1[012])-30|(0[13578]|1[02])-31)
  • Дата в формате DD/MM/YYYY:
    (0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d
  • Целые числа и числа с плавающей точкой (разделитель точка):
    \-?\d+(\.\d{0,})?
  • UUID:
    ^[0-9A-Fa-f]{8}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{12}$
  • Широта или долгота:
    -?\d{1,3}\.\d+
  • E-mail:
    ^[-\w.]+@([A-z0-9][-A-z0-9]+\.)+[A-z]{2,4}$
  • URL на латинице. Если нужно распознавать и кириллические домены, необходимо изменить все «a-z0-9» на «а-яёa-z0-9» и добавить в список доменных зон «рф»:
    ~^(?:(?:https?|ftp|telnet)://(?:[a-z0-9_-]{1,32}(?::[a-z0-9_-]{1,32})?@)?)?(?:(?:[a-z0-9-]{1,128}\.)+(?:ru|su|com|net|org|mil|edu|arpa|gov|biz|info|aero|inc|name|[a-z]{2})|(?!0)(?:(?!0[^.]|255)[0-9]{1,3}\.){3}(?!0|255)[0-9]{1,3})(?:/[a-z0-9.,_@%&?+=\~/-]*)?(?:#[^ '\"&]*)?$~i
  • Время в формате HH:MM:SS:
    ^([0-1]\d|2[0-3])(:[0-5]\d){2}$
  • Mac-адрес:
    ([0-9a-fA-F]{2}([:-]|$)){6}$|([0-9a-fA-F]{4}([.]|$)){3}

Регуля́рные выраже́ния (англ. regular expressions) — формальный язык поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов (символов-джокеров, англ. wildcard characters). По сути это строка-образец (англ. pattern, по-русски её часто называют «шаблоном», «маской»), состоящая из символов и метасимволов и задающая правило поиска.
Регулярные выражения произвели прорыв в электронной обработке текстов в конце XX века. Набор утилит (включая редактор sed и фильтр grep), поставляемых в дистрибутивах UNIX, одним из первых способствовал популяризации регулярных выражений для обработки текстов. Многие современные языки программирования имеют встроенную поддержку регулярных выражений. Среди них ActionScript, Perl, Java[1],PHP, JavaScript, языки платформы .NET Framework[2], Python, Tcl, Ruby, Lua, Gambas, C++ (стандарт 2011 года), Delphi, D и др.
Регулярные выражения используются некоторыми текстовыми редакторами и утилитами для поиска и подстановки текста. Например, при помощи регулярных выражений можно задать шаблоны, позволяющие:
найти все последовательности символов «кот» в любом контексте, как то: «кот», «котлета», «терракотовый»;
найти отдельно стоящее слово «кот» и заменить его на «кошка»;
найти слово «кот», которому предшествует слово «персидский» или «чеширский»;
убрать из текста все предложения, в которых упоминается слово кот или кошка.
Регулярные выражения позволяют задавать и гораздо более сложные шаблоны поиска или замены.

События и их обработка в jQuery

В этой статье мы рассмотрим методы jQuery, которые предназначены для обработки различных событий, возникающих на странице.
Событие– это «сигнал», возникающий в браузере, который сообщает вам, что что-то произошло для того, чтобы вы могли с ним взаимодействовать.
Например, событие click возникает при клике пользователем на DOM-элементе. Кроме click, в браузере генерируются множество других событий. Здесь же мы остановимся на рассмотрение методов, которые предоставляет библиотека jQuery для работы с ними.

Обработка событий с помощью методов jQuery

jQuery - Синтаксис функции on
Перед тем как переходить к добавлению элементам обработчиков событий, эти элементы сначала необходимо получить. 
В jQuery повесить событие (слушатель событий) на определённый элемент можно с помощью функций on и one, а также кратких записей on.
// функция on
.on(events,handler);
// функция one
.one(events,handler);

// events - событие или список событий через пробел, при наступлении которых необходимо выполнить обработчик (handler)
// handler - функция (обработчик события)

// краткая запись функции on
.event(handler);

// event - название события (можно использовать для обработки только тех событий, для которых в jQuery создана такая краткая запись)
Функция one отличается от on только тем, что она выполняет обработчик при наступлении указанного события только один раз.
Например, добавим с помощью функции onсобытие clickдля всех элементов с классом btn:
// использование в качестве обработчика анонимной функции
$('.btn').on('click', function() {
// действия, которые будут выполнены при наступлении события...
console.log($(this).text());
});

// использование обычной функции в качестве обработчика
var myFunction = function() {
console.log($(this).text());
}
$('.btn').on('click', myFunction);
Вышеприведённый код, записанный с использованием короткой записи функции on:
$('.btn').click(function() {
// действия, которые будут выполнены при наступлении события...
console.log($(this).text());
});

Дополнительная информация о событии

При обработке события вы можете узнать некоторую дополнительную информацию о нём. Передача этой информации, а именно объекта Event в обработчик события осуществляется всегда посредством первого аргумента.
$('#demo').on('click', function(e){
// e – объект Event, содержащий дополнительную информацию о произошедшем событии
// часто используемые свойства объекта Event
e.preventDefault(); //отменить выполнение действия по умолчанию
e.stopPropagation(); //остановить дальнейшее всплытие события
// e.type – получить тип события
// e.target – ссылка на элемент, на котором произошло событие
// e.currentTarget - ссылка на текущий элемент (для которого сработал обработчик). Это свойство, как правило, равно функции this.
// e.currentTarget === this
// e.which – код клавиши (для мыши), код кнопки или символа (для клавиатуры)
//e.pageX, e.pageY – координаты курсора, относительно левого верхнего угла документа
});

Пространство имён

В jQuery после указания имени события вы можете ещё дополнительно указать пространство имён.
Например:
// событие click в пространстве имён first
$('#demo').on('click.first',function(){
console.log('1 обработчик события click');
});
// событие click в пространстве имён second
$('#demo').on('click.second',function(){
console.log('2 обработчик события click');
});
Пространство имён - это очень удобная вещь. Она используется, например, когда вам необходимо вызвать не все события, а только с определённым именем.
// вызвать событие click в пространстве имён first
$('#demo').trigger('click.first');


// вызвать событие click в пространстве имён second
$('#demo').trigger('click.second');
Также с его помощью очень просто удалять определённые события:
//удалить обработчики события click в пространстве имён first
$('#demo').off('click.first');


//удалить обработчики события click в пространстве имён second
$('#demo').off('click.second');
Описание и примеры использования функций triggerи offрассматриваются в статье немного ниже.

Передача дополнительных данных в обработчик

При необходимости вы можете передать данные в обработчик события (посредством указания дополнительного аргумента в функции on). Доступ к переданным данным внутри обработчика осуществляется через объект Event.
Осуществляется это так (пример):
<div id="content"></div>
<button id="showContent1">Показать контент 1</button>
<button id="showContent2">Показать контент 2</button>
...


<script>
$('#showContent1').on('click', {title:'Заголовок 1', content: 'Содержимое 1...'}, function(e){
var output = '<b>'+e.data.title+': </b>' + e.data.content;
$('#content').html(output);
});
$('#showContent2').on('click', {title:'Заголовок 2', content: 'Содержимое 2...'}, function(e){
var output = '<b>'+e.data.title+': </b>' + e.data.content;
$('#content').html(output);
});
</script>

Как повесить несколько событий на один элемент

Пример использования одного обработчика для нескольких (2 или более) событий:
$('#id').on('keyup keypress blur change', function(e) {
console.log(e.type); // тип события
});


// или так
var myFunction = function() {
...
}


$('#id')
.keyup(myFunction)
.keypress(myFunction)
.blur(myFunction)
.change(myFunction);
Для каждого события своя функция:
$("#id").on({
mouseenter: function() {
// обработчик события mouseenter...
},
mouseleave: function() {
// обработчик события mouseleave...
},
click: function() {
// обработчик события click...
}
});
Пример использования в jQuery несколько обработчиков (функций) на одно событие:
$("#demo").click(function(){
console.log('1 обработчик события click');
});
$("#demo").click(function(){
console.log('2 обработчик события click');
});
Например, узнать в какой очерёдности будут выполняться события можно так:
var eventList = $._data($('#demo')[0], 'events');
console.log(eventList);

Программный вызов события

Для вызова события из кода в jQuery есть 2 метода:
trigger- вызывает указанное событие у элемента.
triggerHandler- вызывает обработчик события, при этом само событие не происходит.
$('#header').on('click',
function() {
console('Произошёл клик на элементе #header');
}
});


// программный вызов события click элемента
$('#header').trigger('click');
// короткая запись данного вызова
$('#header').click();
// вызов обработчика события click у выбранного элемента
$('#header').triggerHandler('click');

jQuery - Событие загрузки страницы (ready)

Процесс добавления к некоторому элементу обработчика события обычно осуществляется после загрузки страницы, т.е. когда DOM дерево документа уже построено. Иначе при написании обработчиков, вы можете обратиться к элементам, которых ещё нет на странице.
Самая короткая запись события загрузки страницы в jQuery выглядит так:
$(function(){
// действия, которые необходимо выполнить после загрузки документа...
});
Но, можно использовать и более длинную запись:
$(document).ready(function(){
// действия, которые необходимо выполнить после загрузки документа...
});

jQuery - Событие загрузки (load)

Событие load браузер генерирует элементу, когда он и все вложенные в него элементы были полностью загружены. Данное событие предназначено только для элементов, в которых присутствует URL: image, script, iframe и window.
Например, выполнить функцию, когда страница будет полностью загружена (включая картинки):
$(window).on('load', function() {
// действия после полной загрузки страницы...
});
Например, выведем сообщение в консоль, когда указанное изображение будет загружено:
<img id="image" src="image.png">
...
<script>
$('#image').on('load', function() {
console.log('Изображение загружено');
});
</script>

jQuery - События мыши

В jQuery для отслеживания действий мыши наиболее часто используют следующие события:
  • mousedown
  • mouseup
  • click
  • mousemove
  • wheel
  • hover
  • mouseenter
  • mouseover
  • mouseleave
  • mouseout

jQuery - Событие клик (click)

Событие click является сложным событием, оно возникает после генерирования событий mousedown и mouseup. Событие mousedownвозникает, когда указатель находится над элементом и кнопка мыши нажата. Событие mouseupпроисходит, когда указатель находится над элементом и кнопка мыши отпущена. Событие click генерируется, когда курсор находится над элементом, и клавиша мыши нажата и отпущена. Эти события могут получать любые HTML элементы.
Например, повесим обработчик на событие onclickэлемента window. При наступлении данного события обработчик будет выводить номер нажатой клавиши и координаты курсора:
$(window).on('click', function(e) {
// обработка события click...
console.log('Нажата кнопка: ' + e.which); //1 - левая кнопка, 2 - средняя кнопка, 3 - правая
console.log('Координаты курсора: x = ' + e.pageX + ' ; y = ' + e.pageY);
});
Например, повесим событие onclickна все элементы с классом btn:
$('.btn').on('click', function(){
// код обработчика события нажатия кнопки
...
});
Краткая запись события по клику:
$('.btn').click(function(){
...
});
Например, разберем, как можно скрыть блок через некоторое время после события click:
<div class="message">
...<a class="hide-message" href="#">Скрыть блок через 5 секунд</a>...
</div>
...
<script>
$('.hide-message').click(function(e){
e.preventDefault();
var _this = this;
setTimeout(function(){
$(_this).closest('.message').hide();
}, 5000);
});
</script>

jQuery - Событие при наведении (hover)

jQuery - Событие hover
Событие при поднесении курсора является сложным и состоит из 2 событий:
  • вхождения (mouseenter, mouseover);
  • покидания (mouseleave, mouseout).
События mouseenter и mouseleave в jQuery отличаются от mouseoverи mouseoutтолько тем, что они не возникают когда курсор соответственно входит и покидает внутренние элементы прослушиваемого элемента. Другими словами события mouseover и mouseoutвсплывают, а mouseenter и mouseleave– нет.
Например, изменим цвет элемента списка при поднесении к нему курсора:
<ul id="list">
<li>Ручка</li>
<li>Карандаш</li>
<li>Линейка</li>
</ul>
...
<script>
$('ul>li').
mouseenter(function(){
// при вхождении в элемент
$(this).css('color','orange');
}).
mouseleave(function(){
// при покидании элемента
$(this).css('color','black');
});
</script>
Те же самые действия, но использованиям mouseoverи mouseout:
$('ul>li').
mouseover(function(){
// при вхождении в элемент
$(this).css('color','orange');
}).
mouseout(function(){
// при покидании элемента
$(this).css('color','black');
});
Данные методы необязательно использовать вместе, их можно также применять по отдельности.
Например, подсчитаем количество посещений элемента при наступлении события "Навести мышью":
<div id="count">Количество: <span>0</span></div>
...
<script>
$('#count').mouseenter(function(){
var count = parseInt($(this).find('span').text());
count++;
$(this).find('span').text(count);
});
</script>
Вместо использования mouseenter и mouseleave можно использовать событие hover.
Например, перепишем вышеприведённый пример, используя hover:
$('ul>li').hover(
function(){
// при вхождении в элемент
$(this).css('color','orange');
},
function(){
// при покидании элемента
$(this).css('color','black');
}
);
При использовании события hover в jQuery, первый обработчик используется для задания действий при вхождении курсора в элемент (mouseenter), а второй - при покидании (mouseleave).
Если указать событию hover один обработчик, то он будет выполняться как для обработки наведения мыши, так и для её покидания.
Например:
$('h1').hover(function(){
console.log('Произошло события входа в элемент или выхода из него');
});

jQuery - Событие движения мыши

Событие mousemove посылается элементу, когда указатель мыши перемещается внутри него. Любой HTML элемент может получать это событие.
$('.target').mousemove(function(e) {
console.log('Вызван обработчик для события mousemove.');
console.log('Координаты относительно левого верхнего угла документа: ' + e.pageX + ', ' + e.pageY);
console.log('Координаты курсора внутри цели: ' + e.offsetX + ', ' + e.offsetY);
});

jQuery - Событие колёсика мыши (wheel)

Прослушивание события прокрутки колёсика (wheel) мышки можно осуществить так:
$(window).on('wheel', function(e) {
// код обработчика (например)...
console.log('Количество прокрученных пикселей: ' + e.originalEvent.deltaY);
if (e.originalEvent.deltaY < 0){
console.log('Прокручиваем вверх');
} else {
console.log('Прокручиваем вниз');
}
});
Данное событие в отличие от scroll генерируется браузером только для колёсика мышки, при этом неважно прокручивается элемент или нет, т.е. с ним можно работать на элементах с overflow, равным hidden. Еще одно отличие заключается в том, что wheel генерируется до прокрутки, а scroll- после неё.

jQuery – События клавиатуры

При нажатии клавиши клавиатуры браузер генерирует события в следующем порядке:
keydown -> keypress -> keyup


keydown(клавиша нажата, но ещё не отпущена);
keypress(событие генерируется для букв, цифр и других клавиш, за исключением управляющих) – предназначено для того чтобы получить код символа (события keydownи keyupпозволяют узнать только о коде клавиши, но не символа);
keyup(генерируется браузером при отпускании клавиши).
Например, напишем обработчик для прослушивания всех событий, которые происходят при нажатии клавиши:


<input id="target" type="text" value="">
...
<script>
$('#target').on('keydown keypress keyup', function(e) {
console.log('Тип события: ' + e.type); // keydown, keypress, keyup
console.log('Код нажатой клавиши или символа: ' + e.which); // код символа позволяет получить только keypress
console.log('Нажата клавиша Alt: ' + e.altKey);
console.log('Нажата клавиша Ctrl: ' + e.ctrlKey);
console.log('Нажата клавиша Shift: ' + e.shiftKey);
console.log('Нажата клавиша Cmd (osMac): ' + e.metaKey);
});
</script>
Пример, в котором показано, как можно прослушать событие keypress и узнать, нажато ли указанное сочетание клавиш:
$(document).keypress("c",
function(e) {
if(e.ctrlKey) {
console.log('Нажато сочетание клавиш Ctrl+c');
}
});
Пример, как можно прослушать сочетание клавиш Ctrl+Enter:
$(document).keydown(function(e) {
// с поддержкой macOS X
if ((e.ctrlKey || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) {
// ваши действия...

}
}
Пример, с использованием событий keydown и keyup:
<input id="name" type="text">
...
<script>
$('#name').
keydown(function(){
$(this).css('background-color', 'yellow');
}).
keyup(function(){
$(this).css('background-color, 'pink');
});
</script>

jQuery – События элементов формы

В jQuery можно выделить следующие события для элементов формы и не только:
  • focus (focusin)
  • blur (focusout)
  • change
  • input (для текстовых элементов формы)
  • select
  • submit

jQuery - События получения и потери фокуса

Событие focus посылается элементу, когда он получает фокус. Данное событие генерируется для элементов input, selectи ссылок (a href="..."), а также любых других элементов, у которых установлено свойство tabindex. Получение элементом фокуса обычно осуществляется посредством клика или нажатия клавиши Tab на клавиатуре. Событие focus не всплывает.
Кроме focus есть ещё похожее событие, называется оно focusin. В отличие от focusданное событие всплывает, и оно может, например, использоваться для обнаружения события фокуса родительскими элементами.
Событие blur посылается элементу, когда он теряет фокус. Так же как и focus, событие blurимеет похожее событие focusout. Данное событие отличается от blur тем, что оно может всплывать. Это возможность можно использовать, например, для получения его на родительских элементах, а не только на том элементе, который его вызвал (target).
Например, при получении элементом divсобытия фокуса установим ему фон оранжевого цвета:
<div id="demo"><input type="text"></div>
...
<script>
$('#demo').
focusin(function(){
$(this).css('background-color','orange');
})
.focusout(function(){
$(this).css('background-color','inherit');
});
</script>
Точно такое же выполнить с помощью событий focus и blur не получится, т.к. они не всплывают:
$('#demo input').
focus(function(){
$(this).parent().css('background-color','orange');
})
.blur(function(){
$(this).parent().css('background-color','inherit');
});

jQuery - Событие изменения (change)

Событие change предназначено для регистрации изменения значения элементов input, textarea и select. Для элементов select, checkboxes, и radi oкнопок данное событие возникает сразу (т.е. как только пользователь делает какой-то выбор). Но для других элементов данное событие не будет происходить до тех пор, пока этот элемент не потеряет фокус.
Пример использования события changeдля слежения за состоянием элемента checkbox. Доступность кнопки будет определять в зависимости от того в каком состоянии (checked или нет) находиться флажок:
<input type="checkbox" id="agree">
<button type="submit" id="send" disabled>Отправить</button>
...
<script>
$(function() {
$('#agree').on('change', function(){
if (this.checked) {
$('#send').prop('disabled',false);
} else {
$('#send').prop('disabled',true);
}
});
});
</script>
Пример, в котором рассмотрим, как получить значение элемента selectпри его изменении:
<p id="result"></p>
<select id="list">
<option value="1">Первое значение</option>
<option value="2">Второе значение</option>
<option value="3">Третье значение</option>
</select>
...
<script>
$(function() {
$('#list').on('change', function(){
var value = $(this).val();
$('#result').text(value);
});
});
</script>
Пример, в котором рассмотрим, как получить все выбранные элементы select при его изменении:
<select name="colors" multiple="multiple">
<option>Зелёный</option>
<option>Красный</option>
<option>Синий</option>
<option>Коричневый</option>
<option>Фиолетовый</option>
<option>Серый</option>
</select>
...
<script>
$('select[name="colors"]')
.change(function () {
var colors = [];
$('select[name="colors"] option:selected').each(function() {
colors.push($(this).text());
});
console.log('Выбранные цвета: ' + colors.join());
})
.change();
</script>
Пример программного вызова события changeдля элемента select:
// list - id элемента change
$('#list').trigger('change');
// краткая запись
$('#list').change();
// вызов только обработчика события change
$('#list').triggerHandler('change');
Пример использования события изменения change для получения значения элемента input:
<input type="text" name="name">
...
<script>
$(function() {
// событие изменения значения input (возникает только после потери фокуса)
$('input[name="name"]').on('change',
function(){
var value = $(this).val();
console.log(value);
});
});
</script>
Но кроме события change есть ещё событие input для текстовых элементов. Данное событие в отличие от change генерируется сразу, а не после того как данный элемент потеряет фокус.
Пример, использования события ввода для получения значения элемента input:
$('input[name="name"]').on('input',function(){
var value = $(this).val();
console.log(value);
});
Пример, в котором представлен один из способов получения значения элемента textarea:
<textarea name="text"></textarea>
...
<script>
$('textarea[name="text"]').on('input',function(){
var value = $(this).val();
console.log(value);
});
</script>
Пример, в котором рассмотрим, как с помощью события changeполучить значение выбранного элемента inputс type, равным radio:
<input type="radio" name="platform" checked="checked" value="windows">Windows
<input type="radio" name="platform" value="linux">Linux
<input type="radio" name="platform" value="macos">macOS
...
<script>
$(function() {
$('input[name="platform"]').on('change', function(){
var value = $(this).val();
console.log(value);
});
});
</script>

jQuery - Событие выбора (select)

Событие выбора select генерируется браузером, когда пользователь внутри элементов input с type="text" или textarea выделяет текст.
$( "#target" ).select(function() {
console.log('Вызван обработчик события select');
});

jQuery – Событие отправки формы (submit)

Событие submitвозникает у элемента, когда пользователь пытается отправить форму. Данное событие может быть добавлено только к элементам form.
Пример, использования события submit:
<form id="feedback" action="action.php">
...
</form>
...
<script>
$('#feedback').submit(function(e) {
// отменить отправку формы
e.preventDefault();
// другие действия, например, для отправки формы по ajax...

});
</script>
Программный вызов отправки формы:
$('#feedback').submit();
$('#feedback').trigger('submit');

jQuery - Событие прокрутки (scroll)

Для отслеживания состояния скроллинга в jQuery используется событие scroll.
Например, повесим на событие прокрутки страницы функцию, которая будет отображать элемент с классом scrollup, если величина прокрутки больше 200px и скрывать его, если значение прокрутки меньше этой величины.
// краткая запись функции
$(window).scroll(function() {
// действия при скроллинге страницы...
if ($(this).scrollTop()>200) {
$('.scrollup').fadeIn();
} else {
$('.scrollup').fadeOut();
}
});

jQuery - Событие изменения размеров окна (resize)

Для прослушивания события изменения окна браузера используется resize:
Например, создадим обработчик, который будет при изменении окна браузера выводить в конец страницы её ширину и высоту:
$(window).resize(function() {
$('body').append('<p>Ширина x Высота = ' + window.innerWidth + ' x ' + window.innerHeight + '</p>');
});

jQuery - Отмена стандартного поведения события

Некоторые элементы в HTML имеют стандартное поведение. Например, когда пользователь нажимает на ссылку, он переходит по адресу указанному в атрибуте href. Если вам это действие не нужно, то его можно отменить. Для отмены стандартного поведения необходимо вызвать в обработчике этого события метод preventDefault объекта event.
Например, отменим стандартное поведение всех ссылок на странице, имеющих класс service:
$('a.service').on('click',function(e){
//отменяем стандартное действие браузера
e.preventDefault();
// действия, которые будет выполнять ссылка
...
});

Что такое всплытие и как его остановить

jQuery - Как работает метод stopPropagation
Кроме отмены стандартного действия, в механизме событий есть ещё такое понятие как всплытие. Оно заключается в том, что когда браузер генерирует событие, то он это делает не только для текущего элемента (цели), но и для всех его потомков включая родителя:
текущий элемент (цель) -> родительский элемент цели -> прародительский элемент -> ... -> document -> window
В jQuery бывают сценарии, когда в представленной цепочке у какого-нибудь элемента то же есть обработчик для этого события, который выполнять не нужно. И чтобы это событие не распространилось на этот элемент, его необходимо остановить. Для этого в обработчике нужного элемента необходимо вызвать метод stopPropagationобъекта event. После вызова этого метода событие остановится, и не будет всплывать.
Например, необходимо чтобы при поднесении курсора к элементу с классом mark, его содержимое становилось оранжевым цветом.
<div class="mark">Некоторый текст...<span class="mark">фрагмент...</span>...продолжение...</div>
...
<script>
$('.mark').on('hover',
function(e){
e.stopPropagation();
$(this).css('color',orange);
},
function(e){
e.stopPropagation();
$(this).css('color',black);
}
});
</script>
В данном случае если не указывать метод stopPropagation, то при поднесении курсора к элементу span с классом mark данное событие возникнет не только у него, но и у всех его родительских элементов. А это в этом примере приведёт к тому, что изменится цвет не только текста, заключенного в span, но и всего абзаца.

Если вам необходимо отменить стандартное поведение браузера и остановить всплытие события, то в jQuery вместо вызова двух этих методов можно просто вернуть в качестве результата функции значение false.
$('a').on('click', function(e){
//e.preventDefault();
//e.stopPropagation();
...
return false;
});

Добавление событий к динамически созданным объектам

Для того чтобы повесить событие на элемент, которого ещё нет, можно использовать следующую конструкцию функции on:
$(document).on(eventName, selector, handler);


// document или любой другой существующий родительский элемент
// eventName - имя события
// selector - селектор, осуществляющий фильтрацию потомков, для которых необходимо запустить обработчик события
// handler - обработчик события
Это действие можно осуществить благодаря тому, что событие всплывает, и, следовательно, возникает у всех предков этого элемента. А объект, до которого всплывают все события на странице, является document. Поэтому в большинстве случаев выбирают именно его. После этого зная селектор, функция onможет программно отобрать среди элементов (элемента, который вызвал это событие (target) и всех его предков включая родителя) те, которые соответствуют ему. И затем для всех отобранных элементов выполнить указанный в функции onобработчик. Действия, посредством которых обработка события переносится на другой элемент (предок), называется в jQuery ещё процессом делегирования события.
Например, добавим событие к элементу, которого ещё нет на странице:
<button id="addButton" type="button">Добавить кнопку</button>


<script>
// при нажатии на элемент с id="addButton" добавить в начало страницы новую кнопку
$('#addButton').on('click', function(e) {
$('body').prepend('<button class="deleteMe" type="button">Удалить меня</button>');
});
// добавить событие click, которое будет выполнено для элементов, которых ещё нет на странице
$(document).on('click','.deleteMe', function() {
$(this).remove();
});
</script>
Делегирование может применяться не только для обработки событий динамически созданных объектов, но и для того чтобы не привязывать к каждому элементу (если их на странице может быть очень много) обработчик.
Например, запретим в комментариях переходить по внешним ссылкам (такие действия будем перенаправлять на страницу away):
$(document).on('click','#comment a',function(e) {
if(!(location.hostname === this.hostname || !this.hostname.length)) {
e.preventDefault();
location.href='away?link='+encodeURIComponent($(this).attr('href'));
}
});

jQuery - Удалить обработчик события

Удаление обработчиков события осуществляется с помощью метода off. При этом с помощью него можно удалить только те обработчики, которые добавлены посредством метода on.
Вызов метода off без аргументов снимет у указанных элементов все добавленные к ним обработчики событий.
Например, отключим все обработчики у элементов с классом link:
$('.link').off();
Например, удалим событие clickу всех элементов с классом link:
$('.link').off('link');
Специальный селектор (**) позволяет удалить только делегированные события с сохранением не делегированных:
$('.link').off('click','**');
Удалить только указанные делегированные события (с помощью селектора):
// добавление делегированного события
$('ul').on('click','li', function(){
// выводим в консоль контент элемента li
console.log($(this).text());
});


// удаление делегированного события
$('ul').off('click','li');
Удалить все обработчики openModal делегированного события click в пространстве имён modalдля элементов с классом show:
$('body').on('click.modal', '.show', openModal);

Создание пользовательского события

Для создания пользовательских событий в jQuery используются методы onи trigger.
Принцип создания специального события jQuery рассмотрим посредством следующего примера:
<div class="module">
<p>...</p>
<button class="success" type="button">Вызвать пользовательское событие highlight (цвет зелёный)</button>
<button class="error" type="button">Вызвать пользовательское событие highlight (цвет красный)</button>
</div>
...
<script>
// добавляем ко всем элементам p пользовательское событие, которое будет изменять цвет текста и его содержимое
// получения данных, переданных методом trigger, осуществим посредством аргументов color и title
$('.module p').on('highlight', function(e, color, title) {
$(this).css('color',color);
$(this).text('Вызвано пользовательское событие highlight ' +title);
});
// при нажатии на элемент с классом success вызвать кастомное событие highlight и передать ему параметры
$('.success').click(function(){
// используя второй аргумент передадим данные в обработчик события
$(this).closest('.module').find('p').trigger('highlight',['green','(цвет зелёный)']);
});
// при нажатии на элемент с классом error вызвать кастомное событие highlight и передать ему параметры
$('.error').click(function(){
// используя второй аргумент передадим данные в обработчик события
$(this).closest('.module').find('p').trigger('highlight',['red','(цвет красный)']);
});
</script>

Управление HTML5-плеером с помощью JavaScript

На данный момент мы изучили довольно сложный материал. Мы теперь знаем, как с помощью элементов <audio> и <video> создать решение с достаточно удовлетворительным уровнем браузерной поддержки, которое работает на большем количестве браузеров, чем сегодняшние решения на основе Flash. Совсем неплохо для необкатанной новой технологии!
Используя только HTML-разметку, это, пожалуй, все, что можно выжать из элементов <audio> и <video>. Но для обоих элементов существует обширная объектная модель JavaScript, посредством которой можно управлять воспроизведением с помощью сценариев. Более того, используя этот подход, можно даже настроить некоторые детали, такие как, например, скорость воспроизведения, что невозможно со стандартными аудио- и видеопроигрывателями.
В следующих разделах мы изучим поддержку JavaScript, рассмотрев практический пример - создадим специализированный видеопроигрыватель. В завершение мы рассмотрим решения, созданные другими разработчиками с использованием мощной смеси HTML5 и JavaScript, включая высокопроизводительные проигрыватели со скинами и редактируемыми субтитрами.

Создание своего видеопроигрывателя

Одним из основных поводов заняться углубленным изучением программирования элементов <audio> и <video> в JavaScript будет создание собственного проигрывателя. Основная идея заключается в простоте процесса — удаляем атрибут controls, чтобы было только окно воспроизведения, и добавляем внизу его свои кнопки управления воспроизведением. А чтобы эти кнопки функционировали, добавляем соответствующий JavaScript-код.
Любому видеопроигрывателю требуется базовый набор кнопок управления воспроизведением. Стандартные кнопки управления воспроизведением проигрывателя создаются следующим кодом:
<video id="videoPlayer" width="600" height="400">
<source src="video.mp4" type="video/mp4">
<source src="video.ogv" type="video/ogg"></video><div>
<button onclick="play()">Play</button>
<button onclick="pause()">Pause</button>
<button onclick="stop()">Stop</button>
<button onclick="speedUp()">Быстрей</button>
<button onclick="slowDown()">Медленее</button>
<button onclick="normalSpeed()">Нормальная скорость</button></div>

var video;
var display;

window.onload = function() {
video = document.getElementById("videoPlayer");
};

function play() {
video.play();
}

function pause() {
video.pause();
}

function stop() {
video.pause();
video.currentTime = 0;
}

Функции других кнопок управления воспроизведением не совсем стандартные - они используются для регулирования скорости воспроизведения путем значения свойства playbackRate. Например, при значении playbackRate равным 2 видео проигрывается вдвое быстрее нормальной скорости, но с откорректированной высотой тона, вследствие чего звук воспроизводится нормально, только вдвое быстрее.

Это замечательная возможность для ускоренного просмотра медленных видеоинструкций. Аналогично, при значении playbackRate равном 0.5 видео проигрывается со скоростью, составляющей половину нормальной скорости. При значении playbackRate равном -1 видео должно проигрываться с нормальной скоростью, только в обратном направлении, но браузеры сталкиваются с проблемой реализации этого режима должным образом. Код для реализации этих функций следующий:

function speedUp() { video.play(); video.playbackRate = 2; }
function slowDown() { video.play(); video.playbackRate = 0.5; }
function normalSpeed() { video.play(); video.playbackRate = 1; }
Задача создания индикатора хода воспроизведения представляет несколько больший интерес. В отношении разметки он создается из двух элементов <div>, один из которых вложен в другой, который находится в корневом контейнере (в данном примере тоже div):
<div class="barContainer"> 
   <div id="durationBar"> 
       <div id="positionBar">
           <span id="displayStatus">0</span>
       </div> 
   </div>
</div>
Индикатор хода воспроизведения является примером ситуации, идеально подходящей для использования элемента <progress>. Но уровень браузерной поддержки элемента <progress> все еще низкий, намного ниже, чем возможности видео HTML5. Поэтому в этом примере подобно выглядящий индикатор создается с помощью двух элементов <div>.
Внешний элемент <div> (с именем durationBar) рисует черную рамку, которая обрамляет весь индикатор и имитирует полную продолжительность видео. Внутренний элемент <div> (с именем positionBar) указывает текущую точку воспроизведения, заполняя часть черного индикатора синим цветом. Наконец, элемент <span> внутри внутреннего элемента <div> содержит текст, указывающий текущую позицию воспроизведения в секундах.

Далее приведены правила таблицы стилей, которые устанавливают размер индикаторов и окрашивают их в соответствующие цвета:

#positionBar { height: 30px; color: white; font-weight: bold; background: steelblue; text-align: center; }
#positionBar span { display: inline-block; margin-top: 5px; }
#durationBar { border: solid 1px black; width: 100%; margin-bottom: 5px; }
В процессе воспроизведения элемент <video> постоянно активирует событие onTimeUpdate. Реагируя на это событие, обновляем индикатор хода воспроизведения:
<video id="videoPlayer" onTimeUpdate="progressUpdate()">
А этот код получает от свойства currentTime значение текущей позиции в видео, разделяет его на общее время (свойство duration) и преобразует результат в процентное значение, которое используется для установления размера div-элемента postitionBar:
function progressUpdate() {
// Устанавливаем позицию воспроизведения
var positionBar = document.getElementById("positionBar");
positionBar.style.width = (video.currentTime / video.duration * 100) + "%"; // Заполняем текстовую надпись текущим значением
displayStatus = document.getElementById("displayStatus");
displayStatus.innerHTML = (Math.round(video.currentTime*100)/100) + " сек";
}

Стилизованный видеопроигрыватель HTML5

Можно сделать индикатор выполнения немного замысловатей, добавив индикатор загрузки, показывающий объем содержимого, загруженного и помещенного в буфер в данный момент. Эта возможность уже имеется в проигрывателях, встроенных в браузеры. Чтобы добавить этот индикатор, нужно обрабатывать событие onProgress и работать со свойством seekable. Дополнительную информацию о свойствах, методах и событиях элемента <video> смотрите на странице msdn.microsoft.com.

Проигрыватели на JavaScript

Если вы не хотите ни от кого зависеть в своей работе, то можете создать свой аудио- или видеопроигрыватель с чистого листа. Но это будет очень трудоемким проектом, особенно если вы хотите обеспечить разные вычурные возможности, наподобие интерактивного списка воспроизведения. К тому же, если за вашей спиной нет небольшого дизайнерского отдела, то существует отчетливая вероятность, что конечный продукт ваших усилий будет иметь слегка уродливый внешний вид (как показано выше ;).

К счастью, для веб-дизайнеров, блуждающих в поисках идеального HTML5-проигрывателя, имеется лучший вариант. Вместо того чтобы разрабатывать мультимедийный проигрыватель самому, можно взять бесплатный, настраиваемый посредством JavaScript проигрыватель в Интернете. Два из них — это VideoJS и, для фанатов библиотеки jQuery - jPlayer. Оба проигрывателя легковесные, удобны в использовании, а также имеют функцию смены скинов с помощью таблицы стилей. Например сервис VideoJS позволяет создать вручную скин медиаплеера:

Большинство проигрывателей мультимедиа на JavaScript (включая проигрыватели VideoJS и jPlayer) содержат встроенные резервные решения Flash, что позволяет разработчику сэкономить время и усилия на поиски отдельного Flash-проигрывателя. А проигрыватель jPlayer имеет удобную функцию создания списка воспроизведения, позволяющего выбрать для проигрывания или просмотра несколько файлов.
Чтобы использовать VideoJS в своих разработках, сначала загрузите его JavaScript-файлы с веб-сайта проигрывателя. Потом вставьте ссылку на его сценарий и таблицы стилей, как показано в следующем коде:
<!DOCTYPE html>
<html>
<head>
<title>...</title>
<link href="video-js.css" rel="stylesheet" type="text/css">
<script src="video.js"></script>
</head>
...

Затем используется обычный элемент <video> с несколькими элементами <source> и резервным решением Flash. (Для резервного Flash-решения в проигрывателе VideoJS используется Flowplayer, но его можно удалить и использовать другой Flash-проигрыватель.) По сути, единственная разница между обычной страницей HTML5 и страницей с проигрывателем VideoJS состоит в том, что в последней нужно использовать специальный класс в элементе <video>:

<video class="video-js vjs-default-skin"...

Учебник NodeJS 16+

Введение в NodeJS

Данный учебник посвящен NodeJS - серверному JavaScript. Перед изучением данного учебника вам уже необходимо: знать язык JavaScript, иметь установленный NodeJS, представлять себе работу протокола HTTP, уметь работать с npm, уметь работать с терминалом, уметь работать с асинхронностью, знать, что такое CommonJS и ES6 модули в JavaScript.
NodeJS - это обычный JavaScript, но работающий не в браузере на клиенте), а на сервере (на бэке). NodeJS представляет собой альтернативу языкам PHP, Python, и другим, работающим на сервере. При этом преимуществом NodeJS будет то, что JavaScript будет использоваться как на клиенте, так и на сервере. Это значит, что вам не нужно будет переключать свое внимание между двумя языками, да и вообще - для полноценного создания сайта вам потребуется знать лишь один язык.


Запуск NodeJS

В любом месте создайте файл с расширением js, например, app.js. Выведите в этом файле что-нибудь в консоль с помощью команды console.log:

console.log('test');

Откройте папку с вашим файлом в терминале. Напишите в нем следующую команду:

node app.js

В результате NodeJS выполнит код этого файла и вы увидите вывод в консоль.
Можно, конечно же, выполнять любые операции, которые возможно в JavaScript:

let a = 3; let b = 4; console.log(a + b);

В вашем файле запустите цикл и его помощью выведите в консоль числа от 1 до 10.


Таймеры в NodeJS

Давайте запустим таймер, который будет каждую секунду выводить что-нибудь в консоль:

setInterval(function() { console.log('!'); }, 1000);

Запущенный нами таймер будет работать вечно: пока мы не закроем терминал, либо не нажмем в нем Ctrl + С (попробуйте).
Запустите таймер, который каждую секунду будет выводить в консоль текущий момент времени.
Пусть дана переменная, в которой изначально хранится число 1. Запустите таймер, который каждую секунду будет увеличивать значение этой переменной на единицу и выводить это значение в консоль.


Модули ES в NodeJS

По умолчанию NodeJS использует модули по стандарту CommonJS. Так сложилось исторически. Однако, начиная с версии 13.2.0 появилась полноценная поддержка ES модулей. Так как ES модули - это официальный стандарт модулей, то я рекомендую вам пользоваться именно им.
Чтобы перейти на модули ES вам нужно разместить в папке с запускаемым вами файлом файл package.json со следующим содержимым:

{ "type": "module" }

Пример

Давайте для примера сделаем модуль math для математических операций. Разместим его код в файле math.js:

function square(num) { return num * num; } function cube(num) { return num * num * num; }

Выполним экспорт наших функций:

export function square(num) { return num * num; } export function cube(num) { return num * num * num; }

Импортируем теперь этот модуль в файл index.js:

import { square, cube } from './math.js';

Воспользуемся функциями нашего модуля:

let res = square(2) + cube(3); console.log(res);

Встроенные модули

Аналогичным образом импортируются встроенные модули. Например, импортируем модуль fs для работы с файловой системой:

import fs from 'fs';

Не обязательно импортировать все функции модуля. Можно импортировать только нужные нам:

import { open, read, close } from 'fs';

Подключение установленного через npm

Аналогичным образом импортируются модули, установленные через npm. Давайте для примера установим библиотеку underscore:

npm install underscore

Импортируем ее:

import _ from 'underscore';

В настоящее время в документациях почти всех библиотек в инструкциях подключение описано в стиле CommonJS. Вы, однако, можете сами переделать его на ES и подключить к своему проекту.
Установите библиотеку lodash. Подключите ее себе в проект и используйте несколько методов из этой библиотеки.


Подключение модуля fs в NodeJS

Сейчас мы научимся работать с файловой системой. За это в NodeJS отвечает встроенный модуль fs, который необходимо импортировать:

import fs from 'fs';

Каждый метод модуля fs существует в двух вариантах: в синхронном и в асинхронном.


Синхронный вариант

Синхронный вариант работы проще, однако он блокирует весь скрипт на время своего выполнения. К примеру, если у вас выполняется чтение файла, то весь остальной код будет ожидать, пока файл не прочитается.
При этом при чтении файла в основном загружается жесткий диск, а не процессор. То есть процессор мог бы выполнять наш код дальше, но вынужден ожидать загрузки файла.
Это имеет значение, когда у вас на NodeJS в интернете развернут сайт, на который заходят посетители.
Предположим, что при каждом заходе любого пользователя на наш сайт мы должны открыть некоторый файл. Если мы будем делать это синхронно, то при открытии одного файла все остальные пользователи нашего сайта будут ожидать пока этот файл откроется и только потом выполнение скрипта продолжится для них дальше.
В связи с этим синхронный вариант работы используется очень редко, в основном в тех случаях, когда вы делаете какой-то локальный скрипт, разворачиваемый на компьютере пользователя.


Асинхронный вариант

Работать с асинхронным вариантом сложнее, но его преимуществом является то, что он не блокирует скрипт.
Это значит, что пока с жесткого диска будет скачиваться файл, наш скрипт сможет выполнять другие операции, что существенно повышает быстродействие скрипта.
Асинхронный стиль в целом характерен для NodeJS, не только при работе с файлами. Данный подход позволяет существенно повысить быстродействие скриптов.


Асинхронная работа с fs через коллбэки в NodeJS

Давайте теперь рассмотрим асинхронную работу с файловой системой. Как правило, в NodeJS все методы модуля fs существуют в двух вариантах: в синхронном и асинхронном.
Например, для синхронного чтения файла используется метод readFileSync, а для асинхронного - readFile. Аналогично для записи файла существует пара writeFileSync и writeFile.


Асинхронное чтение файла

Метод readFile первым параметром принимает имя или путь к файлу, вторым параметром - кодировку, а третьим - коллбэк, который выполнится после чтения файла.
В коллбэк следует передавать два параметра. В первый параметр попадет объект с ошибкой, если она произойдет, а во второй - текст прочитанного файла.
Давайте для примера прочитаем текст какого-нибудь файла:

fs.readFile('readme.txt', 'utf8', function(err, data) { console.log(data); });

Дан файл с числом. Прочитайте этот файл и выведите в консоль квадрат этого числа.


Проверка асинхронности

Можно убедится в том, что чтение файла происходит асинхронно. Для этого выведем что-нибудь в консоль после работы с методом readFile:

fs.readFile('readme.txt', 'utf8', function(err, data) { console.log(data); }); console.log('!!!');

Как вы уже знаете, коллбэк выполнится, когда файл будет прочитан. А пока файл читается, код скрипта будет выполнятся дальше. Это значит, что в консоли сначала появится результат второго console.log, а потом первого.
Проверьте, что код после метода readFile будет выполнен раньше, чем будет прочитан файл.


Обработка исключительных ситуаций

Так как наш код асинхронный, то исключительные ситуации нельзя поймать через try-catch. Для обработки исключений в коллбэке предназначен первый параметр. Этот параметр будет содержать null, если исключения не случилось, или объект с ошибкой, если исключение произошло.
Давайте допишем код коллбэка так, чтобы он обрабатывал исключительные ситуации:

fs.readFile('readme.txt', 'utf8', function(err, data) { if (!err) { console.log(data); } else { console.log('ошибка', err); } });

Попробуйте прочитать несуществующий файл. Убедитесь, что при этом произойдет исключительная ситуация. Допишите ваш код так, чтобы он обрабатывал эту ситуацию.


Асинхронная запись файла

Асинхронная запись текста в файл выполняется аналогично:

fs.writeFile('readme.txt', 'text', function(err) { if (err) { console.log('ошибка'); } });

С помощью цикла создайте 10 файлов, содержащих целые числа от 1 до 10.


Асинхронное чтение нескольких файлов

Пусть у нас есть два файла с числами. Давайте найдем произведение этих чисел. Очевидно, что для этого нам нужно прочитать оба этих файла.
Но, так как код асинхронный, нам нужно читать второй файл в коллбэке первого:

fs.readFile('readme1.txt', 'utf8', function(err, data1) { if (!err) { fs.readFile('readme2.txt', 'utf8', function(err, data2) { if (!err) { console.log(data1 * data2); 
} else { 
console.log('ошибка чтения файла readme2'); } }); } else { console.log('ошибка чтения файла readme1'); } });

Даны три файла с числами. Выведите в консоль сумму этих чисел.
Даны пять файлов с числами. Выведите в консоль сумму этих чисел.


Асинхронное чтение и запись файла

Предположим нам нужно прочитать файл, сделать его текстом операцию и записать обратно в этот или другой файл. В этом случае запись в файл нужно будет делать в коллбэке чтения:

fs.readFile('readme.txt', 'utf8', function(err, data) { if (!err) { fs.writeFile('readme.txt', data + '!', function(err) { if (err) { console.log('ошибка записи файла'); } }); 
} else { 
console.log('ошибка чтения файла'); } });

Дан файл с числом. Запишите в этот файл квадрат этого числа.
Даны три файла с числами. Запишите в новый файл сумму этих чисел.


Стрелочные функции

Как правило коллбэки в NodeJS делают с помощью стрелочных функций. Это сокращает код, но несколько затрудняет понимание с непривычки.
Давайте переделаем предыдущий код на стрелочные функции:

fs.readFile('readme.txt', 'utf8', (err, data) => { if (!err) { fs.writeFile('readme.txt', data + '!', err => { if (err) { console.log('ошибка записи файла'); } }); 
} else { 
console.log('ошибка чтения файла'); } });

Дан код:

fs.readFile('readme1.txt', 'utf8', function(err, data1) { if (!err) { fs.readFile('readme2.txt', 'utf8', function(err, data2) { if (!err) { fs.writeFile('readme.txt', data1 + data2, function(err) { if (err) { console.log('ошибка записи файла'); } }); 
} else {
 console.log('ошибка чтения файла readme2'); } }); 
} else { 
console.log('ошибка чтения файла readme1'); } });

Перепишите его через стрелочные функции.


Синхронное чтение и запись файлов в NodeJS

В данном уроке мы разберемся с синхронной работой с файлами, так как она более простая для понимания.


Синхронное чтение файла

Для синхронного чтения файла используется метод readFileSync. Первым параметром он принимает имя файла или путь к файлу, а вторым - кодировку файла, как правило это utf8.
Пусть в папке с нашим скриптом располагается файл readme.txt. Давайте прочитаем его текст и выведем на экран:

let text = fs.readFileSync('readme.txt', 'utf8'); console.log(text);

Сделайте два файла, текстом которых будут некоторые числа. Напишите скрипт, который прочитает числа из файлов и выведет в консоль сумму этих чисел.


Синхронная запись файла

Для синхронной записи в файл используется функция writeFileSync. Первым параметром она принимает имя файла или путь к нему, а вторым - текст, который мы хотим записать в этот файл:

fs.writeFileSync('readme.txt', 'text');

Если файл уже существует, то его текст будет перезаписан. А если файл не существует - то он будет автоматически создан.
Дан объект:

let obj = { 'file1.txt': 'text1', 'file2.txt': 'text2', 'file3.txt': 'text3', }

С помощью цикла для каждого элемента объекта создайте файл, именем которого будет свойство элемента, а текстом - значение свойства.
Дан файл с текстом. Запустите таймер, который каждые 5 секунд в конец этого файла будет записывать восклицательный знак.


Синхронные чтение и запись файла

Можно прочитывать данные из файла, совершать над ними какую-нибудь операцию, а затем записывать обратно.
Для примера давайте прочитаем текст из файла readme.txt, добавим ему в конец знак ! и запишем измененный текст обратно в файл:

let text = fs.readFileSync('readme.txt', 'utf8'); fs.writeFileSync('readme.txt', text + '!');

Дан файл, в тексте которого записано некоторое число. Напишите скрипт, который прочитает число из файла, прибавит к нему единицу и запишет новое число обратно в файл.
Даны 3 файла с числами. Напишите скрипт, который прочитает числа из файлов, найдет их сумму и запишет ее в новый файл.

Исключительные ситуации

При работе с файловой системой могут возникать различные исключительные ситуации. Например, файл может отсутствовать, может отказать жесткий диск, диск может быть переполнен, диск может сломаться и не отвечать, у вас может не быть прав для записи в определенную папку и так далее.
Поэтому любую работу с файловой системой необходимо оборачивать в try-catch:

try { let text = fs.readFileSync('readme.txt', 'utf8'); console.log(text); } catch (err) { console.log('при чтении файла возникла ошибка', err); }

Попробуйте прочитать несуществующий файл. Убедитесь, что при этом произойдет исключительная ситуация. Допишите ваш код так, чтобы он обрабатывал эту ситуацию.


Асинхронная работа с fs через then в NodeJS

С методами модуля fs асинхронно можно работать не только через коллбэки, но и через промисы. Для этого есть специальное свойство promises, содержащее в себе промисные аналоги методов для работы с файловой системой. К примеру, для метода fs.readFile его промисный аналог будет fs.promises.readFile.


Чтение файлов

Давайте выведем в консоль содержимое какого-нибудь файла:

fs.promises.readFile('readme.txt', 'utf8').then(data => { console.log(data); });

Пусть в файле записано число. Прочитайте этот файл и выведите в консоль сумму цифр этого числа.


Обработка исключений

Добавим теперь обработку исключительных ситуаций:

fs.promises.readFile('readme.txt', 'utf8').then(data => { console.log(data); }).catch(err => { console.log('ошибка'); });

Попробуйте прочитать несуществующий файл. Убедитесь, что при этом произойдет исключительная ситуация. Допишите ваш код так, чтобы он обрабатывал эту ситуацию.


Чтение и запись

Можно прочитать файл, что-то сделать с его текстом, а потом записать обратно:

fs.promises.readFile('readme.txt', 'utf8').then(data => { return fs.promises.writeFile('readme.txt', data + '!'); }).catch(err => { console.log('ошибка'); });

Пусть в файле через запятую записаны числа. Сделайте скрипт, который запишет каждое из этих чисел в отдельный файл.
Массовая работа
Пусть у нас есть несколько файлов. Давайте прочитаем эти файлы, сольем их текст в одну строку и запишем ее в новый файл.
В отличие от коллбэков, в данном случае нам нет нужды выполнять чтение файлов по очереди. При работе с промисами мы можем записать все промисы для чтения файлов в массив, а потом воспользоваться Promise.all, чтобы осуществить запись в файл только тогда, когда все файлы будут прочитаны.
Давайте сделаем это. Пусть имена файлов у нас есть в виде массива:

let names = ['1.txt', '2.txt', '3.txt'];

Запустим цикл, в котором будем читать файлы, записывая промисы с результатами в массив:

let names = ['1.txt', '2.txt', '3.txt']; let files = []; for (let name of names) { files.push(fs.promises.readFile(name, 'utf8')); } console.log(files); // массив промисов

Имея такой массив, мы можем вызвать then только один раз, когда все промисы выполнятся:

Promise.all(files).then(data => { fs.promises.writeFile('res.txt', data.join('')); });

Добавим обработку исключительных ситуаций:

Promise.all(files).then(data => { fs.promises.writeFile('res.txt', data.join('')); }).catch(err => { console.log('что-то пошло не так'); });

Соберем весь наш код вместе:

let names = ['1.txt', '2.txt', '3.txt']; let files = []; for (let name of names) { files.push(fs.promises.readFile(name, 'utf8')); } Promise.all(files).then(data => { fs.promises.writeFile('res.txt', data.join('')); }).catch(err => { console.log('что-то пошло не так'); });

Пусть у вас есть 5 файлов с числами. Найдите сумму этих чисел и запишите в новый файл.


Сокращение fs.promises в NodeJS

Вспомним, как мы подключали модуль fs:

import fs from 'fs';

После такого подключения нам будут доступны промисные варианты методов этого модуля через fs.promises:

fs.promises.readFile('readme.txt', 'utf8').then(data => { console.log(data); });

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

import fs from 'fs/promises';

И теперь мы можем опускать раздражающее нас свойство:

fs.readFile('readme.txt', 'utf8').then(data => { console.log(data); });

Дан следующий код:

import fs from 'fs'; async function func() { let data = await fs.promises.readFile('readme.txt', 'utf8'); console.log(data); } func();

Упростите его согласно изученной теории.


Относительные пути в NodeJS

Как вы уже знаете, в параметр методов чтения или записи следует писать имя файла. Это, однако, работает только в том случае, если читаемый файл лежит в той же папке, в которой запускается наш скрипт.
Если же файл лежит в другом месте, то в параметр метода нужно писать путь к этому файлу.
Посмотрим на примерах.


Пример

Пусть у нас есть следующая структура файлов:

index.js
/directory/
readme.txt

Давайте прочитаем содержимое текстового файла. Для этого кроме имени файла нам понадобится указать еще и папку, в которой он лежит:

let path = 'directory/readme.txt'; let data = await fs.promises.readFile(path, 'utf8');


Пример

Пусть у нас есть следующая структура файлов:

/script/
index.js
readme.txt

В таком случае попытка прочитать наш файл, указав в качестве пути его имя, закончится ошибкой:

let path = 'readme.txt'; let data = await fs.promises.readFile(path, 'utf8'); // выдаст ошибку

Почему выдается ошибка? Дело в том, что мы в параметр функции написали имя файла. Это значит, что читаемый файл должен размещаться в той же папке, что и запускаемый.
Однако, наш читаемый файл находится на уровень выше, то есть в той папке, которая содержит папку со скриптом.
В таком случае мы должны явно указать в пути к файлу, что этот файл нужно искать на уровень выше. Для этого перед именем файла следует написать ../. Сделаем это:

let path = '../readme.txt'; let data = await fs.promises.readFile(path, 'utf8');

Пример

Пусть у нас есть следующая структура файлов:

/script/
index.js
/directory/
readme.txt

В этом случае при чтении файла мы сначала выйдем на уровень выше, а затем укажем путь к нашему файлу относительно этого уровня:

let path = '../directory/readme.txt'; let data = await fs.promises.readFile(path, 'utf8');


Пример

Пусть у нас есть следующая структура файлов:

/script/
/test/
index.js
/directory/
readme.txt

В этом случае нам потребуется выйти наверх два раза:

let path = '../../directory/readme.txt'; let data = await fs.promises.readFile(path, 'utf8');


Практические задачи

Напишите код, который прочитает содержимое текстового файла:

index.js
/dir1/
/dir2/
readme.txt

Напишите код, который прочитает содержимое текстового файла:

/script/
index.js
/dir1/
/dir2/
readme.txt

Напишите код, который прочитает содержимое текстового файла:

/script1/
/script2/
index.js
/dir/
readme.txt

Напишите код, который прочитает содержимое текстового файла:

/script1/
/script2/
/script3/
index.js
/dir1/
/dir2/
/dir3/
readme.txt

Имя папки со скриптом в NodeJS

Если ваш NodeJS работает в стиле CommonJS, то в файлах с вашими скриптами будет доступна константа __dirname:

console.log(__dirname);

В ES модулях, однако, эта константа была убрана. Впрочем, ее несложно получить самому. Сделаем для этого файл __dirname.js, экспортирующий нужный нам путь к папке со скриптом:

import { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); export default __dirname;

Теперь в нужно исполняемом файле мы можем импортировать созданный нами модуль и получить нужную нам константу:

import __dirname from './__dirname.js'; console.log(__dirname);

Создайте файл __dirname.js. Подключите его к своему исполняемому файлу. Выведите в консоль путь к вашей рабочей папке.

Асинхронная работа с fs через async-await в NodeJS

Давайте теперь вместо then будем использовать альтернативный синтаксис async-await. В этом случае мы получим возможность обращаться с нашим асинхронным кодом также просто и удобно, как с синхронным.
Давайте сразу смотреть на примерах. Прочитаем текст файла и выведем его в консоль:

async function func() { let data = await fs.promises.readFile('readme.txt', 'utf8'); console.log(data); } func();

Добавим обработку ошибок:

async function func() { try { let data = await fs.promises.readFile('readme.txt', 'utf8'); console.log(data); } catch (err) { console.log('что-то пошло не так'); } } func();

Прочитаем три файла, сольем их текст и выведем в консоль:

async function func() { try { let data1 = await fs.promises.readFile('1.txt', 'utf8'); let data2 = await fs.promises.readFile('2.txt', 'utf8'); let data3 = await fs.promises.readFile('3.txt', 'utf8'); console.log(data1 + data2 + data3); } catch (err) { console.log('что-то пошло не так'); } } func();

Запишем текст трех файлов в новый файл:

async function func() { try { let data1 = await fs.promises.readFile('1.txt', 'utf8'); let data2 = await fs.promises.readFile('2.txt', 'utf8'); let data3 = await fs.promises.readFile('3.txt', 'utf8'); await fs.promises.writeFile('res.txt', data1 + data2 + data3); } catch (err) { console.log('что-то пошло не так'); } } func();

Пусть имена наших файлов записаны в массиве. Давайте прочитаем данные наших файлов в цикле, а затем запишем их в новый файл:

async function func() { try { let names = ['1.txt', '2.txt', '3.txt']; let data = []; for (let name of names) { data.push(await fs.promises.readFile(name, 'utf8')); } await fs.promises.writeFile('res.txt', data.join('')); } catch (err) { console.log('что-то пошло не так'); } } func();

Даны два файла с числами. Найдите сумму этих чисел и запишите результат в третий файл.
Дан массив имен файлов. Переберите этот массив циклом и создайте файлы с этими именами, записав при создании в каждый файл случайное число. После этого в цикле прочитайте содержимое всех файлов и найдите сумму их чисел. Запишите ее в новый файл.


Проверка существования файла в NodeJS

Давайте научимся проверять существование файла или папки. В современном NodeJS это делается особым образом через метод fs.access, проверяющий возможность доступа к файлу.
Для начала импортируем асинхронную часть модуля fs:

import fs from 'fs/promises';

Теперь из общей части модуля fs импортируем специальные константы:

import { constants } from 'fs';

После этого в переменной constants будет специальный объект с константами, содержащими специальные флаги, которые мы будем передавать параметром в метод access, изменяя его поведение.
Следующий флаг используется для проверки существования файла:

constants.F_OK

Давайте с помощью этого флага проверим существование файла test.txt:

fs.access('test.txt', constants.F_OK);

Наш метод access своим резульатом возвращает промис. Если файл существует, но промис зарезолвится, а если нет - то зареджектится:

fs.access('test.txt', constants.F_OK).then(() => { console.log('file exists'); }).catch(() => { console.error('file does not exists'); });

Давайте перепишем наш код через await. Для этого обернем вызов access в конструкцию try-catch:

try { await fs.access('test.txt', constants.F_OK); console.log('file exists'); } catch { console.error('file does not exists'); }

Проверьте, существует ли файл test.txt. Если существует, прочитайте его текст.


Другие флаги

В наш метод access можно передавать и другие флаги. К примеру, следующий флаг используется для проверки разрешения на чтение файла:

constants.R_OK

А следующий флаг используется для проверки разрешения на запись файла:

constants.W_OK

Давайте выполним проверку файла на возможность чтения:

try { await fs.access('test.txt', constants.R_OK); console.log('can read'); } catch { console.error('cannot read'); }

А теперь выполним проверку файла на возможность записи:

try { await fs.access('test.txt', constants.W_OK); console.log('can write'); } catch { console.error('cannot write'); }

А теперь проверим файл и на то, и на другое:

try { await fs.access('test.txt', constants.R_OK | constants.W_OK); console.log('can access'); } catch { console.error('cannot access'); }


Работа с каналами в NodeJS

Ситуация, в которой нам нужно прочитать данные из одного потока и записать их в другой, является достаточно распространенной. Для упрощения такой операции придуманы каналы (англ. pipes).
Давайте посмотрим, как это делается. Пусть у нас есть поток чтения и поток записи:

let readableStream = fs.createReadStream('readme.txt', 'utf8'); let writeableStream = fs.createWriteStream('writeme.txt');

Сделаем так, чтобы поток чтения сразу перенаправлялся в поток записи. Для этого у потоков чтения существует метод pipe:
readableStream.pipe(writeableStream);
С помощью канала прочитайте файл и запишите его данные в другой файл.


Цепочки

Методы pipe можно вызывать цепочкой друг за другом. При этом каждый вызов метода в цепочке позволяет выполнять над данными некоторые операции:

readableStream.pipe(операция).pipe(операция).pipe(операция);

Для примера давайте выполним архивацию файла. Для этого нам надо сначала считать файл, затем сжать данные и в конце записать сжатые данные в файл-архив. В этом нам поможет встроенная в NodeJS библиотека zlib:

import { createGzip } from 'zlib';

Теперь с помощью цепочки методов сначала заархивируем поток чтения, а потом запишем в поток записи:

readableStream.pipe(createGzip()).pipe(writeableStream);

Даны 10 файлов. Напишите код, который зархивирует каждый из этих файлов в свой архив.


Потоки чтения и записи в NodeJS

Представим, что у вас есть достаточно большой файл, скажем размером в 100 мегабайт. Пусть мы хотим что-то сделать с данными этого файла. Очевидно, что для этого нужно прочитать содержимое этого файла в переменную:

let data = await fs.promises.readFile('readme.txt', 'utf8'); console.log(data);

Проблема в том, что в таком случае все 100 мегабайт нашего файла попадут в оперативную память сервера. Поэтому попытка одновременно прочитать десяток-другой таких файлов запросто исчерпает доступную нам память сервера, что приведет к его падению.
Для решения такой проблемы придуманы потоки. Потоки позволяют прочитывать файл по кусочкам. Давайте посмотрим, как это делается. Для начала создадим поток чтения с помощью метода createReadStream:

let readableStream = fs.createReadStream('readme.txt', 'utf8');

Теперь прочитаем наш файл по кусочкам:

readableStream.on('data', function(chunk) { console.log(chunk); });

Сделайте файл достаточно большого размера. Прочитайте его по кусочкам и выведите каждый кусочек в консоль.


Потоки записи

Можно также создавать потоки записи. Это делается с помощью метода createWriteStream:

let writeableStream = fs.createWriteStream('writeme.txt');

Давайте что-нибудь запишем в наш поток с помощью метода write:

writeableStream.write('text1\n'); writeableStream.write('text2\n'); writeableStream.write('text3\n');

Завершим процедуру записи с помощью метода end:

writeableStream.end();

Запишите в файл столбец чисел от одного до миллиона.


Чтение и запись

Можно прочитывать один файл, записывая по частям его данные в другой файл:

let readableStream = fs.createReadStream('readme.txt', 'utf8'); let writeableStream = fs.createWriteStream('writeme.txt'); readableStream.on('data', function(chunk) { writeableStream.write(chunk); });

Дан файл большого размера. Напишите код, который сделает три копии этого файла.


Создание HTTP сервера на NodeJS

Давайте теперь развернем HTTP сервер на NodeJS . Этот сервер будет принимать запросы от браузера и отдавать в ответ HTML код страниц.
Для начала нам нужно импортировать модуль http:

import http from 'http';

После этого мы можем стартовать наш сервер с помощью метода createServer:

http.createServer();

Этот метод параметром принимает коллбэк. Этот коллбэк будет выполнятся каждый раз, когда кто-то через браузер обращается к нашему сайту:

http.createServer(() => { });

В нашем коллбэке мы должны определить два параметра. В первый параметр попадет объект с данными запроса пользователя, а во второй параметр - объект, с помощью которого мы сфомируем наш ответ, отправляемый в браузер:

http.createServer((request, response) => { });

С помощью метода write мы можем говорить, какой текст отправить в браузер:

http.createServer((request, response) => { response.write('text1'); response.write('text2'); response.write('text3'); response.end(); });

С помощью метода end мы командуем завершить наш ответ и отправить его в браузер:

http.createServer((request, response) => { response.write('text1'); response.write('text2'); response.write('text3'); response.end(); });

После запуска сервера мы должны сказать, на каком порте наш сервер будет ожидать запросы от браузера. Это делается с помощью метода listen. Давайте укажем традиционный порт 3000:

http.createServer((request, response) => { response.write('text1'); response.write('text2'); response.write('text3'); response.end(); }).listen(3000);

Теперь мы можем обратиться к нашему серверу через браузер, набрав http://localhost:3000, где после двоеточия указан заданный нами порт.
Разверните сервер, отправляющий в браузер какой-нибудь текст.


Остановка сервера

В предыдущих уроках наши скрипты отрабатывали сразу и после этого консоль была готова к вводу новых команд. С сервером это будет не так, так как он представляет собой некоторый исполняемый процесс.
Чтобы завершить такой процесс, вам нужно в терминале нажать клавиши Ctrl + C. В этом случае сервер остановится и затем его можно будет запустить опять.
Остановите запущенный сервер.


Настройка ответа сервера на NodeJS

С помощью свойства statusCode мы указываем код HTTP ответа. Как правило это код 200, сообщающий браузеру о том, что все ок и страница найдена:

http.createServer((request, response) => { response.statusCode = 200; response.write('hello world'); response.end(); });


Поменяйте код ответа на 404. В качестве текста страницы выведите текст о том, что страница не найдена.


HTTP заголовки

С помощью метода setHeader можно отправлять HTTP заголовки. Для примера давайте отправим заголовок Content-Language:

http.createServer((request, response) => { response.setHeader('Content-Language', 'ru'); response.statusCode = 200; response.write('hello world'); response.end(); });


Отправьте заголовок Cache-Control со значением 'no-cache'.

Отправка HTML

Пока, однако, браузер трактует наш ответ как обычный (plain) текст. Сделаем так, чтобы наш ответ трактовался как HTML. Для этого с помощью метода setHeader отправим соответствующий HTTP заголовок:

http.createServer((request, response) => { response.setHeader('Content-Type', 'text/html'); response.statusCode = 200; response.write('<b>hello world</b>'); response.end(); }).listen(3000);

Сделайте так, чтобы ваш сервер при запросе отдавал текущее время, оформленное в каком-нибудь теге.


Код ответа

Код ответа и заголовки можно объединить в одном методе writeHead. Первым параметром этот метод принимает код ответа, а вторым - объект с заголовками:

http.createServer((request, response) => { response.writeHead(200, {'Content-Type': 'text/html'}); response.end(); });

Дан следующий код:

http.createServer((request, response) => { response.setHeader('Content-Type', 'text/plain'); response.statusCode = 404; response.write('page not found'); response.end(); }).listen(3000);

Упростите его через метод writeHead.
Дан следующий код:

http.createServer((request, response) => { response.setHeader('Content-Type', 'text/html'); response.setHeader('Content-Language', 'ru'); response.statusCode = 200; response.write('{}'); response.end(); }).listen(3000);

Упростите его через метод writeHead.


Понимание работы сервера на NodeJS

Вам следует понимать, что наш сервер, будучи один раз запущенным, обрабатывает запросы всех пользователей нашего сайта. Этим подход NodeJS отличается от подхода PHP в котором скрипт запускается, отрабатывает и умирает.
В случае NodeJS получается, что функция-коллбэк нашего сервера вызывается на каждый запрос. Это значит, что внешние переменные этой функции будут общими для всех запросов.
Таким образом можно сделать, к примеру, счетчик запросов к нашему серверу и отдавать его значение каждому запросу:

let i = 0; http.createServer((request, response) => { response.setHeader('Content-Type', 'text/html'); response.statusCode = 200; response.write(String(++i)); response.end(); }).listen(3000);

Пусть изначально наш счетчик имеет значение 100. Каждый запрос уменьшайте это значение на единицу и отдавайте новое значение в браузер. Как только счетчик дойдет до нуля, выведите результатом запроса сообщение об этом.


Объект запроса Http сервера на NodeJS

Объект запроса request содержит данные запроса браузера к серверу. В этом объекте свойство url содержит адрес запрошенной страницы, свойство method содержит HTTP метод запроса, а свойство headers содержит массив отправленных заголовков.
Давайте выведем значения указанных свойств на экран:

http.createServer((request, response) => { console.log(request.url); console.log(request.method); console.log(request.headers); response.writeHead(200, {'Content-Type': 'text/html'}); response.write('text'); response.end(); }).listen(3000);

Пообращайтесь к серверу с различными URL. Посмотрите, что будет выводится в консоль в этом случае.
Выведите в консоль содержимое заголовка host.


Простой роутинг в NodeJS

Давайте теперь будем выдавать разное содержимое при обращении к разным URL:

http.createServer((request, response) => { if (request.url != '/favicon.ico') { if (request.url == '/page1') { response.writeHead(200, {'Content-Type': 'text/html'}); response.write('1'); response.end(); } if (request.url == '/page2') { response.writeHead(200, {'Content-Type': 'text/html'}); response.write('2'); response.end(); } if (request.url == '/page3') { response.writeHead(200, {'Content-Type': 'text/html'}); response.write('3'); response.end(); } } }).listen(3000);

Очевидно, что часть кода у нас дублируется. Давайте его упростим:

http.createServer((request, response) => { if (request.url != '/favicon.ico') { let text; if (request.url == '/page1') { text = '1'; } if (request.url == '/page2') { text = '2'; } if (request.url == '/page3') { text = '3'; } response.writeHead(200, {'Content-Type': 'text/html'}); response.write(text); response.end(); } }).listen(3000);

Как вы видите, у нас есть три адреса, доступных для запроса: /page1, /page2 и /page3. Давайте сделаем так, чтобы при обращении к любому другому адресу выдавалось сообщение о том, что страница не найдена:

http.createServer((request, response) => { if (request.url != '/favicon.ico') { let text; if (request.url == '/page1') { text = '1'; } else if (request.url == '/page2') { text = '2'; } else if (request.url == '/page3') { text = '3'; } else { text = 'page not found'; } response.writeHead(200, {'Content-Type': 'text/html'}); response.end(); } }).listen(3000);

Чтобы браузер и поисковые системы правильно обрабатывали отсутствие страницы, мы должны для существующей страницы отдавать статус 200, а для не существующей - 404. Давайте сделаем это:

http.createServer((request, response) => { if (request.url != '/favicon.ico') { let text; let status; if (request.url == '/page1') { text = '1'; status = 200; } else if (request.url == '/page2') { text = '2'; status = 200; } else if (request.url == '/page3') { text = '3'; status = 200; } else { text = 'page not found'; status = 404; } response.writeHead(status, {'Content-Type': 'text/html'}); response.end(); } }).listen(3000);

Можно упростить код следующим образом:

http.createServer((request, response) => { if (request.url != '/favicon.ico') { let text; let status = 200; if (request.url == '/page1') { text = '1'; } else if (request.url == '/page2') { text = '2'; } else if (request.url == '/page3') { text = '3'; } else { text = 'page not found'; status = 404; } response.writeHead(status, {'Content-Type': 'text/html'}); response.end(); } }).listen(3000);

Перепишите приведенный код через оператор switch-case.
Дан объект с URL-лами и соответствующими им текстами страниц:

let obj = { '/page1': '1', '/page2': '2', '/page3': '3', }

Сделайте сервер на основе этого объекта. При запросе существующего в объекте адреса отдавайте соответствующий текст, а при запросе отсутствующего - сообщение об ошибке и статус 404.


Отдача HTML страниц из файлов в NodeJS

Давайте теперь по запросу будем отдавать не какой-то текст, а корректно оформленную HTML страницу. Пусть такая страница хранится в файле page.html:

<!DOCTYPE html> 
   <html> 
      <head> 
         <meta charset="utf-8"> 
         <title>my page</title> 
      </head> 
      <body> 
         <p> my first page on NodeJS </b> 
      </body> 
   </html>

Давайте по запросу прочитаем эту страницу из файла и отправим ее в браузер:

http.createServer(async (request, response) => { if (request.url != '/favicon.ico') { let text = await fs.promises.readFile('page.html', 'utf8'); response.writeHead(200, {'Content-Type': 'text/html'}); response.write(text); response.end(); } }).listen(3000);

Как вы видите, чтение файла осуществляется асинхронным образом. В этом случае в ожидании считывания файла с диска сервер сможет обрабатывать запросы других пользователей к нашему серверу.
Дан объект с URL-лами и соответствующими им именам HTML страниц:

let obj = { '/page1': 'file1.html', '/page2': 'file2.html', '/page3': 'file3.html', }

Сделайте сервер на основе этого объекта. При запросе существующего в объекте адреса отдавайте соответствующую страницу, а при запросе отсутствующего - сообщение об ошибке и статус 404.


Убираем двойной запрос HTTP сервера на NodeJS

При выводе URL сайта вы могли обратить внимание на то, что кроме запрошенного адреса в консоль выводится еще и '/favicon.ico'.
Дело в том, что при заходе на сайт браузер автоматически запрашивает фавиконку. Это приводит к тому, что к серверу отправляется два запроса.
Наиболее правильным действием здесь будет на запрос фавиконки отдавать файл с ней. Вы, однако, еще это делать не умеете, поэтому просто сделаем условие, отсекающее двойной вывод в консоль:

http.createServer((request, response) => { if (request.url != '/favicon.ico'){ console.log(request.url); // теперь выполнится один раз response.writeHead(200, {'Content-Type': 'text/html'}); response.write('text'); response.end(); } }).listen(3000);

Такой шаг не избавляет нас от двойного запроса (он всегда будет двойной), но раздражающий двойной вывод в консоль исчезнет.
В дальнейшем мы поправим наш код так, чтобы в браузер действительно отдавалась фавиконка.
Сделайте так, чтобы у вас не было двойного вывода в консоль.


Файлы ресурсов в NodeJS

Давайте теперь научимся отдавать браузеру файлы ресурсов. Под ресурсами понимаются файлы картинок, файлы CSS стилей, файлы клиентских скриптов и так далее.
Давайте рассмотрим работу с ресурсами на примере картинок. Пусть, например, по адресу /page.html мы хотим отдавать HTML файл, а по адресу /image.png - картинку.
Сделаем это:

http.createServer(async (request, response) => { if (request.url != '/favicon.ico') { let data; let type; if (request.url === '/page.html') { data = await fs.promises.readFile('page.html', 'utf8'); type = 'text/html'; } if (request.url === '/image.png') { data = await fs.promises.readFile('image.png'); type = 'image/png'; // правильно укажем mime-тип } response.writeHead(200, {'Content-Type': type}); response.write(data); response.end(); } }).listen(3000);

Создайте файл styles.css. Отдайте его по соответствующему запросу. Не забудьте правильно указать тип данных.
Создайте файл script.js. Отдайте его по соответствующему запросу. Не забудьте правильно указать тип данных.
Разместите у себя файл с фавиконкой, назвав его favicon.ico. Уберите в вашем коде условие для блокировки двойного запроса, а вместо этого отдавайте корректную фавиконку.
Автоматические запросы браузера
Рассмотрим следующую HTML страницу:

<!DOCTYPE html> 
   <html> 
      <head> 
         <meta charset="utf-8"> 
         <title>my page</title> 
         <link rel="stylesheet" href="styles.css"> 
         <script src="script.js"></script> 
      </head> 
      <body> 
         <p> my first page on NodeJS </b> <img src="img.png"> 
      </body> 
   </html>


Как вы видите, на данной странице подключается CSS файл, JavaScript файл, а также картинка. Это значит, что когда браузер получит и прочитает HTML код нашей страницы, он автоматически отправит серверу еще 3 запроса для получения указанных файлов.
Скопируйте приведенный код HTML страницы и разместите его в файле. Отдайте этот файл браузеру по соответствующему запросу. Сделайте так, чтобы браузер получил запрошенные им файлы ресурсов, на которые ссылается наша HTML страница.


Движок сайта на NodeJS

Давайте рассмотрим структуру типичного сайта:

<!DOCTYPE html> 
   <html> 
      <head> 
         <title>page title</title> 
      </head> 
      <body> 
         <div id="wrapper"> 
            <header> common header <nav> common menu </nav> </header> 
            <main> page content </main> 
            <aside> common sidebar </aside>
            <footer> common footer </footer> 
        </div> 
      </body> 
   </html>


Как вы видите, у страницы сайта как правило есть хедер, футер, сайдбары и контент.
При этом на многостраничном сайте все страницы сайта отличаются лишь контентом и тайтлом, а хедер, футер и сайдбары остаются одинаковым.
Это создает определенные неудобства в поддержке сайта: если у вас на сайте сто страниц, то у них будет сто одинаковых хедеров, раскиданных по ста файлам. Если вам что-то нужно будет поменять в хедере сайта - вам придется сделать это сто раз! Это, конечно же, не удобно.
Поэтому в современном сайтостроении принят другой подход: создается один файл шаблона сайта, содержащий все одинаковые места, а также места, в которые будут вставляться изменяемые части, например, тайтл и контент.
При этом тайл и контент страницы хранятся в отдельных файлах. То есть получается, что для каждой страницы сайта в файле хранится только изменяющиеся тайтл и контент, а общие хедер, футер и сайдбары хранятся в общем файле с шаблоном сайта.
Когда браузер запрашивает страницу сайта, то сервер по запрошенному URL получает файл с тайтлом и контентом, затем берет файл шаблона и подставляет тайл и контент в специально обозначенные места шаблона.
В результате получается готовая HTML страница со всеми частями, которая и отправляется в браузер.
Места для вставки изменяющихся частей в шаблоне обозначаются с помощью придуманных нами команд. Например, я могу сказать, что место вставки тайтла обозначим как {% get title %}, а место вставки контента как {% get content %}. С такими командами файл шаблона может иметь следующий вид:

<!DOCTYPE html> 
   <html> 
      <head> 
         <title>{% get title %}</title> 
      </head> 
      <body> 
         <div id="wrapper"> 
            <header> common header </header> 
            <main> {% get content %} </main> 
            <aside> common sidebar </aside> 
            <footer> common footer </footer> 
        </div> 
      </body> 
   </html>


Реализация
Итак, давайте реализуем описанное. Для начала продумаем файловую структуру. Пусть в папке page лежат изменяющиеся части, причем тайтл и контент одной страницы в разных файлах в одной папке. Адрес их папки пусть соответствует URL страницы.
Пусть, к примеру, запрошен адрес /dir/name/. Тогда тайтл этой страницы будет лежать в файле /page/dir/name/title.html, а контент - в файле /page/dir/name/content.html.
В папке root разместим ресурсы сайта (для простоты их пока проигнорируем), а шаблон сайта - в файле layout.html.
Давайте теперь напишем код. Наш сервер при запросе должен прочитать файл с тайтлом, файл с контентом и файл с шаблоном. Затем нужно в тексте шаблона поменять наши спец команды на текст тайтла и текст контента. Полученную в результате страницу можно отправлять в браузер.
Реализуем описанное:

http.createServer(async (request, response) => { let lpath = 'layout.html'; let cpath = 'page' + request.url + 'content.html'; let tpath = 'page' + request.url + 'title.html'; let layout = await fs.promises.readFile(lpath, 'utf8'); let content = await fs.promises.readFile(cpath, 'utf8'); let title = await fs.promises.readFile(tpath, 'utf8'); layout = layout.replace(/\{% get content %\}/, content); layout = layout.replace(/\{% get title %\}/, title); response.writeHead(200, {'Content-Type': 'text/html'}); response.write(layout); response.end(); }).listen(3000);

Возьмите приведенный код и потестируйте его работу.
Реализуйте показ 404 страницы в случае, если URL не соответствует файлу. Пусть 404 страница хранится в файле page/404/title.html и page/404/content.html.
Добавьте к вашему коду работу с ресурсами.
Кроме тайтла и контента на страницах сайта может также изменяться мета-описание (погуглите). Реализуйте возможность добавлять его к страницам сайта.
Возьмите созданный вами в предыдущем уроке сайт из 6-ти страниц. Переделайте его в соответствии с данным уроком.
Реализуем статический сервер на NodeJS
Давайте теперь сделаем так, чтобы по URL на нашем сайте искался соответствующий ему HTML файл. Например, если запрашивается /page.html, то мы должны отдать такой же файл, а если запрашивается /dir/test.html, то мы должны отдать файл test.html из папки dir.
Давайте все наши HTML файлы разместим в папке root. Это будет корневая папка нашего сайта и поиск HTML файлов мы будем начинать относительно этой папки.
Реализуем описанное:

http.createServer(async (request, response) => { if (request.url != '/favicon.ico') { let path = 'root' + request.url'; // преобразуем URL в путь к файлу let text = await fs.promises.readFile(path, 'utf8'); response.writeHead(200, {'Content-Type': 'text/html'}); response.write(text); response.end(); } }).listen(3000);

Давайте теперь обработаем URL вида /dir/sub/. Как вы видите, в этом адресе не указано имя файла и его расширение. В интернете принято считать, что такой адрес ссылается на файл index.html, находящийся в этой папке. То есть наш URL следует трактовать как /dir/sub/index.html.
Давайте модифицируем наш код:

http.createServer(async (request, response) => { if (request.url != '/favicon.ico') { let path = 'root' + request.url'; if (await fs.promises.stat(path).isDirectory()) { path += 'index.html'; } let text = await fs.promises.readFile(path, 'utf8'); response.writeHead(200, {'Content-Type': 'text/html'}); response.write(text); response.end(); } }).listen(3000);

Если запрошенный URL не соответствует файлу на нашем сайте, очевидно, что мы должны отдавать 404 ошибку. Сделаем это:

http.createServer(async (request, response) => { if (request.url != '/favicon.ico') { let text; let status; let path = 'root' + request.url; if (await fs.promises.stat(path).isDirectory()) { path += '/index.html'; } try { status = 200; text = await fs.promises.readFile(path, 'utf8'); } catch (err) { status = 404; text = 'page not found'; } response.writeHead(status, {'Content-Type': 'text/html'}); response.write(text); response.end(); } }).listen(3000);

Рееализуйте описанный статический сервер.
Сделайте так, чтобы 404 страница тоже бралась из файла, например, из файла root/404.html.
При обращении к папке URL со слешем /dir/sub/ и без слеша считаются одинаковым /dir/sub и оба ведут на index папки. Проверьте, как наш сервер справляется с этим. Если есть какие-то проблемы - исправьте их.


Выдача ресурсов

Давайте теперь модифицируем наш сервер так, чтобы кроме HTML файлов, автоматически также выдавались запрошенные ресурсы.
Для начала давайте сделаем функцию, которая будет принимать путь к файлу и по расширению этого файла выдавать его mime тип:

function getMimeType(path) { let mimes = { html: 'text/html', jpeg: 'image/jpeg', jpg: 'image/jpeg', png: 'image/png', svg: 'image/svg+xml', json: 'application/json', js: 'text/javascript', css: 'text/css', ico: 'image/x-icon', }; let exts = Object.keys(mimes); let extReg = new RegExp('\\.(' + exts.join('|') + ')$'); let ext = path.match(extReg)[1]; if (ext) { return mimes[ext]; } else { return 'text/plain'; } }

Имея такую функцию, легко адаптировать наш сервер для выдачи файлов любого типа:

http.createServer(async (request, response) => { let text; let status; let path = 'root' + request.url; if (fs.promises.stat(path).isDirectory()) { path += '/index.html'; } try { status = 200; text = await fs.promises.readFile(path, 'utf8'); } catch (err) { status = 404; text = 'page not found'; } response.writeHead(status, {'Content-Type': getMimeType(path)}); // изменение response.write(text); response.end(); }).listen(3000);

Скопируйте функцию getMimeType из учебника. Исправьте код вашего сервера, используя эту функцию.
Сделайте сайт о вашем городе. Пусть сайт состоит из 6-ти HTML страниц. К этим страницам должен быть подключен общий CSS файл, общий JavaScript файл, добавлены картинки. На каждой странице должна быть менюшка, с помощью которой можно будет перемещаться по страницам сайта.


Убираем расширение из URL

Мы реализовали наш сервер так, что все URL (кроме адресов папок) заканчиваются расширением .html. Однако, в современном мире наличие расширения файла в адресе считается признаком дурного тона и старомодности.
Для красоты необходимо сделать так, чтобы адрес вида /page/ соответствовал файлу root/page.html, адрес вида /dir/page/ - файлу root/dir/page.html, а адрес главной страницы / - файлу root/index.html.
При этом запросы к ресурсам должны работать, как и работали.
Уберите из ваших адресов расширения HTML файлов.


Элементы в шаблоне сайта на NodeJS

В предыдущем уроке вы научились хранить макет сайта в одном файле шаблона. Как вы уже знаете, в этом файле остаются только общие части: хедер, сайдбары, футер. Как правило, это удобно, однако, HTML код этих частей может быть достаточно большим, что мешает восприятию общей структуры сайта.
Было бы удобно вынести эти части в отдельные файлы, так сказать "с глаз долой", чтобы обращаться к ним по мере необходимости, а в остальное время чтобы они не мешали чтению другого кода.
Давайте придумаем для этого специальную команду {% get element '' %}, где в кавычках будет писаться имя элемента. В этом случае наш файл шаблона превратится в нечто такое:

<!DOCTYPE html> 
   <html> 
       <head> 
           <title>{% get title %}</title> 
       </head> 
       <body> 
          <div id="wrapper"> 
              <header> {% get element 'header' %} </header> 
              <main> {% get content %} </main> 
              <aside> {% get element 'aside' %} </aside> 
              <footer> {% get element 'footer' %} </footer> 
         </div> 
      </body> 
   </html>


Давайте будем хранить элементы макета в папке elems. По запросу пользователя нам нужно будет прочитать файл с шаблоном сайта, затем найти все места-команды на вставку элементов и заменить их на тексты соответствующих файлов.
Давайте напишем код, реализующий описанное, для краткости опустив работу с тайтлом и контентом, а также ресурсами сайта:

http.createServer(async (request, response) => { let lpath = 'layout.html'; let layout = await fs.promises.readFile(lpath, 'utf8'); let reg = /\{% get element '(.+?)' %\}/g; layout = layout.replace(reg, async (match0, match1) => { return await fs.promises.readFile('elems/' + match1 + '.html', 'utf8'); }); response.writeHead(200, {'Content-Type': 'text/html'}); response.write(layout); response.end(); }).listen(3000);

Разберите приведенный мною код. Напишите текст, в котором вы объясните работу этого кода.
Добавьте этот код к коду сервера, созданному вами в предыдущем уроке.
Возьмите созданный вами сайт из 6-ти страниц и разделите шаблон сайта на элементы.

Функции в JavaScript. Классический способ создания

Статья, в которой рассмотрим, что такое функция и зачем она нужна. Разберём классический способ её объявления, параметры, аргументы и оператор return.

Что такое функция

Функция — это некоторый фрагмент кода, который можно описать один раз, а затем вызвать на выполнение в разных частях программы какое-угодно число раз.Это классическое назначение функции. В этом сценарии код, который у нас повторяется несколько раз на странице, мы выносим в функцию, а затем используем её в тех местах программы, в которых нам необходимо его выполнить.
Ещё один вариант традиционного применения функций — это когда мы их используем в качестве основных строительных блоков приложения. В этом случае поставленная задача разбивается на подзадачи, а затем каждую из них мы решаем посредством создания отдельных функций. После этого мы обычно переходим к разработке центрального кода, используя в нём все ранее разработанные функции.
В результате такая программа становится более структурированной. В неё более просто вносить различные изменения и добавлять новые возможности.
В JavaScript создать функцию можно различными способами. Это:
Function Declaration;
Function Expression;
Arrow Function;
Для начала мы рассмотрим способ создания функции с помощью Function Declaration. Function Declaration - это «классический» вариант объявления функции.

Объявление и вызов функции

Операции с функцией в JavaScript можно разделить на 2 шага:
  • объявление (создание) функции;
  • вызов (выполнение) этой функции.
1. Объявление функции. Написание функции посредством Function Declaration начинается с написания ключевого слова function. После этого указывается имя функции, круглые скобки в которых при необходимости перечисляются через запятую параметры и код функции, заключённый в фигурные скобки.
function имя (параметры) {
// код функции
}
Например:
// объявление функции someName
function someName() {
alert('Вы вызвали функцию someName!');
}
// function - ключевое слово, которое означает что мы хотим создать функцию
// someName - имя функции
// () - круглые скобки внутри которых при необходимости мы можем поместить параметры через запятую
// { ... } - код или тело функции
При составлении имени функции необходимо руководствоваться теме же правилами, что и при создании имени переменной. Т.е. оно может содержать буквы, цифры (0-9), знаки «$» и «_». В качестве букв рекомендуется использовать только буквы английского алфавита (a-z, A-Z). Имя функции, также как и имя переменной не может начинаться с цифры.
Параметров у функции может быть сколько угодно много или не быть вообще. Круглые скобки в любом случае указываются. Если параметров несколько, то их между собой необходимо разделить посредством запятой. Они позволяют более удобно (по имени) получить переданные аргументы функции при её вызове.
Код функции - это набор инструкций, заключенный в фигурные скобки, которые необходимо выполнить при её вызове. Код функции называют ещё её телом.
2. Вызов функции. Объявленная функция сама по себе не выполняется. Для того чтобы функцию запустить, её необходимо вызвать. Вызов функции осуществляется посредством указания её имени и двух круглых скобок. Внутри скобок при необходимости ей можно передать аргументы (дополнительные данные) отделяя их друг от друга с помощью запятой.
// выполнение функции, приведённой в предыдущем примере (без передачи ей аргументов)
someName();

Параметры и аргументы функции

Параметры функции - это локальные переменные функции, которые определяются на этапе объявления функции в круглых скобках. Обратиться к параметрам можно только внутри функции, снаружи они не доступны.
Аргументы функции - это значения переданные в функцию при её вызове.
Например:
// объявление функции sayWelcome (userFirstName и userLastName - это параметры)
// userFirstName - будет принимать значения 1 аргумента, а userLastName соответственно значение 2 аргумента
function sayWelcome (userFirstName, userLastName) {
console.log('Добро пожаловать, ' + userLastName + ' ' + userFirstName);
}

// вызов функции sayWelcome ('Иван' и 'Иванов' - это аргументы)
sayWelcome('Иван', 'Иванов');

// ещё один вызов функции sayWelcome ('Петр', 'Петров' - это аргументы)
sayWelcome('Петр', 'Петров');
Таким образом, параметры – это один из способов в JavaScript, с помощью которого можно получить аргументы функции.
В JavaScript при вызове функции количество аргументов не обязательно должно совпадать с количеством параметров. Если функции не был передан аргумент, который мы хотим получить с помощью параметра, то в этом случае он будет иметь значение undefined.
Например, вызовем функцию из примера, приведённого выше, без передачи ей одного аргумента:
// вызов функции sayWelcome с одним аргументом 'Петр'
sayWelcome('Петр'); // Добро пожаловать, undefined Петр
Вызов функции sayWelcome без аргументов:
// вызов функции sayWelcome без аргументов
sayWelcome(); // Добро пожаловать, undefined undefined
Специальный объект внутри функции arguments. Получить переданные функции аргументы в JavaScript можно не только с помощью параметров. Это ещё можно выполнить через специальный массивоподобный объект arguments. Он позволяет получить все аргументы, переданные в функцию, а также их количество с помощью свойства length.
function f() {
// проверим является ли arguments обычным массивом
console.log(Array.isArray(arguments));
}

f(); // false
Доступ к аргументам через arguments выполняется точно также как к элементам обычного массива, т.е. по их порядковым номерам. Таким образом, argument[0] - позволяет получить первый аргумент, arguments[1] – второй аргумент и т.д.
// объявление функции sum
function sum() {
const num1 = arguments[0] // получаем значение 1 аргумента
const num2 = arguments[1] // получаем значение 2 аргумента

console.log(num1 + num2);
}

sum(7, 4); // 11
Получение аргументов через arguments в основном используется, когда мы заранее не знаем точное их количество.
Например создадим функцию, которые будет подсчитывать сумму всех аргументов, являющихся числами:
function sum() {
let sum = 0;
// arguments.length - число аргументов
for (let i = 0, length = arguments.length; i < length; i++) {
if (typeof arguments[i] === 'number') {
sum += arguments[i];
}
}

console.log(sum);
}

sum(4, 20, 17, -6); // 35
sum('', 3, -5, 32, null); // 30
Через цикл for...of этот пример можно записать так:
function sum() {
let sum = 0;
for (argument of arguments) {
if (typeof argument === 'number') {
sum += argument;
}
}

console.log(sum);
}
При необходимости мы можем arguments преобразовать в обычный массив. Как это выполнить показано в следующем примере:
function names() {
// превращаем arguments в полноценный массив
var argsArr = Array.from(arguments);
console.log(argsArr);
}

names('Даша', 'Маша', 'Нина'); // ["Даша", "Маша", "Нина"]
В JavaScript arguments предоставляет возможность писать более гибкие функции. В них вы можете реализовать проверку количества аргументов, и, выполнение в зависимости от них разной логики.
Все переменные, созданные внутри функции и её параметры являются локальными переменными этой функции. Они доступны только внутри этой функции, а также в других функциях, вложенных в неё, если там нет переменных с такими же именами. Вне функции её локальные переменные не доступны.
function fa(a, b) {
const c = 4;
function fb(d) {
console.log(a + b + c + d);
}
fb(1);
}
fa(3, 5); // 13
fa(4, 7); // 16

// параметр a не доступен за пределами функции fa
console.log(a); // Uncaught ReferenceError: a is not defined
При этом внешняя переменная или функция будет доступна внутри неё.
const a = 10;
function fa(b) {
console.log(a + b);
}

fa(7); // 17

Передачи одной функции в другую. Колбэки

Передаче одной функции в другую в основном используется для создания колбэков. Колбэк (callback) - это функция, которая должна быть выполнена после завершения работы другой функции. Её ещё называют функцией обратного вызова. Колбэки в основном применяются в асинхронном коде.
// функция, которая будет выводить в консоль то, что ей передали в виде аргумента
function outputResult(result) {
console.log(result);
}

// функция, которая принимает на вход 2 аргумента и колбэк
function sum(num1, num2, callback) {
// вычислим сумму 2 значений и сохраним его в result
const result = num1 + num2;

// вызовем колбэк
callback(result);
}

sum (5, 11, outputResult);
В JavaScript это возможно, т.к. функция в этом языке является значением. Её можно передавать в другие функции в виде аргументов. По сути, функция в JavaScript - это определённый тип объектов, который можно вызывать.
Узнать является ли некоторый идентификатор функцией можно с помощью typeof:
function myfunc() {};

console.log(typeof myfunc); // function
Например, проверим является ли колбэк функцией перед тем как его вызвать:
function sum(num1, num2, callback) {
const result = num1 + num2;

if (typeof callback === 'function') {
callback(result);
}
}

Возвращаемое значение (return)

Оператор return предназначен для возвращения значения выражения в качестве результата выполнения функции. Значение выражения должно быть указано после ключевого слова return.
function f() {
return выражение;
}
Если оно не указано, то вместо этого значения будет возвращено undefined.
Без использования return:
function sum(a, b) {
const result = a + b;
}

// вызовем функцию и сохраним её результат в константу result
const result = sum(4, 3);
// выведем результат функции sum(4, 3) в консоль
console.log(result); // undefined
С использованием return:
function sum(a, b) {
// вернём в качестве результата сумму a + b
return a + b;
}

// вызовем функцию и сохраним её результат в константу result
const result = sum(4, 3);
// выведем результат функции sum(4, 3) в консоль
console.log(result); // 7
Инструкции, расположенные после return никогда не выполняются:
function sum(a, b) {
// вернём в качестве результата сумму a + b
return a + b;
// код, расположенный после return никогда не выполняется
console.log('Это сообщение не будет выведено в консоль');
}

sum(4, 90);

Функция, которая возвращает функцию

В качестве результата функции мы можем также возвращать функцию.
Например:
function outer(a) {
return function(b) {
return a * b;
}
}

// в one будет находиться функция, которую возвращает outer(3)
const one = outer(3);
// в two будет находиться функция, которую возвращает outer(4)
const two = outer(4);

// выведем в консоль результат вызова функции one(5)
console.log(one(5)); // 15
// выведем в консоль результат вызова функции two(5)
console.log(two(5)); // 20
Вызовы функции outer(3) и outer(4) возвращают одну и туже функцию, но первая запомнила, что a = 3, а вторая - что a = 4. Это происходит из-за того, что функции в JavaScript «запоминают» окружение, в котором они были созданы. Этот приём довольно часто применяется на практике. Так как с помощью него мы можем, например, на основе одной функции создать другие, которые нужны.
В примере, приведённом выше, мы могли также не создавать дополнительные константы one и two. А вызвать сразу после вызова первой функции вторую.
// выведем в консоль результат вызова функции one(5)
console.log(outer(3)(5)); // 15
// выведем в консоль результат вызова функции two(5)
console.log(outer(4)(5)); // 20
При создании таких конструкций нет ограничений по уровню вложенности, но с точки зрения разумности этим лучше не злоупотреблять.
Функцию, приведённую в коде мы можем также создать и так:
function outer(a) {
function inner(b) {
return a * b;
}
return inner;
}
Кроме этого в качестве результата мы можем также возвратить внешнюю функцию:
function fa() {
return 'Привет!';
}

function fb() {
return fa;
}

fb()(); // Привет!

Рекурсия

Функцию можно также вызвать внутри самой себя. Это действие в программировании называется рекурсией.
Кроме этого необходимо предусмотреть условия для выхода из рекурсии. Если это не сделать функция будет вызывать сама себя до тех пор, пока не будет брошена ошибка, связанная с переполнением стека.
Например, использование рекурсии для вычисления факториала числа:
function fact(n) {
// условие выхода из рекурсии
if (n === 1) {
return 1;
}

// возвращаем вызов функции fact(n - 1) умноженное на n
return fact(n - 1) * n;
}
console.log(fact(5)); // 120
Пример, в котором используя рекурсию выведем числа от указанного до 10:
function counter(value) {
// условие выхода из рекурсии
if (value < 10) {
console.log(value);

// возвращаем вызов функции counter(value + 1)
return counter(value + 1);
}
}

counter(1); // 1, 2, 3, 4, 5, 6, 7, 8, 9
counter(7); // 7, 8, 9

Перегрузка функций в JavaScript

Перегрузка функций в программировании – это возможность объявлять в одном месте несколько функций с одинаковыми именами. Отличаются такие функции друг от друга параметрами. Используется перегрузка функций для того, чтобы можно было вызвать подходящую под переданные аргументы функцию.
В JavaScript не реализована перегрузка функций в том виде, как это реализовано в Си или других языках. Но подобную функциональность можно имитировать в JavaScript. Для этого у нас есть всё, что для этого необходимо.
Например, того чтобы проверить имеет параметр значение или нет, мы можем проверить его значения на undefined. Узнать количества переданных аргументов функции можно через arguments.length. Определить значения параметра можно используя typeof или instanceof.
Например, создадим функцию bodyBgColor, которая будет иметь 2 режима работы. Если её вызвать без аргументов, то она будет возвращать цвет фона body. А если с текстовым аргументом, то она будет устанавливать цвет фона body.
// объявление функции bodyBgColor
function bodyBgColor(color) {
// если параметр color имеет в качестве значения строку, то установим цвет фона body
if (typeof color === 'string') {
document.body.style.backgroundColor = color;
}

// вернём в качестве результата текущий цвет фона body
return getComputedStyle(document.body).backgroundColor;
}

// получим текущий цвет body и выведем его в консоль
console.log(bodyBgColor());

// установим новый цвет фона body
bodyBgColor('green');
Пример реализации «перегруженной» функции для вычисления оптимального количества ккал, которых необходимо человеку в день:
function calculateСalories(gender, height) {
let result = gender === 'man' ? (height - 100) * 20 : (height - 105) * 19;
if (typeof arguments[2] === 'number') {
result *= arguments[2];
}
return result.toFixed(0);
}

console.log(`Оптимальное кол-во ккал: ${calculateСalories('man', 185)}`);
console.log(`Оптимальное кол-во ккал: ${calculateСalories('woman', 168, 1.2)}`);
console.log(`Оптимальное кол-во ккал: ${calculateСalories('woman', 168)}`);

Новые возможности, которые появились в ES6+ для функций

Значение параметра по умолчанию

В языке, начиная с версии ECMAScript 6+ (2015+) параметру функции можно задать значение по умолчанию.
Например, установим параметру color значение по умолчанию '#009688':
function bodyBgColor(color = '#009688') {
document.body.style.backgroundColor = color;
}

// при вызове функции без указания аргументов цвет фона body будет установлен, равным '#009688'
setBGColor();
// при указании аргумента цвет фона body будет установлен в соответствии с его значением
setBGColor('red'); // цвет фона будет равен red
До появление такой возможности в языке, задание параметру значения по умолчанию выполнялось так:
function setBGColor(color) {
// устанавливаем color значение по умолчанию '#009688', если он имеет тип undefined
color = typeof color === 'undefined' ? '#009688' : color;
// устанавливаем цвет фона body равным значению color
document.body.style.backgroundColor = color;
}

Оставшиеся параметры (rest parameters)

Если при вызове функции ей передать аргументов больше, чем у нас есть параметров, то получить оставшиеся значения можно с помощью, так называемых rest patameters.
// ...nums - оставшиеся параметры, к которым можно обратиться в данном случае по nums
function calculate(action, ...nums) {
let result = 0;
nums.forEach(function(num) {
switch (action) {
case 'sum':
result += num;
break;
case 'multiply':
result *= num;
break;
}
});

return result;
}

console.log(calculate('sum', 3, 4, 21, -4)); // 24
console.log(calculate('multiply', 1, 4, 3)); // 12

Что такое встроенные (стандартные) функции

В JavaScript имеется огромный набор встроенных (стандартных) функций. Данные функции уже описаны в самом движке браузера. Практически все они являются методами того или иного объекта.
Например, для того чтобы вызвать встроенную функцию (метод) alert, её не надо предварительно объявлять. Она уже описана в браузере. Вызов метода alert осуществляется посредством указания имени, круглых скобок и аргумента внутри них. Данный метод предназначен для вывода сообщения на экран в форме диалогового окна. Текстовое сообщение берётся из значения параметра данной функции.
// вызов функции alert
alert("Некоторый текст");
Функция в JavaScript в результате своего выполнения всегда возвращает результат, даже если он явно не определён с помощью оператора return. Этот результат значение undefined.
// 1. функция, не возвращающая никакого результата
function sayWelcome (userFirstName, userLastName) {
console.log("Добро пожаловать, " + userLastName + " " + userFirstName);
}
// попробуем получить результат у функции, которая ничего не возвращает
console.log(sayWelcome ("Иван", "Иванов"));
// 2. функция, содержащая оператор return без значения
function sayDay (day) {
day = "Сегодня, " + day;
return;
//эта инструкция не выполнится, т.к. она идёт после оператора return
console.log(day);
}
// попробуем получить результат у функции, которая содержит оператор return без значения
console.log(sayDay("21 февраля 2016г."));
Такой же результат будет получен, если для оператора return не указать возвращаемое значение.