
هذه المقالة للأشخاص الذين لديهم خبرة في React و RxJS. أقوم فقط بمشاركة القوالب التي وجدتها مفيدة لإنشاء مثل واجهة المستخدم هذه.
إليك ما نقوم به:

بدون دروس ، العمل مع دورة حياة أو setState
.
تحضير
كل ما تحتاجه موجود في مستودعي على GitHub.
git clone https://github.com/yazeedb/recompose-github-ui cd recompose-github-ui yarn install
يوجد في الفرع master
مشروع منتهي. انتقل إلى فرع start
إذا كنت تريد الانتقال خطوة بخطوة.
git checkout start
وتشغيل المشروع.
npm start
يجب أن يبدأ التطبيق في localhost:3000
وهنا واجهة المستخدم الأولية.

قم بتشغيل المحرر المفضل لديك وافتح src/index.js
.

أعد تشكيل
إذا لم تكن على دراية بـ Recompose ، فهذه مكتبة رائعة تسمح لك بإنشاء مكونات تفاعل بأسلوب وظيفي. يحتوي على مجموعة كبيرة من الوظائف. هنا المفضلة لدي .
انها مثل لداش / رمدة ، رد فعل فقط.
أنا سعيد للغاية لأنه يدعم نمط الأوبزرفر. نقلاً عن الوثائق :
اتضح أنه يمكن التعبير عن معظم واجهة برمجة تطبيقات React Component من حيث نمط المراقب.
اليوم سوف نمارس هذا المفهوم!
مكون مضمن
حتى الآن ، لدينا App
- مكون التفاعل الأكثر شيوعًا. باستخدام وظيفة كومبونفرستريم من مكتبة إعادة التركيب يمكننا الحصول عليها من خلال كائن يمكن ملاحظته.
تبدأ وظيفة كومبونفرستريام التقديم على كل قيمة جديدة من ملاحظتنا. إذا لم تكن هناك قيم بعد ، فإنها null
.
التكوين
تتبع التدفقات في إعادة التركيب وثيقة اقتراح ECMAScript الملحوظة . يصف كيفية عمل الكائنات القابلة للرصد عند تنفيذها في المتصفحات الحديثة.
في غضون ذلك ، سنستخدم مكتبات مثل RxJS و xstream ومعظمها و Flyd وما إلى ذلك.
لا تتعرف إعادة setObservableConfig
على المكتبة التي نستخدمها ، لذا فهي توفر وظيفة setObservableConfig
. مع ذلك ، يمكنك تحويل كل ما نحتاجه إلى ES Observable.
أنشئ ملفًا جديدًا في المجلد src
وقم observableConfig.js
.
لتوصيل RxJS 6 بـ Recompose ، اكتب الكود التالي فيه:
import { from } from 'rxjs'; import { setObservableConfig } from 'recompose'; setObservableConfig({ fromESObservable: from });
قم باستيراد هذا الملف إلى index.js
:
import './observableConfig';
هذا كل شيء!
أعد تركيب + RxJS
إضافة استيراد componentFromStream
في index.js
:
import { componentFromStream } from 'recompose';
لنبدأ تجاوز مكون App
:
const App = componentFromStream(prop$ => { ... });
لاحظ أن componentFromStream
fromStream يأخذ كوسيطة دالة مع معلمة prop$
، وهي نسخة يمكن ملاحظتها من props
. الفكرة هي استخدام الخريطة لتحويل props
العادية إلى مكونات تفاعل.
إذا كنت تستخدم RxJS ، فيجب أن تكون على دراية بعامل الخريطة .
الخريطة
كما يوحي الاسم ، تحول الخريطة Observable(something)
إلى Observable(somethingElse)
. في حالتنا - Observable(props)
في Observable(component)
.
استيراد عامل تشغيل map
:
import { map } from 'rxjs/operators';
App
مكون App
لدينا:
const App = componentFromStream(prop$ => { return prop$.pipe( map(() => ( <div> <input placeholder="GitHub username" /> </div> )) ) });
باستخدام RxJS 5 ، نستخدم pipe
بدلاً من سلسلة من العبارات.
احفظ الملف وتحقق من النتيجة. لم يتغير شيء!

أضف معالج حدث
الآن سنجعل حقل الإدخال متفاعلًا قليلاً.
إضافة استيراد createEventHandler
:
import { componentFromStream, createEventHandler } from 'recompose';
سنستخدمها على النحو التالي:
const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); return prop$.pipe( map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) });
يحتوي الكائن الذي تم إنشاؤه بواسطة createEventHandler
على createEventHandler
: handler
stream
.
تحت الغطاء ، handler
هو باعث الحدث الذي ينقل القيم إلى stream
. stream
بدوره ، هو كائن يمكن ملاحظته ويمرر القيم للمشتركين.
سنقوم بربط stream
و prop$
للحصول على القيمة الحالية لحقل الإدخال.
في حالتنا ، سيكون الاختيار الجيد هو استخدام وظيفة combineLatest
.
مشكلة البيض والدجاج
لاستخدام combineLatest
، يجب أن ينتج عن كل من stream
و prop$
قيمًا. لكن stream
لن يطلق أي شيء حتى prop$
بعض إصدارات القيمة prop$
والعكس صحيح.
يمكنك إصلاح ذلك عن طريق تعيين stream
الأولية.
استيراد بيان startWith من RxJS:
import { map, startWith } from 'rxjs/operators';
قم بإنشاء متغير جديد للحصول على القيمة من stream
المحدث:
// App component const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value) startWith('') );
نحن نعلم أن stream
سيلقي الأحداث عندما يتغير حقل الإدخال ، لذلك دعونا نترجمها على الفور إلى نص.
وبما أن القيمة الافتراضية لحقل الإدخال هي سلسلة فارغة ، فقم بتهيئة الكائن value$
متماسكة معا
الآن نحن مستعدون لربط كلا التدفقات. استيراد combineLatest
كطريقة لإنشاء كائنات يمكن ملاحظتها ، وليس كعامل .
import { combineLatest } from 'rxjs';
يمكنك أيضًا استيراد عبارة tap
لفحص قيم الإدخال.
import { map, startWith, tap } from 'rxjs/operators';
استخدمها على النحو التالي:
const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value), startWith('') ); return combineLatest(prop$, value$).pipe( tap(console.warn),
الآن ، إذا بدأت في كتابة شيء ما في حقل الإدخال الخاص بنا ، فستظهر القيم [props, value]
في وحدة التحكم.

مكون المستخدم
سيكون هذا المكون مسؤولاً عن عرض المستخدم الذي سننقل اسمه إليه. ستحصل على value
من مكون App
وترجمته إلى طلب AJAX.
Jsx / css
كل هذا مبني على مشروع بطاقات جيثب الرائعة. يتم نسخ أو تكييف معظم التعليمات البرمجية ، وخاصة الأنماط.
قم src/User
مجلد src/User
. قم User.css
ملف User.css
فيه وانسخ هذا الرمز إليه.
وانسخ هذا الرمز إلى ملف src/User/Component.js
.
يملأ هذا المكون القالب ببساطة ببيانات من مكالمة إلى GitHub API.
الحاوية
الآن هذا المكون "غبي" ولسنا على الطريق ، دعونا نجعل مكون "ذكي".
هنا هو src/User/index.js
import React from 'react'; import { componentFromStream } from 'recompose'; import { debounceTime, filter, map, pluck } from 'rxjs/operators'; import Component from './Component'; import './User.css'; const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(user => ( <h3>{user}</h3> )) ); return getUser$; }); export default User;
قمنا بتعريف User
أنه ComponFromStream ، والذي يقوم بإرجاع كائن prop$
قابل للرصد يحول الخصائص الواردة إلى <h3>
.
debounceTime
سيحصل مستخدمنا على قيم جديدة في كل مرة يتم فيها الضغط على مفتاح على لوحة المفاتيح ، لكننا لسنا بحاجة إلى هذا السلوك.
عندما يبدأ المستخدم في الكتابة ، سوف يتخطى debounceTime(1000)
جميع الأحداث التي تستمر أقل من ثانية واحدة.
نتف
نتوقع تمرير كائن user
على أنه props.user
. يأخذ عامل النتف الحقل المحدد من الكائن ويعيد قيمته.
مرشح
هنا نتأكد من تمرير user
وليس سلسلة فارغة.
الخريطة
نصنع علامة <h3>
من user
.
ربط
ارجع إلى src/index.js
مكون User
:
import User from './User';
نقوم بتمرير قيمة value
كمعلمة user
:
return combineLatest(prop$, value$).pipe( tap(console.warn), map(([props, value]) => ( <div> <input onChange={handler} placeholder="GitHub username" /> <User user={value} /> </div> )) );
الآن يتم عرض قيمنا مع تأخير لمدة ثانية واحدة.

ليس سيئًا ، نحن الآن بحاجة إلى الحصول على معلومات حول المستخدم.
طلب بيانات
يوفر GitHub واجهة برمجة تطبيقات للحصول على معلومات المستخدم: https://api.github.com/users/${user} . يمكننا بسهولة كتابة وظيفة مساعدة:
const formatUrl = user => `https://api.github.com/users/${user}`;
والآن يمكننا إضافة map(formatUrl)
بعد filter
:
const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl),
والآن ، بدلاً من اسم المستخدم ، يتم عرض عنوان URL على الشاشة.
نحن بحاجة لتقديم طلب! يأتي switchMap
و ajax
إلى switchMap
.
سويتش ماب
هذا المشغل مثالي للتبديل بين عدة ملاحظات
لنفترض أن المستخدم كتب الاسم ، switchMap
بطلب داخل switchMap
.
ماذا يحدث إذا أدخل المستخدم شيئًا قبل وصول الرد من واجهة برمجة التطبيقات؟ هل يجب أن نقلق بشأن الطلبات السابقة؟
لا.
switchMap
بإلغاء الطلب القديم والتبديل إلى الطلب الجديد.
اجاكس
توفر RxJS تطبيق ajax
الخاص بها والذي يعمل بشكل رائع مع switchMap
!
جرب
نستورد كلا العاملين. الرمز الخاص بي يبدو كالتالي:
import { ajax } from 'rxjs/ajax'; import { debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators';
واستخدامها مثل هذا:
const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), switchMap(url => ajax(url).pipe( pluck('response'), map(Component) ) ) ); return getUser$; });
switchMap
من حقل الإدخال الخاص بنا إلى طلب AJAX. عندما تصل الإجابة ، فإنها تمررها إلى مكوننا الغبي.
وهذه هي النتيجة!

معالجة الخطأ
حاول إدخال اسم مستخدم غير موجود.

طلبنا مكسور.
قبض على الخطأ
باستخدام عامل catchError
يمكننا عرض استجابة catchError
بدلاً من catchError
بهدوء.
نستورد:
import { catchError, debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators';
وأدخله في نهاية طلب AJAX:
switchMap(url => ajax(url).pipe( pluck('response'), map(Component), catchError(({ response }) => alert(response.message)) ) )

بالفعل ليس سيئًا ، ولكن بالطبع يمكنك القيام بعمل أفضل.
خطأ في المكون
قم src/Error/index.js
بالمحتويات:
import React from 'react'; const Error = ({ response, status }) => ( <div className="error"> <h2>Oops!</h2> <b> {status}: {response.message} </b> <p>Please try searching again.</p> </div> ); export default Error;
سيعرض بشكل رائع response
status
طلب AJAX.
نقوم باستيراده إلى User/index.js
، وفي نفس الوقت عامل التشغيل من RxJS:
import Error from '../Error'; import { of } from 'rxjs';
تذكر أنه يجب إرجاع الوظيفة التي تم تمريرها إلى ComponFromStream بشكل ملحوظ. يمكننا تحقيق ذلك باستخدام عامل التشغيل:
ajax(url).pipe( pluck('response'), map(Component), catchError(error => of(<Error {...error} />)) )
الآن تبدو واجهة المستخدم لدينا أفضل بكثير:

مؤشر التحميل
حان الوقت لتقديم إدارة الدولة. وإلا كيف يمكنك تنفيذ مؤشر التحميل؟
ماذا لو كان مكان setState
سنستخدم BehaviorSubject
؟
تقترح إعادة التوثيق ما يلي:
بدلاً من setState () ، قم بدمج سلاسل محادثات متعددة
حسنًا ، أنت بحاجة إلى عمليتي استيراد جديدتين:
import { BehaviorSubject, merge, of } from 'rxjs';
سيحتوي BehaviorSubject
على حالة التنزيل ، وسوف merge
مع المكون.
componentFromStream
الداخلي componentFromStream
:
const User = componentFromStream(prop$ => { const loading$ = new BehaviorSubject(false); const getUser$ = ...
BehaviorSubject
تهيئة BehaviorSubject
بقيمة أولية أو "حالة". نظرًا لأننا لا نفعل أي شيء حتى يبدأ المستخدم في إدخال النص ، فقم بتهيئته على false
.
سنغير حالة loading$
باستخدام عامل تشغيل tap
:
import { catchError, debounceTime, filter, map, pluck, switchMap, tap
سنستخدمها على النحو التالي:
const loading$ = new BehaviorSubject(false); const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), tap(() => loading$.next(true)), // <--- switchMap(url => ajax(url).pipe( pluck('response'), map(Component), tap(() => loading$.next(false)), // <--- catchError(error => of(<Error {...error} />)) ) ) );
قبل طلب switchMap
و AJAX مباشرةً ، switchMap
القيمة loading$
، و false
بعد الاستجابة الناجحة.
والآن نربط فقط loading$
و getUser$
.
return merge(loading$, getUser$).pipe( map(result => (result === true ? <h3>Loading...</h3> : result)) );
قبل أن نلقي نظرة على العمل ، يمكننا استيراد بيان delay
حتى لا تكون التحولات سريعة جدًا.
import { catchError, debounceTime, delay, filter, map, pluck, switchMap, tap } from 'rxjs/operators';
أضف delay
قبل map(Component)
:
ajax(url).pipe( pluck('response'), delay(1500), map(Component), tap(() => loading$.next(false)), catchError(error => of(<Error {...error} />)) )
النتيجة؟

الكل :)