JavaScript работает повсюду, и ваши серверы тоже должны — вот как это сделать


Эта статья была опубликована в понедельник, 22 августа 2022 года, Арда Танрикулу @ The Guild Blog

TLDR

  • Мы сделали GraphQL Yoga 2.0 платформо-агностичным, поэтому он может работать везде, где может работать JavaScript (Cloudflare Workers, Deno, Next.js. AWS Lambdas и т.д.) благодаря новому стандарту Fetch API.
  • Мы создали Ponyfills, так что он будет работать так же на старых версиях Node, в которых элементы Fetch API не доступны глобально.
  • Мы создали новую общую библиотеку, чтобы любой другой фреймворк или приложение могли добиться того же самого.
  • Давайте поможем другим фреймворкам в экосистеме перейти на эту новую библиотеку и стандарт.

В начале года мы запустили GraphQL Yoga 2.0 — серверный фреймворк для GraphQL API.

Планируя версию 2.0 Yoga, мы думали обо всем, что изменилось в экосистеме и что разработчики, использующие JavaScript, ожидают от своих серверных фреймворков сейчас и в будущем.

Одним из самых мощных трендов в экосистеме JS стало распространение новых сред и платформ, которые могут выполнять JS (Lambdas, Cloudflare Workers, Deno, Bun и т.д.). Поэтому мы решили создать единый серверный фреймворк GraphQL, который мог бы работать на любой из этих платформ.

То, что мы обнаружили в процессе, было захватывающим, и мы считаем, что это изменит то, как строятся любые JS HTTP фреймворки, без какого-либо отношения к GraphQL.

Хотя Node.js является наиболее популярной средой, множество платформ могут выполнять JavaScript, обычно имея свой собственный способ создания серверов и используя различные API.

С другой стороны, клиентская часть JavaScript с годами в основном перешла на общий набор стандартов (fetch), независимо от базовой платформы.

Именно здесь мы поняли, что WHATWG Fetch API, использующий Request, Response и ReadableStream, также может быть использован на стороне сервера, чтобы обеспечить единый API для запуска JavaScript HTTP серверов повсеместно.

Зачем нужен стандарт Fetch API?

Когда вы посылаете запрос от клиента, fetch(...requestArgs) использует объект Request, который содержит все детали (заголовки, метод и т.д.) и поток данных, необходимый для этой коммуникации. Затем вы берете объект Response, который содержит поток соединения, и обрабатываете его как вам нужно с помощью .json(), .arrayBuffer(), .formData() или просто получаете доступ к самому потоку с помощью .body() как ReadableStream.

На стороне сервера вы можете взять объект Request и обработать его теми же методами, не обращаясь к внутренностям вашей платформы.

Потоковые ответы, такие как SSE, используют ReadableStream, а для многочастных запросов (например, загрузки файлов) используется FormData, который точно так же передается из браузера или любого другого клиента, использующего Fetch API.

Вы можете видеть, как легко обрабатывать загрузку файлов;

const formData = await request.formData()
const myFile = await formData.get('myFile')
const fileContents = await myFile.text()
Вход в полноэкранный режим Выход из полноэкранного режима

Смотрите больше в нашем коде; https://github.com/dotansimha/graphql-yoga/blob/master/packages/common/src/plugins/requestParser/POSTMultipart.ts#L18

Потоковые ответы, как в примере Server Sent Events:

let interval
new Response(
  new ReadableStream({
    start() {
      interval = setInterval(() => {
        this.enqueue(`data: ${Date.now()}nn`)
      }, 1000)
    },
    cancel() {
      clearInterval(interval)
    }
  }),
  {
    headers: {
      'Content-Type': 'text/event-stream'
    }
  }
)
Вход в полноэкранный режим Выход из полноэкранного режима

См. подробнее в нашем коде; https://github.com/dotansimha/graphql-yoga/blob/master/packages/common/src/plugins/resultProcessor/push.ts#L42

Что насчет Node.js?

Несмотря на то, что многие новые платформы поддерживают стандарт Fetch API, что означает, что мы можем иметь единое решение для всех, в настоящее время в старых LTS-версиях Node.js у нас нет встроенной реализации Fetch API.

Более того, Node.js не использует потоки Web-стандарта и Fetch API в своих модулях http и https.

Поэтому мы создали пакет @whatwg-node/fetch (ранее известный как cross-undici-fetch ), который заполняет пробелы различных реализаций fetch во всех LTS версиях Node.js. Под капотом, @whatwg-node/fetch использует undici, если он доступен, или же возвращается к использованию node-fetch, с которым вы, вероятно, уже знакомы.

Если @whatwg-node/fetch импортируется в среду, в которую уже встроен Fetch API, например, Cloudflare Workers, то понифиллы не добавляются в собранный пакет приложений.

Ponyfill против Polyfill

Polyfill исправляет собственные части окружения, в то время как ponyfill просто экспортирует «исправленный» материал, не затрагивая внутренние компоненты окружения. Мы предпочитаем ponyfill, потому что он позволяет нам не ломать другие библиотеки и функциональные возможности среды.

Возможно ли иметь библиотеку, которая создает кроссплатформенный сервер?

При создании GraphQL Yoga с нуля, кросс-платформенная поддержка была одной из самых важных функций, которые мы хотели реализовать. Мы хотели создать серверную библиотеку GraphQL, которая может быть интегрирована с различными серверными фреймворками Node.js и другими JS-окружениями, такими как CF Workers и Deno, с помощью нескольких дополнительных строк кода.

После нескольких итераций нам стало ясно, что это, безусловно, возможно, и в итоге мы выпустили эту библиотеку как часть GraphQL Yoga v2.

Сам экземпляр GraphQL Yoga можно использовать непосредственно как слушатель запросов, который вы передаете в app.use Express, родной http.createServer Node, функции Next.js и другие не-Node.js среды; мы просто передаем GraphQL Yoga как слушатель событий для CF Workers self.addEventListener('fetch', yoga).

Как мы уже упоминали в части «Зачем нужен Fetch API?», серверной библиотеке не нужно заботиться о специфических для платформы деталях соединения, таких как IncomingMessage и ServerResponse Node или объекты NextApi.Request Next.js. Теперь вы можете сосредоточиться на деталях реализации вашего сервера, потребляя «универсальный стандарт» Request и возвращая «другой стандарт» Response.

Когда мы поняли, насколько хорошо это работает для наших пользователей, мы решили, что нам нужно вывести это на новый уровень и извлечь эту логику в отдельную библиотеку под названием @whatwg-node/server .

Вы просто предоставляете свой обработчик запроса, который имеет один параметр Request и ожидает возврата экземпляра Response. Сгенерированный экземпляр обработчика запроса можно интегрировать с обычными HTTP-серверами Node, Fastify, Koa, Deno, CF Workers, Next.js и т.д. с помощью нескольких строк кода.

import { createServerAdapter } from '@whatwg-node/server'
import { Request, Response } from '@whatwg-node/fetch'

const myServer = createServerAdapter({
  handleRequest(request: Request) {
    return new Response('Hello world', {
      status: 200
    })
  }
})

// Node.js
import { createServer } from 'http'

const nodeServer = createServer(myServer)
nodeServer.listen(4000)

// CF Workers
self.addEventListener('fetch', myServer)

// Next.js
export default myServer

// Deno
serve(myServer, { addr: ':4000' })
Вход в полноэкранный режим Выход из полноэкранного режима

Как это выглядит в реальном использовании сегодня?

Вы можете проверить репозиторий GraphQL Yoga, чтобы увидеть, как мы используем эту библиотеку;

https://github.com/dotansimha/graphql-yoga/blob/no-more-node/packages/graphql-yoga/src/server.ts#L628

И простоту интеграций в наших примерах;

https://github.com/dotansimha/graphql-yoga/tree/no-more-node/examples

Наконец, насколько мал код, когда мы хотим обработать объекты запроса и ответа;

https://github.com/dotansimha/graphql-yoga/tree/no-more-node/packages/graphql-yoga/src/plugins

В GraphQL Yoga нет буквально ничего специфичного для конкретной платформы, и это позволяет нам сосредоточиться на создании хорошей реализации GraphQL Server для всей экосистемы GraphQL JS.

Node.js Server Frameworks, Routers & Middlewares

Node.js решил проблемы за несколько лет до того, как это смогли сделать веб-стандарты. Тем не менее, мы считаем, что многие идеи серверной стороны, вдохновленные Node.js, теперь легко могут быть реализованы на JavaScript с помощью Fetch, Web Streams и других веб-стандартов в экосистеме JavaScript.

Существует множество зрелых библиотек, таких как Fastify, Koa, Express и Hapi, которые реализованы только для Node.js без использования стандарта Fetch API. Опыт работы с этими библиотеками в нынешнюю эру Node.js научил нас многому о том, как может быть спроектирован сервер, но, возможно, пришло время сократить специфичные для среды API в экосистеме JS.

Основной причиной для использования серверного фреймворка обычно является «Маршрутизация», а затем «Средние устройства», поэтому возникает вопрос: «Почему мы не можем сделать это с помощью Fetch API?».

В принципе, вы можете добиться маршрутизации, как показано ниже, однако это выглядит немного небезопасно.

createServerAdapter({
  handleRequest(request: Request) {
    if (request.url.endsWith('/hello')) {
      return new Response('{ "message": "hello" }', {
        status: 200,
        headers: {
          'Content-Type': 'application/json'
        }
      })
    }
    if (request.url.endsWith('/secret')) {
      return new Response('No way!', {
        status: 401
      })
    }
    return new Response('Nothing here!', {
      status: 404
    })
  }
})
Вход в полноэкранный режим Выйти из полноэкранного режима

Существует другая библиотека под названием itty-router, которая может быть использована для маршрутизации с помощью Fetch API. Вы можете видеть, как просто добиться «маршрутизации» независимым от платформы способом.

import { Router } from 'itty-router'
import { createServerAdapter } from '@whatwg-node/server'

// now let's create a router (note the lack of "new")
const router = Router()

// GET collection index
router.get('/todos', () => new Response('Todos Index!'))

// GET item
router.get('/todos/:id', ({ params }) => new Response(`Todo #${params.id}`))

// POST to the collection (we'll use async here)
router.post('/todos', async request => {
  const content = await request.json()

  return new Response('Creating Todo: ' + JSON.stringify(content))
})

// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }))

// attach the router "handle" to our server adapter
const myServer = createServerAdapter({
  handleRequest: router.handle
})

// Then use it in any environment
import { createServer } from 'http'

const httpServer = createServer(myServer)
httpServer.listen(4000)
Вход в полноэкранный режим Выход из полноэкранного режима

Какие NodeJS-фреймворки вы используете сегодня?

Возможно, стоит открыть новый вопрос в их репо и посмотреть, сможем ли мы все вместе помочь им стать платформо-независимыми, сохраняя при этом поддержку старых версий Node, благодаря этой новой библиотеке.

Пожалуйста, попробуйте и дайте нам обратную связь в репозитории!

Оцените статью
devanswers.ru
Добавить комментарий