От автора: большую часть своей веб-карьеры я работал исключительно на стороне клиента. Проектирование адаптивных макетов, создание визуализаций из больших объемов данных, создание панелей управления приложений и т. д. Но мне никогда не приходилось иметь дело с маршрутизацией или HTTP-запросами напрямую. До не давнего времени.
В этом посте приводится описание того, в чем заключается разработка серверной части веб с помощью Node.js, и краткое сравнение написания простого HTTP-сервера с использованием 3 различных фреймворков: Экспресс, Koa.js и Hapi.js.
Некоторые базовые принципы
Здесь я только хочу привести краткий обзор вещей для контекста. HTTP (Hypertext Transfer Protocol) — это протокол связи, используемый в компьютерных сетях. В Интернете их много, такие как SMTP (Simple Mail Transfer Protocol), FTP (File Transfer Protocol), POP3 (Post Office Protocol 3) и так далее.
Эти протоколы позволяют устройствам с совершенно разным аппаратным / программным обеспечением связываться друг с другом, поскольку они предоставляют четко определенные форматы сообщений, правила, синтаксис и семантику и т. д. Это означает, что пока устройство поддерживает определенный протокол, оно может связываться с любым другим устройством. в сети.
Операционные системы обычно поставляются с поддержкой сетевых протоколов, таких как HTTP, из коробки, что объясняет, почему нам не нужно отдельно устанавливать какое-либо дополнительное программное обеспечение для доступа в Интернет. Большинство сетевых протоколов поддерживают открытое соединение между двумя устройствами, что позволяет им передавать данные туда и обратно.
HTTP, на котором работает сеть, отличается. Он известен как протокол без установления соединения, потому что он основан на режиме работы запрос / ответ. Веб-браузеры отправляют на сервер запросы на изображения, шрифты, контент и т. д., но после выполнения запроса соединение между браузером и сервером разрывается.
Серверы и клиенты
Термин «сервер» может немного сбивать с толку тех, кто плохо знаком с отраслью, поскольку он может относиться как к аппаратному обеспечению (физические компьютеры, на которых размещены все файлы и программное обеспечение, требуемое веб-сайтами), так и к программному обеспечению (программе, которая позволяет пользователям получать доступ к этим файлам в Интернете).
Сегодня мы поговорим о программной части. Но сначала несколько определений. URL обозначает Universal Resource Locator и состоит из 3 частей: протокола, сервера и запрашиваемого файла.
Протокол HTTP определяет несколько методов, которые браузер может использовать, чтобы попросить сервер выполнить кучу различных действий, наиболее распространенными из которых являются GET и POST. Когда пользователь кликает ссылку или вводит URL-адрес в адресную строку, браузер отправляет серверу GET-запрос на получение ресурса, определенного в URL-адресе.
Сервер должен знать, как обрабатывать этот HTTP-запрос, чтобы получить правильный файл, а затем отправить его обратно браузеру, который его запросил. Наиболее популярное программное обеспечение веб-сервера, которое обрабатывает это Apache и NGINX.
Оба представляют собой полный пакет программ с открытым исходным кодом, которые включают в себя такие функции, как схемы аутентификации, перезапись URL-адресов, ведение журнала и проксирование, и это лишь некоторые из них. Apache и NGINX написаны на C. Технически, вы можете написать веб-сервер на любом языке. Python, Go, Ruby, этот список можно продолжаться довольно долго. Просто некоторые языки лучше делают определенные вещи, чем другие.
Создание HTTP-сервера с помощью Node.js
Node.js — это среда выполнения Javascript, построенная на движке Chrome V8 Javascript. Он поставляется с модулем http, который предоставляет набор функций и классов для построения HTTP-сервера.
Для этого простого HTTP-сервера мы также будем использовать file system, path и url, все они являются нативными модулями Node.js. Начните с импорта необходимых модулей.
1 2 3 4 |
const http = require('http') // Чтобы использовать интерфейс HTTP в Node.js const fs = require('fs') // Для взаимодействия с файловой системой const path = require('path') // Для работы с путями файлов и папок const url = require('url') // Для разрешения и парсинга URL-адресов |
Мы также создадим словарь MIME типов, чтобы мы могли назначить соответствующий MIME тип запрашиваемому ресурсу на основе его расширения. Полный список MIME типов можно найти в Internet Assigned Numbers Authority.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const mimeTypes = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.ico': 'image/x-icon', '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.json': 'application/json', '.woff': 'font/woff', '.woff2': 'font/woff2' } |
Теперь мы можем создать HTTP-сервер с помощью функции http.createServer(), которая будет возвращать новый экземпляр http.Server.
1 |
const server = http.createServer() |
Мы будем передавать функцию обработчика запроса в createServer() с объектами request и response. Эта функция вызывается каждый раз, когда к серверу поступает HTTP-запрос.
1 2 3 |
server.on('request', (req, res) => { // другие вещи, которые нам нужны }) |
Сервер запускается путем вызова метода listen для объекта сервера с номером порта, который мы хотим, чтобы сервер прослушивал, например 5000.
1 |
server.listen(5000) |
Объект request является экземпляром IncomingMessage и позволяет получить доступ к разного рода информации о запросе, такой как ответ состояния, заголовки и данные.
Объект response является экземпляром ServerResponse, который является потоком с возможностью записи, а также предоставляет многочисленные методы для отправки данных обратно клиенту.
В обработчике запросов мы хотим сделать следующее:
Парсим входящий запрос и обрабатываем без расширений
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const parsedUrl = new URL(req.url, '//node-http.glitch.me/') let pathName = parsedUrl.pathname let ext = path.extname(pathName) // Чтобы обрабатывать URL-адреса с '/' в конце, удаляя '/' и затем // перенаправляя пользователя к этому URL-адресу, используя заголовок 'Location' if (pathName !== '/' && pathName[pathName.length - 1] === '/') { res.writeHead(302, {'Location': pathName.slice(0, -1)}) res.end() return } // Если запрос выполняется к корневой директории, возвращаем index.html // В противном случае, добавляем '.html' к любому другому запросу без расширения if (pathName === '/') { ext = '.html' pathName = '/index.html' } else if (!ext) { ext = '.html' pathName += ext } |
Выполняем некоторые элементарные проверки, чтобы определить, существует ли запрошенный ресурс, и соответственно отвечаем
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Составляем валидный путь к файлу, чтобы иметь возможность получить доступ к релевантным ресурсам const filePath = path.join(process.cwd(), '/public', pathName) // Проверяем, существуют ли на сервере запрашиваемые ресурсы fs.exists(filePath, function (exists, err) { // Если ресурсы не существуют, отвечаем с 404 Not Found if (!exists || !mimeTypes[ext]) { console.log('File does not exist: ' + pathName) res.writeHead(404, {'Content-Type': 'text/plain'}) res.write('404 Not Found') res.end() return } // В противном случае отвечаем со статусом 200 OK, // и добавляем корректный заголовок content-type res.writeHead(200, {'Content-Type': mimeTypes[ext]}) // Считываем файл с компьютера и передаем в ответ const fileStream = fs.createReadStream(filePath) fileStream.pipe(res) }) |
Весь код размещен на Glitch, и вы, если хотите, можете сделать ремикс проекта.
Создание HTTP-сервера с помощью фреймворков Node.js
Среды Node.js, такие как Express, Koa.js и Hapi.js, поставляются с различными полезными функциями промежуточного программного обеспечения, в дополнение к множеству других удобных функций, которые избавляют разработчиков от необходимости писать код самим.
Лично я думаю, что полезно сначала изучить основы без фреймворков, просто для понимания того, что происходит под капотом, а затем после этого сходить с ума с любым фреймворком, который вам нравится.
Express имеет собственное встроенное промежуточное программное обеспечение для обслуживания статических файлов, поэтому код, необходимый для выполнения того же действия, что и в самом Node.js, значительно короче.
1 2 3 4 5 6 7 8 9 10 |
const express = require('express') const app = express() // Обслуживаем статические файлы вне папки 'public' app.use(express.static('public')) // Обслуживаем index.html, когда пользователь запрашивает // корневую директорию с помощью res.sendFile() app.get('/', (req, res) => { res.sendFile(__dirname + '/public/index.html') }) app.listen(5000) |
Koa.js не включает в ядре никакого промежуточного программного обеспечения, поэтому любое необходимое промежуточное программное обеспечение должно быть установлено отдельно. Последняя версия Koa.js использует асинхронные функции вместо обратных вызовов. Для обслуживания статических файлов вы можете использовать промежуточное ПО koa-static.
1 2 3 4 5 6 7 |
const serve = require('koa-static') const koa = require('koa') const app = new koa() // Обслуживаем статические файлы вне папки 'public' // По умолчанию koa-static обслуживаем файл index.html file для корневой директории app.use(serve(__dirname + '/public')) app.listen(5000) |
Для использования Hapi.js необходима настройка объекта server. Он использует плагины для расширения возможностей, таких как маршрутизация, аутентификация и так далее. Для обслуживания статических файлов нам понадобится плагин под названием inert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
const path = require('path') const hapi = require('hapi') const inert = require('inert') // Маршруты могут быть сконфигурированы для объекта сервера const server = new hapi.Server({ port: 5000, routes: { files: { relativeTo: path.join(__dirname, 'public') } } }) const init = async () => { // Команда server.register() добавляет плагин для приложения await server.register(inert) // inert добавляет обработчик папок, чтобы // указать маршрут для обслуживания нескольких файлов server.route({ method: 'GET', path: '/{param*}', handler: { directory: { path: '.', redirectToSlash: true, index: true } } }) await server.start() } init() |
У каждой из этих платформ есть свои плюсы и минусы, и они проявляются очевиднее при работе с более крупными приложениями. Выбор фреймворка будет зависеть от реальных требований проекта, над которым вы работаете.
Завершение
Если сетевая сторона всегда была для вас черным ящиком, надеюсь, эта статья может послужить полезным введением в протоколы, которые обеспечивают работу сети. Я также настоятельно рекомендую прочитать документацию по API Node.js, которая будет полезна для любого новичка в Node.js.
Автор: Chen Hui Jing
Источник: //blog.bitsrc.io/
Редакция: Команда webformyself.