Прощай Redux, MobX, Apollo! Грань между бэкендом и фронтендом сломана! Инновационый шаг эволюции стейт менеджеров.

Одна из самых сложных задачах при разработке веб и мобильных приложений — это синхронизация данных между устройствами и выполнение автономных операции. В идеале, когда устройство находится в автономном режиме, ваши клиенты должны иметь возможность продолжать использовать ваше приложение не только для доступа к данным, но также для их создания и изменения. Когда устройство возвращается в оперативный режим, приложение должно повторно подключиться к бэкэнду, синхронизировать данные и разрешить конфликты, если таковые имеются. Для правильной обработки всех крайних случаев требуется много недифференцированного кода, даже при использовании кэша AWS AppSync SDK на устройстве с автономными мутациями и дельта-синхронизацией.
Amplify DataStore предоставляет постоянное хранилище на устройстве для записи, чтения и наблюдения за изменениями данных, если вы подключены к Интернету или в автономном режиме, а также позволяет легко синхронизировать данные с облаком и между устройствами.
Amplify DataStore позволяет разработчикам писать приложения, используя распределенные данные, без написания дополнительного кода для автономного или онлайн-сценария.
Вы можете использовать Amplify DataStore для автономного использования в режиме «только локальный» без учетной записи AWS или предоставить весь бэкэнд с помощью AWS AppSync и Amazon DynamoDB.
DataStore включает в себя Delta Sync с использованием вашего бэкенда GraphQL и несколько стратегий разрешения конфликтов.
Преимущества DataStore от AWS Amplify над Redux, MobX, Apollo, Relay, селектрорами, деселекторами и прочими флаксами:
Сравнивать AWS Amplify с Redux, MobX не корректно, так как AWS Amplify это не только стейт-менеджер, но и клиент-сервер, поэтому в классе клиент-сервер мы будем сравнивать его с Apollo и Relay.
1. Real time из коробки
Не думаю, что можно считать бизнес серьезным, если у его мобильного приложения отсустствуют события подписок реализованых на технологии web sockets.
А многие ли приложения в наше время работают на web sockets?
Думаю нет, по причине того, что real time это дополнительная работа разработчиков на бэке и фронтенде.
Для нас же, fullStack serverless разработчиков на AWS Amplify, real time идет из коробки, как на фронте так и на бэке и нам не надо писать код реализации для интеграции вэбсокетов на каждую модель, так как он генерируется автоматически, также как и написание документации для всего нашего сгенерированого кода, имплементированого в наш проект на основоании инструкции GraphQL схемы. Чтобы не пугать громкими словами, я покажу вам пример, из прошлого урока, того как в AWS Amplify определяется Store:
type Job @model @auth( rules: [ {allow: owner, ownerField: "owner", operations: [create, update, delete]}, ]) { id: ID! position: String! rate: String! description: String! owner: String }
Так определяется модель в сторе, не только для фронтенда, но и для бэкенда. Один источник правды для фронтенда и для бэкенда. Да да, вижу я, что еще не раз повтою это в своей жизни, так как это киллер фича и панч лайн vs Redux, MobX, Apollo, Relay.
Вот именно эта отличная от Redux, MobX, Apollo архитектура, стерает грань между бэкендом и фронтендом. И ставит AWS Amplify DataStore над всеми
Все!!!
Если вы с бэкенда, то вам больше не нужно писать резолверы к базе данных и тащить подписки на каждую модель данных.
Serverless — это когда разработчикам бэкенда пришла пора учить фронтенд, так как их услуги нужны исключительно легаси проектам, не шагающим в ногу со временем, от чего и не живущими real time.
2. Генерация кода
Что такое кодогенерация вы можете прочитать и без меня в википедии, если конечно же не знаете его значения, которое и в этом панче напоминает нам о себе.
Old schoolщик?
Юзаем fetch или axios?
Отправляя запросы в дремучий лес API, который еще и сами пишим в связке с Redux, MobX, Apollo, Relay.
Так вот вам еще одна новость дня!
Вам больше не нужно писать эти запросы к API, вам их нужно только вызвать.
Это значит, что больше не нужно создовать эту немаленькую папочку с кодом запроса к серверу, так как в AWS Amplify DataStore они также генерируются в вашем проекте на основании вашего стора, определенного все той же GraphQL схемы их первого пункта. И выполняется это одной командой:
npm run amplify-modelgen
В итоге получаем папку models с генерированным кодом.
И папка graphql после пуша на сервер, со всем запросом во Flow, TS или ваниле JavaScript.
3. Offline data & cloud sync
Не нужно писать дополнительный код, для отправки запроса на сервер, после выхода приложения в онлайн.
Иногда вы попадаете в ненадежную ситуацию, но вам лучше подождать дольше, чем явно провалить операцию.
У Apollo есть apollo-link-retry который обеспечивает экспоненциальный откат и запросы на сервер между попытками по умолчанию. Правда он (в настоящее время) не обрабатывает повторы для ошибок GraphQL в ответе, только для сетевых ошибок.
У Redux, MobX понятное дело под капотом этого решения нет так как они не клиенты и приходится задействовать сторонние мидлвари, по причине того, что REST как дедушка на пенсии с поддержкой любимых внуков. Подробный разбор GraphQL vs REST.
У AWS Amplify DataStore есть не только аналог apollo-link-retry, но и встроенная в него и настраиваемая привычную знакомая модель программирования с автоматическим контролем версий, обнаружением конфликтов и разрешением в облаке.
Из минусов AWS Amplify хочу назвать то, что хуки Apollo c его loading и error из коробки сокращают количество написанного кода на фронте, поэтому написал open source библиотеку, которая решает это недоразумение.
В конце этого урока мы соберем с вами это мобильное приложение c использованием Amplify DataStore:
Поехали!
Данный урок является продолжением урока по аутентификции, так как работа с DataStore будет выполняться аутентифицированым пользователем. Поэтому, если вы его не прошли, то вернитесь на шаг назад.
Чат поддержки AWS Amplify: Telegram
Финальный код этой части можно найти на Github.
Клонируем репозиторий
Если вы продолжаете прошлый урок, то можете сразу перейти к шагу 5
git clone https://github.com/fullstackserverless/startup.git
Переходим в папку проекта
cd startup
Install dependencies
yarn
или
npm install
Регистрируем свой AWS account
Шаг для тех, кто еще не зарегистрирован на AWS
Регистрируемся согласно этой инструкции и по видео учебнику чекаем все 5 шагов.
Внимание!!!
Потребуется банковская карта, где должно быть более 1$
Там же смотрим и ставим Amplify Command Line Interface (CLI)
Инициализация AWS Amplify в проект React Native
В корневой директории проекта React Native инициализируем наш AWS Amplify проект
amplify init
Отвечаем на вопросы:
Проект инициализацировался
Подключаем плагин аутентификации
Теперь, когда приложение находится в облаке, вы можете добавить некоторые функции, такие как предоставление пользователям возможности зарегистрироваться в нашем приложении и войти в систему.
Командой
amplify add auth
подключаем функцию аутентификации. Выбираем конфигурацию по умолчанию. Это добавляет конфигурации ресурсов auth локально в ваш каталог ampify/backend/auth
Выбираем профиль, который мы хотим использовать. default. Enter и как пользователи будут входить в систему. Email(За SMS списывают деньги).
Отправляем изменения в облако
amplify push
All resources are updated in the cloud
Собираем проект и проверяем работоспособность аутентификации.
ampify-app
Самый быстрый способ начать работу c DataStore — использовать npx-скрипт ampify-app.
npx amplify-app@latest
Установка зависимостей
Подробная установка здесь
Если у вас React Native Cli, то
yarn add @aws-amplify/datastore @react-native-community/netinfo @react-native-community/async-storage
И если вы используете React Native> 0.60, то выполните следующую команду для iOS:
cd ios && pod install && cd ..
Подключаем плагин API(App Sync)
Если подключали его в прошлом уроке, то пропускаем этот шаг.
Если нет то, подключаем плагин API
amplify add api
После выбранных пунктов откроется схема GraphQL в amplify/backend/api/<datasourcename>/schema.graphql куда вставляем эту модель:
type Job @model @auth( rules: [ {allow: owner, ownerField: "owner", operations: [create, update, delete]}, ]) { id: ID! position: String! rate: String! description: String! owner: String }
Подробней о ней здесь
Генерация моделей
Моделирование ваших данных и создание моделей, используемых DataStore, — это первый шаг к началу работы. GraphQL используется в качестве общего языка для JavaScript, iOS и Android для этого процесса, а также используется в качестве сетевого протокола при синхронизации с облаком. GraphQL также поддерживает некоторые функции, такие как Automerge в AppSync. Генерация модели может быть выполнена с помощью сценария NPX или из командной строки с помощью Amplify CLI.
Вам не нужна учетная запись AWS для ее запуска и локального использования DataStore, однако, если вы хотите синхронизироваться с облаком, рекомендуется установить и настроить Amplify CLI как в прошлом уроке.
Так как схему мы описали в прошлом уроке, то сейчас нам достаточно запустить команду
npm run amplify-modelgen
и получить сгенерированную модель в папке src/models
Обновляем API
Включаем DataStore для всего API
amplify update api
Отправляем изменения в облако
amplify push
All resources are updated in the cloud
READ
Создаем экран JobsMain src/screens/Jobs/JobsMain.js
На этом экране мы сделаем запрос Query, с опцией пагинации, где число через хук useQuery и он нам вернет массив, который мы отправим в Flatlist.
import React, { useEffect, useState } from 'react' import { FlatList } from 'react-native' import { Auth } from 'aws-amplify' import { AppContainer, CardVacancies, Space, Header } from 'react-native-unicorn-uikit' import { DataStore } from '@aws-amplify/datastore' import { Job } from '../../models' import { goBack, onScreen } from '../../constants' const JobsMain = ({ navigation }) => { const [data, updateJobs] = useState([]) const fetchJobs = async () => { const mess = await DataStore.query(Job) updateJobs(mess) } useEffect(() => { fetchJobs() const subscription = DataStore.observe(Job).subscribe(() => fetchJobs()) return () => { subscription.unsubscribe() } }, [data]) const _renderItem = ({ item }) => { const owner = Auth.user.attributes.sub const check = owner === item.owner return ( <> <CardVacancies obj={item} onPress={onScreen(check ? 'JOB_ADD' : 'JOB_DETAIL', navigation, item)} /> <Space height={20} /> </> ) } const _keyExtractor = (obj) => obj.id.toString() return ( <AppContainer onPress={goBack(navigation)} flatlist> <FlatList scrollEventThrottle={16} data={data} renderItem={_renderItem} keyExtractor={_keyExtractor} onEndReachedThreshold={0.5} ListHeaderComponent={ <Header onPress={goBack(navigation)} onPressRight={onScreen('JOB_ADD', navigation)} iconLeft="angle-dobule-left" iconRight="plus-a" /> } stickyHeaderIndices={[0]} /> </AppContainer> ) } export { JobsMain }
Для раскрытия подробностей вакансии создаем экран JobDetail src/screens/Jobs/JobDetail.js
import React from 'react' import { Platform } from 'react-native' import { AppContainer, CardVacancies, Space, Header } from 'react-native-unicorn-uikit' import { goBack } from '../../constants' const JobDetail = ({ route, navigation }) => { return ( <AppContainer> <Header onPress={goBack(navigation)} iconLeft="angle-dobule-left" /> <CardVacancies obj={route.params} detail /> <Space height={Platform.OS === 'ios' ? 100 : 30} /> </AppContainer> ) } export { JobDetail }
CREATE UPDATE DELETE
Создаем экран JobAdd src/screens/Jobs/JobAdd.js, где мы выполняем функции CREATE UPDATE DELETE
import React, { useState, useEffect, useRef } from 'react' import { AppContainer, Input, Space, Button, Header, ButtonLink } from 'react-native-unicorn-uikit' import { DataStore } from '@aws-amplify/datastore' import { Formik } from 'formik' import * as Yup from 'yup' import { Job } from '../../models' import { goBack } from '../../constants' const JobAdd = ({ route, navigation }) => { const [loading, setLoading] = useState(false) const [check, setOwner] = useState(false) const [error, setError] = useState('') const [input, setJob] = useState({ id: '', position: '', rate: '', description: '' }) const formikRef = useRef() useEffect(() => { const obj = route.params if (typeof obj !== 'undefined') { setOwner(true) setJob(obj) const { setFieldValue } = formikRef.current const { position, rate, description } = obj setFieldValue('position', position) setFieldValue('rate', rate) setFieldValue('description', description) } }, [route.params]) const createJob = async (values) => (await DataStore.save(new Job({ ...values }))) && goBack(navigation)() const updateJob = async ({ position, rate, description }) => { try { setLoading(true) const original = await DataStore.query(Job, input.id) const update = await DataStore.save( Job.copyOf(original, (updated) => { updated.position = position updated.rate = rate updated.description = description }) ) update && goBack(navigation)() setLoading(false) } catch (err) { setError(err) } } const deleteJob = async () => { try { setLoading(true) const job = await DataStore.query(Job, input.id) const del = await DataStore.delete(job) del && goBack(navigation)() setLoading(false) } catch (err) { setError(err) } } return ( <AppContainer onPress={goBack(navigation)} loading={loading} error={error}> <Header onPress={goBack(navigation)} iconLeft="angle-dobule-left" /> <Space height={20} /> <Formik innerRef={formikRef} initialValues={input} onSubmit={(values) => (check ? updateJob(values) : createJob(values))} validationSchema={Yup.object().shape({ position: Yup.string().min(3).required(), rate: Yup.string().min(3).required(), description: Yup.string().min(3).required() })} > {({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => ( <> <Input name="position" value={values.position} onChangeText={handleChange('position')} onBlur={() => setFieldTouched('position')} placeholder="Position" touched={touched} errors={errors} /> <Input name="rate" keyboardType="numeric" value={`${values.rate}`} onChangeText={handleChange('rate')} onBlur={() => setFieldTouched('rate')} placeholder="Rate" touched={touched} errors={errors} /> <Input name="description" value={values.description} onChangeText={handleChange('description')} onBlur={() => setFieldTouched('description')} placeholder="Description" touched={touched} errors={errors} multiline numberOfLines={5} /> <Space height={40} /> <Button title={check ? 'Update' : 'Create'} disabled={!isValid} onPress={handleSubmit} formik /> {check && ( <> <Space height={10} /> <ButtonLink title="or" textStyle={{ alignSelf: 'center' }} /> <Space height={15} /> <Button title="DELETE" onPress={deleteJob} cancel /> </> )} </> )} </Formik> <Space height={100} /> </AppContainer> ) } export { JobAdd }
и в screens/Jobs/index.js экспортируем экраны
export * from './JobsMain' export * from './JobDetail' export * from './JobAdd'
Навигация
Добавляем импорт экранов Jobs и подключаем их в StackNavigator
import * as React from 'react' import { createStackNavigator } from '@react-navigation/stack' import { enableScreens } from 'react-native-screens' // eslint-disable-line import { Hello, SignUp, SignIn, ConfirmSignUp, User, Forgot, ForgotPassSubmit } from './screens/Authenticator' import { JobsMain, JobDetail, JobAdd } from './screens/Jobs' enableScreens() const Stack = createStackNavigator() const AppNavigator = () => { return ( <Stack.Navigator screenOptions={{ headerShown: false }} initialRouteName="HELLO" > <Stack.Screen name="HELLO" component={Hello} /> <Stack.Screen name="SIGN_UP" component={SignUp} /> <Stack.Screen name="SIGN_IN" component={SignIn} /> <Stack.Screen name="FORGOT" component={Forgot} /> <Stack.Screen name="FORGOT_PASSWORD_SUBMIT" component={ForgotPassSubmit} /> <Stack.Screen name="CONFIRM_SIGN_UP" component={ConfirmSignUp} /> <Stack.Screen name="USER" component={User} /> <Stack.Screen name="JOBS_MAIN" component={JobsMain} /> <Stack.Screen name="JOB_DETAIL" component={JobDetail} /> <Stack.Screen name="JOB_ADD" component={JobAdd} /> </Stack.Navigator> ) } export default AppNavigator
Кнопка Jobs
Редактируем экран User в screens/Authenticator/User/index.js
import React, { useState, useEffect } from 'react' import { Auth } from 'aws-amplify' import * as Keychain from 'react-native-keychain' import { AppContainer, Button } from 'react-native-unicorn-uikit' import { goHome, onScreen } from '../../../constants' const User = ({ navigation }) => { const [loading, setLoading] = useState(false) const [error, setError] = useState('') useEffect(() => { const checkUser = async () => { await Auth.currentAuthenticatedUser() } checkUser() }) const _onPress = async () => { setLoading(true) try { await Auth.signOut() await Keychain.resetInternetCredentials('auth') goHome(navigation)() } catch (err) { setError(err.message) } } const _onPressJob = () => onScreen('JOBS_MAIN', navigation)() // переход на экран JOBS_MAIN return ( <AppContainer message={error} loading={loading}> <Button title="Sign Out" onPress={_onPress} /> <Button title="Jobs" onPress={_onPressJob} /> </AppContainer> ) } export { User }
Собираем приложение и тестируем
Done
References
https://learning.oreilly.com/library/view/full-stack-serverless/9781492059882/
https://www.altexsoft.com/blog/engineering/graphql-core-features-architecture-pros-and-cons/
https://engineering.fb.com/core-data/graphql-a-data-query-language/
ссылка на оригинал статьи https://habr.com/ru/post/503964/
Добавить комментарий