Записи с меткой "NodeJS"
Учебник 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-ти страниц и разделите шаблон сайта на элементы.