Сегодня расскажу как я мигрировал больше приложение с express на нативный нодовый сервер.
Идея пришла когда я взглянул сколько express тащит ненужного.
packagephobia.com/result?p=expre…
npm.anvaka.com/#/view/2d/expr…
Его API вообще кошмар, особенно с точки зрения типов.
Express патчит req и res своими пропертями и даже перезаписывает нативные. Такая конструкция очень напоминает дженгу.
Поэтому в первую очень я стал заменять специфичные для express методы на нативные.
Например, вот JSON ответ
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(data))
Не такая большая проблема указать один хедер вручную сериализовать данные.
У редиректа аналогичная структура
res.writeHead(302, { Location: url })
res.end()
Для большинства случаев двух методов writeHead и end достаточно.
Самая неприятная проблема express это отсутствие поддержки async/await. Middleware как бы могут быть асинхронными, но любое исключение приведет к зависшему роуту, а начиная с node 15 вырубит приложение.
Решают эту проблему оборачивая каждый роут в try/catch, что, конечно, не очень практично и легко забыть.
const middleware = async (req, res, next) => {
try {
await rejectedPromise
} catch (error) {
next(error)
}
}
Вернемся к началу, как создать нодовый сервер. Для примера буду использовать http. Конфигурация https не сильно отличается.
const http = require('http')
const server = http.createServer((req, res) => {
appRoute(req, res)
})
server.listen(port, host)
appRoute сам обрабатывает ошибки и может быть асинхронным. Весь код оборачивается в общий try/catch как централизованный обработчик ошибок.
const app = async (req, res) => {
try {
// app code
} catch (error) {
res.writeHead(500)
res.end(error.message)
}
}
Если использовать пакет вроде github.com/jshttp/http-er… или кастомные ошибки, можно подменять код с фолбеком на 500
res.writeHead(error.code ?? 500)
Если код ни один роут не сработал, выкидываем 404.
res.headersSent - полпулярный способ проверки, отправился ли респонз.
try {
// app code
if (res.headersSent === false) {
throw new NotFoundError();
}
} catch(error) {...}
Теперь приступим к роутам.
Отделим желток от белка
const [pathname, search] = req.url.split('?')
и взобьем венчиком
if (pathname === '/users' && req.method === 'GET') {
await usersListRoute(req, res)
}
Вуаля, во многих случаях в роутере нет необходимости.
Однако бывают требования с параметрами в роутах, к примеру, /user/:id. Воспользуемся старым добрым регэкспом
const userByIdMatch = pathname.match(/^/user/([^/]+)$/)
if (userByIdMatch) {
await userByIdRoute(req, res, { id: userByIdMatch[1] })
}
Если приложение большое и параметров много, можно воспользоваться микро роутером, с которым можно подстраивать под себя как душе угодно.
github.com/lukeed/trouter
Вот пример в sofa-api
github.com/Urigo/SOFA/blo…
Альтернатива req.query
В ноде уже давно появилcz совместимыq с вебом класс URLSearchParams. Он проще с точки зрения типов чем querystring, qs, qss и тп. Нет необходимости проверять является ли параметр массивом или строкой.
const [pathname, search] = req.url.split('?')
const searchParams = new URLSearchParams(search)
searchParams.get('id')
searchParams.getAll('item')
Альтернатива req.body
Каждый роут знает в каком формате и какие данные ему необходимы. К примеру данные переданы постом как json. Чтобы получить строку из стрима, воспользуемся микро пакетом get-stream, а затем парсим результат.
const data = JSON.parseawait getStream(req))
И наконец, что если необходимо переиспользовать готовый express middleware.
await new Promise((resolve, reject) => {
middleware(req, res, (error) => {
if (error) reject(error)
else resolve()
})
})
очепятка
const data = JSON.parse(await getStream(req))
И в заключение немного мнения. Не стоит использовать абстракции, если доступны простые решения. Разбираться в черной коробке бывает сложнее чем прочитать немного бойлерплейта.