وداعا Redux ، MobX ، أبولو! الخط الفاصل بين الواجهة الأمامية والخلفية مكسور! خطوة مبتكرة في تطور مديري الدولة.

تتمثل إحدى المهام الأكثر صعوبة في تطوير تطبيقات الويب والجوّال في مزامنة البيانات بين الأجهزة وتنفيذ العمليات في وضع عدم الاتصال. من الناحية المثالية ، عندما يكون الجهاز غير متصل بالإنترنت ، يجب أن يتمكن عملاؤك من الاستمرار في استخدام تطبيقك ليس فقط للوصول إلى البيانات ، ولكن أيضًا لإنشاءه وتعديله. عندما يعود الجهاز عبر الإنترنت ، يجب على التطبيق إعادة الاتصال بالواجهة الخلفية ومزامنة البيانات وحل التعارضات ، إن وجدت. تتطلب المعالجة الصحيحة لجميع الحالات القصوى الكثير من التعليمات البرمجية غير المميزة ، حتى عند استخدام ذاكرة التخزين المؤقت 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 }
بناء التطبيق والاختبار
منجز
المراجع
https://aws-amplify.imtqy.com
https://learning.oreilly.com/library/view/full-stack-serverless/9781492059882/
https://www.altexsoft.com/blog/engineering/graphql-core-features-arch architecture-pros-and-cons/
https://engineering.fb.com/core-data/graphql-a-data-query-language/
https://graphql.org/learn