Простейший пример «Привет, мир!»
Требуемые знания и навыки
Здесь и далее подразумевается, что Вы уже умеете/знаете:
- Создавать Node.js-проект и устанавливать npm-зависимости (если нет, то Вам сначала следует изучить основы Node.js).
- Настраивать компилятор языка TypeScript
(фреймворк Yamato Daiwa Backend (далее: YDB) предполагает
использование языка TypeScript,
причём без типа
any
). - Основы Объектно-Ориентированного Программирования (ООП) — предлагаемые Yamato Daiwa Backend подходы предполагают широкое использование ООП. Хотя YDB не навязывает конкретной архитектуры, использование ООП желательно, а потому будет в следующих уроках.
Развёртывание Node.js-проекта
Создайте новый Node.js-проект и установите приведённые ниже npm-зависимости. Во избежание проблем, связанных с меняющимся программным интерфейсом по мере выхода новых версией этих зависимостей, в рамках данного урока настоятельно рекомендуется устанавливать версии, указанные в квадратных скобках.
- @yamato-daiwa/backend [0.3.0]
- Главный npm-пакет данного фреймворка
- @yamato-daiwa/es-extensions [1.7.0]
- Вспомогательная функциональность, которая актуальна как для Node.js, так и для браузерного JavaScript-а. В данном уроке нам потребуется лишь перечисление HTTP_Methods, но в будущем — гораздо больше.
- ts-node [10.9.2]
- REPL для языка TypeScript. Создаёт эффект того, что TypeScript имеет собственную среду выполнения, что позволит нам на время этого урока рутины, связанные с выходными JavaScript-файлами.
- typescript [5.5.4]
- Основной npm-пакет языка TypeScript. ts-node требует отдельной установки этого пакета (говоря терминологией npm, typescript является одноранговой зависимостью по отношению к ts-node). Хотя в современный npm устанавливает одноранговые зависимости автоматически, мы будем осознанно устанавливать конкретную версию этого пакета.
npm i @yamato-daiwa/backend@0.3.0 @yamato-daiwa/es-extensions@1.7.0
npm i ts-node@10.9.2 typescript@5.5.4 -D
Код
Это приложение отправит HTML-код, состоящий из заголовка h1
с
текстовым содержимым «Hello, world!»
в ответ на HTTP-запрос типа
GET по адресу http://127.0.0.1:80/
.
Вообще, такой код не является валидной HTML-страницей, но
в тестовых целях отправка одного только тела HTML-документа возможна — современные браузеры
его отобразят.
Перед тем, как отправить запрос, разберём код данного простейшего примера.
import { Server, Request, Response, ProtocolDependentDefaultPorts } from "@yamato-daiwa/backend";
import { HTTP_Methods } from "@yamato-daiwa/es-extensions";
Server.initializeAndStart({
initializeAndStart
класса
Server
принимает
объект конфигурации в качестве первого и единственного параметра.
Перевод имени метода с английского языка «инициализировать и запустить» вкупе с контекстом
Server
(«сервер») не должен оставлять вопросов о том, что этот
метод делает. IP_Address: "127.0.0.1",
IP_Address
сделано обязательным
не случайно, потому что Вы как инженер обязаны понимать,
по какому IP-адресу хотите сделать доступным своё серверное приложение.
Если говорить о реальном проекте, то IP-адрес будет зависеть от режима (локальная разработка,
инсценирование, продакшен).
В будущих уроках мы обсудим, как эту опцию сделать зависимой от режима, а сейчас установим значение
127.0.0.1 — первый адрес в диапазоне
частных IP-адресов. HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
ProtocolDependentDefaultPorts.HTTP
—
это номер порта по умолчанию для протокола HTTP.
Указывать его явно нужно по той же причине, что и IP_Address
, однако
поскольку полагаться на человеческую память опасно, номера портов
по умолчанию для разных протоколов были помещены в перечисление
ProtocolDependentDefaultPorts
, а от Вас лишь требуется осознавать,
на порту по умолчанию будет доступно приложение или же на каком-то другом.
routing: [
{
route: { HTTP_Method: HTTP_Methods.get, pathTemplate: "/" },
routing
, как это очевидно и должно быть очевидно при качественном
именовании, позволяет определить роутинг —
в контексте серверной веб-разработки, формирование различных
ответов в зависимости от соответствия запроса конкретным шаблонам
URI.
Мы разберём роутинг подробнее в одном из следующих уроков, а сейчас важно лишь то, что мы
будем отправлять ответ только для одного, корневого
маршрута, соответствующего URI
http://127.0.0.1:80/
.
По всем остальным маршрутам, скажем
/foo
(соответствует URI
http://127.0.0.1:80/foo
) или /bar/baz
(соответствует URI http://127.0.0.1:80/bar/baz
),
сервер отправит ответ с ошибкой «не найдено».
async handler(request: Request, response: Response): Promise<void> {
handler — обработчик запроса — определяет, какие действия нужно
выполнить, когда отправленный запрос удовлетворяет маршруту, указанному выше
в route
.
Это функция априори асинхронная (явно
или неявно возвращает экземпляр Promise
,
хотя с точки зрения ECMAScript
это не совсем
точное определение, так как функции, получающие через параметры
коллбэки — функции для их вызова по завершению какого-либо процесса —
тоже называют «асинхронными»), так как отправка ответа, которую надо в итоге сделать,
является асинхронной по своей природе.
К тому же, в обработчике запроса часто выполняются и другие асинхронные операции,
например обращение к базе данных или работа с файлами.
По сути, route
и handler
— это как
условное выражение и действие, выполняемое когда это выражение истинно.
Обычно ответ формируется в зависимости от запроса, экземпляр которого представлен первым параметром. Например, в случае GET-запроса нам могут понадобиться query parameters (дословно это означает «параметры запроса», но без контекста — URI — этот перевод может сильно запутать), а в случае POST- или PUT-запросов — доступ к данным, содержащимся с теле запроса. Однако работу с экземпляром класса Response мы отложим на следующие, более сложные уроки, а в этом простейшем примере будем всегда отправлять один и тот же ответ, потому первый параметр не будет использован.
return response.submitWithSuccess({ HTML_Content: "<h1>Hello, world!</h1>" });
Как известно, HTTP-ответ имеет статус, идентифицируемый трёхзначным шифром. Сейчас важно, что эти статусы разделяются на 5 групп:
- Информационные
- Успешные
- Перенаправления
- Клиентские ошибки
- Серверные ошибки
Метод submitWithSuccess
класса
Response
отправляет ответ с кодом из «успешного» диапазона
(по умолчанию — 200
).
Указывая свойство HTML_Content
первого и единственного параметра метода
submitWithSuccess
, мы сообщаем, что хотим
отправить HTML-код, в связи с чем фреймворк автоматически установит
нужные HTTP-заголовки.
}
}
]
});
Тестирование
Запустим приложение с помощью ts-node.
ts-node EntryPoint.ts
Если в коде нет ошибок, то Вы будете проинформированы об успешном запуске приложения следующим выводом в терминал:
Откройте указанный URI в браузере. Если Ваш терминал распознаёт ссылки, то Вы можете по ней перейти кликом мыши. В терминале отобразится как минимум один лог о новом запросе по корневому маршруту:
Возможно, будет логирован ещё один запрос по маршруту
/favicon.ico
:
Если такой лог присутствует, значит соответствующий запрос был отправлен браузером автоматически, чтобы отобразить иконку на вкладке браузера. Если бы фреймворк не был готов к такому запросу, то отправил ответ с ошибкой «не найдено», однако фреймворк YDB по умолчанию отправляет свою иконку. Разумеется, её можно заменить на другую, что мы и сделаем в одном из будущих уроков.
Поскольку localhost является «местоимением» для IP-адреса
127.0.0.1
, а 80 — порт
по умолчанию для протокола HTTP, то помимо
http://127.0.0.1:80/
мы можем отправлять запросы
на http://localhost
, http://localhost:80
или
http://127.0.0.1:80/
.
Наконец, посмотрим как фреймворк поведёт себя, когда будет отправлен запрос, обработка которого
не прописана в routing
.
Например, для запроса http://127.0.0.1:80/foo
лог будет таким:
Фреймворк сообщил, что запрашиваемый ресурс не найден. Аналогичное сообщение должен показать браузер — на основании отправленного статуса ответа 404 Not found.