اذهب إلى المحتوى

البحث في الموقع

المحتوى عن 'react native'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 5 نتائج

  1. لقد حان الوقت لتوسيع مشروعنا بعد أن أسسنا له قاعدةً معرفيةً جيدة، وسنسخّر لهذه المهمة كل إمكانيات React Native التي تعلمناها حتى الآن. سنغطي بالإضافة إلى توسيع المشروع نواحٍ جديدة، مثل الاختبارات والموارد الإضافية. اختبار تطبيقات React Native سنحتاج إلى إطار عمل للاختبارات عند البدء باختبار أي نوع من الشيفرة، لتنفيذ مجموعة من الحالات المختبرة، والتدقيق في نتائجها. فلاختبار تطبيق جافا سكربت JavaScript سنجد أن إطار العمل Jest هو الأكثر شعبية لأداء المهمة. ولاختبار تطبيقات React Native المبنية على المنصة Expo باستخدام Jest، ستزودنا Expo بمجموعة من إعدادات تهيئة على هيئة مجموعة جاهزة تدعى jest-expo. ولكي نستخدم المدقق ESLint في ملف اختبار Jest، سنحتاج إلى الإضافة eslint-plugin-jest الخاصة به. لنبدأ إذًا بتثبيت الحزم: npm install --save-dev jest jest-expo eslint-plugin-jest لاستخدام المجموعة jest-expo في Jest، لا بدّ من إضافة إعدادات التهيئة التالية في الملف "package.json"، مع سكربت الاختبار: { // ... "scripts": { // other scripts... "test": "jest" }, "jest": { "preset": "jest-expo", "transform": { "^.+\\.jsx?$": "babel-jest" }, "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-router-native)" ] }, // ... } يطلب الخيار transform من Jest تحويل شيفرة الملفين "js." و"jsx." باستخدام مصرّف Babel. بينما يُستخدم الخيار transformIgnorePatterns لتجاهل مجلدات محددة ضمن المجلد "node_modules" أثناء تحويل الملفات. وتتطابق تقريبًا إعدادات Jest هذه مع تلك المقترحة في توثيق Expo. لاستخدام الإضافة eslint-plugin-jest، لا بدّ من وضعها ضمن مصفوفة الإضافات والموسِّعات في الملف "eslintrc.". { "plugins": ["react", "react-native"], "settings": { "react": { "version": "detect" } }, "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jest/recommended"], "parser": "@babel/eslint-parser", "env": { "react-native/react-native": true }, "rules": { "react/prop-types": "off", "react/react-in-jsx-scope": "off" } } وللتأكد من نجاح التهيئة، أنشئ المجلد "tests" في المجلد "src"، ثم أنشئ ضمنه الملف "example.js" وضع فيه الشيفرة التالية: describe('Example', () => { it('works', () => { expect(1).toBe(1); }); }); لننفذ الآن هذا المثال من خلال الأمر: npm test. ينبغي أن يشير خرج العملية إلى نجاح الاختبار المتواجد في الملف "src/tests/example.js". تنظيم الاختبارات تقتضي إحدى طرق تنظيم ملفات الاختبارات بوضعها في مجلد وحيد يُدعى "tests". ويفضّل عند استخدام هذه الطريقة وضع ملفات الاختبار ضمن المجلدات الفرعية الملائمة كما نفعل مع ملفات الشيفرة، إذ ستُوضع الاختبارات التي تتعلق بالمكوّنات مثلًا في المجلّد "components"، وتلك التي تتعلق بالخدمات ستُوضع في المجلّد "utils"، وهكذا. سينتج عن هذا التنظيم الهيكلية التالية: src/ __tests__/ components/ AppBar.js RepositoryList.js ... utils/ authStorage.js ... ... أما الطريقة الأخرى فهي وضع ملف الاختبار بجانب ملف الشيفرة الذي سيستخدمه. ويعني ذلك أننا سنضع الملف الذي يتضمن اختبارات للمكوّن AppBar مثلًا في نفس المجلد الذي يحتوي شيفرة هذا المكوّن. وستنتج عن هذه الطريقة في التنظيم الهيكلية التالية: src/ components/ AppBar/ AppBar.test.jsx index.jsx ... ... شيفرة المكوّن في المثال السابق موجودةٌ في الملف "index.jsx"، بينما ستجد الاختبارات في الملف "AppBar.test.jsx". ولكي تجد بسهولة الملفات التي تحتوي الاختبارات، عليك أن تضعها في المجلد "tests" أو في ملفات تحمل إحدى اللاحقتين "test." أو "spec."، أو أن تهيئ يدويًا الأنماط العامة global patterns. اختبار المكونات بعد أن ضبطنا إعدادات Jest وجربناها بتنفيذ مثال بسيط، حان الوقت لنتعرف على كيفية اختبار المكوّنات. يتطلب اختبار المكوّنات، كما نعلم، طريقة لتفسير الخرج الناتج عن تصيير المكوّن ومحاكاة عملية معالجة الأحداث المختلفة مثل الضغط على زر. ولحسن الحظ تتوافر عائلة من مكتبات الاختبار تؤمن مكتبات لاختبار مكوًنات واجهة المكوّن في منصات عدة. وتتشارك جميع هذه المكتبات بواجهة برمجية واحدة API لاختبار مكونات واجهة المستخدم بأسلوب يركّز على المستخدم. تعرّفنا في القسم 5 على إحدى هذه المكتبات وهي React Testing Library. لكن لسوء الحظ فهذه المكتبة مخصصة فقط لاختبار تطبيقات الويب المبنية باستخدام React. لكن بالطبع هناك مقابل لها في React Native وهي المكتبة React Native Testing Library التي سنستخدمها في اختبار مكوّنات تطبيقات React Native. وكما ذكرنا فإن جميع هذه المكتبات تتشارك بنفس الواجهة البرمجية، ولن تضطر إلى تعلّم الكثير من المفاهيم الجديدة. بالإضافة إلى هذه المكتبة، سنحتاج إلى مجموعة من مُطابقات Jest مخصصة للمكتبة مثل toHaveTextContent و toHaveProp. تزوّدنا المكتبة jest-native بهذه المُطابقات، لذلك لا بدّ أولًا من تثبيت هذه الحزم: npm install --save-dev react-test-renderer@17.0.1 @testing-library/react-native @testing-library/jest-native ملاحظة: إذا واجهت مشاكل في اعتمادية النظير peer، فتأكد من مطابقة إصدار react-test-renderer مع نسخة react للمشروع في أمر npm install المذكور أعلاه. يمكنك التحقق من إصدار react من خلال تنفيذ الأمر: npm list react --depth=0 وفي حال فشل التثبيت بسبب مشكلات اعتمادية النظير، فحاول مرةً أخرى باستخدام الراية ‎--legacy-peer-deps في الأمر npm install. لنتمكن من استخدام هذه المُطابقات، علينا توسيع الكائن expect العائد للمكتبة Jest. ويجري تنفيذ ذلك باستخدام ملف إعداد عام. لذلك أنشئ الملف "setupTests.js" في المجلد الجذري لمشروعك، وهو نفسه المجلد الذ++ي يحتوي الملف "package.json". أضف الشيفرة التالية إلى هذا الملف: import '@testing-library/jest-native/extend-expect'; هيئ الملف السابق على انه ملف إعداد ضمن أوامر تهيئة Jest الموجودة في الملف "package.json": { // ... "jest": { "preset": "jest-expo", "transform": { "^.+\\.jsx?$": "babel-jest" }, "transformIgnorePatterns": [ "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*|react-router-native)" ], "setupFilesAfterEnv": ["<rootDir>/setupTests.js"] } // ... } تعتمد المكتبة على مفهومين أساسيين هما الاستعلامات (query) وإطلاق الأحداث (fire event)؛ إذ تُستخدم الاستعلامات لاستخلاص مجموعة من العقد من المكوّن الذي يُصيّر بواسطة الدالة render، ونستفيد منها في اختبار وجود عنصر أو كائن ما، مثل نص معين، ضمن المكوّن المُصيَّر. يمكنك تحديد العقد باستخدام الخاصية testID ثم الاستعلام عنها باستخدام الدالة getByTestId. تقبل جميع المكوّنات البنيوية في الخاصية testID. وإليك مثالًا عن كيفية استخدام الاستعلامات: import { Text, View } from 'react-native'; import { render } from '@testing-library/react-native'; const Greeting = ({ name }) => { return ( <View> <Text>Hello {name}!</Text> </View> ); }; describe('Greeting', () => { it('renders a greeting message based on the name prop', () => { const { debug, getByText } = render(<Greeting name="Kalle" />); debug(); expect(getByText('Hello Kalle!')).toBeDefined(); }); }); تعيد الدالة render الاستعلامات ودوال مساعدة أخرى مثل الدالة debug التي تطبع شجرة React بطريقة واضحة بالنسبة للمستخدم. استخدم هذه الدالة إن كنت غير متأكد من شكل المكوّن الذي صيَّرته الدالة render. يمكننا أن نحصل على العقدة Text التي تحتوي نصًا محددًا عن طريق الدالة getByText. وللاطلاع على كل الاستعلامات المتاحة، راجع توثيق المكتبة React Native Testing. يُستخدم المُطابق toHaveTextContent للتأكد من أن المحتوى النصي للعقدة صحيح. وللاطلاع كذلك على كل المُطابقات الخاصة بالمكتبة React Native، راجع توثيق jest-native. وتذكر أنك ستجد معلومات عن كل مُطابقٍ عام في توثيق Jest. أما المفهوم الثاني الذي ترتكز عليه المكتبة React Native Testing في عملها هو إطلاق الأحداث firing events، إذ يمكننا إطلاق حدث ضمن عقدة معينة باستخدام توابع الكائن fireEvent. سنستفيد من ذلك على سبيل المثال في طباعة نص ضمن حقل نصي أو على زر. وإليك مثال عن اختبار تسليم نموذج بسيط: import { useState } from 'react'; import { Text, TextInput, Pressable, View } from 'react-native'; import { render, fireEvent } from '@testing-library/react-native'; const Form = ({ onSubmit }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = () => { onSubmit({ username, password }); }; return ( <View> <View> <TextInput value={username} onChangeText={(text) => setUsername(text)} placeholder="Username" /> </View> <View> <TextInput value={password} onChangeText={(text) => setPassword(text)} placeholder="Password" /> </View> <View> <Pressable onPress={handleSubmit}> <Text>Submit</Text> </Pressable> </View> </View> ); }; describe('Form', () => { it('calls function provided by onSubmit prop after pressing the submit button', () => { const onSubmit = jest.fn(); const { getByPlaceholderText, getByText } = render(<Form onSubmit={onSubmit} />); fireEvent.changeText(getByPlaceholderText('Username'), 'kalle'); fireEvent.changeText(getByPlaceholderText('Password'), 'password'); fireEvent.press(getByText('Submit')); expect(onSubmit).toHaveBeenCalledTimes(1); // تحوي أول وسيط من أول استدعاء onSubmit.mock.calls[0][0] expect(onSubmit.mock.calls[0][0]).toEqual({ username: 'kalle', password: 'password', }); }); }); نريد في هذا الاختبار أن نتأكد من أنّ الدالة onSubmit ستُستدعى على نحوٍ صحيح بعد أن تُملأ حقول النموذج باستخدام التابع fireEvent.changeText ويُضغط على الزر باستخدام التابع fireEvent.press. ولتحري إذا ما استُدعيت الدالة onSubmit وبأية وسطاء، يمكن استخدام الدالة المقلّدة mock function؛ والدوال المقلدة هي دوال ذات سلوك مبرمج مسبقًا كأن تعيد قيمةً محددة. كما يمكننا تحديد توقّعات expectations لهذه الدوال، فمثلًا "توقّع أن الدالة المقلّدة قد استدعيت مرةً واحدة". يمكنك الاطلاع على قائمة بكل التوقعات المتاحة في توثيق توقعات Jest. عدّل وجرّب في تلك الأمثلة السابقة، وأضف اختبارات جديدة في المجلد "tests "، بحيث تتأكد من فهمك للأفكار السابقة قبل الغوص أعمق في موضوع الاختبارات. التعامل مع الاعتماديات أثناء الاختبارات يُعد اختبار المكوّنات سهلًا نوعًا ما لأنها صرفة بشكلٍ أو بآخر. فلا تعتمد المكوّنات الصرّفة على التأثيرات الجانبية side effects، مثل طلبات الشبكة أو استخدام واجهات برمجية أصيلة، مثل AsyncStorage. فالمكوّن Form ليس صرفًا بقدر المكون Greeting، لأن تغيرات حالته قد تُعد أثرًا جانبيًا. لكن الاختبارات تبقى سهلةً مع ذلك. لنلقِ الآن نظرةً على استراتيجية لاختبار المكوّنات مع الآثار الجانبية، وسنختار على سبيل المثال المكوّن RepositoryList من تطبيقنا. لهذا المكوّن حاليًا أثر جانبي واحد وهو استعلام GraphQl الذي يحضر قائمة بالمستودعات المقيّمة. يُستخدم المكوّن حاليًا كالتالي: const RepositoryList = () => { const { repositories } = useRepositories(); const repositoryNodes = repositories ? repositories.edges.map((edge) => edge.node) : []; return ( <FlatList data={repositoryNodes} // ... /> ); }; export default RepositoryList; ينتج التأثير الجانبي عن استخدام الخطاف useRepositories الذي يرسل استعلام GraphQL. هناك عدة طرق لاختبار المكوّن، إحدى هذه الطرق هي تقليد استجابات المكتبة Apollo Client كما هو مشروح في توثيقها. أما الطريقة الأبسط هي افتراض أن المكوّن يعمل كما نتوقع (ويُفضل من خلال اختباره)، ثم استخلاص الشيفرة "الصرفة" له ووضعها في مكوّن آخر مثل RepositoryListContainer: export const RepositoryListContainer = ({ repositories }) => { const repositoryNodes = repositories ? repositories.edges.map((edge) => edge.node) : []; return ( <FlatList data={repositoryNodes} // ... /> ); }; const RepositoryList = () => { const { repositories } = useRepositories(); return <RepositoryListContainer repositories={repositories} />; }; export default RepositoryList; يحتوي المكوّن الآن التأثيرات الجانبية فقط وتنفيذه سهل. يمكن اختبار المكوّن RepositoryListContainer بتمرير بيانات عن مستودع من القائمة من خلال الخاصية repositories، والتأكد أنّ معلومات المحتوى المُصيَّر صحيحة. التمرينان 10.17 - 10.18 10.17 اختبار قائمة المستودعات المقيمة أنجز اختبارًا يتأكد أن المكوّن RepositoryListContainer سيصيِّر بصورةٍ صحيحة كلًا من: اسم المستودع ووصفه واللغة وعدد التشعبات وعدد النجوم ومعدل التقييم وعدد التقييمات. تذكر أنه بالإمكان الاستفادة من المطابق toHaveTextContent في التحقق من احتواء العقدة على سلسلة نصية محددة. const RepositoryItem = (/* ... */) => { // ... return ( <View testID="repositoryItem" {/* ... */}> {/* ... */} </View> ) }; كما يمكنك استخدام الاستعلام getAllByTestId للحصول على كل العقد التي تمتلك قيمة محددة للخاصية testID على هيئة مصفوفة، وإن كنت غير متأكد من المكوّن الذي صُيَّر، استفد من الدالة debug في تفكيك نتيجة التصيير. const repositoryItems = getAllByTestId('repositoryItem'); const [firstRepositoryItem, secondRepositoryItem] = repositoryItems; // توقع شيئًا من عنصر المستودع الأول والثاني استخدم الشيفرة التالية أساسًا لاختبارك: describe('RepositoryList', () => { describe('RepositoryListContainer', () => { it('renders repository information correctly', () => { const repositories = { pageInfo: { totalCount: 8, hasNextPage: true, endCursor: 'WyJhc3luYy1saWJyYXJ5LnJlYWN0LWFzeW5jIiwxNTg4NjU2NzUwMDc2XQ==', startCursor: 'WyJqYXJlZHBhbG1lci5mb3JtaWsiLDE1ODg2NjAzNTAwNzZd', }, edges: [ { node: { id: 'jaredpalmer.formik', fullName: 'jaredpalmer/formik', description: 'Build forms in React, without the tears', language: 'TypeScript', forksCount: 1619, stargazersCount: 21856, ratingAverage: 88, reviewCount: 3, ownerAvatarUrl: 'https://avatars2.githubusercontent.com/u/4060187?v=4', }, cursor: 'WyJqYXJlZHBhbG1lci5mb3JtaWsiLDE1ODg2NjAzNTAwNzZd', }, { node: { id: 'async-library.react-async', fullName: 'async-library/react-async', description: 'Flexible promise-based React data loader', language: 'JavaScript', forksCount: 69, stargazersCount: 1760, ratingAverage: 72, reviewCount: 3, ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/54310907?v=4', }, cursor: 'WyJhc3luYy1saWJyYXJ5LnJlYWN0LWFzeW5jIiwxNTg4NjU2NzUwMDc2XQ==', }, ], }; // أضف شيفرة اختبارك هنا }); }); }); يمكن أن تضع ملف الاختبار أينما تشاء، لكن من الأفضل أن تتبع إحدى الطرق التي ذكرناها في تنظيم ملفات الاختبار. استخدم المتغير repositories مثل مصدر للبيانات في الاختبار، ولا حاجة لتغيير قيمته. تحتوي البيانات على مستودعين، فعليك بالتالي التحقق من وجود المعلومات في كليهما. 10.18 اختبار نموذج تسجيل الدخول أنجز اختبارًا يتحقق أن الدالة onSubmit ستُستدعى بوسطاء مناسبين عندما يُملأ حقلي اسم المستخدم وكلمة المرور في النموذج ويُضغط على زر الإرسال. ينبغي أن يكون الوسيط الأول كائنًا يمثّل قيم النموذج، ويمكنك إهمال بقية وسطاء الدالة. تذكر أنك قد تستعمل توابع fireEvent لتنفيذ أحداث ودوال مقلِّدة للتحقق من استدعاء معالج الحدث onSubmit أو لا. لا حاجة لاختبار أي شيفرة متعلقة بالمكتبة Apollo Client أو AsyncStorage والموجودة في الخطاف useSignIn. وكما فعلنا في التمرين السابق، استخلص الشيفرة الصرفة وضعها في مكوّن خاص واختبره. انتبه إلى إرسالات نماذج Formik فهي غير متزامنة، فلا تتوقع استدعاء الدالة onSubmit مباشرة بعد الضغط على زر الإرسال. يمكنك الالتفاف على المشكلة بجعل دالة الاختبار دالة غير متزامنة باستخدام التعليمة Async واستخدام الدالة المساعدة waitFor العائدة للمكتبة React Native Testing. كما يمكنك يمكن أن تستخدم الدالة waitFor لانتظار تحقق التوقعات. فإن لم يتحقق التوقع خلال فترة محددة، سترمي الدالة خطأً. إليك مثالًا يشير إلى أسلوب استخدام هذه الدالة: import { render, fireEvent, waitFor } from '@testing-library/react-native'; // ... describe('SignIn', () => { describe('SignInContainer', () => { it('calls onSubmit function with correct arguments when a valid form is submitted', async () => { //صيّر المكوّن ثم املأ الحقول النصية ثم اضغط زر الإرسال await waitFor(() => { // توقع أن تُستدعى دالة الإرسال مرة واحد وأن يكون الوسيط الأول صحيحًا }); }); }); }); توسيع التطبيق سنبدأ باستخدام كل ما تعلمناه في توسيع تطبيقنا، فلا يزال التطبيق يفتقر لبعض النواحي، مثل عرض معلومات مستودع وتسجيل مستخدمين جدد. سنركز في التمارين القادمة على هذه النقاط. التمرينات 10.19 - 10.24 10.19: واجهة لعرض مستودع واحد أنجز واجهةً لعرض معلومات عن مستودع واحد، تتضمن نفس المعلومات التي تُعرض في قائمة المستودعات بالإضافة إلى زر لفتح المستودع في غيت هب Github. من الجيد أن تجد طريقةً لإعادة استخدام المكونين RepositoryItem و RepositoryList وأن تعرض الزر الذي أشرنا إليه بناء على خاصية محددة تعيد قيمة منطقية مثلًا. ستجد عنوان المستودع ضمن الحقلurl العائد للنوع "Repository" في تخطيط GraphQL. تستطيع إحضار مستودع واحد من خادم Apollo بإجراء الاستعلام repository. يُمرَّر إلى هذا الاستعلام وسيط واحد وهو المعرّف المميز id للمستودع. إليك مثالًا عن استخدام الاستعلام repository. { repository(id: "jaredpalmer.formik") { id fullName url } } اختبر استعلامك كما هو معتاد في أرضية عمل Apollo قبل استخدامه في التطبيق. إن لم تكن على دراية بتخطيط GrapQL أو الاستعلامات التي يتيحها، افتح النافذة "docs" أو النافذة "schema" في أرضية عمل GraphQL. وإن واجهتك صعوبة في استخدام المعرف id مثل متغير في الاستعلام، توقف قليلًا لتطلع على توثيق Apollo Client بما يخص الاستعلامات. لتطلع على طريقة التوجه إلى عنوان URL في المتصفح، اقرأ توثيق Expo حول واجهة الربط البرمجية Linking API، لأنك ستحتاج ذلك عند تنفيذك للزر الذي يفتح المستودع في GitHub. ينبغي أن تمتلك واجهة العرض عنوانًا خاصًا بها، ومن الجيد أن تحدد المعرّف المميز id للمستودع ضمن المسار الذي يوجهك نحو هذا العنوان مثل معامل مسار، كي تستطيع الوصول إليه باستخدام الخطاف useParams. ينبغي أن يكون المستخدم قادرًا على الوصول إلى واجهة العرض بالضغط على المستودع ضمن قائمة المستودعات. ومن الممكن تنفيذ ذلك بتغليف المكوّن RepositoryItem داخل المكّون البنيوي Pressable في المكوّن RepositoryList، ثم استخدام الدالة navigate لتغيير العنوان من خلال معالج الحدث onPress. استخدم الخطاف useNavigate أيضًا للوصول إلى الكائن دالة navigate. ستبدو النسخة النهائية لواجهة عرض مستودع واحد قريبة من الشكل التالي: 10.20: قائمة بالآراء حول مستودع بعد أن أنجزنا واجهة عرض لمستودع وحيد، سنعرض تقييمات المستخدمين ضمن هذه الواجهة. ستجد التقييمات في الحقل reviews العائد للنوع "Repository" في تخطيط GraphQL. تشكل التقييمات قائمةً صغيرةً مرقمةً يمكن الحصول عليها باستخدام الاستعلام repositories. وإليك مثالًا عن طريقة إحضار قائمة التقييمات لمستودع واحد: { repository(id: "jaredpalmer.formik") { id fullName reviews { edges { node { id text rating createdAt user { id username } } } } } } يحتوي الحقل text لكل تقييم على رأي المستخدم للنص، ويحتوي الحقل rating على قيمة عددية بين 0 و100، أما الحقل craetedAt فيحتوي على بيانات إنشاء هذا التقييم، وأخيرًا يحتوي الحقل user معلومات عن مُنشئ التقييم ويحمل النوع "User". نريد أن نعرض التقييمات على شكل شريط تمرير، مما يجعل المكوّن البنيوي FlatList ملائمًا لأداء المهمة. ولكي تعرض معلومات مستودع محدد في أعلى الواجهة، استخدم الخاصية ListHeaderComponent للمكون FlatList. يمكنك أيضًا استخدام المكوّن البنيوي ItemSeparatorComponent لإضافة مساحة بيضاء بين العناصر المعروضة كما هو الحال بين عناصر المكوّن RepositoryList مثلًا. إليك مثالًا عن هيكلية التنفيذ: const RepositoryInfo = ({ repository }) => { // Repository's information implemented in the previous exercise }; const ReviewItem = ({ review }) => { // Single review item }; const SingleRepository = () => { // ... return ( <FlatList data={reviews} renderItem={({ item }) => <ReviewItem review={item} />} keyExtractor={({ id }) => id} ListHeaderComponent={() => <RepositoryInfo repository={repository} />} // ... /> ); }; export default SingleRepository; ستبدو النسخة النهائية عن تطبيق قائمة المستودعات مشابهة للشكل التالي: البيانات التي تراها تحت اسم المستخدم لمنشئ التقييم هي تاريخ الإنشاء الذي نجده في الحقل createdAt وهو من النوع "Review"، وينبغي أن يكون تنسيق البيانات مريحًا للمستخدم مثل الصيغة "يوم.شهر.سنة". قد يساعدك تثبيت المكتبة date-fns على تنسيق تاريخ الإنشاء بالاستفادة من الدالة format. يمكن إنجاز الحاوية ذات الشكل الدائري باستخدام خاصية التنسيق borderRadius. ولتفعل هذا عليك تثبيت قيمتي الخاصيتين width و height، ثم ضبط خاصية التنسيق border-radius عند القيمة "width/2". 10.21: نموذج التقييم أنجز نموذجًا لإنشاء تقييم جديد لمستودع مستخدمًا المكتبة Formik. ينبغي أن يضم النموذج أربعة حقول هي: اسم دخول مالك المستودع. اسم المستودع. التقييم على هيئة قيمة عددية. رأي المستخدم على هيئة نص. تحقق من الحقول مستخدمًا تخطيط Yup، بحيث تتأكد أن: اسم المستخدم لمالك المستودع هو قيمة نصية إجبارية. اسم المستودع هو قيمة نصية إجبارية. التقييم هو قيمة عددية إجبارية بين 0 و100. رأي المستخدم هو قيمة نصية اختيارية. اطلع على توثيق Yup لإيجاد المُقيَّمات validators المناسبة، واستخدم رسائل خطأ مناسبة عند استخدامك لهذه المُقيِّمات، إذ يمكنك تعريف رسالة الخطأ مثل وسيط للتابع message العائد للمقيّم. يمكنك أيضًا توسيع نص الحقل ليمتد على عدة أسطر باستعمال الخاصية multiline العائدة للمكون TextInput. استخدم الطفرة createReview لإنشاء تقييم جديد، واطلع على وسطاء الطفرة بفتح إحدى النافذتين "docs" أو "schema " في أرضية عمل GraphQL. ويمكنك استخدام الخطاف useMutation لإرسال الطفرة إلى خادم Apollo. بعد نجاح عمل الطفرة createReview، حوّل المستخدم إلى واجهة عرض المستودع التي أنجزناها في التمرين السابق. نفّذ ذلك باستخدام التابع navigate بعد أن تستخرج كائن التاريخ باستخدام الخطاف useNavigate. يمتلك التقييم الذي أنشأته حقلًا باسم "repositoryId" يمكنك استخدامه لتأسيس مسار لعنوان التقييم. استخدم سياسة الإحضار "cache-and-network" لتمنع جلب بيانات الذاكرة المؤقتة مع الاستعلام repository في واجهة عرض مستودع وحيد. يمكنك استخدامها مع الخطاف على النحو التالي: useQuery(GET_REPOSITORY, { fetchPolicy: 'cache-and-network', // خيارات أخرى }); انتبه إلى أن التقييم سيكون فقط لمستودعات GitHub العامة الموجودة فعلًا، وأن المستخدم سيقيَّم مستودعًا محددًا مرة واحدة فقط. ليس عليك الآن التعامل مع حالات الخطأ هذه، بل فقط إظهار الخطأ مع الرمز المحدد والرسالة المناسبة. جرّب ما نفّذته على أحد المستودعات العامة التي تملكها، أو أية مستودعات عامة أخرى. ينبغي أن يتمكن المستخدم من الوصول إلى واجهة التقييم من خلال شريط التطبيق. لذلك أنشئ نافذة جديدة عنوانها "Create a review". ينبغي أن تكون النافذة مرئية للمستخدم الذي سجل دخوله فقط. وعليك أن تحدد أيضًا عنوانًا لواجهة التقييم. ستبدو النسخة النهائية للتطبيق مشابهة للشكل التالي: التُقطت شاشة التطبيق هذه بعد إخفاق إرسال بيانات نموذج لإظهار شكله في هذه الحالة. 10.22: نموذج تسجيل مستخدم جديد أنجز نموذجًا لتسجيل مستخدم جديد مستخدمًا Formik. ينبغي أن يضم النموذج ثلاثة حقول: اسم مستخدم، كلمة مرور، وتأكيدًا لكلمة المرور. تحقق من صحة البيانات في النموذج مستخدمًا تخطيط Yup ومتبعًا مايلي: اسم المستخدم هو سلسلة نصية إجبارية طولها بين 1 و 30. كلمة المرور هي سلسلة نصية إجبارية طولها بين 1 و 5. تأكيد كلمة المرور يتطابق تمامًا مع كلمة المرور. قد يربكك قليلًا تأكيد كلمة المرور، لكن يمكن إنجاز الأمر بالاستفادة من التابعين oneOf وref كما هو مبين في إرشادات هذا المشروع. يمكنك إنشاء مستخدم جديدة باستخدام الطفرة createUser، ويمكنك الإطلاع على كيفية استخدامها من خلال توثيق أرضية عمل Apollo. سجّل دخول المستخدم الجديد بعد إنشاء حسابه الخاص مستخدمًا الخطاف useSignIn كما فعلنا في نموذج تسجيل الدخول. وجِّه المستخدم الجديد بعد تسجيل دخوله إلى واجهة عرض المستودعات المقيّمة. ينبغي أن يكون المستخدم قادرًا على الوصول إلى واجهة تسجيل مستخدم جديد من خلال شريط التطبيق، وذلك بالضغط على نافذة "Sign up" التي ستظهر فقط للمستخدم قبل أن يسجّل دخوله. ستبدو النسخة النهائية لواجهة تسجيل مستخدم جديد مشابهة للصورة التالية: التُقطت شاشة التطبيق هذه بعد إخفاق إرسال بيانات نموذج لإظهار شكله في هذه الحالة. 10.23: ترتيب بيانات تطبيق قائمة المستودعات المقّيمة تُرتّب المستودعات حتى هذه اللحظة وفقًا لتاريخ تقييم المستودع. أنجز آلية تسمح للمستخدم أن يختار أساسًا لترتيب هذه المستودعات وفقًا لما يلي: المستودعات الأخيرة: سيكون المستودع الذي قُيَّم آخيرًا وللمرة الأولى في أعلى القائمة. هذا الترتيب هو المتبع حاليًا، وينبغي أن يكون الخيار الإفتراضي. المستودعات الأعلى تقييمًا: سيكون المستودع الذي يحمل أعلى قيمة لمعدّل التقييم في أعلى القائمة. المستودعات الأدنى تقييمًا: سيكون المستودع الذي يحمل أدنى قيمة لمعدّل التقييم في أعلى القائمة. يمتلك الاستعلام repositories الذي يحضر قائمة المستودعات وسيطًا يدعى orderby يمكنك استخدامه لتحديد أسلوب الترتيب المتبع، إذ يمتلك هذه الوسيط قيمتين فقط هما: CREATED_AT: الترتيب وفق تاريخ التقييم لأول مرة. RATING_AVERAGE: الترتيب على أساس معدل التقييم. كما يمتلك الاستعلام وسيطًا يدعى orderDirection ويستخدم لتغيير جهة الترتيب. يملك الوسيط الأخير قيمتين، هما: ASC (تصاعدي، من التقييم الأدنى إلى الأعلى) و DESC (تنازلي، من التقييم الأعلى إلى الأدنى). يمكن المحافظة على خيار الترتيب باستخدام الخطاف useState على سبيل المثال، كما يمكن تمرير المتغيرات التي يستخدمها الاستعلام repositories إلى الخطاف useRepositories مثل وسطاء. يمكنك أيضًا استخدام المكتبة react-native-picker أو المكوّن Menu العائد للمكتبة React Native Paper لإنجاز شيفرة ترتيب القائمة، كما يمكنك استخدام الخاصية ListHeaderComponent العائدة للمكوّن FlatList لتزويدك بترويسة تحتوي على مكوّن الاختيار. ستبدو النسخة النهائية لهذه الميزة، وبناء على مكوَّن الاختيار الذي اعتمدته، قريبةً من الصورة التالية: 10.24: انتقاء مستودعات محددة من القائمة يتيح خادم Apollo ترشيح المستودعات بناءً على اسمها أو اسم مالكها، ويمكن إنجاز ذلك باستخدام الوسيط searchKeyword العائد للاستعلام repositories. إليك مثالًا عن كيفية استخدام الوسيط مع الاستعلام: { repositories(searchKeyword: "ze") { edges { node { id fullName } } } } أنجز ميزةً في التطبيق لترشيح قائمة المستودعات المقيّمة بناءً على كلمة محددة. ينبغي أن يكون المستخدم قادرًا على الكتابة ضمن مربع إدخال نصي وستظهر نتائج الفلترة وفقًا للكلمة المكتوبة مباشرة أثناء الكتابة. يمكنك استخدام المكوّن البسيط TextInput، أو استخدام مكوّن أكثر عصرية مثل المكوّن Searchbar العائد للمكتبة React Native Paper على أنه مربع إدخال نصي. ضع مكوّن الإدخال النصي ضمن ترويسة المكوّن FlatList. ولتفادي تنفيذ عدد كبير من الاستعلامات أثناء كتابة المستخدم للكلمة بسرعة، اختر آخر سلسة كتبها بعد تأخير بسيط. تدعى هذه التقنية بالتريّث debouncing. تمثّل المكتبة use-debounce حلًا جيدًا لتأخير متغيّر الحالة، بعد أن تستخدم زمن انتظار معقول مثل 500 ميلي ثانية. خزّن قيمة النص الذي يُكتب في مربع الإدخال مستخدمًا الخطاف useState، ومرر القيمة المُؤخَّرة إلى الاستعلام كقيمة للوسيط searchKeyword. قد تعترضك مشكلة فقدان مكون الإدخال النصي تركيز الدخل بعد كتابة كل حرف، وذلك لأن المحتوى الذي تقدّمه الخاصية ListHeaderComponent سيُلغى باستمرار. يمكن حل المشكلة بتحويل المكوّن المسؤول عن تصيير المكوّن FlatList إلى مكوّن صنف class component ومن ثم تعريف دالة تصيير الترويسة إلى خاصية للصنف على النحو التالي: export class RepositoryListContainer extends React.Component { renderHeader = () => { // يحوي خاصيات المكون this.props const props = this.props; // ... return ( <RepositoryListHeader // ... /> ); }; render() { return ( <FlatList // ... ListHeaderComponent={this.renderHeader} /> ); } } ستبدو النسخة النهائية لميزة انتقاء المستودعات قريبةً من الصورة التالية: الترقيم بطريقة المؤشرات عندما تعيد الواجهة البرمجية قائمةً مرتبةً من العناصر من مجموعةٍ ما، فإنها ستعيد مجموعةً جزئيةً من العناصر من أصل المجموعة الكاملة لتقليل عرض حزمة الاتصال مع الخادم وتقليل الذاكرة المستخدمة لتخزين القائمة في تطبيق المستخدم. يمكن أن نطلب تلك المجموعة الجزئية لتوافق معاملات محددة لكي يستطيع المستخدم أن يطلب مثلًا أول عشرين عنصرًا منها ابتداءًا من عنصر محدد. تدعى هذه التقنية عادة بالترقيم pagination. وعندما نستطيع الحصول على قائمة بعد عنصر محدد بمؤشر معيّن، سنكون أمام الترقيم المبني على المؤشرات cursor-based pagination. فالمؤشر إذًا هو تحديدٌ لعنصر في قائمة مرتبة. لنلقي نظرةً على قائمة المستودعات المرقمة التي يعيدها الاستعلام repositories باستخدام الاستعلام التالي: { repositories(first: 2) { totalCount edges { node { id fullName createdAt } cursor } pageInfo { endCursor startCursor hasNextPage } } } يبلِّغ الوسيط first الواجهة البرمجية ان تعيد فقط أول مستودعين. وإليك مثالًا عن الاستجابة لهذا الاستعلام: { "data": { "repositories": { "totalCount": 10, "edges": [ { "node": { "id": "zeit.next.js", "fullName": "zeit/next.js", "createdAt": "2020-05-15T11:59:57.557Z" }, "cursor": "WyJ6ZWl0Lm5leHQuanMiLDE1ODk1NDM5OTc1NTdd" }, { "node": { "id": "zeit.swr", "fullName": "zeit/swr", "createdAt": "2020-05-15T11:58:53.867Z" }, "cursor": "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=" } ], "pageInfo": { "endCursor": "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=", "startCursor": "WyJ6ZWl0Lm5leHQuanMiLDE1ODk1NDM5OTc1NTdd", "hasNextPage": true } } } } يعتمد تنسيق الكائن الناتج والوسطاء على مواصفات اتصالات مؤشر أرضية عمل GraphQL للترحيل، والتي أصبحت مواصفات ترقيم صفحات شائعة جدًا واعتُمدت على نطاقٍ واسع في واجهة برمجة تطبيقات GitHub's GraphQL API. سيحتوي الكائن الذي يحمل النتيجة مصفوفةً باسم edges تحتوي بدورها على عناصر لها سمتين الأولى هي العقدة node والأخرى هي المؤشر cursor؛ إذ تحتوي العقدة على المستودع بحد ذاته كما نعرف؛ أما المؤشر فهو تمثيلٌ مشفّر للعقدة بأسلوب "Base64". ويحتوي المؤشر على المعرّف id للمستودع بالإضافة إلى تاريخ إنشائه. هذه هي كل المعلومات التي نريدها لكي نشير إلى العنصر عندما تُرتَّب العناصر وفق تاريخ إنشاء المستودع. يحتوي الكائن pageInfoعلى معلومات مثل المؤشرات على بداية ونهاية المصفوفة. دعونا نتأمل الحالة التي نريد فيها مجموعة العناصر التي تلي آخر عنصر من المجموعة الحالية وهو العنصر "zeit/swr". يمكننا إسناد قيمة الحقل endCursor إلى الوسيط after للاستعلام على النحو التالي: { repositories(first: 2, after: "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=") { totalCount edges { node { id fullName createdAt } cursor } pageInfo { endCursor startCursor hasNextPage } } } والآن سنحصل على العنصرين التاليين، وسنستمر في هذه العملية حتى يأخذ الحقل hasNextPage القيمة false، وهذا يعني أننا وصلنا آخر القائمة، ولتتعمق أكثر في الترقيم المتعلق بالمؤشرات، إقرأ المقالة التي عنوانها Pagination with Relative Cursors على موقع Shopify، حيث تقدم المقالة الكثير من التفاصيل عن طريقة كتابة الشيفرة بالإضافة إلى الفوائد من استخدام الترقيم بالمؤشرات مقارنةً بالترقيم وفق ترتيب العنصر index-based. التمرير اللانهائي تُصمّم القوائم المرتبة عموديًا في تطبيقات سطح المكتب أو تطبيقات الهواتف الذكية بتقنية تدعى التمرير اللانهائي - infinite scrolling. ويعتمد مبدأ هذه التقنية على النقاط التالية: إحضار المجموعة الأولية من العناصر. عند وصول المستخدم إلى آخر عنصر من هذه المجموعة، يجري إحضار المجموعة التالية من العناصر التي تبدأ من العنصر الذي يلي آخر عنصر من المجموعة السابقة. تتكرر الخطوة الثانية حتى يتوقف المستخدم عن تمرير شريط القائمة أو عندما يتجاوز حدًا معينًا من مرات التمرير. ويشير الاسم "تمرير لانهائي" إلى أن القائمة ستبدو لانهائية، فسيبقى المستخدم قادرًا على تمرير العناصر ضمن الشريط وستُعرض العناصر بصورةٍ مستمرة أيضًا. لنرى كيف سننفذ ذلك عمليًا من خلال خطاف Apollo Client الذي يُدعى useQuery. يضم توثيق Apollo Client تفاصيل كثيرة عن طريقة تنفيذ الترقيم باستخدام المؤشرات. لننجز إذًا ميزة التمرير اللانهائي لعرض قائمة المستودعات المقيّمة. علينا أولًا أن نحدد متى يصل المستخدم إلى آخر عنصر في القائمة. ولحسن الحظ، يمتلك المكون FlatList الخاصية onEndReached التي تستدعى الدالة التي ستنفذ العملية عندما يصل المستخدم إلى آخر عنصر من القائمة. يمكنك التحكم بوقت استدعاء الدالة onEndReach باستخدام الخاصية onEndReachedThreshold. غيِّر شيفرة المكوّن FlatList الموجود ضمن المكوّن RepositoryList لكي يستدعي الدالة حالما يصل المستخدم إلى العنصر الأخير: export const RepositoryListContainer = ({ repositories, onEndReach, /* ... */, }) => { const repositoryNodes = repositories ? repositories.edges.map((edge) => edge.node) : []; return ( <FlatList data={repositoryNodes} // ... onEndReached={onEndReach} onEndReachedThreshold={0.5} /> ); }; const RepositoryList = () => { // ... const { repositories } = useRepositories(/* ... */); const onEndReach = () => { console.log('You have reached the end of the list'); }; return ( <RepositoryListContainer repositories={repositories} onEndReach={onEndReach} // ... /> ); }; export default RepositoryList; جرّب عملية التمرير إلى نهاية قائمة المستودعات، وستظهر رسالةً في سجل العمل مفادها أنك وصلت إلى العنصر الأخير. علينا الآن أن نحضر مزيدًا من المستودعات في اللحظة التي نصل فيها إلى نهاية العناصر. يمكن إنجاز ذلك باستخدام الدالة fetchMore التي يؤمنها الخطاف useQuery. يمكننا استخدام سياسة الحقل field policy لوصف عميل Apollo وكيفية دمج المستودعات الموجودة في ذاكرة التخزين المؤقت مع المجموعة التالية من المستودعات، إذ تُستخدم سياسات الحقول عمومًا لتخصيص سلوك ذاكرة التخزين المؤقت أثناء عمليات القراءة والكتابة باستخدام دوال القراءة والدمج. دعنا نضيف سياسة حقل للاستعلام repositories في ملف "apolloClient.js" على النحو التالي: import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; import Constants from 'expo-constants'; import { relayStylePagination } from '@apollo/client/utilities'; const { apolloUri } = Constants.manifest.extra; const httpLink = createHttpLink({ uri: apolloUri, }); const cache = new InMemoryCache({ typePolicies: { Query: { fields: { repositories: relayStylePagination(), }, }, }, }); const createApolloClient = (authStorage) => { const authLink = setContext(async (_, { headers }) => { try { const accessToken = await authStorage.getAccessToken(); return { headers: { ...headers, authorization: accessToken ? `Bearer ${accessToken}` : '', }, }; } catch (e) { console.log(e); return { headers, }; } }); return new ApolloClient({ link: authLink.concat(httpLink), cache, }); }; export default createApolloClient; كما ذكرنا سابقًا، يعتمد تنسيق كائن نتيجة ترقيم الصفحات والوسطاء على مواصفات ترقيم الصفحات الخاصة بالترحيل Relay، ولحسن الحظ، يوفر Apollo Client سياسة حقل محددة مسبقًا، relayStylePagination، والتي يمكن استخدامها في هذه الحالة. غيّر في شيفرة الخطاف useRepositories بحيث يعيد الدالة fetchMore بعد فَكِّ تشفيرها، بحيث تستدعي الدالة fetchMore الفعلية مع الحقل endCursor، ثم يُحدّث بعدها الاستعلام بصورة صحيحة وفقًا للبيانات المُحضرة: const useRepositories = (variables) => { const { data, loading, fetchMore, ...result } = useQuery(GET_REPOSITORIES, { variables, // ... }); const handleFetchMore = () => { const canFetchMore = !loading && data?.repositories.pageInfo.hasNextPage; if (!canFetchMore) { return; } fetchMore({ variables: { after: data.repositories.pageInfo.endCursor, ...variables, }, }); }; return { repositories: data?.repositories, fetchMore: handleFetchMore, loading, ...result, }; }; تأكد من وجود الحقلين pageInfo و cursor في الاستعلام repositories كما هو موضح في مثال الترقيم. كما ينبغي عليك أن تضيف الوسيطين first و after إلى الاستعلام. ستستدعي الدالة handleFetchMore الدالة fetchMore العائدة إلى Apollo client إن كان هناك المزيد من العناصر التي ينبغي إحضارها، وذلك بناء على قيمة الخاصية hasNextPage، ومن المفترض أن نمنع إحضار عناصر جديدة طالما أن عملية الإحضار سابقةً لا تزال قيد التنفيذ، إذ ستكون قيمة الحقل loading في هذه الحالة true. سنزود الاستعلام في الدالة fetchMore بالمتغير after الذي يتلقى آخر قيمة endCursor للحقل. وتكون الخطوة الأخيرة استدعاء الدالة fetchMore ضمن دالة معالجة الحدث onEndReach: const RepositoryList = () => { // ... const { repositories, fetchMore } = useRepositories({ first: 8, // ... }); const onEndReach = () => { fetchMore(); }; return ( <RepositoryListContainer repositories={repositories} onEndReach={onEndReach} // ... /> ); }; export default RepositoryList; استخدم قيمة صغيرة نسبيًا (8 مثًلا) للوسيط first عندما تجرب التمرير اللانهائي، وهكذا لن تضطر إلى عرض كثيرٍ من المستودعات، وقد تواجهك أيضًا مشكلة الاستدعاء المباشر لمعالج الحدث onEndReach بعد تحميل واجهة العرض، والسبب على الأرجح، هو العدد الكبير للمستودعات في القائمة، وبالتالي ستصل إلى نهاية القائمة مباشرةً. يمكنك الالتفاف على هذه المشكلة بزيادة قيمة الوسيط first. وبمجرد أن ترى أن قائمة التمرير اللانهائي تعمل جيدًا، يمكنك تجريب قيم كبيرة للوسيط first. التمرينات 10.25- 10.27 10.15: شريط تمرير لانهائي لقائمة المستودعات المقيّمة نفذ شريط تمرير لانهائي لقائمة المستودعات المقيّمة. يمتلك الحقل reviews العائد للنوع "Repository" الوسيطينafter و first كما في الاستعلام repositories. ويمتلك النوع "ReviewConnection" الحقل pageInfo كما يمتلكه النوع "RepositoryConnection". إليك مثالًا عن استعلام: { repository(id: "jaredpalmer.formik") { id fullName reviews(first: 2, after: "WyIxYjEwZTRkOC01N2VlLTRkMDAtODg4Ni1lNGEwNDlkN2ZmOGYuamFyZWRwYWxtZXIuZm9ybWlrIiwxNTg4NjU2NzUwMDgwXQ==") { totalCount edges { node { id text rating createdAt repositoryId user { id username } } cursor } pageInfo { endCursor startCursor hasNextPage } } } } يمكن أن تتشابه سياسة حقل ذاكرة التخزين المؤقت مع استعلام repositories: const cache = new InMemoryCache({ typePolicies: { Query: { fields: { repositories: relayStylePagination(), }, }, Repository: { fields: { reviews: relayStylePagination(), }, }, }, }); استخدم قيمةً صغيرةً نسبيًا للوسيط first عندما تحاول إنجاز قائمة التمرير اللانهائي، وربما ستضطر إلى إنشاء عدة مستخدمين جدد لكي تُقيّم عددًا من المستودعات، وبذلك تصبح قائمة المستودعات المقيّمة طويلة بما يكفي لتجرّب شريط التمرير. اجعل قيمة الوسيط كبيرةً بما يكفي لعدم استدعاء معالج الحدث onEndReach مباشرةً عند عرض الواجهة، وصغيرةً في نفس الوقت بحيث يعرض الشريط المستودعات من جديد عند الوصول إلى نهاية القائمة. وبمجرد أن ترى أن قائمة التمرير اللانهائي تعمل جيدًا، يمكنك تجريب قيم كبيرة للوسيط first. 10.26: واجهة عرض لتقييمات المستخدم أنجز واجهة تُمكِّن المستخدم من عرض ما قيّمه. ينبغي أن يكون المستخدم قادرًا على الوصول إلى هذه الواجهة من خلال الضغط على النافذة "My reviews" في شريط التطبيق بمجرد أن يسّجل دخوله. يمكنك تطبيق أسلوب التمرير اللانهائي إن أردت في هذا التمرين. وإليك الشكل الذي يمكن أن تظهر عليه هذه الواجهة: تذكر أنه بإمكانك الحصول على المستخدم الذي سجّل دخول من خادم Apollo مستعينًا بالاستعلام me. سيعيد الاستعلام النوع "User" الذي يمتلك الحقل review. إن كنت قد أنجزت مسبقًا الاستعلام me يمكنك إعادة استخدامه بعد جعله قادرًا على إحضار الحقل review شرطيWh، وذلك بالاستفادة من توجيه GraphQL الذي يُدعى include. لنفترض أن الاستعلام الحالي قد أنجز بصورةٍ قريبة من التالي: const GET_CURRENT_USER = gql` query { me { # user fields... } } `; يمكنك تزويد الاستعلام بالوسيط includeReviewواستخدام ذلك مع التوجيه include: const GET_CURRENT_USER = gql` query getCurrentUser($includeReviews: Boolean = false) { me { # user fields... reviews @include(if: $includeReviews) { edges { node { # review fields... } cursor } pageInfo { # page info fields... } } } } `; يمتلك الوسيط includeReview القيمة الافتراضية false، لأننا لا نريد أن نسبب حملًا زائدًا على الخادم ما لم نرد صراحةً إحضار المستودعات التي قيّمها المستخدم. إن مبدأ هذا التوجيه بسيط على النحو التالي: إن كانت قيم الوسيط if هي true، أحضر الحقل، وإلا احذفه. 10.27: إضافة أفعال إلى واجهة عرض التقييمات بعد أن أصبح المستخدم قادرًا على استعراض ما قيّمه، لنضف إلى الواجهة بعض الأفعال. ضع أسفل كل تقيم زرين، أحدهما لعرض المستودع، بحيث ينتقل المستخدم بالضغط عليه إلى واجهة عرض مستودع واحد، والآخر لحذف تقييم هذا المستودع. إليك ما قد يبدو عليه الوضع بعد إضافة الزرين: ينبغي أن يلي الضغط على زر الحذف رسالة تأكيد للحذف، فإن أكد المستخدم أمر الحذف ستُنفّذ العملية، وإلا سيُهمل الأمر. يمكنك إنجاز ذلك باستخدام الوحدة البرمجية Alert. وانتبه إلى أن استدعاء التابع Alert.alert لن يفتح رسالة التأكيد في واجهة عرض المنصة Expo، بل عليك استخدام تطبيق جوّال Expo أو المقلّد لترى كيف ستبدو هذه الرسالة. إليك الرسالة التي ينبغي أن تظهر عند الضغط على زر الحذف: يمكنك حذف التقييم باستخدام الطفرة deleteReview. تمتلك هذه الطفرة وسيطًا واحدًا وهو المعرّف id للتقييم الذي سيُحذف. ومن السهل بعد تنفيذ الطفرة تحديث استعلام قائمة المستودعات باستدعاء الدالة refetch. هكذا نكون قد وصلنا إلى التمرين الأخير في هذا المقال، وقد حان وقت تسليم الإجابات إلى GitHub والإشارة إلى التمارين التي أكملتها في منظومة تسيم التمارين. انتبه إلى وضع تمارين هذا المقال في القسم 4 من منظومة التسليم. مصادر إضافية طالما أننا وصلنا إلى نهاية هذا القسم، دعونا نلقي نظرةً على بعض المصادر الهامة للتطوير باستخدام React Native. سنبدأ من توثيق React Native باللغة العربية الذي تقدمه أكاديمية حسوب، والتي تقدم أيضًا توثيق React باللغة العربية، بالإضافة إلى القسم المتخصص باللغة React على موقع الأكاديمية، كما نشير إلى الموقع Awesome React Native الذي يقدم قائمةً مهمةً جدًا من المصادر مثل المكتبات والدورات التعليمية. وبما أن القائمة طويلة جدًا، لنطلع على بعض النقاط. المكتبة React Native Paper المكتبة Paper هي مجموعةٌ من المكوّنات المخصصة والجاهزة للاستخدام في React Native، مبنيةٌ على معايير تصميم Material التي وضعتها غوغل Google. صُممت React Native Paper من أجل لتقدم وظيفة تماثل وظائف المكتبة Material-UI، إذ تقدم مجموعةً واسعةً من مكوّنات واجهة المستخدم UI عالية الجودة التي تدعم إنشاء سمات مخصصة للتطبيق. وتتميز إعدادات استخدام React Native Paper مع تطبيقات React Native المبنية على منصة Expo بالسهولة، حيث يمكنك تجربتها مباشرة في التمارين القادمة إن أردت. المكتبة Styled-components سيعطيك استخدام القوالب المعرّفة المجرّدة tagged template literal -وهي إضافةٌ جديدة في JavaScript-، مع المكتبة styled-components إمكانية كتابة شيفرة CSS فعلية لتنسيق المكوّنات، كما يلغي استخدام هذه المكتبة الحاجة إلى الربط بين المكوّنات والتنسيق، وسيكون استخدام المكوّنات مثل وحدات بناءً تنسيقات على المستوى البرمجي المنخفض سهلًا جدًا. تستخدم المكتبة Styled-components لتنسيق مكوّنات React Native باستخدام تقنية CSS-in-JS. لقد اعتدنا على تعريف تنسيق المكوّنات في React Native باستخدام كائنات JavaScript، وبالتالي لن يكون استخدام CSS-in-JS غريبًا. لكن مقاربة المكتبة Styled-components ستختلف تمامًا عن استخدام التابع StyleSheet.createو الخاصية style. يُعرّف تسيق المكوّن في Styled-components باستخدام ميزة تُدعى القوالب المعرّفة المجرّدة tagged template literal أو كائنات جافا سكربت JavaScript الصِّرفة، التي تُمكننا من تعريف خصائص تنسيق جديدة للمكونات بناءً على خصائص تلك المكوّنات في زمن التنفيذ. سيتيح ذلك الكثير من الإمكانات مثل الانتقال بين السمات القاتمة والمضيئة، كما أنها تدعم كليًا موضوع السمات. إليك مثالًا عن إنشاء المكوّن Text مع إمكانية تغيير تنسيقه بناءً على خصائصه: import styled from 'styled-components/native'; import { css } from 'styled-components'; const FancyText = styled.Text` color: grey; font-size: 14px; ${({ isBlue }) => isBlue && css` color: blue; `} ${({ isBig }) => isBig && css` font-size: 24px; font-weight: 700; `} `; const Main = () => { return ( <> <FancyText>Simple text</FancyText> <FancyText isBlue>Blue text</FancyText> <FancyText isBig>Big text</FancyText> <FancyText isBig isBlue> Big blue text </FancyText> </> ); }; وطالما أن المكتبة styled-components ستعالج تعريف التنسيق، يمكننا استخدام أسلوب كتابة الأفعى snake case المشابه لطريقة كتابة CSS لتسمية الخصائص والواحدات (مثل % أو px)، لكن لا أهمية في الواقع للواحدات، لأن قيم خصائص التنسيق لا تقبل ذلك. للاطلاع على مزيدٍ من المعلومات عن استخدام هذه المكتبة، اقرأ التوثيق الخاص بها. المكتبة React-spring react-spring هي مكتبة رسوم متحركة أساسها المكتبة spring-physics، تغطي معظم احتياجاتك لواجهات مزودة برسوم متحركة، إذ تؤمن لك هذه المكتبة أدوات بالمرونة الكافية لتحويل جميع أفكارك إلى واجهات متحركة. تؤمن لك المكتبة React-spring واجهة برمجية ذات خطافات hook API واضحة الاستخدام لتحريك مكونات React Native. المكتبة React Navigation تؤمن مكتبة React Navigation التنقل والتوجه في تطبيقات React Native هي مكتبةٌ للتحكم بالتنقلات بين وجهات مختلفة في تطبيقات React Native. تتشابه في بعض النواحي مع المكتبة React Router. لكن وعلى خلاف React Router، تقدم مميزات أكثر قربًا للمنصة الأصيلة مثل الإيماءات الأصيلة والرسوميات الانتقالية التي تظهر عند التنقل بين الواجهات المختلفة للتطبيق. كلمة ختامية وهكذا نكون قد أكملنا تطبيقنا وأصبح جاهزًا. لقد تعلمنا خلال هذه الرحلة العديد من المفاهيم الجديدة، مثل إعداد تطبيقات React Native باستخدام المنصة Expo، وطريقة العمل مع مكوّنات React Native البنيوية، وتنسيق مظهر هذه المكوّنات، إضافةً إلى الاتصال مع الخادم واختبار التطبيقات. ستكون الخطوة الأخيرة هي نشر التطبيق على متجر Apple App أو متجر Google Play. يبقى موضوع نشر التطبيق خيارًا شخصيًا وليس أمرًا مُلحًّا، لأنك ستحتاج أيضًا إلى نشر وتوزيع الخادم rate-repository-api، أما من أجل تطبيق React Native بحد ذاته، فعليك أن تُنشئ نسخة بناء لمنصة iOS أو لمنصة أندرويد باتباع الإرشادات في توثيق Expo. وبعد ذلك ستتمكن من توزيع هذه النسخ على متجري Apple App أو Google Play، وستجد تفاصيل عن هذه الخطوة أيضًا في توثيق Expo. ترجمة -وبتصرف- للفصل Testing and extending our application من سلسلة [Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: تواصل تطبيق React Native مع الخادم أساسيات React Native مدخل إلى التحريك في React Native
  2. لقد أضفنا حتى الآن عدة ميزات إلى تطبيقنا لكن دون أي اتصال بالخادم، إذ يستخدم تطبيق قائمة المستودعات المقيَّمة الذي نفذناه في المقال السابق أساسيات React Native -على سبيل المثال- بيانات وهمية، ولا يرسل نموذج تسجيل الدخول بيانات التحقق من المستخدم إلى أية وصلة تخديم endpoint مفوَّضة باستقبال هذه البيانات، لذا سنتعلم في هذا المقال كيف سنتصل بالخادم مستخدمين طلبات HTTP، وكيف سنستخدم المكتبة Apollo Client ضمن تطبيقات React Native، وكذلك طريقة تخزين البيانات في جهاز المستخدم. سنحتاج قبل الشروع بتعلم تلك النقاط إلى خادم لنتواصل معه، لذلك فقد أنجزنا خادمًا كاملًا ووضعناه في المستودع rate-repository-api. سيلبي الخادم "rate-repository-api" كل ما تحتاجه الواجهة البرمجية لتطبيقنا خلال هذا القسم، إذ يستخدم "rate-repository-api" قاعدة بيانات SQLite والتي لا تحتاج إلى إعدادات، كما يؤمن واجهة برمجية خاصة بالمكتبة Apollo GraphQL، إضافةً إلى بعض وصلات التخديم التي تؤمن واجهة برمجية متوافقة مع REST. قبل أن تغوص أعمق في مادة هذا المقال، عليك أن تُعدَّ الخادم "rate-repository-api" باتباع الإرشادات الموجودة في ملف README الخاص بالمستودع. ننصحك إن كنت ستستخدم المقلّد emulator لتطوير التطبيق، أن تشغله على نفس الحاسوب الذي يشغّل الخادم، فسيسهل ذلك طلبات الاتصال عبر الشبكة على نحوٍ واضح. طلبات HTTP تزودنا React Native بالواجهة البرمجية Fetch API لتنفيذ طلبات HTTP الخاصة بتطبيقنا، كما تدعم أيضًا الواجهة البرمجية القديمة XMLHttpRequest API والتي ستمكننا من استخدام مكتبات خارجية مصدرها طرف ثالث مثل Axios. وهذه الواجهات هي نفسها التي استخدمناها في بيئة المتصفح، كما أنها متاحة لكامل التطبيق ولا حاجة لإدراجها. قد يتفق المطورون الذين تعاملوا مع الواجهتين البرمجيتين السابقتين، أن Fetch API أسهل استخدامًا وأكثر عصرية، لكن للواجهة XMLHttpRequest API استعمالاتها أيضًا، ولأجل تبسيط الموضوع سنتعامل فقط مع Fetch API في أمثلتنا. يمكن استخدام Fetch API لإرسال طلبات HTTP باستخدام الدالة fetch وسيكون الوسيط الأول لهذه الدالة عنوان المورد URL وطلب HTTP الافتراضي لها هو GET: fetch('https://my-api.com/get-end-point'); أما الوسيط الثاني للدالة فهو كائن خيارات يمكنك استعماله لإنشاء طلب HTTP آخر، أو ترويسة طلب، أو متن body طلب: fetch('https://my-api.com/post-end-point', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ firstParam: 'firstValue', secondParam: 'secondValue', }), }); انتبه إلى أنّ العناوين "URL's" مصطنعة ولن تستجيب لطلباتك غالبًا. تعمل Fetch API موازنةً بالمكتبة Axios في مستوى أدنى قليلًا، فلا توجد مثلًا أية عمليات تحويل أو تفسير لمتن الطلب أو الاستجابة، فعليك مثلًا أن تحدد نوع المحتوى content-type بنفسك وأن تستخدم التابع JSON.stringify لتحويل متن الطلب إلى نص. تعيد الدالة fetch وعدًا (promise) يُحَل resolves إلى كائن الاستجابة Response، وانتبه إلى أنّ رموز الحالة للأخطاء التي تحصل مثل 400 أو 500 لن تُرفض كما هو الوضع في Axios مثلًا. يمكن أن نفسّر متن الاستجابة المنسقة بأسلوب JSON باستخدام التابع Response.json: const fetchMovies = async () => { const response = await fetch('https://reactnative.dev/movies.json'); const json = await response.json(); return json; }; وللاطلاع على أساسيات Fetch API بشيء من التفصيل، اقرأ مقال الواجهة البرمجية fetch في JavaScript ومقال استعمالات متقدمة للواجهة البرمجية Fetch في جافاسكربت وأضف إليهما مقال تتبع تقدم عملية تنزيل البيانات عبر Fetch ومقاطعتها في جافاسكربت. لنبدأ بالاستخدام التطبيقي للمكتبة Fetch API، إذ يؤمن الخادم rate-repository-api وصلة تخديم تعيد قائمةً مرقمةً من المستودعات المقيَّمة. ستتمكن من الولوج إلى وصلة التخديم الموجودة على العنوان "http://localhost:5000/api/repositories" بمجرد أن يعمل الخادم. تُرقّم البيانات بأسلوب الترقيم المعتمد على المؤشرات cursor based pagination format، وستجد البيانات الفعلية خلف المفتاح "node" في المصفوفة "edges". لا يمكن، لسوء الحظ، الوصول إلى الخادم rate-repository-api مباشرةً من خلال تطبيقنا بطلب العنوان "http://localhost:5000/api/repositories"، إذ نحتاج لإرسال طلب إلى وصلة التخديم هذه عبر التطبيق الوصول إلى الخادم باستخدام عنوان IP الخاص به في الشبكة المحلية التي ينتمي إليها. ولإيجاد هذا العنوان، افتح أدوات تطوير Expo بتنفيذ الأمر npm start. سترى في أدوات التطوير عنوانًا له بالبادئة "//:exp" فوق شيفرة QR: انسخ عنوان IP الموجود بين البادئة "//:exp" والنهاية ":"، وسيكون في مثالنا 192.168.100.16. أنشئ بعد ذلك عنوان URL بالتنسيق التالي "http://:5000/api/repositories" ثم توجه إليه من خلال المتصفح، وستحصل على نفس الاستجابة التي حصلت عليها باستخدام عنوان الخادم المحلي "localhost". بعد أن حددنا عنوان وصل التخديم، سنستخدم البيانات الفعلية التي يزودنا بها الخادم في تطبيق قائمة المستودعات المقيَّمة. أشرنا سابقًا أننا نستخدم بيانات وهمية مخزّنة في المتغير repositories. اِحذف هذا المتغير واستعض عن البيانات الوهمية في الملف "RepositoryList.jsx" الموجود في المجلد "components" بهذه الشيفرة: import { useState, useEffect } from 'react'; // ... const RepositoryList = () => { const [repositories, setRepositories] = useState(); const fetchRepositories = async () => { // ‫صحح عنوان IP إن كان مختلفًا لديك const response = await fetch('http://192.168.100.16:5000/api/repositories'); const json = await response.json(); console.log(json); setRepositories(json); }; useEffect(() => { fetchRepositories(); }, []); // Get the nodes from the edges array const repositoryNodes = repositories ? repositories.edges.map(edge => edge.node) : []; return ( <FlatList data={repositoryNodes} // Other props /> ); }; export default RepositoryList; استخدمنا الخطاف useState للحفاظ على حالة قائمة المستودعات، كما استخدمنا الخطاف useEffect لاستدعاء الدالة fetchRepositoriesعندما يُثبّت المكوّن RepositoryList ضمن شجرة المكوّنات. استخلصنا المستودعات الفعلية ووضعناها في المتغير repositoryNode واستبدلنا به المتغير السابق repositories الموجود في الخاصية data العائدة للمكوّن FlatList، وبذلك يمكننا أن نرى البيانات الفعلية التي يعطيها الخادم ضمن التطبيق. من الجيد عادةً تسجيل استجابة الخادم لتتمكن من تحرّيها كما فعلنا في الدالة fetchRepositories. ينبغي أن تكون قادرًا على الاطلاع على السجلات في أدوات تطوير Expo عن طريق الوصول إلى سجلات جهازك كما تعلمنا في المقال السابق (فقرة عرض سجلات العمل). إن استخدمت تطبيق جوّال Expo وأخفق طلب HTTP، تأكد أن هاتفك وحاسوبك الذي يشغّل الخادم ينتميان إلى نفس الشبكة اللاسلكية. وإن لم تستطع ذلك، استخدم المقلّد على نفس الحاسوب الذي يعمل عليه الخادم أو هيئ نفق توصيل tunnel إلى الخادم المحلي. يمكنك استخدام المكتبة Ngrok مثلًا لإنجاز ذلك. يمكن إعادة كتابة شيفرة إحضار البيانات الموجودة في المكوّن RepositoryList، إذ يعرف المكوّن مثلًا تفاصيل الطلبات عبر الشبكة كعنوان وصلة التخديم، وبالإضافة إلى ذلك، تحمل شيفرة إحضار البيانات إمكانية لإعادة استخدامها. لنعد إذًا كتابة شيفرة المكوّن بنقل شيفرة إحضار البيانات إلى خطاف خاص بها. سننشئ أولًا مجلدًا باسم "hooks" ضمن المجلد "src" ومن ثم سنضع فيه الملف "useRepositories.js" الذي يحتوي الشيفرة التالية: import { useState, useEffect } from 'react'; const useRepositories = () => { const [repositories, setRepositories] = useState(); const [loading, setLoading] = useState(false); const fetchRepositories = async () => { setLoading(true); // ‫صحح عنوان IP إن كان مختلفًا لديك const response = await fetch('http://192.168.100.16:5000/api/repositories'); const json = await response.json(); setLoading(false); setRepositories(json); }; useEffect(() => { fetchRepositories(); }, []); return { repositories, loading, refetch: fetchRepositories }; }; export default useRepositories; لنستخدم الآن الخطاف الجديد useRepositories في المكوّن RepositoryList: // ... import useRepositories from '../hooks/useRepositories'; const RepositoryList = () => { const { repositories } = useRepositories(); const repositoryNodes = repositories ? repositories.edges.map(edge => edge.node) : []; return ( <FlatList data={repositoryNodes} // Other props /> ); }; export default RepositoryList; وهكذا لن يكون لدى المكوّن RepositoryList أية فكرة عن طريقة الحصول على مستودعات القائمة، وربما سنطلب هذه المستودعات مستقبلًا باستخدام الواجهة البرمجية للمكتبة GraphQL بدلًا من واجهة REST، ونرى ما الذي سيحدث. العمل مع المكتبتين GraphQL و Apollo client تعرفنا في القسم 8 على GraphQL وكيفية إرسال استعلامات إلى خادم Apollo في تطبيقات React بالاستفادة من المكتبة Apollo Client. ولحسن الحظ، يمكنك استخدام Apollo client في تطبيقات React Native بنفس الطريقة التي استخدمناها في بناء تطبيقات React الخاصة بالويب. وكما أشرنا سابقًا، سيزودنا الخادم "rate-repository-api" بواجهة برمجية للمكتبة GraphQL نُفِّذت باستخدام Apollo client، وبالتالي ستتمكن من الولوج إلى أرضية عمل GraphQL بالتوجه إلى العنوان http://localhost:5000/graphq. تمثل أرضية عمل GraphQL أداة تطوير لإرسال استعلامات GraphQL، وتفقد تخطيط الواجهة البرمجية للمكتبة GraphQL، وعرض توثيق هذه المكتبة. ويفضل أن تجرب دائمًا أية استعلامات سيرسلها تطبيقك من خلال أرضية عمل GraphQL قبل تضمينها في الشيفرة. ومن الأسهل أيضًا تنقيح الأخطاء المحتملة في الاستعلامات ضمن أرضية عمل GraphQL موازنةً مع تنقيحها باستخدام التطبيق. وإن لم تكن متأكدًا من الاستعلامات المتاحة أو كيفية استخدامها، انقر على النافذة "docs" لفتح التوثيق: سنستعمل في تطبيقات React Native المكتبة Apollo Boost، وهو أسلوب لا يتطلب أية قواعد للتهيئة zero-config للبدء باستخدام Apollo client. وللتكامل مع React، سنستخدم المكتبة apollo/react-hooks@ والتي تؤمن خطافات مثل useQuery و useMutation تساعدنا في العمل مع المكتبة Apollo client. لنبدأ بتثبيت الاعتماديات اللازمة: npm install @apollo/client graphql سنحتاج قبل البدء باستخدام عميل Apollo إلى إعدادٍ جزئي لمجمّع Metro لمعالجة الملفات ذات الامتداد "cjs." التي يستخدمها عميل Apollo. لنثبّت أولًا الحزمة "expo/metro-config@" التي تحتوي على إعدادات Metro الافتراضية: npm install @expo/metro-config يمكننا بعد ذلك إضافة الإعداد التالي إلى الملف "metro.config.js" في المجلد الجذري للمشروع: const { getDefaultConfig } = require('@expo/metro-config'); const defaultConfig = getDefaultConfig(__dirname); defaultConfig.resolver.sourceExts.push('cjs'); module.exports = defaultConfig; أعِد تشغيل أدوات تطوير Expo لتطبيق الإعدادات المُجراة. لننشئ الآن دالةً خدميّة utility function لتكوين عميل Apollo وتهيئته على النحو المطلوب. أنشئ بدايةً المجلد "utils" ضمن المجلد "src"، وأنشئ ضمنه الملف "apolloClient.js"، ثم هيئ عميل Apollo ضمن هذا الملف ليتصل مع خادم Apollo: import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'; const httpLink = createHttpLink({ // ‫صحح عنوان IP إن كان مختلفًا لديك uri: 'http://192.168.100.16:4000/graphql', }); const createApolloClient = () => { return new ApolloClient({ link: httpLink, cache: new InMemoryCache(), }); }; export default createApolloClient; يستخدم عميل Apollo للاتصال بخادم Apollo العنوان URL ذاته الذي استخدمناه مع Fetch API ونفس رقم المنفذ 4000، ما عدا أنّ المسار هو "graphql/". وأخيرًا علينا تزويد التطبيق بعميل Apollo وذلك باستخدام سياق العمل ApolloProvider. سنضيفه إذًا إلى المكوّن App في الملف "App.js": import { NativeRouter } from 'react-router-native'; import { ApolloProvider } from '@apollo/client'; import Main from './src/components/Main'; import createApolloClient from './src/utils/apolloClient'; const apolloClient = createApolloClient(); const App = () => { return ( <NativeRouter> <ApolloProvider client={apolloClient}> <Main /> </ApolloProvider> </NativeRouter> ); }; export default App; تنظيم الشيفرة المتعلقة بالمكتبة GraphQL سيعود الأمر إليك دائمًا في تنظيم الشيفرة المتعلقة بالمكتبة GraphQL ضمن تطبيقك. لكن ولأجل الحصول على هيكلية مرجعية، لنلق نظرةً على طريقة بسيطة لكنها فعالة في تنظيم هذه الشيفرة. عرّفنا في هذه الهيكلية كلًا من الاستعلامات والطفرات mutations والاجتزاءات fragments وغيرها من الكيانات التي قد نستخدمها في ملفات خاصة بها وتتواجد في نفس المجلد. إليك صورة توضح هيكلية يمكنك اعتمادها: يمكنك استيراد معرّف القالب المجرّد gql المُستخدم في تعريف استعلامات من مكتبة "apollo/client@". وإن اتبعنا الهيكلية التي اقترحناها منذ قليل، فمن الأفضل إنشاء الملف "queries.js" في المجلد "graphql" ليحتوي الشيفرة اللازمة لتنفيذ استعلامات GraphQL في تطبيقنا. يمكن أن نخزّن كل استعلام ضمن متغّير خاص ثم نصدّره كالتالي: import { gql } from '@apollo/client'; export const GET_REPOSITORIES = gql` query { repositories { ${/* ... */} } } `; // other queries... يمكن إدراج تلك المتغيرات واستخدامها بالاستفادة من الخطاف useQuery على النحو التالي: import { useQuery } from '@apollo/client'; import { GET_REPOSITORIES } from '../graphql/queries'; const Component = () => { const { data, error, loading } = useQuery(GET_REPOSITORIES); // ... }; وينطبق على الطفرات ما ينطبق على الاستعلامات. طبعًا لا بدّ من تعريف الطفرات في ملف منفصل مثل "mutations.js"، ويُفضّل استخدام الاجتزاءات (fragments) في الاستعلامات لتفادي تكرار كتابة الحقول نفسها عدة مرات. تطوير هيكلية التطبيق عندما يأخذ التطبيق في النمو، سيزداد حجم بعض الملفات لتصبح ربما كبيرة جدًا لإدارتها بصورةٍ صحيحة. لنفترض أنه لدينا مكوّن A سيصيّر مكونين B و C. ستُعرّف جميع هذه المكونات ضمن الملف "A.jsx" والذي سيوضع في المجلد"components". نريد الآن أن ننقل المكونين A و B إلى ملفين مستقلين "B.jsx" و "C.jsx" دون أن نجري تغييرًا كبيرُا في تنظيم الشيفرة. سيكون أمامنا في هذه الحالة خيارين: الأول، إنشاء الملفين "B.jsx" و"C.jsx" في المجلد "components". وستكون هيكلية التطبيق على النحو التالي: components/ A.jsx B.jsx C.jsx ... الثاني، إنشاء مجلد A في المجلد "components" ثم إنشاء الملفين "B.jsx"و"C.jsx" ضمنه. ولتفادي إخفاق المكونات التي تُدرِج الملف "A.jsx" في شيفرتها، انقل الملف "A.jsx" إلى المجلد "A" وغيّر اسمه إلى "index.jsx". سينتج عن هذا الأسلوب الهيكلية التالية: components/ A/ B.jsx C.jsx index.jsx ... يُعد الخيار الأول مناسبًا تمامًا إن لم يكن المكونين C وB قابلين للاستخدام خارج حدود المكوّن A، فلا فائدة من وضعهما في ملفات منفصلة في المجلد "components"؛ أما الخيار الثاني فهو مناسبٌ لأسلوب الوحدات المستقلة، إذ أنه لا يسبب أية مشاكل في إدراج المكونات، لأن إدراج مسار مثل "A/." سيطابق إدراج كلا الملفين "A.jsx" و"A/index.jsx". التمرين 10.11 إحضار المستودعات باستخدام Apollo client نريد استبدال ما نفذناه ضمن الخطاف useRepositories مستخدمين المكتبة Fetch API باستعلام GraphQL. شغّل أرضية عمل Apollo على العنوان "http://localhost:4000"، ثم افتح توثيق المكتبة بالنقر على النافذة "docs". يستخدم الاستعلام عدة وسطاء اختيارية، فلا حاجة لتحديدها الآن. كوّن استعلامًا ضمن أرضية عمل Apollo لإحضار المستودعات بالاستفادة من قيم الحقول التي تعرضها حاليًا في واجهة التطبيق. ستُرقّم النتائج التي تضم قرابة 30 نتيجة افتراضيًا. يمكنك تجاهل موضوع الترقيم حاليًا. عندما يعمل الاستعلام ضمن أرضية عمل Apollo، اِستخدمه ليحل مكان شيفرة المكتبة FetchAPI في الخطاف useRepositories. يمكن أن تنفِّذ ذلك باستخدام الخطاف useQuery. أدرج بعدها معّرف القالب المجرّد gql من المكتبة بالطريقة التي شرحناها سابقًا. حاول أن تستخدم الهيكلية التي اقترحناها سابقًا لتنظيم الشيفرة المتعلقة بالمكتبة GraphQL. ولتجنب مشاكل الذاكرة الخبيئة مستقبلًا، استخدم سياسة الإحضار (fetch policy) "cache-and-network"، التي يمكن استخدامها مع الخطاف useQuery على النحو التالي: useQuery(MY_QUERY, { fetchPolicy: 'cache-and-network', // خيارات أخرى }); يجب أن لا تؤثر التغييرات في شيفرة الخطاف useRepositories على أداء المكوّن RepositoryList بأي شكل من الأشكال. متغيرات البيئة سيعمل كل تطبيق على الأرجح في أكثر من بيئة، ومن الواضح أن بيئتي التطوير والإنتاج أقرب مثالين عن الفكرة، لكن خارج هاتين البيئتين، هناك البيئة التي نشغّل عليها التطبيق حاليًا. تحتاج البيئات المختلفة عادةً اعتماديات مختلفة، فقد يستخدم الخادم المحلي الذي نتعامل معه قاعدة بيانات محليّة، بينما سيتعامل الخادم الذي سيُنشَر في بيئة الإنتاج مع قاعدة بيانات بيئة الإنتاج. ولكي نجعل شيفرتنا مستقلة عن بيئة التشغيل، لا بد من ربط هذه الاعتماديات بمعاملات أو متغيرات. لاحظ مثلًا أننا نستخدم في تطبيقنا قيمة جاهزة تعتمد بشدة على بيئة التشغيل وهي عنوان الخادم. تعلمنا سابقًا أنه بالإمكان تزويد البرامج قيد التشغيل بمتغيرات بيئة، إذ يمكن تعريف هذه المتغيرات من خلال سطر الأوامر أو من خلال ملف تهيئة بيئة التشغيل الذي ينتهي بلاحقة مثل "env." ومكتبات يؤمنها طرف ثالث مثل Dotenv. لكن ولسوء الحظ لا تدعم React Native الاستخدام المباشر لمتغيرات البيئة، لكن يمكننا الوصول إلى إعدادت تهيئة Expo الموجودة في الملف "app.json" في زمن التشغيل من خلال شيفرة JavaScript. وبالتالي يمكن استخدام هذه الإعدادات لتعريف المتغيرات المتعلقة بالبيئة والوصول إليها. نستطيع الوصول إلى إعدادات التهيئة بإدراج الثابت Constants من الوحدة "expo-constants" كما فعلنا ذلك عدة مرات مسبقًا. وبإدراج هذا الثابت، ستزودنا الخاصية Constants.manifest بكل الإعدادات. لنجرب ذلك بكتابة شيفرة ضمن المكوّن App وظيفتها طباعة محتويات هذه الخاصية في سجل العمل: import { NativeRouter } from 'react-router-native'; import { ApolloProvider } from '@apollo/client'; import Constants from 'expo-constants'; import Main from './src/components/Main'; import createApolloClient from './src/utils/apolloClient'; const apolloClient = createApolloClient(); const App = () => { console.log(Constants.manifest); return ( <NativeRouter> <ApolloProvider client={apolloClient}> <Main /> </ApolloProvider> </NativeRouter> ); }; export default App; ستجد الآن كل إعدادات التهيئة في سجل العمل. تقتضي الخطوة الثانية استخدام الإعدادات لتعريف متغيّرات تعتمد على بيئة التشغيل. لنبدأ المهمة بتغير اسم الملف "app.json" إلى "app.config.json". بهذا التغيير سنصبح قادرين على استخدام شيفرة جافا سكريبت داخل ملف التهيئة. غير محتوى الملف بحيث يتحوّل الكائن: { "expo": { "name": "rate-repository-app", // … بقية الضبط } } إلى كائن قابل للتصدير، يحتوي على الخاصية expo: export default { name: 'rate-repository-app', // … بقية الضبط }; يتلقى الكائن expo الخاصية extra ضمن إعدادات التهيئة، وذلك لتمرير أية إعدادات خاصة بالتطبيق. ولنرى كيف يعمل هذا الأمر، سنضيف المتغير env إلى إعدادات تهيئة تطبيقنا: export default { name: 'rate-repository-app', // … بقية الضبط extra: { env: 'development' }, }; أعد تشغيل أدوات تطوير Expo لتطبيق التغييرات. من المفترض أن تتغير قيمة الخاصية Constans.manifest بحيث تضم الخاصية extra التي تحتوي على إعدادات التهيئة الخاصة بتطبيقنا. يمكننا الآن الوصول إلى المتغير env من خلال الخاصية Constants.manifest.extra.env ولأن وضع القيم الجاهزة في الشيفرة أمر طائش، سنستخدم متغير بيئة بديلًا لذلك: export default { name: 'rate-repository-app', // rest of the configuration... extra: { env: process.env.ENV, }, }; تعلمنا سابقًا كيفية إسناد قيمة لمتغير البيئة من خلال سطر الأوامر، وذلك بتحديد اسم المتغيّر وقيمته قبل الأمر الفعلي. فعلى سبيل المثال، شغل أدوات تطوير Expo، ثم أسند القيمة "test" إلى متغير البيئة ENV: ENV=test npm start لو ألقيت نظرةً على سجل العمل، فستجد أن الخاصية Constants.manifest.extra.env قد تغيرت. يمكننا كذلك تحميل متغيرات البيئة من الملف "env." كما تعلمنا في الأقسام السابقة. لكن علينا أولًا تثبيت المكتبة Dotenv: npm install dotenv لنضف الآن الملف "env." إلى المجلد الجذري للمشروع على أن يحتوي ما يلي: ENV=development سندرج أخيرًا المكتبة ضمن الملف "app.config.js": import 'dotenv/config'; export default { name: 'rate-repository-app', // … بقية الضبط extra: { env: process.env.ENV, }, }; أعد تشغيل أدوات تطوير Expo لتطبيق التغييرات التي أجريتها على الملف "env". ملاحظة: لا تضع بيانات حساسة ضمن إعدادات التطبيق، لأن المستخدم بعد تنزيل التطبيق قد يكون قادرًا من الناحية النظرية على تحليل شيفرتك وسيحصل بالتالي على هذه البيانات الحساسة. التمرين 10.12 متغيرات البيئة استخدم متغير بيئة ضمن ملف "env." لتحديد عنوان خادم Apollo بدلًا من كتابته ضمن الشيفرة، وذلك عند تهيئة عميل Apollo. يمكنك تسمية هذا المتغير "APOLLO_URI" مثلًا. لا تحاول الوصول إلى متغير البيئة على النحو التالي: process.env.APOLLO_URI خارج الملف "app.config.js"، بل استخدم الكائن Constants.manifest.extra كما فعلنا في المثال السابق. كذلك لا تدرج المكتبة Dotenv خارج الملف "app.config.js" وإلا قد تواجهك المشاكل. تخزين البيانات في جهاز المستخدم قد تحتاج في بعض الأحيان إلى تخزين بعض البيانات في جهاز المستخدم. ومن أبرز السيناريوهات التي تضطرك إلى ذلك هو تخزين شهادة التحقق من المستخدم token لكي تكون قادرًا على الحصول عليها عندما يغلق المستخدم التطبيق ثم يعيد فتحه. لقد استخدمنا عند تطوير تطبيقات الويب الذاكرة المحلية للمتصفح من خلال الكائن localStorage. تؤمن Recat Native بالمقابل ذاكرة تخزين مقيمة هي الذاكرة غير المتزامنة (AsyncStorage). يمكن استخدام أمر التثبيت في Expo لتثبيت نسخة من الحزمة reactnativecommunity/async-storage@ متوافقة مع نسخة Expo SDK المستخدمة: expo install @react-native-async-storage/async-storage تتشابه الواجهة البرمجية للحزمة AsyncStorage مع الواجهة البرمجية للذاكرة المحلية localStorage بعدة نواحٍ، فكلاهما ذاكرة تعتمد مبدأ "مفتاح-قيمة". ويكمن الاختلاف الكبير بينهما، كما يوحي الاسم، أنّ عمليات AsyncStorage غير متزامنة. من المفيد أن نُجري تجريدًا بسيطًا لشيفرة العمليات، طالما أن AsyncStorage ستعمل مع مفاتيح نصية ضمن فضاء الأسماء العام global namespace. يمكن إنجاز هذا التجريد باستخدام الأصناف class. إذ يمكن، على سبيل المثال، إنشاء صنف لبطاقة تسوق، نخزّن فيها المنتجات التي يريد أن يشتريها مستخدم: import AsyncStorage from '@react-native-async-storage/async-storage'; class ShoppingCartStorage { constructor(namespace = 'shoppingCart') { this.namespace = namespace; } async getProducts() { const rawProducts = await AsyncStorage.getItem( `${this.namespace}:products`, ); return rawProducts ? JSON.parse(rawProducts) : []; } async addProduct(productId) { const currentProducts = await this.getProducts(); const newProducts = [...currentProducts, productId]; await AsyncStorage.setItem( `${this.namespace}:products`, JSON.stringify(newProducts), ); } async clearProducts() { await AsyncStorage.removeItem(`${this.namespace}:products`); } } const doShopping = async () => { const shoppingCartA = new ShoppingCartStorage('shoppingCartA'); const shoppingCartB = new ShoppingCartStorage('shoppingCartB'); await shoppingCartA.addProduct('chips'); await shoppingCartA.addProduct('soda'); await shoppingCartB.addProduct('milk'); const productsA = await shoppingCartA.getProducts(); const productsB = await shoppingCartB.getProducts(); console.log(productsA, productsB); await shoppingCartA.clearProducts(); await shoppingCartB.clearProducts(); }; doShopping(); ومن الجيد أيضًا أن نحدد فضاء أسماء للمفاتيح في AsyncStorage، ذلك أنها قيم نصيّة. وفي هذا السياق، سنجد أن فضاء الأسماء هو بادئة نزوّد بها مفاتيح التخزين، إذ سيمنع استخدام فضاء الأسماء مفاتيح التخزين من التصادم مع مفاتيح أخرى للمكتبة AsyncStorage. عرّفنا فضاء الأسماء في المثال السابق على شكل وسيط للدالة البانية، كما استخدمنا التنسيق "namespace:key مفتاح: فضاء أسماء" من أجل مفاتيح التخزين. يمكن إضافة عنصر إلى ذاكرة التخزين مستخدمين التابع AsyncStorage.setItem. سيكون مفتاح العنصر هو الوسيط الأول للتابع بينما ستكون قيمة المفتاح هي المعامل الثاني. وطالما أن قيمة المفتاح هي قيمة نصية، فلا بد من تحويل القيم غير النصية إلى نصية باستخدام التابع JSON.stringify. يُستخدَم التابع AsyncStorage.getItem للحصول على عنصر محدد من ذاكرة التخزين، ووسيط هذا التابع هو مفتاح العنصر. أما التابع AsyncStorage.removeItem فيُستخدم لحذف عنصر من ذاكرة التخزين بتحديد مفتاحه. التمرينان 10.13 - 10.14 10.13: استخدام الطفرات في نموذج تسجيل الدخول لا يستطيع نموذج تسجيل الدخول حاليًا فعل أي شيء حيال معلومات توثيق المستخدم التي يرسلها، لذلك سنحاول تحسين الوضع قليلًا في هذا التمرين. اطلع أولًا على توثيق الاستيثاق authentication documentation الخاص بالخادم "rate-repository-api"، ثم اختبر الاستعلامات التي تُزوَّد بها ضمن أرضية عمل Apollo. إن لم تحتوي قاعدة البيانات على أي مستخدمين يمكن وضع بعض البيانات فيها، وستجد إرشاداتٍ لذلك في الفقرة "getting started"من الملف "README" الموجود في المستودع "rate-repository-ap". عندما تتفهم الطريقة التي تعمل بها استعلامات الاستيثاق، أنشئ الملف "useSignIn.js" في المجلد "hooks" وضع فيه شيفرة الخطافuseSignIn الذي يرسل الطفرة باستخدام الخطاف useMutation. تقبل الطفرة authenticate وسيطًا وحيدًا هو credentials من النوع "AuthenticateInput". يحتوي نوع عنصر الإدخال (input type) حقلين لاسم المستخدم وكلمة المرور. ستكون القيمة التي يعيدها الخطاف على الشكل [signIn, result]، إذ يمثل الوسيط result نتيجة الطفرات كما يعيدها الخطاف useMutation، ويمثل الوسيط signIn دالة تنفيذ الطفرة باستخدام وسيط هو كائن من الشكل {username, password}. تلميح: لا تمرر دالة الطفرة إلى القيمة المعادة مباشرةً، بل أعد دالة تستدعي دالة الطفرة على النحو التالي: const useSignIn = () => { const [mutate, result] = useMutation(/* mutation arguments */); const signIn = async ({ username, password }) => { // استدعي دالة الطفرة بالوسطاء المناسبين }; return [signIn, result]; }; بعد إنجاز الخطاف، استخدمه في دالة الاستدعاء onSubmit العائدة للمكوّن SignIn على النحو التالي مثلًا: const SignIn = () => { const [signIn] = useSignIn(); const onSubmit = async (values) => { const { username, password } = values; try { const { data } = await signIn({ username, password }); console.log(data); } catch (e) { console.log(e); } }; // ... }; يمكنك عدُّ التمرين مكتملًا عندما تُسجَّل نتيجة طفرات authenticate في سجل عمل التطبيق بعد إرسال بيانات النموذج. وينبغي أن تتضمن نتيجة الطفرة شهادة التحقق من المستخدم. 10.14: تخزين شهادة التحقق- الخطوة 1 بعد أن حصلنا على شهادة التحقق لا بدّ من تخزينها. أنشئ الملف "authStorage.js" في المجلد "utils" بحيث يحتوي الشيفرة التالية: import AsyncStorage from '@react-native-async-storage/async-storage'; class AuthStorage { constructor(namespace = 'auth') { this.namespace = namespace; } getAccessToken() { // احصل على شهادة التحقق } setAccessToken(accessToken) { // أضف الشهادة إلى ذاكرة التخزين } removeAccessToken() { // احذف شهادة التحقق من ذاكرة التخزين } } export default AuthStorage; أنجز شيفرة التوابع التالية: AuthStorage.getAccessToken و AuthStorage.setAccessToken و AuthStorage.removeAccessToken. استخدم المتغير namespace لمنح المفاتيح فضاء أسماء كما فعلنا في المثال السابق. تحسين طلبات المكتبة Apollo Client بعد أن أنجزنا آلية لتخزين شهادة التحقق من المستخدم، حان الوقت لاستخدامها. هيئ ذاكرة التخزين في المكوّن App: import { NativeRouter } from 'react-router-native'; import { ApolloProvider } from '@apollo/client'; import Main from './src/components/Main'; import createApolloClient from './src/utils/apolloClient'; import AuthStorage from './src/utils/authStorage'; const authStorage = new AuthStorage(); const apolloClient = createApolloClient(authStorage); const App = () => { return ( <NativeRouter> <ApolloProvider client={apolloClient}> <Main /> </ApolloProvider> </NativeRouter> ); }; export default App; مررنا نسخةً من الصنف الذي يتعامل مع ذاكرة التخزين AuthStorage إلى الدالة createApolloClient مثل وسيط، لأننا سنرسل لاحقًا شهادة التحقق إلى خادم Apollo مع كل طلب. يتوقع خادم Apollo أن تكون شهادة التحقق ضمن ترويسة الاستيثاق وبنفس تنسيق حامل الشهادة <ACCESS_TOKEN>. يمكن تحسين طلب Apollo Client باستخدام الدالة setContext. لنرسل شهادة التحقق إلى خادم Apollo من خلال عميل Apollo بتعديل الدالة createApolloClientفي الملف "apolloClient.js": import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'; import Constants from 'expo-constants'; import { setContext } from '@apollo/client/link/context'; // ‫قد تحتاج إلى تغيير هذا بناءً على كيفية تكوين URI لخادم Apollo const { apolloUri } = Constants.manifest.extra; const httpLink = createHttpLink({ uri: apolloUri, }); const createApolloClient = (authStorage) => { const authLink = setContext(async (_, { headers }) => { try { const accessToken = await authStorage.getAccessToken(); return { headers: { ...headers, authorization: accessToken ? `Bearer ${accessToken}` : '', }, }; } catch (e) { console.log(e); return { headers, }; } }); return new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache(), }); }; export default createApolloClient; استخدام React Context لتوزيع البيانات يبقى علينا في النهاية إجراء تكامل بين ذاكرة التخزين والخطاف useSignIn. ولكي ننجز ذلك لا بدّ من السماح للخطاف بولوج شهادة التحقق التي هيئنا نسخة منها في المكوّن App. سنستخدم الواجهة Context التي تزودنا بها React لأداء هذه المهمة. أنشئ المجلد "contexts" ضمن المجلد "src"، ثم أنشئ ضمنه الملف "AuthStorageContext.js" وضع فيه الشيفرة التالية: import React from 'react'; const AuthStorageContext = React.createContext(); export default AuthStorageContext; ستتمكن الآن من استخدام المكوّن AuthStorageContext.Provider لتوزيع نسخة من ذاكرة التخزين على المكوّنات الأبناء ضمن سياق التنفيذ context. لنضفه إلى المكوّن App: import { NativeRouter } from 'react-router-native'; import { ApolloProvider } from '@apollo/client'; import Main from './src/components/Main'; import createApolloClient from './src/utils/apolloClient'; import AuthStorage from './src/utils/authStorage'; import AuthStorageContext from './src/contexts/AuthStorageContext'; const authStorage = new AuthStorage(); const apolloClient = createApolloClient(authStorage); const App = () => { return ( <NativeRouter> <ApolloProvider client={apolloClient}> <AuthStorageContext.Provider value={authStorage}> <Main /> </AuthStorageContext.Provider> </ApolloProvider> </NativeRouter> ); }; export default App; يمكن الوصول إلى نسخة ذاكرة التخزين في دالة الخطاف useSignIn باستخدام الخطاف useContext العائد للمكتبة React على النحو التالي: // ... import { useContext } from 'react'; import AuthStorageContext from '../contexts/AuthStorageContext'; const useSignIn = () => { const authStorage = useContext(AuthStorageContext); // ... }; انتبه إلى أنه من غير الممكن الولوج إلى القيمة التي يعيدها سياق العمل باستخدام الخطاف useContext مالم يُستخدم هذا الخطاف في مكوّن ابن للمكون Context.Provider. يُعد الوصول إلى نسخة ذاكرة التخزين باستخدام (useContext(AuthStorageContext مطولًا تمامًا ويكشف عن تفاصيل التنفيذ. دعنا نحسن هذا من خلال تنفيذ خطاف useAuthStorage في ملف "useAuthStorage.js" في مجلد "hooks": import { useContext } from 'react'; import AuthStorageContext from '../contexts/AuthStorageContext'; const useAuthStorage = () => { return useContext(AuthStorageContext); }; export default useAuthStorage; يعد تنفيذ الخطاف بسيطًا جدًا ولكنه يحسن إمكانية قراءة الخطافات والمكونات التي تستخدمها وصيانتها. يمكننا استخدام الخطاف لإعادة تشكيل خطاف useSignIn كما يلي: // ... import useAuthStorage from '../hooks/useAuthStorage'; const useSignIn = () => { const authStorage = useAuthStorage(); // ... }; ستجعل إمكانية توزيع البيانات على المكونات الأبناء من React Context مفتاحًا لحل عدد هائل من الحالات. لتطلع أكثر على هذه الحالات، اقرأ المقالة How to use React Context effectively للمؤلف "Kent C. Dodd"، لتعرف كيف توافق بين استخدام الخطاف useReducer وReact context لتنفيذ آلية لإدارة الحالة. وربما ستجد هذا مفيدًا لحل التمارين القادمة. التمرينان 10.15 و 10.16 10.15: تخزين شهادة التحقق- الخطوة 2 حسّن شيفرة الخطاف useSignIn لكي يخزّن شهادة التحقق التي تنتج عن الطفرة authenticate، بحيث لا تتغير القيمة التي يعيدها الخطاف. ينبغي أن يكون التغيير الوحيد في المكوّن SignIn هو تحويل المستخدم إلى واجهة عرض قائمة المستودعات بعد أن يسجل دخوله بنجاح. يمكنك إنجاز ذلك باستخدام الخطاف useNavigate. عليك أن تعيد ضبط مخزن عميل Apollo بعد تنفيذ الطفرة authenticate وتخزين شهادة التحقق في ذاكرة التخزين. سيسبب ذلك مسح الذاكرة المؤقتة وإعادة تنفيذ كل الطلبات النشطة. يمكن إنجاز ذلك باستخدام التابع resetStore العائد للمكتبة Apollo Client. يمكن الوصول إلى عميل Apollo عبر الخطاف useSignIn باستخدام الخطاف useApolloClient. وانتبه إلى أنّ ترتيب خطوات التنفيذ مهم جدًا وينبغي أن يكون على النحو التالي: const { data } = await mutate(/* خيارات */); await authStorage.setAccessToken(/* شهادة التحقق مأخوذة من البيانات */); apolloClient.resetStore(); 10.16: تسجيل الخروج ستكون الخطوة الأخيرة هي تنفيذ ميزة تسجيل الخروج. يمكن استخدام الاستعلام me للتحقق من بيانات الشخص الذي سجّل دخوله. فإن كانت نتيجة الاستعلام "null" فلن يكون الشخص مخوّلًا بالوصول. شغّل أرضية عمل Apollo، ثم نفّذ الاستعلام التالي: { me { id username } } ستكون النتيجة غالبًا "null"، والسبب في ذلك أنّ أرضية عمل Apollo غير مفوّضة بالوصول. ويعني ذلك أنها لم ترسل شهادة تحقق مع الطلب. راجع توثيق الاستيثاق لتفهم آلية استخلاص شهادة تحقق من الطفرة authenticate. استخدم هذه الشهادة في ترويسة الاستيثاق كما هو موّضح في التوثيق. نفّذ بعد ذلك الاستعلام me من جديد وسترى معلومات المستخدم الذي سجّل دخوله. افتح الملف "AppBar.jsx" الذي يحتوي على المكوّنAppBar والذي يحتوي بدوره على النافذتين "Sign in" و"Repositories". عدّل النوافذ بحيث تظهر النافذة "Sign out" إن سجل المستخدم دخوله، والنافذة "Sign in" إن سجّل خروجه. يمكنك إنجاز ذلك باستخدام الاستعلام me مع الخطاف useQuery. من المفترض أن تُحذف شهادة التحقق من ذاكرة التخزين عند الضغط على النافذة "Sign out"، وأن يُعاد ضبط مخزن عميل Apollo باستخدام التابع resetStore، إذ سيُعاد تنفيذ كل الاستعلامات النشطة تلقائيًا عند استدعاء التابع resetStore، ويعني ذلك إعادة تنفيذ الاستعلام me. تذكّر أن ترتيب خطوات التنفيذ مهم جدًا، ويجب إزالة شهادة التحقق من ذاكرة التخزين قبل إعادة ضبط مخزن عميل Apollo. ترجمة -وبتصرف- للفصل Communicating with server من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: أساسيات React Native مدخل إلى React Native مدخل إلى التحريك في React Native الاتصال مع الخادم في تطبيق React معتمد على Redux
  3. الآن وقد انتهينا من إعداد بيئة التطوير، سنتعرف على أساسيات React Native وسنبدأ بتطوير تطبيقنا. سنتعلم في هذا الفصل كيف سنبني واجهة مستخدم بالاستعانة بمكوّنات React Native البنيوية، وكيف سننسق مظهر هذه المكونات البنيوية، وكيف سننتقل من واجهة عرض إلى أخرى، كما سنتعلم كيف سندير حالة النماذج بفعالية. المكونات البنيوية تعلمنا في الأقسام السابقة طريقة استخدام React في تعريف المكوّنات كدوال تتلقى الخصائص كوسطاء وتعيد شجرة من عناصر React. تُمثَّل هذه الشجرة عادةً باستخدام شيفرة JSX. كما رأينا كيفية استخدام المكتبة ReactDOM في بيئة المتصفح من أجل تحويل المكوّنات إلى شجرة DOM قابلة للتصيير ضمن المتصفح. لاحظ الشيفرة التالية التي تمثل مكوّنًا بسيطًا: import React from 'react'; const HelloWorld = props => { return <div>Hello world!</div>; }; يعيد المكوّن HelloWorld عنصر <div> أُنشئ باستخدام عبارة JSX. وتذكر أن عبارة JSX ستُصرّف إلى استدعاءات للتابع React.createElement كالتالي: React.createElement('div', null, 'Hello world!'); سيُنشئ سطر الشيفرة السابق عنصر <div> دون أية خصائص props وله عنصر ابن وحيد هو النص "Hello World". وعندما يُصيَّر هذا المكوّن إلى عنصر DOM جذري بتطبيق التابع ReactDOM.render، سيُصيَّر العنصر <div> إلى عنصر DOM المقابل. وهكذا نرى أن React لا ترتبط ببيئة محددة كبيئة المتصفح، لكن وبالاستعانة بمكتبات مثل ReactDOM ستتمكن من تصيير مجموعة من المكوّنات المعرّفة مسبقًا كعناصر DOM في بيئة محددة. ندعو هذه المكوّنات المعرّفة مسبقًا في React Native بالمكوّنات البنيوية core components. فالمكوّنات البنيوية هي مجموعة مكوّنات تؤمنها React Native قادرة على استخدام المكونات الأصلية لمنصة خلف الستار. لننفذ المثال السابق باستخدام React Native: import React from 'react'; import { Text } from 'react-native'; const HelloWorld = props => { return <Text>Hello world!</Text>;}; لاحظ كيف أدرجنا المكوّن Text من React Native، واستبدلنا العنصر<div> بالعنصر <text>. ستجد الكثير من عناصر DOM المقابلة لعناصر React Native، وسنستعرض بعض الأمثلة التي استقيناها من توثيق المكونات البنيوية: المكوّن Text: ويعتبر المكون الوحيد من مكونات React Native والذي يمتلك أبناء على شكل قيم نصية، ويشابه العناصر <h1> و<strong>. المكوّن View: ويمثل وحدات البناء الرئيسية لواجهة المستخدم، ويشابه العنصر <div>. المكوّن TextInput: وهو مكوّن لحقل نصي، ويشابه العنصر <input>. المكوّن TouchableWithoutFeedback: وغيره من المكونات القابلة للمس Touchable ويستخدم لالتقاط مختلف حوادث الضغط والنقر، ويشابه العنصر <button>. هنالك بعض الاختلافات الواضحة بين المكوّنات البنيوية وعناصر DOM. يظهر الاختلاف الأول بأن المكوّن Text هو المكوّن الوحيد الذي يمتلك أبناء على شكل قيم نصية في React Native. وبمعنًى آخر، لن تستطيع استبدال هذا المكوّن بالمكوّن View مثلًا في المثال الذي أوردناه سابقًا. يتعلق الاختلاف الثاني بمعالجات الأحداث. فقد اعتدنا عند العمل مع عناصر DOM على إضافة معالج أحداث مثل onClick إلى أية عناصر مثل <div> و<btton>. لكن عليك قراءة توثيق الواجهة البرمجية في React Native لمعرفة أية معالجات وأية خصائص أخرى سيقبلها المكوّن الذي تستخدمه. إذ تؤمن -على سبيل المثال- عائلة مكوّنات اللمس Touchable components القدرة على التقاط الحركات التي تُرسم على الشاشة، كما يمكنها إظهار استجابات محددة عند تمييز هذه الحركات. ومن هذه المكوّنات أيضًا TouchableWithoutFeedback الذي يقبل الخاصية onPress: import React from 'react'; import { Text, TouchableWithoutFeedback, Alert } from 'react-native'; const TouchableText = props => { return ( <TouchableWithoutFeedback onPress={() => Alert.alert('You pressed the text!')} > <Text>You can press me</Text> </TouchableWithoutFeedback> ); }; سنبدأ الآن في هيكلة مشروعنا بعد أن اطلعنا على أساسيات المكونات البنيوية. لننشئ مجلدًا باسم "src" في المجلد الجذري للمشروع، ولننشئ ضمنه مجلدًا آخر باسم "components". أنشئ ملفًا باسم "Main.js" وضعه في المجلد "components"، على أن يحتوي هذا الملف الشيفرة التالية: import React from 'react'; import Constants from 'expo-constants'; import { Text, StyleSheet, View } from 'react-native'; const styles = StyleSheet.create({ container: { marginTop: Constants.statusBarHeight, flexGrow: 1, flexShrink: 1, }, }); const Main = () => { return ( <View style={styles.container}> <Text>Rate Repository Application</Text> </View> ); }; export default Main; لنستخدم الآن المكوّن Main ضمن المكوّن App الموجود في الملف "App.js" والذي يقع بدوره في المجلد الجذري للمشروع. استبدل محتويات الملف السابق بالشيفرة التالية: import React from 'react'; import Main from './src/components/Main'; const App = () => { return <Main />; }; export default App; إعادة تحميل التطبيق يدويا رأينا كيف يعيد Expo تحميل التطبيق تلقائيًا عندما نجري تغييرات على الشيفرة. لكن قد تفشل عملية إعادة التحميل تلقائيًا في حالات معينة، وسنضطر إلى إعادة تحميل التطبيق يدويًا. تُنجز هذه العملية من خلال قائمة المطوّر التي يمكن الوصول إليها بهزّ جهازك أو باختيار الرابط "Shake Gesture" ضمن قائمة العتاد الصلب في محاكي iOS. كما يمكن أن تستخدم أيضًا الاختصار "D⌘" عندما يعمل تطبيقك على محاكي iOS، أو الاختصار "M⌘" عندما يعمل التطبيق على مقلّد Android في نظام macOS، والاختصار "Ctrl+M" لمقلد Android في نظامي Linux وWindows. اضغط على "Reload" عندما تفتح قائمة المطوّر لإعادة تحميل التطبيق. لن تحتاج بعد ذلك إلى إعادة تحميل التطبيق، بل ستحدث العملية تلقائيًا. التمرين 10.3 قائمة المستودعات المقيمة سننجز في هذه التمارين النسخة الأولى من تطبيق قائمة المستودعات المُقيَّمة. ينبغي أن تتضمن القائمة الاسم الكامل للمستودع ووصفه ولغته وعدد التشعبات وعدد النجمات ومعدل التقييم وعدد التقييمات. ولحسن الحظ، تؤمن مكوّنًا مفيدًا لعرض قائمة من البيانات وهو FlatList. انجز المكوّنين RepositoryList وRepositoryItem في الملفين "RepositoryList.jsx" و"RepositoryItem.jsx" وضعهما في المجلد "componenets". ينبغي أن يصيّر المكوّن RepositoryList المكوّن البنيوي FlatList وأن يصيّر المكوّن RepositoryItem عنصرًا واحدًا من القائمة استخدم ذلك كأساس للملف "RepositoryItem.jsx": import React from 'react'; import { FlatList, View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ separator: { height: 10, }, }); const repositories = [ { id: 'jaredpalmer.formik', fullName: 'jaredpalmer/formik', description: 'Build forms in React, without the tears', language: 'TypeScript', forksCount: 1589, stargazersCount: 21553, ratingAverage: 88, reviewCount: 4, ownerAvatarUrl: 'https://avatars2.githubusercontent.com/u/4060187?v=4', }, { id: 'rails.rails', fullName: 'rails/rails', description: 'Ruby on Rails', language: 'Ruby', forksCount: 18349, stargazersCount: 45377, ratingAverage: 100, reviewCount: 2, ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4', }, { id: 'django.django', fullName: 'django/django', description: 'The Web framework for perfectionists with deadlines.', language: 'Python', forksCount: 21015, stargazersCount: 48496, ratingAverage: 73, reviewCount: 5, ownerAvatarUrl: 'https://avatars2.githubusercontent.com/u/27804?v=4', }, { id: 'reduxjs.redux', fullName: 'reduxjs/redux', description: 'Predictable state container for JavaScript apps', language: 'TypeScript', forksCount: 13902, stargazersCount: 52869, ratingAverage: 0, reviewCount: 0, ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4', }, ]; const ItemSeparator = () => <View style={styles.separator} />; const RepositoryList = () => { return ( <FlatList data={repositories} ItemSeparatorComponent={ItemSeparator} // other props /> ); }; export default RepositoryList; لا تبدّل محتويات المتغيّر repositories، فمن المفترض أن يحتوي على كل ما تحتاج إليه لإكمال التمرين. صيّر المكوّن ReositoryList ضمن المكوّن Main الذي أضفناه سابقّا إلى الملف "Main.jsx". ستبدو قائمة الكتب المُقَّيمة مشابهةً للتالي: تنسيق التطبيق بعد أن تعرفنا على آلية عمل المكوّنات، وكيفية استخدامها في بناء واجهة مستخدم بسيطة، لا بد من الالتفات إلى طرق تنسيق مظهر هذه التطبيقات. لقد رأينا في القسم 2 أنه من الممكن في بيئة المتصفح تعريف خصائص لتنسيق المكوّنات باستخدام CSS. كما رأينا أن هناك طريقتان لإنجاز ذلك: إما التنسيق ضمن السياق inline باستخدام الخاصية style، أو عن طريق محددات التنسيق المناسبة selectors المكتوبة في ملف CSS منفصل. ستجد تشابهًا واضحًا في الطريقة التي ترتبط بها خصائص التنسيق بمكونات React Native البنيوية والطريقة التي ترتبط بها هذه الخصائص بعناصر DOM. فمعظم مكوّنات React Native البنيوية تقبل الخاصية style، وتقبل هذه الخاصية بدورها كائنًا يحمل خصائص تنسيقٍ مع قيمها. وتتطابق في أغلب الأحيان خصائص التنسيق هذه مع مقابلاتها في CSS، مع اختلاف بسيط وهو كتابة أسماء هذه الصفات بطريقة سنام الجمل camelCase. أي ستكتب خصائص مثل "padding-top" على الشكل "paddingTop". وسيوضح المثال التالي طريقة استخدام الخاصية style: import React from 'react'; import { Text, View } from 'react-native'; const BigBlueText = () => { return ( <View style={{ padding: 20 }}> <Text style={{ color: 'blue', fontSize: 24, fontWeight: '700' }}> Big blue text </Text> </View> ); }; بالإضافة إلى أسماء الخصائص، ستلاحظ اختلافًا آخر في المثال السابق. إذ ترتبط بخصائص CSS التي تقبل قيمًا عددية واحدات مثل px أو % أو em أو rem. بينما لا تمتلك الخصائص التي تتعلق بالأبعاد مثل width وheight وpadding وكذلك font-size في React Native أية واحدات. تمثل هذه القيم العددية بيكسلات مستقلة عن الكثافة density-independent pixels. ولو تساءلت عن خصائص التنسيق المتاحة لمكوّن بنيوي محدد، ستجد الجواب في أوراق التنسيق الزائفة Styling Cheat Sheet لإطار عمل React Native. وبشكل عام، لا يعتبر تنسيق المكوًنات باستخدام الخاصية style مباشرة فكرة جيدة، لأنها ستجعل شيفرة المكونات غير واضحة. ويفضّل بدلًا عن ذلك، تعريف التنسيق خارج دالة المكوّن باستخدام التابع StyleSheet.create. يقبل هذا التابع وسيطًا واحدًا على شكل كائن يتألف بحد ذاته من كائنات تنسيق مسماة ومحددة القيم، كما ينشئ مرجعًا للمكوّن البنيوي StyleSheet من هذا الكائن. سنستعرض فيما يلي طريقة إعادة كتابة المثال السابق باستخدام التابع StyleSheet.create: import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ container: { padding: 20, }, text: { color: 'blue', fontSize: 24, fontWeight: '700', },}); const BigBlueText = () => { return ( <View style={styles.container}> <Text style={styles.text}> Big blue text <Text> </View> ); }; أنشأنا في الشيفرة السابقة كائني تنسيق مسمّيين هما: styles.container وstyles.text. يمكننا الوصول ضمن مكوّن إلى كائن تنسيق محدد بنفس الطريقة التي نصل بها إلى كائن صرف. تقبل الخاصية style مصفوفة من الكائنات بالإضافة إلى الكائنات المفردة. وتتالى كائنات التنسيق في المصفوفة من اليسار إلى اليمين، وبالتالي سيكون للتنسيقات التي تظهر لاحقًا أولوية التنفيذ. كما يمكن تطبيق ذلك بشكل عودي recursive، إذ يمكن على سبيل المثال، أن تحتوي مصفوفة على مصفوفة أخرى لكائنات التنسيق وهكذا. إن كانت إحدى قيم المصفوفة غير صالحة أو كانت "null" أو "undefined"، سيتم تجاهل هذه القيم. وهكذا يصبح من السهل تعريف تنسيقات شرطية كتلك التي تُبنى على أساس قيمة خاصية مثلًا، وإليكم مثالًا عن ذلك: import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ text: { color: 'grey', fontSize: 14, }, blueText: { color: 'blue', }, bigText: { fontSize: 24, fontWeight: '700', }, }); const FancyText = ({ isBlue, isBig, children }) => { const textStyles = [ styles.text, isBlue && styles.blueText, isBig && styles.bigText, ]; return <Text style={textStyles}>{children}</Text>; }; const Main = () => { return ( <> <FancyText>Simple text</FancyText> <FancyText isBlue>Blue text</FancyText> <FancyText isBig>Big text</FancyText> <FancyText isBig isBlue> Big blue text </FancyText> </> ); }; استخدمنا في الشيفرة السابقة العامل && في العبارة condition && exprIfTrue. سيعطي تنفيذ هذه العبارة قيمة "experIfTrue" إن كانت قيمة "condition" هي "true"، وإلا سيعطي القيمة "condition" والتي ستأخذ في هذه الحالة القيمة "false". هذه الطريقة المختصرة شائعة جدًا ومفيدة جدًا. كما يمكنك استخدام العامل الشرطي الثلاثي ":?" كالتالي: condition ? exprIfTrue : exprIfFalse سمات بواجهة مستخدم متناسقة لنبقى في مجال تنسيق التطبيقات لكن من منظورٍ أوسع قليلًا. لقد استخدم معظمنا تطبيقات متنوعة، وربما سنتفق أن ما بجعل واجهة المستخدم جيدة هو تناسق العناصر. فمظهر مكوّنات واجهة المستخدم كحجم الخط وعائلته ولونه ستتبع أسلوبًا متناسقًا. ولإنجاز ذلك لابد من ربط قيم خصائص التنسيق المختلفة بمعاملات، وهذا ما يعرف ببناء السمات theming. ربما يكون مصطلح "بناء السمات" مألوفًا لمستخدمي مكتبات بناء واجهات المستخدم المشهورة مثل Bootstrap وMaterial UI. وعلى الرغم من اختلاف طرق بناء السمات، إلا أنّ الفكرة الرئيسية ستبقى دائمًا استخدام المتغيرات مثل colors.primary بدلًا من الأرقام السحرية مثل 0366dd# عند تعريف التنسيق، وهذا ما سيقود إلى زيادة في الاتساق والمرونة. دعونا نلقي نظرة على طريقة التنفيذ العملي لبناء السمات في تطبيقنا. سنستخدم في عملنا العديد من النصوص وبتنسيقات مختلفة مثل حجم الخط ولونه. وطالما أنّ React Native لا تدعم التنسيق العام، لا بد من إنشاء مكوّنات Text خاصة بنا لنبقي المحتوى النصي متسقًا. لنبدأ إذًا بإضافة شيفرة كائن "تهيئة التنسيق" التالية إلى الملف "theme.js" الموجود في المجلد "src": const theme = { colors: { textPrimary: '#24292e', textSecondary: '#586069', primary: '#0366d6', }, fontSizes: { body: 14, subheading: 16, }, fonts: { main: 'System', }, fontWeights: { normal: '400', bold: '700', }, }; export default theme; علينا الآن إنشاء الكائن Text الفعلي الذي يستخدم قواعد تهيئة التنسيق. لننشئ إذًا الملف "Text.jsx" في المجلد "components" الذي يحتوي ملفات بقية المكوّنات، ولنضع فيه الشيفرة التالية: import React from 'react'; import { Text as NativeText, StyleSheet } from 'react-native'; import theme from '../theme'; const styles = StyleSheet.create({ text: { color: theme.colors.textPrimary, fontSize: theme.fontSizes.body, fontFamily: theme.fonts.main, fontWeight: theme.fontWeights.normal, }, colorTextSecondary: { color: theme.colors.textSecondary, }, colorPrimary: { color: theme.colors.primary, }, fontSizeSubheading: { fontSize: theme.fontSizes.subheading, }, fontWeightBold: { fontWeight: theme.fontWeights.bold, }, }); const Text = ({ color, fontSize, fontWeight, style, ...props }) => { const textStyle = [ styles.text, color === 'textSecondary' && styles.colorTextSecondary, color === 'primary' && styles.colorPrimary, fontSize === 'subheading' && styles.fontSizeSubheading, fontWeight === 'bold' && styles.fontWeightBold, style, ]; return <NativeText style={textStyle} {...props} />; }; export default Text; وهكذا نكون قد أنجزنا مكوّن النص الخاص بنا من حيث اللون وحجم الخط وكثافته لنستخدمه في أي مكان من تطبيقنا. يمكننا الحصول على تغييرات مختلفة في هذا المكوّن باستخدام خصائص مختلفة كالتالي: import React from 'react'; import Text from './Text'; const Main = () => { return ( <> <Text>Simple text</Text> <Text style={{ paddingBottom: 10 }}>Text with custom style</Text> <Text fontWeight="bold" fontSize="subheading"> Bold subheading </Text> <Text color="textSecondary">Text with secondary color</Text> </> ); }; export default Main; لك كامل الحرية في توسيع أو تعديل هذا المكوّن إن أردت. وقد يكون إنشاء مكوّنات نصية قابلة لإعادة الاستخدام تستخدم المكوّن Text مثل Subheading فكرة جيدة. حاول أيضًا أن توسع أو تعدل قواعد تهيئة السمة كلما تقدمنا في بناء تطبيقنا. استخدام flexbox في تصميم واجهة التطبيق سنعرّج أخيرًا في عرضنا لمواضيع تنسيق التطبيقات على تصميم الواجهات باستخدام flexbox. يعرف المطورون الذين ألفوا اللغة CSS أنّ flexbox لا يتعلق فقط بإطار العمل React Native، فقد يستعمل في حالات عدة لتطوير صفحات الويب أيضًا. وعمليًا لن يتعلم المطورون الذين يعرفون آلية عمل flexbox الكثير من هذه الفقرة، لكن دعونا على الأقل نراجع أو نتعلم أساسيات flexbox. flexbox هو كيان لتصميم الواجهات يتألف من مكونين منفصلين هما: حاوية flex وضمنها مجموعة من عناصر flex. تمتلك حاوية flex مجموعة من الخصائص التي تتحكم بتوزيع العناصر. ولجعل مكوّن ما حاوية flex، لا بد من أن يمتلك خاصية التنسيق display وقد أسندت لها القيمة "flex" وهي القيمة الافتراضية لهذه الخاصية. تظهر الشيفرة التالية طريقة تعريف حاوية flex: import React from 'react'; import { View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ flexContainer: { flexDirection: 'row', }, }); const FlexboxExample = () => { return <View style={styles.flexContainer}>{/* ... */}</View>; }; ربما تكون الخصائص التالية أهم خصائص حاوية flex: الخاصية flexDirection: وتتحكم بالاتجاه الذي ستأخذه عناصر flex عند ترتيبها ضمن الحاوية. وتأخذ هذه الخاصية القيم row وrow-reverse وcoulmn وcolumn-reverse. سيرتب row عناصر flex من اليسار إلى اليمين، بينما سيرتبها column من الأعلى للأسفل (القيمة الافتراضية). وستعكس القيم ذات اللاحقة reverse- اتجاه ترتيب العناصر. الخاصية justifyContent: وتتحكم بمحاذاة عناصر flex على طول المحور الرئيسي للترتيب الذي تحدده الخاصية السابقة. تأخذ هذه الخاصية القيم flex-start (القيمة الافتراضية) وflex-end وcenter وspace-between وspace-around وspace-evenly. الخاصية alignItems: وتقوم بوظيفة الخاصية السابقة لكن على المحور الآخر لترتيب العناصر. تأخذ هذه الخاصية القيم flex-start وflex-end وcenter وbaseline و stretch (القيمة الافتراضية). لننتقل الآن إلى عناصر flex والتي أشرنا سابقًا على أنها محتواة ضمن حاوية flex. تمتلك هذه العناصر خصائص تتحكم بتنسيق وسلوك هذه العناصر بالنسبة لبعضها في نفس الحاوية. ولكي يكون المكوّن عنصر flex يكفي أن نجعله ابنا مباشرًا لحاوية flex: import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ flexContainer: { display: 'flex', }, flexItemA: { flexGrow: 0, backgroundColor: 'green', }, flexItemB: { flexGrow: 1, backgroundColor: 'blue', }, }); const FlexboxExample = () => { return ( <View style={styles.flexContainer}> <View style={styles.flexItemA}> <Text>Flex item A</Text> </View> <View style={styles.flexItemB}> <Text>Flex item B</Text> </View> </View> ); }; إنّ أكثر خصائص العناصر استخدامًا هي flexGrow. حيث تقبل هذه الخاصية قيمة تحدد قدرة العنصر على النمو إن دعت الحاجة. فلو كانت قيمة هذه الخاصية لكل عناصر flex في حاوية هي 1، ستتقاسم هذه العناصر المساحة المتاحة للحاوية بالتساوي. بينما لو كانت قيمتها 0 لعنصر ما، فسيأخذ المساحة التي يتطلبها محتواه فقط، وسيترك المساحة الباقية لبقية العناصر. ننصحك بدراسة المثال النموذجي Flexbox example، والذي يشكّل مثالًا رئيسًا أكثر تفاعلية لاستخدام flexbox في تنفيذ بطاقة بسيطة لها حاشية علوية وجسم وحاشية سفلية. إقرأ تاليًا المقالة تعرف على CSS Flexbox وأساسيات استعماله لهيكلة صفحات الويب التي تحتوي على أمثلة مصوّرة شاملة عن استخدام flex. ومن الجيد أيضًا أن تجرب خصائص flexbox ضمن أرضية العمل Flexbox Playground لترى كيفية تأثير الخصائص على تصميم الواجهة. وتذكر أنّ أسماء الخصائص في React Native هي نفسها في CSS ما عدا نقطة التسمية بأسلوب سنام الجمل، إلا أن قيم هذه الخاصيات مثل flex-start وflex-end ستبقى نفسها. التمارين 10.4 - 10.5 10.4 شريط تنقل للتطبيق سنحتاج قريبًا إلى التنقل بين الواجهات المختلفة للتطبيق، لذلك سنحتاج إلى شريط تنقل بين هذه الواجهات. أنشئ الملف "AppBar.jsx" وضعه في المجلد "components" ثم ضع الشيفرة التالية ضمنه: import React from 'react'; import { View, StyleSheet } from 'react-native'; import Constants from 'expo-constants'; const styles = StyleSheet.create({ container: { paddingTop: Constants.statusBarHeight, // ... }, // ... }); const AppBar = () => { return <View style={styles.container}>{/* ... */}</View>; }; export default AppBar; وبما أنّ المكوّن AppBar سيمنع شريط الحالة من تغطية المحتوى، يمكن إزالة خاصية التنسيق marginTop التي وضعناها للمكوّنMain في الملف "Main.jsx". ينبغي أن يضم المكوّن AppBar نافذة عنوانها "Repositories". اجعل النافذة مستجيبة للمس touchable باستخدام المكوّن TouchableWithoutFeedback، لكن لا حاجة الآن لتعالج الحدث onPress. أضف المكوّن AppBar إلى المكوّن Main ، بحيث يبقى المكوّن الأعلى في الواجهة. من المفترض أن يبدو المكوّن AppBar شبيهًا للتالي: إن لون خلفية التطبيق كما تظهره الصورة السابقة هو 24292e#، لكن يمكنك استخدام اللون الذي تشاء. يمكنك أيضًا تهيئة لون خلفية التطبيق ضمن قواعد تهيئة السمة، وسيصبح تغيير اللون أمرًا سهلًا إن احتجت لذلك. ومن الجيد أيضًا فصل شيفرة النافذة الموجودة في المكوّن AppBar لتصبح ضمن مكوّن مستقل يمكن أن تسميه AppBarTab، سيسهّل ذلك إضافة نوافذ أخرى مستقبلًا. 10.5 قائمة محسنة المظهر للمستودعات المقيمة تبدو النسخة الحالية من تطبيق قائمة المستودعات المقيَّمة كئيبة المظهر. عدّل المكوّن RepositoryListItem ليعرض أيضًا الصورة التمثيلية لمؤلف المستودع. ويمكنك إنجاز ذلك باستخدام المكوّن Image. ينبغي إظهار القيم العددية كعدد النجمات وعدد التشعبات التي تزيد عن 1000 برقم مرتبة الآلاف وبدقة فاصلة عشرية واحدة فقط تليها اللاحقة "k". فلو كان مثلًا عدد التشعبات هو 8439، ستُعرض على الشاشة القيمة "8.4k". حسِّن أيضًا المظهر الكلي للمكوّن بحيث تبدو قائمة المستودعات مشابهة للتالي: إنّ لون الخلفية للمكوّن Main في الصورة السابقة هو e1e4e8# بينما ستظهر خلفية للمكوّن RepositoryListItem باللون الأبيض. كما يأخذ لون خلفية محدد اللغة القيمة 0366d6 #، وهي نفسها قيمة المتغيّر colors.primary في إعدادات تهيئة السمة. لا تنس الاستفادة من المكوّن Text الذي أنجزناه سابقًا. افصل المكوّن RepositoryListItem -إن دعت الحاجة- إلى مكوّنات أصغر. مسارات التنقل في React Native سرعان ما سنحتاج إلى آلية للانتقال بين مختلف واجهات العرض في التطبيق كواجهة المستودعات وواجهة تسجيل الدخول، وذلك عندما نبدأ بتوسيعه. وكنا قد تعلمنا في القسم 7 استخدام المكتبة React router لتنفيذ مهمة التنقل والتوجه في تطبيقات الويب. تختلف آلية التنقل في React Native قليلًا عن التنقل في تطبيقات الويب. ويظهر الاختلاف الرئيسي في عدم القدرة على الإشارة المرجعية للصفحات من خلال العناوين "URL" التي نكتبها في شريط تنقل المتصفح، ولا يمكننا كذلك التنقل بين الصفحات التالية والسابقة اعتمادًا على الواجهة البرمجية لتاريخ تنقلات المستخدم history API في المتصفح. لكن تبقى هذه الإشكالية مسألة تتعلق بواجهة التنقل التي نستخدمها. يمكننا في ReactNative استخدام متحكمات المسار البنيوية React Router's core بالكامل كالخطافات والمكوّنات. لكن علينا استبدال BrowserRouter الذي يستخدم في بيئة المتصفح بمقابله NativeRouter الملائم لإطار عمل React Native والذي تؤمنه المكتبة react-router-native. لنثبّت إذًا هذه المكتبة: npm install react-router-native سيؤدي استخدام المكتبة react-router-native إلى توقف عرض التطبيق ضمن متصفح ويب المنصة Expo، بينما ستعمل بقية طرق عرض التطبيق بشكل جيد. يمكن إصلاح هذه المشكلة بتوسيع إعدادات تهيئة المجمع webpack للمنصة Expo بحيث ينقل transpile الشيفرة المصدرية للمكتبة باستخدام Babel. ولتوسيع إعدادات التهيئة، لابدّ من تثبيت المكتبة expo/webpack-config@: npm install @expo/webpack-config --save-dev سننشئ بعد ذلك الملف "webpack.config.js" في المجلد الجذري للمشروع وضمنه الشيفرة التالية: const path = require('path'); const createExpoWebpackConfigAsync = require('@expo/webpack-config'); module.exports = async function(env, argv) { const config = await createExpoWebpackConfigAsync(env, argv); config.module.rules.push({ test: /\.js$/, loader: 'babel-loader', include: [path.join(__dirname, 'node_modules/react-router-native')], }); return config; }; أعد تشغيل أدوات تطوير لكي تُطبق إعدادات تهيئة webpack الجديدة، وستجد أن المشكلة قد حُلّت. افتح الآن الملف "App.js" وأضف المكوّن NativeRouter إلى المكوّن App: import React from 'react'; import { NativeRouter } from 'react-router-native'; import Main from './src/components/Main'; const App = () => { return ( <NativeRouter> <Main /> </NativeRouter> ); }; export default App; بعد أن أنشأنا متحكمًا بالمسار، سنضيف أول مسار تنقل (وجهة- Route) في المكوّن Main، وذلك ضمن الملف "Main.jsx": import React from 'react'; import { StyleSheet, View } from 'react-native'; import { Route, Switch, Redirect } from 'react-router-native'; import RepositoryList from './RepositoryList'; import AppBar from './AppBar'; import theme from '../theme'; const styles = StyleSheet.create({ container: { backgroundColor: theme.colors.mainBackground, flexGrow: 1, flexShrink: 1, }, }); const Main = () => { return ( <View style={styles.container}> <AppBar /> <Switch> <Route path="/" exact> <RepositoryList /> </Route> <Redirect to="/" /> </Switch> </View> ); }; export default Main; التمرينان 10.6 - 10.7 10.6 واجهة عرض تسجيل الدخول سننفذ قريبًا نموذجًا لكي يصبح المستخدم قادرًا على تسجيل دخوله إلى التطبيق. لكن علينا قبل ذلك أن ننشئ واجهة عرض يمكن الوصول إليها من شريط التطبيق. أنشئ الملف "Signin.jsx" في المجلد "components"، ثم ضع الشيفرة التالية ضمنه: import React from 'react'; import Text from './Text'; const SignIn = () => { return <Text>The sign in view</Text>; }; export default SignIn; هيئ مسارًا وجهته المكوّن Signin وضعه في المكوًن Main. أضف بعد ذلك نافذة عنوانها النص "Sign in" في أعلى شريط التطبيق إلى جوار النافذة "Repositories". ينبغي أن يكون المستخدم قادرًا على التنقل بين النافذتين السابقتين بالضغط على عنوان كل نافذة 10.7 شريط تطبيق قابل للتمرير scrollable طالما أننا سنضيف نوافذ جديدة إلى شريط التطبيق، فمن الجيد أن يكون قابلًا للتمرير أفقيًا عندما لا تتسع الواجهة لكل النوافذ. سنستخدم في ذلك المكوّن الأنسب لأداء المهمة وهو ScrollView. ضع النوافذ الموجودة في المكوّن AppBar ضمن المكوّن ScrollView: const AppBar = () => { return ( <View style={styles.container}> <ScrollView horizontal>{/* ... */}</ScrollView> </View> ); }; عندما تُسند القيمة "true" إلى الخاصية horizontal العائدة للمكون ScrollView، سيجعله قابلًا للتمرير أفقيًا عندما لا تتسع الشاشة للنوافذ التي يحتويها شريط التطبيق. وانتبه إلى ضرورة إضافة خصائص تنسيق ملائمة للمكوّن ScrollView، بحيث تُرتَّب النوافذ على شكل صف "row" ضمن حاوية flex. للتأكد من إنجاز المطلوب أضف نوافذ جديدة إلى الشريط حتى لا تتسع الشاشة لها جميعًا، ولا تنس إزالة هذه النوافذ التجريبية بعد التحقق من إنجاز المهمة. إدارة حالة النموذج بعد أن حصلنا على حاضنة مناسبة لواجهة عرض تسجيل الدخول، ستكون المهمة التالية إنجاز نموذج لتسجيل الدخول. لكن قبل الشروع في ذلك، سنتحدث قليلًا عن النماذج وبمنظور أوسع. يعتمد تنفيذ النماذج بشدة على إدارة الحالة. فقد يقدم لنا الخطاف useState حلًا جيدًا للنماذج الصغيرة، لكن سيكون التعامل مع الحالة بنفس الأسلوب مرهقًا عندما يغدو النموذج أكثر تعقيدًا. ولحسن الحظ سنجد مكتبات تتعايش مع بيئة React لتسهيل عملية إدارة حالة النماذج ومنها المكتبة Formik. تعتمد المكتبة Formik على مفهومي سياق العمل context والحقل field. يؤمن سياق العمل المكوّن Formik الذي يحتوي على حالة النموذج. وتتكون الحالة من معلومات عن حقول النموذج، وتتضمن هذه المعلومات مثلًا قيم الحقول والأخطاء الناتجة عن تقييم كلٍ منها. يمكن الإشارة إلى حالة الحقول باستخدام اسم الحقل عن طريق الخطاف useField أو المكوّن Field. لنرى كيف سيعمل الأمر من خلال إنشاء نموذج لحساب "مؤشر كتلة الجسم" عند البشر: import React from 'react'; import { Text, TextInput, TouchableWithoutFeedback, View } from 'react-native'; import { Formik, useField } from 'formik'; const initialValues = { mass: '', height: '', }; const getBodyMassIndex = (mass, height) => { return Math.round(mass / Math.pow(height, 2)); }; const BodyMassIndexForm = ({ onSubmit }) => { const [massField, massMeta, massHelpers] = useField('mass'); const [heightField, heightMeta, heightHelpers] = useField('height'); return ( <View> <TextInput placeholder="Weight (kg)" value={massField.value} onChangeText={text => massHelpers.setValue(text)} /> <TextInput placeholder="Height (m)" value={heightField.value} onChangeText={text => heightHelpers.setValue(text)} /> <TouchableWithoutFeedback onPress={onSubmit}> <Text>Calculate</Text> </TouchableWithoutFeedback> </View> ); }; const BodyMassIndexCalculator = () => { const onSubmit = values => { const mass = parseFloat(values.mass); const height = parseFloat(values.height); if (!isNaN(mass) && !isNaN(height) && height !== 0) { console.log(`Your body mass index is: ${getBodyMassIndex(mass, height)}`); } }; return ( <Formik initialValues={initialValues} onSubmit={onSubmit}> {({ handleSubmit }) => <BodyMassIndexForm onSubmit={handleSubmit} />} </Formik> ); }; لا يعتبر هذا المثال جزءًا من تطبيقنا، لذا لا حاجة لإضافة شيفرته إلى التطبيق. لكن يمكنك تجريب الشيفرة في Expo Snack مثلًا، وهو محرر لشيفرة React Native على شبكة الإنترنت على غرار JSFiddle وCodePen، ويمثل منصة مفيدة لتجريب الشيفرات بسرعة. كما يمكنك مشاركة Expo Snacks مع الآخرين بتحديد رابط إلى عملك أو تضمين الشيفرة على شكل مشغّل Snack ضمن أي صفحة ويب. ولربما قد صادفت مسبقًا مشغل Snack في مادة منهاجنا أو أثناء اطلاعك على توثيق React Native. عرّفنا في المثال السابق سياق عمل Formik ضمن المكوّن BodyMassIndexCalculator، وزودناه بالقيم الأولية وباستدعاء لإرسال محتويات النموذج submit callback. كما استخدمنا الخاصية initialValues لتزويد السياق بالقيم الأولية على شكل كائنات مفاتيحها أسماء الحقول وقيمها هي القيم الأولية المقابلة. كما تزودنا الصفة onSubmit باستدعاء إرسال محتويات النموذج، حيث ينفَّذ هذا الاستدعاء عندما تُستدعى الدالة handleSubmit شريطة أن لا تظهر أية أخطاء تقييم لمحتويات الحقول. تُستدعى الدالة التي تمثل ابنا مباشرًا للمكوّن Formik من خلال الخصائص props التي تحتوي معلومات متعلقة بالحالة، كما تحتوي أفعالًا كالدالة handleSubmit. يحتوي المكوّن BodyMassIndexForm ارتباطات الحالة بين السياق وعناصر الإدخال النصيّة. ويُستخدم الخطاف useField للحصول على قيمة حقل أو تعديل قيمته. يقبل useField وسيطًا واحدًا هو اسم الحقل ويعيد مصفوفة من ثلاث كائنات هي [الحقل field, البيانات الوصفية meta، المُساعدات helpers]. يحتوي الكائن field على قيمة الحقل، بينما يحتوي الكائن meta على المعلومات الوصفية للحقل كرسائل الخطأ التي قد يرميها. أما الكائن الأخير helpers، فيحتوي على الأفعال التي تُستخدم لتغيير حالة الحقل كالدالة setValue. وتجدر الإشارة إلى أنّ المكوّن الذي يستخدم الخطاف لابدّ أن يكون ضمن سياق Formik. أي يجب أن يكون هذا المكوّن من أبناء المكوّن Formik. يمكنك الاطلاع على نسخة تفاعلية عن المثال السابق باسم Formik example على موقع Expo Snack. لقد سبب استخدام الخطاف useField مع المكوّن TextInput شيفرة مكررة. لنعزل هذه الشيفرة ونضعها في مكوّن جديد باسم FormikTextInput، ولننشئ مكوّنًا مخصصًا باسم TextInput لإضافة مظهر مرئي أفضل لعنصر الإدخال النصي. لنثبّت أولًا Formik: npm install formik سننشئ الآن الملف "TextInput.jsx" في المجلد "components" ونضع فيه الشيفرة التالية: import React from 'react'; import { TextInput as NativeTextInput, StyleSheet } from 'react-native'; const styles = StyleSheet.create({}); const TextInput = ({ style, error, ...props }) => { const textInputStyle = [style]; return <NativeTextInput style={textInputStyle} {...props} />; }; export default TextInput; لننتقل إلى المكوّن FormikTextInput الذي ينشئ رابطًا لحالة Formik بالمكوّن TextInput. سننشئ الآن الملف "FormikTextInput.jsx" في المجلد "components" ونضع فيه الشيفرة التالية: import React from 'react'; import { StyleSheet } from 'react-native'; import { useField } from 'formik'; import TextInput from './TextInput'; import Text from './Text'; const styles = StyleSheet.create({ errorText: { marginTop: 5, }, }); const FormikTextInput = ({ name, ...props }) => { const [field, meta, helpers] = useField(name); const showError = meta.touched && meta.error; return ( <> <TextInput onChangeText={value => helpers.setValue(value)} onBlur={() => helpers.setTouched(true)} value={field.value} error={showError} {...props} /> {showError && <Text style={styles.errorText}>{meta.error}</Text>} </> ); }; export default FormikTextInput; يمكننا بعد استخدام المكوّن FormikTextInput أن نعيد كتابة المكوّن BodyMassIndexForm في المثال السابق كالتالي: const BodyMassIndexForm = ({ onSubmit }) => { return ( <View> <FormikTextInput name="mass" placeholder="Weight (kg)" /> <FormikTextInput name="height" placeholder="Height (m)" /> <TouchableWithoutFeedback onPress={onSubmit}> <Text>Calculate</Text> </TouchableWithoutFeedback> </View> ); }; وكما نرى سيقلص إنشاء المكوّن FormikTextInput الذي يعالج ارتباطات حالة Formik بالمكوّن TextInput الشيفرة اللازمة. ومن الجيد أن تكرر نفس العملية إن استخدَمَت نماذج Formik التي لديك مكونات إدخال بيانات. التمرين 10.8 10.8 نموذج تسجيل الدخول أنجز نموذجًا لتسجيل الدخوّل يرتبط بالمكوّن SignIn الذي أضفناه سابقًا في الملف"SignIn.jsx". يجب أن يحتوي النموذج مربعي إدخال نصيين، أحدهما لاسم المستخدم والآخر لكلمة السر. كما يجب أن يحتوي زرًا لتسليم بيانات النموذج. لا حاجة لكتابة دالة الاستدعاء onSubmit، بل يكفي إظهار قيم الحقول باستخدام الأمر console.log عند تسليم البيانات: const onSubmit = (values) => { console.log(values); }; تذكّر أن تستخدم المكوّن FormikTextInput الذي أنشأناه سابقّا. كما يمكنك استخدام الخاصية secureTextEntry في المكوّن TextInput لحجب كلمة السر عند كتابتها. سيبدو نموذج تسجيل الدخول مشابهًا للتالي: تقييم النموذج تقدم Formik مقاربتين لتقييم النماذج: دالة تقييم أو تخطيط تقييم. فدالة التقييم: هي دالة تُمرر للمكون Formik كقيمة للخاصية validate. حيث تستقبل الدالة قيم حقول النموذج كوسطاء وتعيد كائنًا يحتوى الأخطاء المحتملة الخاصة بكل حقل. أما تخطيط التقييم فيمرر إلى المكوّن Formik كقيمة للخاصية validationSchema. ويمكن إنشاء هذا التخطيط باستخدام مكتبة تقييم تُدعى Yup. لنثبّت هذه المكتبة إذًا: npm install yup كمثال على ما أوردنا، سننشئ تاليًا تخطيط تقييم لنموذج "مؤشر كتلة الجسم" الذي أنجزناه سابقًا. نريد أن نتحقق أنّ قيمتي كتلة الجسم والطول موجودتان، وأنهما قيم عددية. كذلك نريد أن نتحقق أنّ كتلة الجسم أكبر أو تساوي1، وأنّ الطول أكبر أو يساوي 0.5. تمثل الشيفرة التالية طريقة تنفيذ التخطيط: import React from 'react'; import * as yup from 'yup'; // ... const validationSchema = yup.object().shape({ mass: yup .number() .min(1, 'Weight must be greater or equal to 1') .required('Weight is required'), height: yup .number() .min(0.5, 'Height must be greater or equal to 0.5') .required('Height is required'),}); const BodyMassIndexCalculator = () => { // ... return ( <Formik initialValues={initialValues} onSubmit={onSubmit} validationSchema={validationSchema} > {({ handleSubmit }) => <BodyMassIndexForm onSubmit={handleSubmit} />} </Formik> ); }; تجري عملية التحقق افتراضيًا عند حدوث أي تغيير في قيم حقول النموذج، وكذلك عند استدعاء الدالة handleSubmit. فإن أخفق التحقق، لن تُستدعى الدالة التي تمرر إلى الكائن Formik عبر الخاصية onSubmit يعرض المكوًن FormikTextInput الذي أنشأناه سابقًا رسائل الخطأ المتعلقة بالحقل إن وقع الخطأ الموافق لها ولُمس الحقل. أي في الحالة التي يتلقى فيها الحقل تركيز الدخل ثم يفقده: const FormikTextInput = ({ name, ...props }) => { const [field, meta, helpers] = useField(name); // Check if the field is touched and the error message is present const showError = meta.touched && meta.error; return ( <> <TextInput onChangeText={(value) => helpers.setValue(value)} onBlur={() => helpers.setTouched(true)} value={field.value} error={showError} {...props} /> {/* Show the error message if the value of showError variable is true */} {showError && <Text style={styles.errorText}>{meta.error}</Text>} </> ); }; التمرين 10.9 10.9 تقييم نموذج تسجيل الدخول تحقق أنّ حقلي كلمة السر واسم المستخدم في نموذج تسجيل الدخول إجباريين. وتذكر أن لا تستدعي الدالة onSubmit التي أنجزناها في التمرين السابق إن أخفق تقييم النموذج. سيعرض المكوّن FormikTextInput حاليًا رسالة خطأ إن حدث خطأ في الحقل الذي تم لمسه، عزز مظهر هذه الرسالة بجعل نصها أحمر اللون. بالإضافة إلى رسالة الخطأ، إجعل إطار حقل الإدخال أحمر اللون ليشير ذلك إلى وقوع خطأ ضمنه. وتذكر أن المكوّن FormikTextInput سيعطي القيمة "true" للخاصية error العائدة للمكوّن TextInput عند حدوث أية أخطاء ضمن الحقل. يمكنك استعمال قيمة هذه الخاصية لإضافة تنسيق شرطي للمكوّن TextInput. سيبدو شكل نموذج تسجيل الدخول عند حدوث خطأ في أحد الحقول قريبًا من الشكل التالي: الشيفرة الخاصة بمنصة من أعظم ميزات استخدام React Native هي أننا لن نهتم بالمنصة التي سيعمل عليها التطبيق سواء Android أو iOS. لكن، قد تصادفنا حالات نضطر فيها إلى تنفيذ شيفرة خاصة بمنصة محددة. من هذه الحالات مثلًا، كتابة مكوّنات بطرق مختلفة لتلائم منصات مختلفة. يمكن الوصول إلى المنصة التي يستخدمها المستخدم من خلال الثابت Platform.OS: import { React } from 'react'; import { Platform, Text, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ text: { color: Platform.OS === 'android' ? 'green' : 'blue', }, }); const WhatIsMyPlatform = () => { return <Text style={styles.text}>Your platform is: {Platform.OS}</Text>; }; يأخذ هذا الثابت إحدى القيمتين: android أو ios. يمكن كتابة شيفرة مخصصة لمنصة أيضًا مستفيدين من التابع Platform.select. إذ يمرر للتابع كائن قد تأخذ مفاتيحه إحدى القيم التالية: ios أو android أو native أو default، ويعيد القيمة الأكثر ملائمة للمنصة التي يعمل عليها المستخدم. يمكننا إعادة كتابة المتغير styles في المثال السابق باستعمال التابع Platform.select كالتالي: const styles = StyleSheet.create({ text: { color: Platform.select({ android: 'green', ios: 'blue', default: 'black', }), }, }); وبالإمكان أيضًا استخدام التابع Platform.select لطلب مكوّن خاص بمنصة محددة: const MyComponent = Platform.select({ ios: () => require('./MyIOSComponent'), android: () => require('./MyAndroidComponent'), })(); <MyComponent />; إنّ الطريقة الأكثر تقدّمًا لكتابة وإدراج مكوّنات خاصة بمنصة محددة أو أية أجزاء من الشيفرة، هي استخدام ملفات لها إحدى اللاحقتين "ios.jsx." أو "android.jsx.". وانتبه إلى إمكانية استخدام أية لاحقة يمكن للمجمع أن يميزها مثل "js.". إذ يمكن مثلًا أن ننشئ ملفات باسم "Button.ios.jsx" أو باسم "Button.android.jsx" بحيث نستطيع إدراجها في شيفرتنا كالتالي: import React from 'react'; import Button from './Button'; const PlatformSpecificButton = () => { return <Button />; }; وهكذا ستضم حزمة Android للتطبيق المكوّن المعرّف في الملف "Button.android.jsx"، بينما تضم حزمة iOS المكوّن المعرّف في الملف "Button.ios.jsx". التمرين 10.10 10.10 خط خاص بمنصة محددة حُددت عائلة الخط الذي نستخدمه حاليًا في تطبيقنا بأنها "System" ضمن إعدادات تهيئة السمة الموجودة في الملف "theme.js". استخدم بدلًا من الخط "system" خطًا من خطوط العائلة Sans-serif ليكون خطًا خاصًا بكل منصة. استخدم الخط "Roboto" في منصة Android والخط Arial في منصة iOS. يمكن أن تُبقي "System" مثل خط افتراضي. هذا هو التمرين الأخير في هذا الفصل، وقد حان الوقت لتسليم التمارين إلى GitHub والإشارة إلى أنك أكملتها في منظومة تسليم التمارين. انتبه إلى وضع الحلول في القسم 2 ضمن المنظومة. ترجمة -وبتصرف- للفصل React Native basics من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: مدخل إلى React Native مدخل إلى التحريك في React Native
  4. تتطلب كتابة تطبيقات أصليّة Native لنظامي التشغيل iOS وAndroid تقليديًا استخدام لغات برمجة وبيئات تطوير خاصة بكل منصة. فقد استخدمت لغات مثل Objective C وSwift لتطوير تطبيقات iOS، ولغات مبنية على JVM مثل Java وScala وKotlin لتطوير تطبيقات موجّهة لنظام Android. وبالتالي وجب من الناحية التقنية كتابة تطبيقين منفصلين وبلغتي برمجة مختلفتين من أجل العمل على المنصتين السابقتين، وهذا ما تطلب الكثير من الموارد. لقد كان استخدام المتصفح كمحرك لتصيير التطبيقات إحدى المقاربات الأكثر شعبية في توحيد تطوير التطبيقات للمنصات المختلفة. وكانت المنصة Cordova من أشهر المنصات التي استخدمت في تطوير تطبيقات تعمل على منصات مختلفة. إذ تسمح هذه المنصة بتطوير تطبيقات قادرة على العمل على منصات مختلفة اعتمادًا على تقنيات الويب المعيارية مثل HTML5 وCSS3 وJavaScript. تعمل تطبيقات Cordova ضمن المتصفح المدمج في جهاز المستخدم، وبالتالي لن تحقق الأداء ولا المظهر الذي يمنحه استخدام التطبيقات الأصلية التي تستخدم مكوّنات واجهة المستخدم الأصلية. تقدم React Native إطار عمل لتطوير تطبيقات أصلية لمنصتي iOS وAndroid باستخدام JavaScript وReact. حيث تؤمن مجموعة من المكوّنات التي تعمل على منصات مختلفة وتستخدم خلف الستار المكوّنات الأصلية بالمنصة التي يعمل عليها التطبيق. وباستخدام React Native يمكننا استحضار كل الميزات المألوفة التي تقدمها React مثل JSX والمكوّنات components والخصائص props والحالة state والخطافات hooks، لتطوير تطبيقات أصلية لمنصة. وفوق كل ذلك، سنتمكن من استخدام مكتبات مألوفة بالنسبة لنا ضمن بيئة عمل React مثل react-redux وreact-apollo وreact-router وغيرها الكثير. تعتبر سرعة التطوير ومنحى التعلم المتصاعد للمطورين الذين ألفوا React، من أبرز الفوائد التي تمنحها React Native. وإليكم اقتباسًا تشجيعيًا ورد في مقال لمدونة Coinbase بعنوان Onboarding thousands of users with React Native، أو بالعربية "ضم آلاف المستخدمين إلى React Native"، حول فوائد React Native. حول هذا القسم سنتعلم خلال هذا القسم بناء تطبيقات React Native فعلية من البداية إلى النهاية. وسنتعلم مفاهيم مثل مكونات React Native البنيوية core components، وكيفية بناء واجهات مستخدم جميلة، وكيف سنتصل مع الخادم، كما سنتعلم كيفية اختبار تطبيقات React Native. سنعمل على تطوير تطبيق لتقييم مستودعات GitHub. وسيمتلك التطبيق ميزاتٍ كفرز وانتقاء المستودعات التي نقيّمها، وكذلك ميزة تسجيل مستخدم جديد، وتسجيل دخول مستخدم، وإنشاء مذكرة تقييم جديدة. سنستخدم واجهة خلفية جاهزة لكي ينصب تركيزنا على تطوير التطبيق باستخدام React Native فقط. ينبغي أن تسلم كل تمارين هذا القسم إلى مستودع GitHub واحد، سيضم مع الوقت الشيفرة الكلية للتطبيق. ستجد أيضًا حلًا نموذجيًا لكل مقال من هذا الجزء يمكنك استخدامه لإكمال التمارين الناقصة عند تسليمها. تعتمد هيكلية القسم على فكرة تطوير التطبيق تدريجيًا مع تقدمك في قراءة مادة القسم العلمية. فلا تنتظر حتى تصل إلى التمارين لتبدأ بتطوير تطبيقك، بل طوّره بما يتناسب مع المرحلة التي وصلت إليها في قراءة مادة القسم. سيعتمد هذا القسم بشدة على مفاهيم ناقشناها في أقسام سابقة. لذا وقبل أن تبدأ رحلتك هنا، عليك امتلاك بعض المعارف الأساسية في JavaScript وReact وGraphQL. لن تحتاج إلى معرفة عميقة في تطوير التطبيقات من ناحية الخادم، فكل الشيفرة التي تحتاجها للواجهة الخلفية محضرة مسبقًا. مع ذلك، سننفذ طلبات اتصال شبكية من خلال تطبيقات React Native مثل استعلامات GraphQL. ننصحك بإكمال الأقسام Part 1 وPart 2 وPart 5 وPart 7 وPart 8 قبل البدء بهذا القسم. تسليم التمرينات والحصول على الساعات المعتمدة ستُسلّم التمارين إلى منظومة استلام التمارين كما هو الحال في الأقسام السابقة. لكن انتبه إلى أنّ تمارين هذا المقال ستُسلّم إلى نسخةً أخرى من المنهاج تختلف عن النسخة التي سلمنا فيها تمارين الأقسام السابقة. فالأقسام من 1 إلى 4 في منظومة التسليم في هذه النسخة تشير إلى الفصول من a إلى d من هذا القسم. ويعني هذا أن عليك تسليم تمارين كل مقال على حدى ابتداءً من هذا المقال "مدخل إلى React Native" والذي سيحمل الاسم "Part 1" في منظومة تسليم التمارين. ستحصل في هذا القسم على ساعات معتمدة بناء على عدد التمارين التي تُكملها. فستحصل على ساعة معتمدة عند إكمالك 19 تمرينًا على الأقل، وعلى ساعتين معتمدتين عند إكمالك 26 تمرينُا على الأقل. وإن أردت الحصول على الساعات عند إكمالك للتمارين، أعلمنا من خلال منظومة التسليم بأنك أكملت المنهاج. وتشير الملاحظة "exam done in Moodle" إلى امتحان منهاج التطوير الشامل لتطبيقات الويب، والذي عليك اجتيازه حتى تحصل على ساعاتك المعتمدة المخصصة لهذا القسم. يمكنك الحصول على شهادة إكمال هذا القسم بالنقر على إحدى أيقونات "العلم". حيث ترتبط أيقونات الأعلام باللغة التي نُظِّمت بها الشهادة. وانتبه إلى ضرورة إكمال ما يعادل ساعة معتمدة من تمارين هذا القسم لتحصل على الشهادة. تهيئة التطبيق علينا تهيئة بيئة التطوير حتى نبدأ بالعمل مع تطبيقنا. وكنا قد تعاملنا في الأقسام السابقة مع أدوات مفيدة في ضبط وتهيئة تطبيقات React بسرعة مثل برنامج "create react app". تمتلك React Native لحسن الحظ أدواتٍ مشابهة أيضًا. سنستخدم لتطوير تطبيقنا منصة Expo، وهي منصة تبسط إعداد وتطوير وبناء ونشر تطبيقات React Native. لنبدأ إذًا بتثبيت واجهة سطر الأوامر الخاصة بهذه المنصة: npm install --global expo-cli يمكننا الآن تهيئة مشروعنا داخل المجلد "rate-repository-app" بتنفيذ الأمر التالي: expo init rate-repository-app --template expo-template-blank@sdk-38 يضبط الوسيط "sdk-38@" نسخة "Expo SDK" على 38 وهي النسخة التي تدعم النسخة 0.62 من React Native. وقد يتسبب لك استخدام نسخة مختلفة من "Expo SDK" مشاكل عند تتبع المادة التعليمية في هذا القسم. وتجدر الإشارة إلى محدودية Expo في بعض النقاط موازنةً بواجهة اللغة المشتركة CLI التي تستخدمها React Native، لكن لن تؤثر هذه المحدودية على التطبيق الذي سننفذه في هذا القسم. افتح المجلد "rate-repository-app" الذي أنشأناه عند تهيئة التطبيق باستخدام محرر شيفرة مثل Visual Studio Code. قد ترى بعض الملفات والمجلدات المألوفة ضمنه مثل "package.json" و"node_modules"، كما سترى أيضًا الملفات الأكثر ارتباطًا بالتطبيق مثل "app.json" الذي يحتوي على أوامر التهيئة الخاصة بالمنصة Expo، والملف "App.js" الذي يمثل المكوِّن الجذري لتطبيقنا. لا تغير اسم الملف "App.js" ولا تنقله إلى مجلد آخر، لأن Expo سيدرجه لتسجيل المكوّن الجذري. لنلق نظرة على قسم السكربتات في الملف "package.json": { // ... "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "eject": "expo eject" }, // ... } سيشغّل تنفيذ الأمر npm start المجمّع Metro وهو مُجمّع JavaScript أي JavaScript bundler يستخدم مع React Native، ويمكن أن نعده مشابهًا لبرنامج Webpack لكنه خاص ببيئة React Native. كما ينبغي أن تُشغَّل أدوات تطوير Expo على العنوان http://localhost:19002 من خلال المتصفح. وهذه الأدوات مفيدةً جدًا في عرض سجلات تنفيذ التطبيق، بالإضافة إلى تشغيل التطبيق من خلال المقلّد emulator، أو من خلال تطبيق جوّال Expo. سنعود إلى هذين الأخيرين لاحقًا، إذا علينا أولًا تشغيل التطبيق في المتصفح بالنقر على الزر "Run": ستجد بعد النقر على الرابط أن النص المكتوب في الملف "App.js" سيظهر على نافذة المتصفح. افتح هذا الملف باستخدام أي محرر مناسب واجر تغييرًا بسيطًا على النص الموجود في المكوّن Text. يجب أن تظهر التعديلات التي أجريتها مباشرة على شاشة المتصفح بعد أن تحفظ التغيرات. إعداد بيئة التطوير لقد ألقينا نظرة أولى على تطبيقنا من خلال متصفح Expo. وعلى الرغم من فائدة هذا المتصفح، إلا أنه يقدم محاكاة بدائية للبيئة الأصلية. سنتعرف تاليًا على بعض البدائل المتوفرة لدينا بما يتعلق ببيئة التطوير. يمكن تقليد أجهزة iOS وAndroid كالأجهزة اللوحية والهواتف عن طريق الحاسوب وباستخدام مقلّدات Emulators خاصة. وهذا أمر مفيد جدًا عند تطوير التطبيقات باستخدام React Native. يمكن لمستخدمي نظام التشغيل macOS استخدام مقلدات iOS وAndroid على حواسيبهم، أما مستخدمي أنظمة التشغيل الأخرى مثل Linux وWindows، فليس بوسعهم سوى العمل مع مقلدات Android. اتبع التعليمات التالية حول إعداد المقلّد، بناء على نظام التشغيل الذي يستخدمه حاسوبك: Set up Android emulator with Android Studio لأي نظام تشغيل Set up iOS simulator with Xcode لنظام التشغيل macOS بعد تثبيت المقلّد واختباره، شغّل أدوات تطوير Expo كما فعلنا سابقًا عن طريق الأمر npm start. وبناء على المقلد الذي تستخدمه، إنقر الزر "Run" على مقلد أجهزة Android، أو "Run" على رابط محاكي iOS. ينبغي أن يتصل Expo بالمقلد بعد النقر على الرابط، وسترى التطبيق عليه. كن صبورًا، فقد يستغرق الأمر وقتًا. بالإضافة إلى المقلدات، ستجد أنّ هناك طريقة غاية في الأهمية لتطوير تطبيقات React Native باستخدام Expo، وهي تطبيق جوّال Expo. فباستخدام هذا التطبيق، ستتمكن من استعراض تطبيقك عبر جهازك النقّال الفعلي، وهذا ما يمنحك خبرة أكثر موازنةً بالمقلدات. ثبّت أولًا تطبيق جوّال Expo باتباع التعليمات الموجودة في توثيق Expo. وانتبه إلى أنّ تطبيق جوّال Expo لن يعمل إن لم يكن جهازك النقًال متصلًا بنفس الشبكة المحلية (نفس الشبكة اللاسلكية على سبيل المثال) التي يتصل بها حاسوب التطوير الذي تعمل عليه. شغل تطبيق جوّال Expo عندما تنتهي من تثبيته. إن لم تكن قد شغّلت أدوات تطوير Expo بعد، فافعل ذلك مستخدمًا الأمر npm start. سترى في الزاوية اليسارية السفلى من نافذة أدوات التطوير شيفرة QR. امسح هذه الشيفرة باستخدام تطبيق جوّال Expo عن طريق الضغط على الرابط "Scan QR Code". ينبغي أن يبدأ تطبيق Expo ببناء مجمّع JavaScript، ومن المفترض أن ترى تطبيقك وهو يعمل بعد انتهاء عملية التجميع. وهكذا ستتمكن في كل مرة تفتح فيها تطبيقك باستخدام تطبيق جوّال Expo من الوصول إليه دون الحاجة إلى مسح شيفرة QR مجددًا، وذلك بالضغط عليه في قائمة التطبيقات التي شُغّلت مؤخرًا، والتي تظهر في نافذة العرض "Project". التمرين 10.1 10.1 تهيئة التطبيق استخدم واجهة سطر أوامر Expo لتهيئة تطبيقك، وجهّز بيئة التطوير باستخدام المقلد أو تطبيق جوّال Expo. ننصحك بتجريب الأسلوبين السابقين لتحدد البيئة الأنسب بالنسبة لك. لا تهتم كثيرًا لاسم التطبيق، فيمكنك أن تستخدم الاسم "rate-repository-app". عليك إنشاء مستودع GitHub جديد لتسليم هذا التمرين وغيره من التمارين اللاحقة، ويمكنك تسمية هذا المستودع باسم التطبيق الذي قمت بتهيئته عند تنفيذك الأمر expo init. إن قررت إنشاء مستودع خاص، أضف المستخدم Kaltsoon كمشارك لك collaborator في هذا المستودع، وذلك للتحقق من التمارين التي أنجزتها وسلمتها. نفذ بعد إنشائك المستودع الأمر git init ضمن المجلد الجذري لمشروعك للتأكد من تهيئة المستودع كمستودع Git. ولإضافة المستودع الجديد إلى قائمة العمل عن بعد نفذ الأمر التالي: git remote add origin git@github.com:<YOUR\*GITHUB\*USERNAME>/<NAME\*OF\*YOUR_REPOSITORY>.git وتذكر أن تستبدل القيم داخل الأقواس بما يناسب عندك كتابتك لسطر الأوامر. ادفع أخيرًا بالتغييرات التي أجريتها إلى المستودع وسيكون كل شيء على ما يرام. المدقق ESLint الآن، وقد ألفنا بيئة التطوير سنعمل على تحسين مهاراتنا في التطوير وذلك بتهيئة مدقق للشيفرة. سنستخدم المدقق ESLint الذي تعاملنا معه سابقًا، وكالعادة سنبدأ بتثبيت الاعتماديات اللازمة: npm install --save-dev eslint babel-eslint eslint-plugin-react سنضع القواعد التالية للمدقق داخل الملف ذو اللاحقة "eslintrc." والموجود في المجلد "rate-repository-app": { "plugins": ["react"], "settings": { "react": { "version": "detect" } }, "extends": ["eslint:recommended", "plugin:react/recommended"], "parser": "babel-eslint", "env": { "browser": true }, "rules": { "react/prop-types": "off", "semi": "error" } } ومن ثم سنضيف السكربت "lint" إلى محتويات الملف "package.json"، وذلك للتحقق من تطبيق قواعد المدقق في الملفات المحددة: { // ... "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "eject": "expo eject", "lint": "eslint ./src/**/*.{js,jsx} App.js --no-error-on-unmatched-pattern" }, // ... } لاحظ أننا استخدمنا الفواصل لإنهاء سطر الشيفرة على نقيض ما فعلنا في الأقسام 1-8، لذلك أضفنا القاعدة semi للتأكد من ذلك. نستطيع الآن التأكد من خضوع ملفات JavaScript الموجودة في المجلد "src" وكذلك الملف "App.js" لقواعد المدقق، وذلك بتنفيذ الأمر npm run lint. سنضع ملفات الشيفرة لاحقًا في المجلد "src"، لكن طالما أننا لم نضف أية ملفات ضمنه حتى اللحظة، سنضطر إلى تفعيل الراية "no-error-on-unmatched-pattern". حاول إن استطعت أيضًا دمج المدقق ESlint مع محرر الشيفرة الذي تعمل عليه. فإن كنت تعمل على Visual Studio Code، يمكنك فعل ذلك بتفقد قسم الموّسعات extensions والتأكد من تثبيت وتفعيل موسِّع ESlint. تمثل قواعد تهيئة المدقق السابقة، القواعد الأساسية للتهيئة. يمكنك أن تعدّل بحرية على هذه القواعد وأن تضيف ملحقات جديدة plugins إن شعرت بالحاجة إلى ذلك. التمرين 10.2 10.2 إعداد وضبط المدقق ESLint هيئ المدقق في مشروعك حتى تتمكن من تدقيق ملفات الشيفرة عند تنفيذك الأمر. ويفضّل عند استخدامك المدقق أن تدمجه مع محررك لكي تستفيد من كامل إمكانياته. هذا هو التمرين الأخير في هذا المقال، وقد حان الوقت لتسليم التمارين إلى GitHub والإشارة إلى أنك أكملتها في منظومة تسليم التمارين. انتبه إلى وضع الحلول في القسم 1 ضمن المنظومة. عرض سجلات العمل يمكن الاستفادة من بيئة تطوير Expo في عرض سجلات العمل للتطبيق، كما يمكن متابعة رسائل الخطأ والتحذير ضمن المقلّد أو ضمن واجهة تطبيق الجوّال. ستظهر رسالة الخطأ باللون الأحمر فوق مكان الخطأ، بينما يمكن عرض رسائل التحذير بالضغط على رسالة التحذير في أسفل الشاشة. كما يمكنك استخدام الأمر console.log لطباعة رسائل محددة، وذلك لغايات التنقيح وكشف الأخطاء. لتجريب ذلك عمليًا، شغّل أدوات تطوير Expo بتنفيذ الأمر npm start، ثم شغّل التطبيق باستخدام المقلّد أو تطبيق الجوّال. ستتمكن عندها من رؤية الأجهزة المتصلة بحاسوبك تحت العنوان "Metro Bundler" في الزاوية العليا اليسارية من نافذة أدوات التطوير: انقر على الجهاز لفتح سجلات العمل عليه. افتح بعد ذلك الملف "App.js" وأضف رسالةً ما باستخدام الأمر console.log إلى المكوِّن App. سترى رسالتك ضمن السجلات بمجرد أن تحفظ التغييرات التي أجريتها على الملف. استخدام منقح الأخطاء Debugger قد يكون فحص الرسائل التي كُتبت باستخدام الأسلوب console.log مفيدًا، لكن سيتطلب الأمر فهمًا أوسع إن كنت تحاول أن تجد ثغرةً ما أو أن تفهم سير عمل التطبيق. فربما سنهتم -على سبيل المثال- بخصائص أو بحالة مكوِّن محدد، أو بالاستجابة لطلبٍ محدد على الشبكة، لذلك استخدمنا أدوات المطوّر التي يؤمنها المتصفح لإنجاز هذا العمل سابقًا. بالمقابل سنجد أنّ المنقّح React Native Debugger سيزوّدك بنفس ميزات التنقيح، لكن لتطبيقات React Native. لنبدأ العمل بتثبيت منقِّح React Native بمساعدة تعليمات التثبيت. شغّل المنقّح عند انتهاء التثبيت، وافتح نافذة تنقيح جديدة (يمكنك استخدام الاختصارات التالية: command+T على macOS وCtrl+T على Windows/Linux)، ثم اضبط رقم منفذ محزِّم React Native على 19001. علينا الآن تشغيل تطبيقنا وربطه مع المنقّح. لننفذ إذًا الأمر npm start أولًا، ثم سنفتح التطبيق ضمن المقلّد أو تطبيق جوّال Expo. افتح قائمة المطوّرين داخل المقلّد أو التطبيق باتباعك الإرشادات الموجودة في توثيق Expo. اختر من قائمة المطوّرين العنصر "Debug remote JS" لتتصل مع المنقّح. إن جرى كل شيء على مايرام سترى شجرة مكونات التطبيق ضمن المنقّح. يمكنك استخدام المنقّح للتحقق من حالة المكوّنات وخصائصها وتغييرها إن أردت. حاول أن تعثر مثلًا على المكوّن Text الذي يصيره المكوّن App باستخدام خيار البحث أو عبر شجرة المكوّنات. انقر المكوّن text وغّير قيمة الخاصية children. من المفترض أن يَظهر التغيير تلقائيًا في نافذة عرض التطبيق. للاطلاع على أدوات تنقيح أكثر فائدة لتطبيقات Expo، اطلع على توثيق عملية التنقيح في Expo. ترجمة -وبتصرف- للفصل Introduction to React Native من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: استخدام TypeScript والأنواع التي توفرها في تطبيقات React. مدخل إلى التحريك في React Native.
  5. يعد التحريك باستخدام مكتبة React Native من المواضيع الأساسية المطروحة في أقسام الدراسة أو ورشات العمل، ربما لكون عدد من المطورين يجدون تحديًا في استخدامها. في الوقت الذي تركّز فيه الكثير من المدونات والمصادر في الويب على جوانب الأداء عند استخدام React Native، لا يخوض إلا عدد قليل منها في الأساسيات. سنعرض في هذه المقالة لأساسيّات تطبيق التحريكات باستخدام مكتبة React Native. أولًا سنلقي نظرة على خلفية المكتبة وتاريخها. بداية التحريكات في React Native وتطوّرها عند تشغيل برنامج عابر للمنصّات مكتوب بلغة جافاسكريبت تحتاج مكوّنات React Native على هاتفك إلى تبادل المعلومات بواسطة عنصر يسمّى الجسر (Bridge). التبادل عبر هذا العنصر غير متزامن (Asynchronous)، وهو ما يجعل التطبيقات المبنية على إطار العمل React Native تصبح بطيئة لأن الطلبات غير المتزامنة المارّة على الجسر تسدّ المسار أمام شفرة جافاسكريبت التي تتفاعل مع أجزاء إطار العمل. للحصول على أداء عال يجب أن تصيّر (Render) التحريكات على خيط (Thread) واجهة المستخدم الأصلي في نظام التشغيل، وبما أن البيانات تحتاج للسَّلسَلة (Serialization) عبر الجسر فإن ذلك يؤدّي غالبًا إلى منع خيط جافاسكريبت، ممّا يتسبّب في نقص عدد الإطارات (Frame) على الشاشة. بقي هذا المشكل حاضرًا منذ 2015 حين كانت التحريكات تمثّل واحدة من العقبات الكبرى أمام إطار العمل React Native. لحسن الحظ، تحسّنت الحالة منذ ذلك الوقت بفضل الدعم الكبير من أعضاء مجتمع المطورين، إذ أصبح من الشائع الآن إنجاز 60 إطارًا في الثانية الواحدة (Frame per second, FPS) في عمليات التحريك ضمن React Native. سهّلت واجهات تطبيقات برمجيّة تصريحيّة (Declarative API) مثل Animated عمليّة تنفيذ التحريكات التفاعلية. استخدام واجهة التطبيقات البرمجية Animated لتحسين الأداء يواجه المطورون حتى الآن العديد من القضايا المتعلقة بالأداء خاصة عندما يعملون على رسومات متحركة معقّدة. كما ذكرنا سابقًا، تتسبب الاختناقات في الأداء التي تحدث عند تنفيذ الرسوم في React Native، بأعباء عمل كبيرة على خيوط جافاسكربت (JavaScript threads)، مما يقلّل من معدل إظهار الإطارات، وبالتالي يتسبّب في البطء عند استخدام البرنامج، وللتغلب على هذه المشكلة نحتاج إلى الحفاظ على معدل 60 إطارًا في الثانية الواحدة. إن أفضل حل للحفاظ على معدّل الإطارات بالثانية هو استخدام Animated API لأنها تحسّن الزمن المطلوب للسَّلسَلة وعكسها (Serialization/Deserialization). تعمل Animated API عبر استخدام واجهة تطبيقات تصريحية لوصف التحريكات. الفكرة التي تقوم عليها هي التصريح بالتحريك كاملًا مرةً واحدة مقدّمًا لكي تُمكِن سَلسَلة التصريح في جافاسكريبت وإرسالها إلى الجسر. ينفّذ مشغّلٌ (Driver) التحريكاتِ إطارًا بعد الآخر. تنفيد التحريكات ضمن React Native توجد طرق عدة لتطبيق التحريك في React Native. في ما يلي الطرق التي أجد أن لديها فائدة أكبر. القيم المتحرّكة Animated values يتربّع تحريك القيم على قائمتي بوصفه لبنة أساسيّة للتحريكات في تطبيقات React. تشير هذه القيم عمومًا إلى قيم حقيقة تُحوَّل إلى أعداد حقيقية عند تمريرها مع مكوّن مُحرَّك. فلننظر إلى المثال التالي. Animated.timing(this.valueToAnimate, { toValue: 42; duration: 1000; }).start() نلاحظ في المثال السابق أننا أسندنا القيمة 42 إلى value.ToAnimate والتي ستنفّذ بعد 1000 ميلي ثانية. كما يمكن التصريح عن قيم خاصيات التحريك الأخرى مثل الشفافية opacity أو الموضع position. في ما يلي مثال تطبيقي على الشفافية بقيم متحرّكة. <Animated.View style={{ opacity: myAnimatedOpacity }} /> مشغلات التحريك: Animated.timing و Animated.event و Animated.decay انظر إلى المشغّلات على أنها عُقد في مخطّط بياني (Graph) تغيّر قيمةً متحرّكة مع كل إطار. على سبيل المثال، ستزداد القيمة المسندة إلى Animated.timing، بينما ستنقص القيمة المسندة إلى Animated.decay في كل إطار. لنلق نظرة على المثال التالي: Animated.decay(this.valueToAnimate, { velocity: 2.0, deceleration: 0.9 }).start(); في هذا المثال، ستنفَّذ الرسوم المتحركة انطلاقًا من سرعة محددة بالقيمة 2.0، ثم تتناقص تدريجيًا خلال الوقت المحدَّد. أصبحت هذه الطريقة شائعة في التطبيقات العابرة للمنصّات مع بداية ظهور التوثيقات عن التصميم المسطّح (Material design). تبدو التحريكات بمظهر جيّد، وتوجد طرق عدّة لجعل التجربة لا تنسى. يمكن كذلك استخدام Animated.event لتشغيل قيمة عند تمرير المستخدم: <ScrollView onScroll={Animated.event( [{nativeEvent: {contentOffset: {y: this.state.scrollY}}}] )} > </ScrollView> في المثال السابق يُعيد Animated.event دالّة تعدّل قيمة الخاصيّة nativeEvent.content.offset.y في المكوّن scrollView لتأخد قيمة scrollY الحالية. عمومًا، يمكن استخدام مشغلّات التحريك مع القيم المتحركة أو مع مشغّلات أخرى. ملحوظة جانبية: انتبه إلى أنه عندما يحدّث المشغّل كل إطار فإن القيمة الجديدة تحدّث مباشرةً قيمة الخاصيّة View، وبالتالي يجب أن تكون حذرًا عند التصريح بالمتغيّرات وتنتبه إلى نطاقها أثناء استخدامها. توابع التحويل Transform methods تتيح لك توابع التحويل تحويل قيمة متحرّكة إلى قيمة متحرّكة جديدة. يمكنك استخدام توابع مثل Animated.add()‎ وAnimated.multiply()‎ وAnimated.interpolate()‎ لتنفيذ عمليّات تحويل بين القيم. يمكنك تنفيذ عمليّة تحويل على أي عقدة في المخطّط البياني قيد التحريك كالتالي: newAnimated.Value(55).interpolate(.....) // Transformation operation using Animated.interpolate() method تحريك الخاصيّات الخاصيّات المتحرّكة هي عقد خاصّة تربط بين القيم المتحرّكة وخاصيّة في مكوّن. تُولَّد تلك الخاصيّات عند تصيير الخاصيّة Animated.view وإسناد خاصيّات لها. فلننظر إلى الشفرة التالية: Var opacity = new Animated.Value(0.7); <Animated.View style={{ opacity }} /> أضفنا خاصيّة متحرّكة تُحوِّل القيمة 0.7 إلى خاصية. في حال حدّث التابع تلك القيمة، سينعكس التغيير على خاصية View. تعمل التوابع الموصوفة أعلاه أساسًا بالتزامن مع الكائنات المتحرّكة (Animated objects) في React، كما أن توابع التحويل تؤدّي دورًا أساسيًّا في عمل تلك الكائنات. يغيّر مشغّل التحريك (Animated.Timing أو Animated.Event أو Animated.Decay) قيم التحريك في كل إطار ثم تُمرر النتيجة عبر أي تابع من توابع التحويل لتُخزَّن بصيغة خاصية عرض (الشفافية Opacity أو قيمة التحويل). يسلم مفسّر جافاسكريبت النتيجة إلى Native React حيث تُحدَّث الواجهة من خلال استدعاء التابع setNativeProps. في الأخير تُمرَّر الخاصيّة إلى iOS أو Android حيث يتسلم المكوّن UIView أو Android.View التحديثات. تنفيذ الرسوم المتحركة باستخدام Animated API و Native Driver تشرف مشغلات جافاسكريبت منذ ظهور Animated API على عملية تنفيذ التحريك، ولكن هذه الطريقة سببت تباطؤًا في عملية التحريك بسبب قلة عدد الإطارات المعروضة في الثانية ويعود السبب في ذلك أن العمل كله يتم في الخيط (Thread) الخاص بجافاسكريبت. لتجاوز هذه المشكلة أضيفت مشغلات خاصة بإطار العمل React Native وبالتالي أصبحت قادرة على تنفيذ عملية التحريك كاملةً، إطارًا بعد الآخر، ضمن React Native. عند استخدام المشغل الخاصة بإطار العمل React native مع Animated API فإن ذلك يسمح بتحديث العرض مباشرة دون الحاجة إلى إعادة حساب القيم مرة أخرى في جافاسكربت. لاستخدام المشغّل الخاص بإطار العمل React يجب تعيين القيمة true إلى الخاصيّة useNativeDriver أثناء ضبط التحريكات. useNativeDriver: true استخدام PanResponder للتعامل مع لمس الشاشة في React Native يمكن لواجهة التطبيقات Animated API تنفيذ غالبية الأعمال الروتينية عند عمل التحريكات في React Native، إلّا أنها محدودة في التعامل مع لمس الشاشة للتفاعل مع التحريكات، فهي غير قادرة على التجاوب مع الحركات التي تحدث خارج المكوّن ScrollView. رغم أنه يمكن تنفيذ أشياء كثيرة عن طريق مكوّن ScrollView بسيط، إلّا أن التطبيقات على الأجهزة الجوّالة تبقى ناقصة بدون لمس الشاشة التي يستعملها المستخدم للتفاعل مع التحريكات عن طريق التمرير أو التدوير على سبيل المثال. يمكن التعامل مع حركات اللمس في React Native بسلاسة باستخدام PanResponder مع Animated API. يدمج PanResponder لمسات عدّة في حركة خاصّة، ويجعل لمسة واحدة تتجاوب مع لمسات إضافية لكي تعمل حركات اليد بسلاسة. افتراضيًّا، يتمثّل PanResponder في مقبض InteractionManager يمنع الأحداث العاملة على خيط جافاسكريبت من تعطيل حركات اللمس. تحسين زمن التشغيل لانتقالات التصفح البطيئة عادةً يحتاج أي تحريك يستدعي الانتقال من شاشة إلى أخرى في React Native إلى استخدام مكوّنات التصفّح. تُستخدَم مكوّنات التصفّح مثل React Navigation عادةً للانتقالات أثناء التصفح. تحدث الانتقالات أثناء التصفّح في React Native عادةً في خيوط جافاسكريبت، ممّا قد يتسبّب في انتقالات بطيئة في الأجهزة الرخيصة أو ذات الذاكرة المحدودة (أجهزة أندرويد أساسًا إذ أن الأجهزة العاملة بنظام iOS تتعامل مع الأمر بفاعلية أكبر). تحدث انتقالات التصفح البطيئة عادة عندما يحاول React Native تصيير شاشة جديدة في حين أنه ما زالت تُنفَّذ تحريكات في الخلفية. لتفادي حالات مثل تلك المذكورة أعلاه، يسمح مدير التفاعلات (InteractionManager) بجدولة الأنشطة التي تأخذ وقتًا في التنفيذ بجدولتها بعد تنفيذ تحريك أو تفاعل في خيط جافاسكريبت. تحريك التخطيطات Layouts تعمل واجهة التطبيقات البرمجية LayoutAnimation على تحريك الشاشة تلقائيًّا إلى الوضعية الموالية عندما يحدث التخطيط (Layout) الموالي. تُنفَّذ واجهة التطبيقات تلك ضمن خيط واجهة المستخدم (UI Thread) وهو ما يجعلها ذات أداء عال. تُطبَّق التحريكات المعدَّة عن طريق LayoutAnimation، فور استدعائها، على كل المكوّنات؛ بخلاف واجهة Animated التي يمكنك عن طريقها التحكّم في القيم المخصوصة بتحريكها. يمكن لواجهة LayoutAnimation تحريك كل شيء في التصيير القادم، لذا يجب استدعاؤها قبل استدعاء setState. يضمن إعداد تحريك للتخطيط قبل استدعاء setState تحريكات سلسة في خيط Native ويحول دون تأثر التحريكات بتنفيذ شفرة برمجية عند إطلاق دور setState أخرى (وهو ما يتسبّب - في الظروف العادية - في تهديد تحريكات تطبيقك). توجد طريقة أخرى لاستخدام LayoutAnimation وتتمثّل في استدعائها داخل التابع WillReceiveProps. استدع بكل بساطة التابع LayoutAnimation.configureNext مع تمرير الوسائط المناسبة لإعداد التحريك، كما هو موضَّح أدناه: LayoutAnimation.configureNext(animationConfiguration, callbackCompletionMethod); this.setState({ stateToChange: newStateValue }); لا تدعم واجهة LayoutAnimation سوى خاصيّتين هما الشفافية (Opacity) وقابلية التكيّف (Scalability). تتعرّف الواجهة على العروض (Views) باستخدام مفاتيحها الفريدة وحساب وضعيّتها المتوقّعة. علاوة على ذلك، تحرّك تغيّرات الإطارات ما دام العرض يحتفظ بالمفتاح نفسه بين تغيّر الحالات. تعمل التحريكات المطبَّقة بواسطة LayoutAnimation ضمن الخيط الأصلي لواجهة المستخدم في نظام التشغيل، وهو أمر جيّد من ناحية الأداء، إلّا أنه يمثّل تحديًّا إذا دعت الحاجة لتحريك كل الخاصيّات بين حالات الإطارات. خاتمة لا يدخل هذا المقال في تفاصيل التحريكات في React Native ويكتفي بأمور سطحية، إلّا أن هناك قاعدة أساسية عند التعامل مع إطار العمل React Native وهي استخدام واجهة التطبيقات البرمجية Animated API ما دام ذلك ممكنا. بالنسبة لحركات اللمس، استخدم PanResponder بجانب Animated API. يمكن التغلّب على الكثير من المشاكل التي تواجهها بالاستفادة من مشغّل Native driver إلى جانب واجهة Animated. إنْ استمرّت مشاكل الأداء رغم ذلك فالزم LayoutAnimation. ترجمة - وبتصرّف - للمقال Getting started with React Native animations لصاحبه Rakshit Soral.
×
×
  • أضف...