Создание веб-сервера в Node.js с помощью модуля HTTP Часть 2
AlexxSafonovШаг 3 — Вывод страницы HTML из файла
Код HTML можно выводить пользователю в виде строк Node.js, но желательно загружать файлы HTML и выводить их содержимое. Так нам не нужно хранить длинные строки кода HTML в файле Node.js, за счет чего код становится более компактным, и мы получаем возможность независимо работать с разными частями сайта. Такая концепция разделения часто используется в веб-разработке, поэтому важно знать, как правильно загружать файлы HTML для их поддержки в Node.js.
Для вывода файлов HTML мы загружаем их с помощью модуля fs
и используем их данные при написании ответа HTTP.
Вначале создадим файл HTML, который будет возвращать наш веб-сервер. Создайте новый файл HTML:
touch index.html
Copy
Откройте файл index.html
в текстовом редакторе:
nano index.html
Copy
Наша веб-страница будет минимальной. Она будет иметь оранжевый фон и содержать текст приветствия в центре. Добавьте в файл следующий код:
first-servers/index.html
<!DOCTYPE html> <head> <title>My Website</title> <style> *, html { margin: 0; padding: 0; border: 0; } html { width: 100%; height: 100%; } body { width: 100%; height: 100%; position: relative; background-color: rgb(236, 152, 42); } .center { width: 100%; height: 50%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-family: "Trebuchet MS", Helvetica, sans-serif; text-align: center; } h1 { font-size: 144px; } p { font-size: 64px; } </style> </head> <body> <div class="center"> <h1>Hello Again!</h1> <p>This is served from a file</p> </div> </body> </html>
Copy
На этой веб-странице отображается две строки текста: Hello Again!
и This is served from a file
. Строки отображаются друг над другом в центре страницы. Первая строка текста отображается как заголовок, то есть она будет больше. Вторая строка текста будет немного меньше. Весь текст будет выводиться белым цветом на оранжевом фоне страницы.
Хотя это не относится к настоящей статье и серии статей, вы можете узнать больше об HTML, CSS и других технологиях создания веб-клиентов с помощью руководства Введение в веб-технологии от Mozilla.
Это весь код HTML, который нам нужен, так что теперь можно сохранить и закрыть файл, нажав CTRL+X
. Теперь мы можем перейти к коду сервера.
В этом упражнении мы будем работать с файлом htmlFile.js
. Откройте этот файл в текстовом редакторе:
nano htmlFile.js
Copy
Поскольку нам нужно прочитать файл, для начала импортируем модуль fs
:
first-servers/htmlFile.js
const http = require("http"); const fs = require('fs').promises; ...
Copy
Этот модуль содержит функцию readFile()
, которую мы будем использовать для загрузки файла HTML. Мы импортируем вариант обещания в соответствии с современными передовыми практиками работы с JavaScript. Мы используем обещания, поскольку с синтаксической точки зрения они лучше функций обратного вызова, к которым нам пришлось бы прибегнуть, если бы мы назначили fs
как require('fs')
. Дополнительную информацию о лучших практиках асинхронного программирования можно найти в нашем руководстве Написание асинхронного кода в Node.js.
Нам нужно, чтобы при отправке пользователем запроса к системе считывался наш файл HTML. Для начала изменим requestListener()
для чтения файла:
first-servers/htmlFile.js
... const requestListener = function (req, res) { fs.readFile(__dirname + "/index.html") }; ...
Copy
Мы используем метод fs.readFile()
для загрузки файла. Он использует аргумент __dirname + "/index.html"
. Специальная переменная __dirname
содержит абсолютный путь к директории запуска кода Node.js. В конце мы добавляем /index.html
, чтобы мы могли загрузить ранее созданный файл HTML.
Возвратим страницу HTML после ее загрузки:
first-servers/htmlFile.js
... const requestListener = function (req, res) { fs.readFile(__dirname + "/index.html") .then(contents => { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(contents); }) }; ...
Copy
Если обещание fs.readFile()
успешно выполняется, оно возвращает свои данные. Для этого случая мы используем метод then()
. Параметр contents
содержит данные файла HTML.
Вначале мы задаем для заголовка Content-Type
значение text/html
, чтобы сообщить клиенту, что мы возвращаем данные HTML. Затем мы пишем код состояния, показывая, что запрос выполнен успешно. В заключение мы отправляем на клиент загруженную страницу HTML с данными в переменной contents
.
Иногда метод fs.readFile()
может выполняться с ошибками, и нам нужно предусмотреть подобные случаи. Добавьте в функцию requestListener()
следующее:
first-servers/htmlFile.js
... const requestListener = function (req, res) { fs.readFile(__dirname + "/index.html") .then(contents => { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(contents); }) .catch(err => { res.writeHead(500); res.end(err); return; }); }; ...
Copy
Сохраните файл и закройте nano
, нажав CTRL+X
.
Когда в обещании возникает ошибка, оно отклоняется. Эта ситуация обрабатывается с помощью метода catch()
. Он принимает ошибку, возвращаемую fs.readFile()
, устанавливает код состояния 500
, сигнализирующий о внутренней ошибке, и возвращает пользователю сообщение об ошибке.
Запустите наш сервер с помощью команды node
:
node htmlFile.js
Copy
Откройте в браузере адрес http://localhost:8000
. Вы увидите следующую страницу:
Мы вывели пользователю страницу HTML с сервера. Теперь мы можем закрыть запущенный сервер, нажав CTRL+C
. Сделав это, мы увидим командную строку терминала.
При написании такого кода в производственной среде не всегда желательно загружать страницу HTML при каждом получении запроса HTTP. В нашем случае страница HTML занимает всего 800 байт, но на сложных сайтах размер страниц может доходить до нескольких мегабайт. Загрузка больших файлов занимает много времени. Если на вашем сайте ожидается большой трафик, лучше всего загружать файлы HTML при запуске и сохранять их содержимое. После их загрузки вы можете настроить сервер так, чтобы он прослушивал запросы адреса.
Чтобы продемонстрировать этот метод, покажем, как можно сделать сервер более эффективным и масштабируемым.
Эффективный вывод кода HTML
Вместо того чтобы загружать страницу HTML для каждого запроса, мы загрузим ее только один раз, в самом начале. Запрос будет возвращать данные, загруженные нами при запуске.
Откройте в терминале скрипт Node.js с помощью текстового редактора:
nano htmlFile.js
Copy
Добавим новую переменную, прежде чем создавать функцию requestListener()
:
first-servers/htmlFile.js
... let indexFile; const requestListener = function (req, res) { ...
Copy
При запуске программы эта переменная будет хранить содержимое файла HTML.
Изменим функцию requestListener()
. Теперь вместо загрузки файла она будет возвращать содержимое indexFile
:
first-servers/htmlFile.js
... const requestListener = function (req, res) { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(indexFile); }; ...
Copy
Далее мы изменим логику чтения файла с функции requestListener()
на момент запуска нашего сервера. Внесите следующие изменения при создании сервера:
first-servers/htmlFile.js
... const server = http.createServer(requestListener); fs.readFile(__dirname + "/index.html") .then(contents => { indexFile = contents; server.listen(port, host, () => { console.log(`Server is running on http://${host}:${port}`); }); }) .catch(err => { console.error(`Could not read index.html file: ${err}`); process.exit(1); });
Copy
Сохраните файл и закройте nano
, нажав CTRL+X
.
Код, считывающий файл, похож на написанный нами при первой попытке. Однако при успешном чтении файла мы можем сохранить его содержимое в глобальной переменной indexFile
. Мы запустим сервер с методом listen()
. Главное — загрузить файл до запуска сервера. Так функция requestListener()
гарантированно возвращает страницу HTML, поскольку переменная indexFile
больше не пустая.
Блок обработки ошибок также изменился. Если файл не удается загрузить, мы записываем ошибку и выводим ее на консоль. Затем мы закрываем программу Node.js с помощью функции exit()
без запуска сервера. Так мы видим, почему не удалось прочитать файл, и можем решить проблему и снова запустить сервер.
Мы создали разные веб-серверы, возвращающие пользователю разные типы данных. Пока что мы не использовали данные запросов для определения конкретного возвращаемого контента. Нам потребуется использовать данные запросов при настройке маршрутов или путей сервера Node.js, так что теперь мы посмотрим, как это работает.
Шаг 4 — Управление маршрутами с использованием объекта HTTP Request
Большинство посещаемых нами сайтов и используемых нами API имеют несколько конечных точек, что позволяет получать доступ к разным ресурсам. Хорошим примером является система управления книгами, которая может использоваться в библиотеке. Ей нужно будет не только управлять данными книг, но и управлять данными авторов для составления каталогов и обеспечения удобства поиска.
Хотя данные книг и авторов связаны, они представляют собой разные объекты. В подобных случаях разработчики обычно программируют каждый объект для разных конечных точек, чтобы показать пользователю API, с какими данными он взаимодействует.
Создадим новый сервер для небольшой библиотеки, который будет возвращать два разных типа данных. Если пользователь откроет адрес сервера с /books
, он получит список книг в формате JSON. Если пользователь откроет раздел /authors
, он получит список с информацией об авторах в формате JSON.
До сих пор мы возвращали одинаковые ответы на каждый получаемый запрос. Рассмотрим небольшой пример.
Запустим заново наш пример с ответом JSON:
node json.js
Copy
Отправим на другом терминале запрос cURL, как мы делали ранее:
curl http://localhost:8000
Copy
Вы увидите следующее:
Output {"message": "This is a JSON response"}
Теперь попробуем другую команду curl:
curl http://localhost:8000/todos
Copy
После нажатия Enter
вы увидите тот же самый результат:
Output {"message": "This is a JSON response"}
Мы не встраивали в функцию requestListener()
никакую специальную логику дял обработки запроса, URL которого содержит /todos
, и поэтому Node.js по умолчанию возвращает то же сообщение JSON.
Поскольку мы хотим создать небольшой сервер для управления библиотекой, мы разделим типы возвращаемых данных в зависимости от конечной точки пользователя.
Для начала закройте сервер, нажав CTRL+C
.
Откройте файл routes.js
в своем текстовом редакторе:
nano routes.js
Copy
Начнем с сохранения наших данных JSON в переменных перед функцией requestListener()
:
first-servers/routes.js
... const books = JSON.stringify([ { title: "The Alchemist", author: "Paulo Coelho", year: 1988 }, { title: "The Prophet", author: "Kahlil Gibran", year: 1923 } ]); const authors = JSON.stringify([ { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 }, { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 } ]); ...
Copy
Переменная books
— это строка, содержащая данные JSON для массива объектов книг. Каждая книга имеет заголовок или название, автора и год издания.
Переменная authors
— это строка, содержащая данные JSON для массива объектов авторов. Каждый автор имеет имя, страну рождения и год рождения.
Теперь у нас имеются данные для ответов, и мы можем начать изменение функции requestListener()
для использования желаемых маршрутов.
Вначале нужно убедиться, что все ответы нашего сервера будут иметь правильный заголовок Content-Type
:
first-servers/routes.js
... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); } ...
Copy
Далее нам нужно возвращать подходящие данные JSON в зависимости от URL, используемого пользователем. Создадим выражение switch
для URL запроса:
first-servers/routes.js
... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) {} } ...
Copy
Чтобы получить путь URL от объекта request, нам потребуется доступ к его свойству url
. Теперь мы можем добавить в выражение switch
варианты для возврата подходящего кода JSON.
Выражение switch
в JavaScript позволяет определять, какой код будет выполняться в зависимости от значения объекта или выражения JavaScript (например, от результата математической операции). Если вы не знаете или забыли, как использовать такие выражения, воспользуйтесь нашим руководством Использование выражения switch в JavaScript.
Продолжим и добавим вариант
, когда пользователь хочет получить список книг:
first-servers/routes.js
... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) { case "/books": res.writeHead(200); res.end(books); break } } ...
Copy
Мы устанавливаем код состояния 200
, указывая, что запрос обработан правильно, и возвращаем данные JSON, содержащие список книг. Добавим еще один вариант
для авторов:
first-servers/routes.js
... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) { case "/books": res.writeHead(200); res.end(books); break case "/authors": res.writeHead(200); res.end(authors); break } } ...
Copy
Как и раньше, мы используем код состояния 200
для подтверждения правильного выполнения запроса. Теперь мы возвращаем данные JSON со списком авторов.
Если пользователь попробует использовать другой маршрут, нужно вывести сообщение об ошибке. Добавим для этой цели вариант по умолчанию:
routes.js
... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) { case "/books": res.writeHead(200); res.end(books); break case "/authors": res.writeHead(200); res.end(authors); break default: res.writeHead(404); res.end(JSON.stringify({error:"Resource not found"})); } } ...
Copy
Мы используем ключевое слово default
в выражении switch
, чтобы учесть все остальные сценарии, кроме описанных в предыдущих вариантах. Мы устанавливаем код состояния 404
, указывающий, что запрошенный URL не найден. Далее мы задаем объект JSON, содержащий сообщение об ошибке.
Протестируем поведение нашего сервера. Запустите на другом терминале команду, чтобы проверить, получим ли мы список книг:
curl http://localhost:8000/books
Copy
Нажмите Enter
, чтобы получить следующий результат:
Output [{"title":"The Alchemist","author":"Paulo Coelho","year":1988},{"title":"The Prophet","author":"Kahlil Gibran","year":1923}]
Пока все хорошо. Попробуем то же самое для /authors
. Введите в терминале следующую команду:
curl http://localhost:8000/authors
Copy
После выполнения команды вы увидите следующее:
Output [{"name":"Paulo Coelho","countryOfBirth":"Brazil","yearOfBirth":1947},{"name":"Kahlil Gibran","countryOfBirth":"Lebanon","yearOfBirth":1883}]
В заключение попробуем ввести ошибочный URL, чтобы функция requestListener()
вывела сообщение об ошибке:
curl http://localhost:8000/notreal
Copy
При вводе этой команды будет выведено следующее сообщение:
Output {"error":"Resource not found"}
Закройте работающий сервер, нажав CTRL+C
.
Мы создали несколько маршрутов для предоставления пользователям разных данных. Также мы добавили ответ по умолчанию, выводящий сообщение об ошибке HTTP, если пользователь вводит неправильный URL.
Заключение
В этом обучающем руководстве мы создали несколько серверов HTTP Node.js. Вначале мы сформировали простой текстовый ответ. Затем мы перешли к возврату с сервера разных типов данных: JSON, CSV и HTML. Затем мы рассмотрели комбинирование загрузки файлов с ответами HTTP для вывода пользователю страниц HTML с сервера и создания API, использующего данные запроса пользователя для определения ответа.
Теперь вы готовы к созданию веб-серверов, которые смогут обрабатывать разнообразные запросы и ответы. Эти знания помогут вам создать сервер, возвращающий пользователю много разных страниц HTML для разных конечных точек. Также вы можете создавать собственные API.