В этой статье я расскажу как написал модуль под Nest.js позволяющий писать классическое RESTFull API со swagger'ом но клиент будет полностью на WebSocket'ах, звучит странно не так ли? Но зато очень быстро и удобно по итогу).
Идея состоит в том, что вы пишите классическое документированное апи, со всеми типами и плюшками.
@Get(':id')
@WsAction('user')
getUser(@Param('id', ParseIntPipe) id: number): UserDto {
return this.appService.getUser(id);
}
Так же при настройка модуля в nest.js можно добавить валидацию сокетов, чтобы всё было под авторизацией. Вот небольшой пример из проекта:
@Module({
imports: [
ConfigModule.forRoot(),
WsModule.registerAsync({
useFactory: (): IWsApiConfig => {
return {
// тут указывается та же валидация что была
// использована для app.useGlobalPipes(new ValidationPipe(validationConfig));
validationConfig,
async validate(socket: Socket) {
try {
const authGuard = new (AuthGuard('jwt'))();
const isAuth = await (authGuard.canActivate(new ExecutionContextHost([socket])) as Promise<boolean>);
if (!isAuth) {
return HttpStatus.UNAUTHORIZED;
}
} catch (e) {
return HttpStatus.UNAUTHORIZED;
}
return HttpStatus.OK;
},
};
},
}),
],
})
Далее на клиенте с помощью пакета swagger-typescript-api к примеру автоматически генерите полноценного HTTP клиента под ts. В котором описаны все типы и методы для http запросов. Но нам от него нужны только типы, впрочем вы можете сделать еще и fallback в случае проблем с сокетами.
Плюс можно сделать логику что при SSR делать запрос по HTTP и далее уже на WS.
export interface UserDto {
id: number;
firstName: string;
email: string;
}
Идем дальше :-)
Что происходит на сервере:
Собирается приложение, при компиляции декоратора WsAction он собирает всю инфу о методе контроллера, и кладет в свою коллекцию.
Далее приложение запускается и собранная коллекция ожидает подключения по сокетам клиента, далее подписывается на евенты, названия которых совпадает с названием метода.
Клиенту отправляется информация с конфигом с сервера если надо + статус подключения, когда можно уже дергать АПИ.
И все это гуляет по одному каналу, что увеличивает скорость в ~10 раз.
[Nest] LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] LOG [WsGateway] Add WS action: AppController => app:getUser
[Nest] LOG [WsGateway] Add WS action: BusinessController => business:createUser
[Nest] LOG [RoutesResolver] AppController {/api}: +2ms
[Nest] LOG [RouterExplorer] Mapped {/api/:id, GET} route +3ms
[Nest] LOG [RoutesResolver] BusinessController {/api/business}: +1ms
[Nest] LOG [RouterExplorer] Mapped {/api/business/create, POST} route +0ms
[Nest] LOG [NestApplication] Nest application successfully started +6ms
Можно заметить что сгенерились экшены на основе неймпейска указанного в контроллере, и названия метода к которому применили декоратор business:createUser и app:getUser
По сути это и есть название евента которое отправляется с клиента:
ws.emit('business:createUser', { payload });
Я бы на этом закончил, но мне не было достаточно асинхронного WS, я хотел чтобы было всё синхронно как все привыкли:
await fetch('api/create-user', { payload })
Для этого я написал небольшой клиент который промисофицирует асинхронные сокеты и получается по итогу:
const result = await webSocket.emit<ResultDto>('business:createUser', { payload });
Не хочется много кода сюда писать)
Модуль я опубликовал в npm, репа https://github.com/gustoase/nestjs-ws-api
Демо проекта чтобы запустить потыкать накидал на коленке https://github.com/gustoase/nestjs-ws-api-demo
Как запустить:
npm i
npm run start:dev
cd client
npm i
npm run dev
Что по итогу хочу сказать, проект на работе который был успешно запущен летает очень быстро, проблем с ним нет, авторизация имеется, валидация тоже.
Так же за счет своевременного оповещения с сервера о состоянии АПИ можно показывать красивые уведомления :-)
Комментарии (11)
antq
30.10.2022 10:17-2К сведению, websoket, server sent events, XHR это уже давно все Легаси, поскольку все можно реализовать через fetch api. Почему здесь все сидят в криокамере не ясно....
shai_hulud
30.10.2022 11:10+1Эмм, как фетч позволяет организовать двухстороннюю связь с сервером?
antq
30.10.2022 12:11https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
Можно отправлять запросы от клиента обычным fetch, а читать через потоки fetch (Chunked transfer encoding).
Соединение HTTP2 поддерживает Мультиплексирование, расходов на открытие нового соединения для каждого нового fetch запроса от клиента быть не должно.
Вот ссылка на обсуждение, правда там тема про SSE, но сказано и про ws
https://github.com/whatwg/html/issues/2177#issuecomment-404249432shai_hulud
30.10.2022 20:10Ну вот когда реализация http2 будет хотябы в 90% веб серверов машин и веб-серверов фреймворков/библиотек можно будет сказать, что оно заменяет. К примеру nginx не проксирует http2 и не планирует, пара пара пам, фьюх. На сколько я помню ws воскрешают в http2 т.к. стримы его не заменили.
anba8005
Хотел поинтересоваться, как Вы решили вопрос с авторизацией, а именно с передачей jwt токена на с клиента на сервер ? Установка header'a с вебсокетами не работает ...
kpmy
Предположу, что отношение к веб-сокету в данном случае как к обычному сокету и весь протокол строится вручную, как в старые-добрые (для кого-то) времена.
PaulIsh
А что именно не работает?
Вот у нас фрагмент на react:
И на сервере всё принимается
anba8005
Понятно. Повезло вам что используете socket.io, на голом websocket такое не сработает ;(
With extraHeaders
This only works if
polling
transport is enabled (which is the default). Custom headers will not be appended when usingwebsocket
as the transport. This happens because the WebSocket handshake does not honor custom headers. (For background see theWebSocket protocol RFC)Я пока-что не придумал ничего лучше как сделать токен короткоживущим и передавать как часть URL