Vue3 + TS + Vue Query + Express + tRPC: пример настройки


Оглавление

  • Введение
  • Установка
    • Структура папок проекта
    • Серверный скрипт
    • Маршрутизатор
    • Инициализация Vue Query
    • Клиент tRPC
    • Компонент приложения
  • Приложение и примеры
  • Больше полезных ссылок

Введение

Недавно я гуглил о тенденциях в веб-разработке, чтобы узнать о современных инструментах/либах/фреймворках, и наткнулся на tRPC.

tRPC означает TypeScript remote procedure call, и, как вы можете прочитать на его домашней странице, его цель состоит в том, чтобы легко иметь End-to-end typesafe API. По сути, это позволяет вам предоставлять серверные функции, которые можно вызывать из вашего клиента, вашего фронтенда, используя все преимущества TS.

Официальный сайт tRPC, хорошая коллекция примеров и документация.

tRPC — это еще один способ обеспечить корректное взаимодействие между клиентом и сервером (через вызовы api). Возможно, вы уже думаете о GraphQL, но с tRPC вам не нужно учить новый язык, и он не является схемой. В то время как GraphQL — это схема и язык, которые вы используете для детализации «формы» функций, которые вы можете вызывать с сервера.

Эксперимент: Почему бы не попробовать, используя последнюю версию Vue, Vite, TypeScript и попытаться подключить tRPC и посмотреть, что получится?
Я попытался найти проекты на базе Vue, использующие tRPC, и подавляющее большинство моих результатов было основано на React/Next.js… Поэтому я решил просто начать с проекта на базе React, а затем экспериментировать дальше.

Примечания:
— Я буду ссылаться на все соответствующие ресурсы по всей статье.
— Это просто экспериментальная идея, чтобы подключить несколько современных пакетов и создать очень простой проект.
— Эта статья больше предназначена для людей, которые уже имеют некоторый опыт в веб-разработке, однако я постараюсь дать некоторые дополнительные объяснения.

Установка

В качестве отправной точки я посмотрел отличное видео Джека Херрингтона «tRPC: умные и простые API», следовал его шагам и думал, насколько сложно будет использовать Vue 3 и Vue Query вместо React и React Query соответственно.

В следующем разделе показано, как выглядит окончательная структура папок, основанная на шагах Джека и после модификации для использования Vue.

Структура папок проекта

Это монорепо, использующее рабочие пространства yarn.
Проект сервера находится в папке api-server, а проект фронтенда — в папке client.

И сервер, и клиент запускаются запуском yarn start на корневом дире, как вы можете видеть в package.json в корневой папке:

Скрипт сервера

Это код сервера, где мы создаем наше экспресс-приложение и говорим ему использовать cors (чтобы разрешить вызовы с порта 3000 на 8080), а также использовать промежуточное ПО trpcExpress и зарегистрировать маршрутизатор.

// packagesapi-serverindex.ts
import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';
import { appRouter } from './router/app';
import cors from 'cors';

const main = async () => {
  const app = express();
  app.use(cors());
  const port = 8080;

  app.use(
    '/trpc',
    trpcExpress.createExpressMiddleware({
      router: appRouter,
      createContext: () => null,
    })
  );

  app.listen(port, () => {
    console.log(`api-server listening at http://localhost:${port}`);
  });
};

main();
Вход в полноэкранный режим Выход из полноэкранного режима

Маршрутизатор

Следующий код показывает маршрутизатор, который содержит точки доступа:

  • 2 конечные точки запроса (аналогично конечной точке GET для остальных):
    • приветствия
    • getMessages
  • 1 конечная точка мутации (аналогично конечной точке rest POST):
    • addMessage

Примечание: помимо добавления данных, мутация может также обновлять или удалять данные.

Вы также можете видеть, что я использую zod, который представляет собой «TypeScript-first schema declaration and validation library».

Этот пакет будет использоваться для проверки моих входных данных для запросов/мутаций (при необходимости эти проверки могут даже выдавать сообщения о проверке).

z.string().uuid({ message: "Invalid UUID" });
Вход в полноэкранный режим Выход из полноэкранного режима

Примечание: Вы также можете использовать zod для извлечения типов из объектов zod, сохраняя их как типы и повторно используя их где угодно:

// packagesapi-serverrouterapp.ts
import * as trpc from '@trpc/server';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';

export interface ChatMessage {
  id: string;
  user: string;
  message: string;
}

const messages: ChatMessage[] = [
  { id: uuidv4(), user: 'User1', message: 'This is my the first message!' },
  { id: uuidv4(), user: 'User2', message: 'Hello there 🎉' },
];

export const appRouter = trpc
  .router()
  .query('greetings', {
    resolve() {
      return {
        message: 'Greetings from /trpc/greetings:)',
      };
    },
  })
  .query('getMessages', {
    input: z.number().default(10),
    resolve({ input }) {
      return messages.slice(-input);
    },
  })
  .mutation('addMessage', {
    input: z.object({
      user: z.string(),
      message: z.string(),
    }),
    resolve({ input }) {
      const newMessage: ChatMessage = {
        id: uuidv4(),
        ...input,
      };
      messages.push(newMessage);
      return input;
    },
  });

export type AppRouter = typeof appRouter;
Войти в полноэкранный режим Выйти из полноэкранного режима

В этом случае сообщения будут храниться только в памяти, потому что я не использую для этого БД. (и это ускоряет процесс демонстрации).
Также можно создавать различные маршрутизаторы, которые будут содержать различные запросы/мутации, а затем вы можете объединить маршрутизаторы, чтобы легко получить доступ к определенному запросу из маршрутизатора на клиенте.

Инициализация Vue Query

Вот как инициализируется vue-запрос через VueQueryPlugin в файле main.ts, который затем используется экземпляром приложения Vue:

// packagesclientsrcmain.ts
import { createApp } from 'vue';
import { VueQueryPlugin } from 'vue-query';
import './style.css';
import App from './App.vue';

createApp(App).use(VueQueryPlugin).mount('#app');
Вход в полноэкранный режим Выход из полноэкранного режима

Зачем вообще использовать Vue Query, спросите вы?
«Я мог бы сделать все вызовы api с помощью fetch/axios, верно?».

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

  • Кэширование… (возможно, самая сложная вещь в программировании).
  • Дедупликация нескольких запросов на одни и те же данные в один запрос
  • Обновление «устаревших» данных в фоновом режиме
  • Знание, когда данные «устарели».
  • Отражение обновлений данных как можно быстрее
  • Оптимизация производительности, такая как пагинация и ленивая загрузка данных
  • Управление памятью и сборка мусора для состояния сервера
  • Мемоизация результатов запросов с помощью структурного разделения

Кроме того, хуки предлагают набор стандартных реквизитов/функций для использования в вашем приложении. Пример использования хука useQuery:

Примечание: Данные, к которым вам нужно получить доступ, находятся в удобно названном реквизите data.

клиент tRPC

Здесь мы указываем url, который нам нужно использовать при вызове клиента tRPC, а также типы, которые мы можем использовать, получая данные от AppRouter. (Позже мы импортируем этот trpc const в компонент App.vue):

// packagesclientsrcapitrpc.ts
import { createTRPCClient } from '@trpc/client';
import { AppRouter } from 'api-server/router/app';

export const trpc = createTRPCClient<AppRouter>({
  url: 'http://localhost:8080/trpc',
});
Вход в полноэкранный режим Выход из полноэкранного режима

Компонент App

Для простоты я решил выполнить клиентские вызовы tRPC именно в этом компоненте.
Примечание: я использую скриптовую настройку Vue и пока что получаю от этого удовольствие 🙂

<template>
  <div class="trpc-example">
    <h1>Vue 3 + vue-query + tRPC example</h1>
    <Error
      v-if="getMessagesHasError"
      error-message="Something went wrong - cannot fetch data"
      cta-text="Refetch data"
      @click="refetch()"
    />
    <Error
      v-if="addMessageHasError"
      error-message="Something went wrong - cannot submit message"
      cta-text="Reset error"
      @click="reset"
    />
    <div v-if="showFormAndMessages" class="trpc-example__container">
      <SendMessageForm :form="form" @submit-form="handleSubmitForm" />
      <h2 v-if="isLoading">Data is being loaded</h2>
      <Message v-for="chatMessage in data" :key="chatMessage.id" :chat-message="chatMessage" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, reactive } from 'vue';
import Message from './components/Message.vue';
import SendMessageForm from './components/SendMessageForm.vue';
import Error from './components/Error.vue';
import { useQuery, useMutation, useQueryClient } from 'vue-query';
import { trpc } from './api/trpc';
import { Form } from '../types';

const queryClient = useQueryClient();

const form = reactive({
  user: '',
  message: '',
});

const getMessages = () => trpc.query('getMessages');
const {
  isError: getMessagesHasError,
  isLoading,
  data,
  refetch,
} = useQuery('getMessages', getMessages, {
  refetchOnWindowFocus: false,
});

const addMessage = (form: Form) => trpc.mutation('addMessage', form);
const { error: addMessageHasError, mutate, reset } = useMutation('addMessage', addMessage);

const handleSubmitForm = () => {
  mutate(form, {
    onSuccess: () => {
      queryClient.invalidateQueries('getMessages');
    },
  });
};

const showFormAndMessages = computed(() => {
  return !getMessagesHasError.value && !addMessageHasError.value;
});
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Приложение и примеры

Лучший способ взаимодействия с этим проектом — это, конечно, запустить его локально и посмотреть, что вы можете с ним сделать. Но вот несколько примеров:

Вот как выглядит клиент (да, я знаю, пользовательский интерфейс выглядит потрясающе!). Vue.js devtools также отображает информацию о запросах:

Данные, поступающие из /trpc/greetings:

Данные, поступающие из /trpc/getMessages:

Примеры изменения функций на стороне сервера и соблюдения проверок безопасности TS на клиенте:

Вы также можете переименовывать свои серверные функции с клиента (по какой-то причине я не смог переименовать символ с сервера):

Пример блокировки запроса запроса и последующего вызова функции refetch и ее повторных попыток:

Пример блокировки запроса на мутацию и последующего вызова функции reset. Это сбрасывает состояние ошибки:

Другие полезные ссылки

  • Мое репо: https://github.com/alousilva/express-vue-trpc
  • Алекс, создатель tRPC: https://twitter.com/alexdotjs
  • Тео — ping․gg, интервью с Алексом: https://www.youtube.com/watch?v=Mm3Z5c1Linw (кстати, у Тео есть тонна интересного контента на его канале youtube).
  • Учитесь с Джейсоном, интервью с Алексом: https://www.youtube.com/watch?v=GryES84SSEU

Я могу создать еще одно репо для изучения более реалистичного проекта с использованием Nuxt, tRPC, Vue Query, где я подключаюсь к базе данных и использую ORM Prisma, аналогично тому, что Алекс сделал в этом довольно аккуратном стартовом репо: https://github.com/trpc/examples-next-prisma-starter.

Надеюсь, вы нашли эту статью полезной, и она позволила вам открыть для себя что-то новое сегодня 🙂

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