Пишем веб сервер на чистом JavaScript
t.me/we_use_js
Уже все привыкли прибегать к фреймворкам, как в бекенде (Express, Koa, Nest, Fastify, Hapi, Sails), так и во фронтенде, даже когда они не особо нужны, и используя их, не совсем понимаешь, что именно ты делаешь на примитивном уровне. Сегодня мы напишем простой веб сервер, с роутингом, промежуточноми обработчиками (middleware) и body-парсингом, не используя ни одной зависимости, только node v10 (на других не тестил).
Структура приложения
Веб приложение мы будем описывать как класс, с методами и свойствами. Всего будет четыре свойства: routes, middleware, data и methods. Все четыре - это массивы.
const { Server } = require('http')
const { log } = console
class App {
constructor() {
this.routes = []
this.middleware = []
this.methods = ['GET', 'POST']
this.data = []
}
...
Начнём с самого простого - middleware. Тут мы просто добавляем хендлер, который берётся из аргументов метода use(), в this.middleware .
...
use(handler) {
this.middleware.push(handler)
}
...
Далее мы пропишем обработчики POST и GET запросов.
...
get(url, handler) {
this.routes.push({
method: 'GET',
url, handler
})
}
...
У POST тоже самое, только метод будет другим (то есть method: 'POST').
Но что если нам нужны другие HTTP методы, например UPDATE? Пропишем метод, добавляющий методы. Мы сделали именно список таких методов, чтобы не пропускать несуществующие в HTTP методы.
...
addMethods(methods) {
this.methods.push(methods)
}
...
Осталось самое последнее, функция listen, которая и запускает сервер, и прослушивает его.
...
listen(port = 80, callback = () => {}) {
new Server((req, res) => {
// Custom routes first
this.routes.map(r => {
this.methods.includes(r.method) && req.url === r.url ? r.handler(req, res) : null
})
// Then middlewares (if no any route matches)
this.middleware.map(h => h(req, res))
}).listen(port, callback())
}
}
Сначала мы пробегаемся по всем роутам, чтобы наш middleware не занимал их место. Далее мы смотрим, на какой URL зашёл юзер, и правильный ли метод выбран, ну и с помощью req, res мы уже обрабатываем запрос. И в конце те роуты, которые не заняты, то есть любой другой URL, который мы не расписали. После установки всех middleware и routes мы прослушиваем сервер на любом порту.
Запуск
Создание встроенного http сервера очень похоже на Express сервер.
const app = new App()
app.use((req, res) => {
res.end('Hello World')
})
app.get('/hello', (req, res) => {
res.end('Hi bros')
console.log(req.headers['user-agent'])
})
app.listen(80, () => console.log('Server started'))
Теперь попробуем посмотреть, что выдаст нам сервер.
$ curl localhost/hello Hi bros⏎ $ curl localhost/another_url Hello World⏎
И, в тот момент когда мы заходим на /hello, В выводе появляется HTTP заголовок User Agent.
$ node server Server Started curl/7.52.1
Парсинг тела запроса
body parser у нас будет написан как middleware, т.е. функция с аргументами req и res.
const bodyParser = (req, res) => {
let body = []
req
.on('data', chunk => body.push(chunk))
.on('end', () => {
res.end('ok')
req.body.push(body.toString())
})
}
Мы делаем максимально простой парсер, без json'а, просто текст. У объекта ClientRequest, то есть у req, есть события data и end. Из названия можно догадаться, первое событие возникает, когда на сервер приходят данные с клиента, будь то cURL или браузер, в байтовом виде, а второе - когда запрос завершён и все данные получены.
Теперь нужно заюзать наш bodyParser:
app.use(bodyParser)
Теперь в классе App мы создадим метод для того чтобы пушить новые данные, пришедшие с сервера.
...
pushData(data) {
this.data.push(data)
}
...
Возвращаемся к методу listen у класса App. Сюда мы впишем интервал, чтобы удостовериться что запрос пришёл. Интервал можно ставить на любое число, я поставил минимальное, потому что я параллельно запускаю сервак и curl.
...
listen(port = 80, callback = () => {}) {
new Server((req, res) => {
req.body = []
this.pushData(req.body)
setInterval(() => log(this.data), 1000)
...
Второй запуск, с парсером
Откроем ещё одно окно терминала / командной строки, и сделаем запрос на наш сервер.
$ curl -d 'hello' localhost Hello World⏎
Как видим, нам пришёл ответ с middeware'а, который мы использовали раннее. Теперь возвращаемся к окну сервера, и видим:
$ node server Server started [ [ 'hello' ], [ 'world' ] ]
Запрос добрался до нас.
Заключение
Всего лишь за 70 строчек кода мы написали базовый HTTP сервер с простым функционалом. Не всегда бывает нужно использовать тонну разных методов, которыми не будешь потом пользоваться. Моя рекомендация - для сложных задач лучше всего использовать фреймворк, для примитивных - ванильный JavaScript.