Пишем веб сервер на чистом JavaScript

Пишем веб сервер на чистом 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.

Report Page