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

Я также следую учебной программе The Odin Project, которая посвящена веб-разработке. Мой последний проект был предложен в их курсе JavaScript. Вы можете увидеть репозиторий Github здесь и живое приложение здесь.

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

Хотя это простое приложение, я смог многому научиться с его помощью.

Основные требования к проекту

Основная цель проекта заключалась в разработке своего рода приложения для пометки фотографий (на самом деле игра в стиле «Где Уолдо» или «Где Уолли», в зависимости от того, откуда вы).

Если вы не знакомы с ним, вот краткое описание: на изображении с множеством символов вам нужно найти несколько конкретных; Вы выиграете, когда найдете их все.

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

Фронтенд должен был быть создан с помощью React, а бэкенд — с использованием Firebase (платформа Google, работающая как сервис).

Полное описание требований можно найти здесь.

Добавление специй: Redux и Tailwind CSS

Я полагал, что разработать приложение React с Firebase в качестве серверной части будет достаточно просто (я новичок в Firebase, но раньше использовал React).

Я решил немного улучшить ситуацию, добавив Redux для управления состоянием и Tailwind CSS для стилей — две библиотеки JavaScript, которые мне еще предстояло научиться использовать.

Я также использовал TypeScript. Хотя я уже кодировал его раньше, это был первый раз, когда я действительно провел исследование, чтобы все правильно напечатать.

Эти дополнения сделали вещи более сложными, но и намного более интересными.

Разработка приложения

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

Поток информации при взаимодействии пользователя с приложением

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

В бэкенде я понял, что мне понадобится хранилище для изображений, а также база данных — соответственно Storage и Firestore, обе от Firebase.

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

С этого момента думать о компонентах пользовательского интерфейса было просто.

Компоненты пользовательского интерфейса

Заранее подумав обо всех компонентах, я мог легче визуализировать отношения между ними (какие компоненты будут дочерними элементами и т. д.). Написание статической версии приложения, как я объясню позже, становится проще.

Как видно на изображениях выше, я выделил компоненты Home и Level, а также все его дочерние компоненты. Некоторые вещи были изменены в процессе разработки, но они послужили хорошей отправной точкой. Я не проектировал компонент High-scores заранее, так как он был достаточно простым.

Состояние приложения

Насмешка над состоянием и его формой помогла мне подумать обо всей информации, которая мне понадобится, и о том, как я буду хранить ее в Redux.

Это также пригодилось, когда мне нужно было написать все интерфейсы и типы, так как я использую Typescript.

Некоторые мысли о развитии

Компоненты пользовательского интерфейса и Tailwind CSS

В общих чертах следуя шагам, описанным в этом удобном разделе документации React, я начал реализацию с разработки статической версии компонентов, как указано в моем дизайне. Это означает кодирование пользовательского интерфейса без состояния или интерактивности, что обычно представляет собой много бессмысленного набора текста, поскольку мы продумали все при его разработке.

Именно тогда я начал углубляться в документацию Tailwind CSS. Tailwind — это CSS-фреймворк, который позволяет нам применять CSS к элементам HTML (или, в случае React, к элементам JSX/TSX), просто добавляя к ним служебные классы. Это не набор пользовательского интерфейса — в нем нет встроенных компонентов или тем, поэтому нам все равно нужно знать наш CSS.

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

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

Состояние приложения, Redux и Firebase

  • Сокращение

Хотя это и не обязательно для такого простого приложения, как это, я решил использовать Redux, чтобы изучить его на практике. Проще говоря, Redux — это контейнер состояния для приложений JavaScript.

В React у каждого компонента может быть свое состояние. Обычно, когда состояние необходимо разделить между компонентами, его необходимо «поднять» к родительскому компоненту, а затем передать дочерним компонентам в качестве свойств. В больших приложениях это может довольно быстро запутаться.

Redux помогает обеспечить предсказуемое состояние. Он работает за счет централизованного хранилища с функциями редуктора, которые содержат логику того, как будет обновляться состояние, когда ему отправляются «действия».

Хотя мы можем использовать Redux напрямую или через привязки React-Redux, это требует от нас настройки всего вручную: от хранилища до промежуточных программ, до редукторов, которые будут принимать вручную написанные действия в качестве аргументов, среди многих других сложных вещей.

  • Инструментарий Redux

Однако на сегодняшний день документация Redux рекомендует всегда использовать Redux Toolkit (или RTK). По их собственным словам, RTK включает в себя утилиты, которые помогают упростить многие распространенные варианты использования, включая «настройку хранилища, создание редюсеров и написание неизменяемой логики обновления и даже одновременное создание целых «срезов состояния».

Вот что я использовал в своем приложении. С помощью RTK я настроил хранилище и разделил состояние на пять срезов: три с помощью функции createSlice для информации о найденных персонажах, для компонента-кнопки, используемого для выбора символа на изображении, и для таймера; и два с использованием функции createApi для извлечения информации об уровнях и персонажах из бэкенда и сохранения ее в состоянии.

  • создать срез

Функция createSlice получает объект функций-редюсеров и автоматически генерирует создателей действий и типы действий, которые можно использовать для отправки действий в хранилище.

(Некоторые фрагменты кода были удалены из приведенных ниже фрагментов кода для краткости и удобочитаемости. Полный исходный код можно найти в репозитории Github.)

// src/features/levels/slices/found-characters-slice.ts
export const foundCharactersSlice = createSlice({
  name: 'foundCharacters',
  initialState: foundCharactersInitialState,
  // Here we add the reducer functions
  reducers: {
    resetScore() {
      return foundCharactersInitialState;
    },
    setFoundCharacter(state, action: PayloadAction<string>) {
      const id = action.payload;
      const character = state.find((char: FoundCharacter) => char.id === id);
      if (character) character.found = true;
    },
  },
});

// RTK generates the action creators for us
export const { resetScore, setFoundCharacter } = foundCharactersSlice.actions;



// src/features/levels/useGame.ts

//...
const dispatch = useAppDispatch();
// We call the action creator with the desired payload
dispatch(setFoundCharacter(charPosition.character_id));

Как только срез зарегистрирован в магазине Redux, мы можем легко использовать создателей действий, чтобы легко обновлять состояние из любого места в приложении. Есть некоторые предостережения при обновлении состояния компонента во время рендеринга другого компонента, о которых я расскажу в следующем посте.

  • createApi и Firebase

Функция createApi является ядром функциональности RTK Query. Из их документации: Это позволяет вам определить набор «конечных точек, которые описывают, как извлекать данные из серверных API и других асинхронных источников, включая конфигурацию того, как извлекать и преобразовывать эти данные. Он генерирует кусок API; структура, которая содержит логику Redux (и, возможно, хуки React), которые инкапсулируют для вас процесс выборки и кэширования данных».

Обычно createApi используется путем определения baseUrl, а затем определения конечных точек для определенных запросов и мутаций:

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  endpoints: (builder) => ({
    getPokemonByName: builder.query<Pokemon, string>({
      query: (name) => `pokemon/${name}`,
    }),
  }),
})

Однако Firebase работает через свой специальный SDK, а не через API. Если мы по-прежнему хотим сохранить преимущества извлечения и изменения данных с помощью RTK Query (такие как кэширование, обработчики useQuery/useMutation и т. д.), нам нужен немного другой подход.

Нам нужно использовать fakeBaseQuery() в качестве базового запроса и определить «конечные точки» (которые на самом деле не являются конечными точками) с помощью queryFn, который должен возвращать объект с формой { data: ResultType } . Внутри queryFn мы можем использовать Firebase SDK для чтения или записи любых данных, которые нам нужны, из Firestore, а затем возвращать их так, как ожидает RTK Query.

В приведенном ниже примере мы получаем один документ из Firestore, используя его идентификатор. При вызове запроса RTK Query берет данные и сохраняет их в соответствующем состоянии.

// src/features/levels/slices/levels-slice.ts
export const levelsApi = createApi({
  reducerPath: 'levels',
  baseQuery: fakeBaseQuery(),
  tagTypes: ['Level', 'Character'],
  endpoints: (builder) => ({
    // We define the async information fetch using Firebase
    fetchSingleLevel: builder.query<Level, string>({
      async queryFn(id) {
        try {
          const ref = doc(firestore, 'levels', id);
          const documentSnapshot = await getDoc(ref);
          const data: Level = { ...documentSnapshot.data() } as Level;
          return { data };
        } catch (error: any) {
          console.error(error.message);
          return { error: error.message };
        }
      },
      providesTags: ['Level'],
    }),
  }),
});

// RTK Query generates a hook we can use to fetch the data
export const { useFetchSingleLevelQuery } = levelsApi;



// src/features/levels/Level.tsx
// We use the hook to get the data, status indicators and a possible error
const { data, isLoading, isSuccess, isError, error } =
    useFetchSingleLevelQuery(levelId);

Делая это, мы гарантируем, что данные извлекаются из Firestore, сохраняя при этом преимущества управления состоянием RTK Query.

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

Чтобы прийти к этому решению, потребовались пробы и ошибки, так как не так много обновленных ресурсов о том, как интегрировать RTK Query и Firebase. Возможно, в ближайшее время я напишу об этом более подробный пост.

Готовое приложение

Готовое приложение было развернуто с помощью Firebase Hosting и доступно здесь: https://top-project-photo-tagging.web.app/.

Ниже приведены скриншоты страниц Home, Level и Highscores. Как видим, по сравнению с первоначальным дизайном произошли некоторые изменения, но общая идея осталась прежней.

Заключительные замечания

Это лишь некоторые из основных мыслей, которые пришли в голову при проектировании и разработке приложения. Резюмируя:

  • Важно разработать приложение до написания кода. Думая и визуализируя поток информации, я смог легче проектировать компоненты и состояние приложения, что, возможно, сэкономило мне часы кодирования.
  • Tailwind CSS может быть очень удобен для быстрого создания адаптивного, но настраиваемого приложения.
  • React отлично подходит для управления состоянием, поскольку он централизует информацию в одном хранилище, к которому можно получить доступ во всем приложении. При использовании RTK это становится еще проще, если вы потратите время на изучение документации. RTK Query также является полезным инструментом, если вам нужно получить данные из внешних источников.

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