اختبار تطبيقنا وتوسيعه
الآن بعد أن أرسينا أساساً جيداً لمشروعنا، حان وقت البدء بتوسيعه. في هذا القسم يمكنك أن توظّف كل معرفتك بـReact Native التي اكتسبتها حتى الآن. وإلى جانب توسيع تطبيقنا، سنغطّي بعض المجالات الجديدة مثل الاختبار، وموارد إضافية.
اختبار تطبيقات React Native
للبدء باختبار شيفرة من أي نوع، أول ما نحتاجه هو إطار عمل للاختبار يمكننا استخدامه لتشغيل مجموعة من حالات الاختبار وفحص نتائجها. لاختبار تطبيق JavaScript، يُعدّ Jest مرشحاً شائعاً لهذا النوع من أطر الاختبار. ولاختبار تطبيق React Native قائم على Expo باستخدام Jest، يوفّر Expo مجموعة من إعدادات Jest على شكل إعداد مسبق jest-expo. لنبدأ بتثبيت الحزم:
npx expo install jest-expo jest @types/jest --dev
لاستخدام الإعداد المسبق jest-expo في Jest، نحتاج إلى إضافة إعداد Jest التالي إلى ملف package.json إلى جانب سكربت test:
{
// ...
"scripts": {
// سكربتات أخرى...
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|react-router-native)"
]
}
// ...
}
نحتاج أيضاً إلى قليل من الإعداد حتى يتمكن VS Code من اقتراح المطابِقات (matchers) المناسبة لكلمة expect مثلاً. ويمكن فعل ذلك بإنشاء ملف jsconfig.json في جذر المشروع بالمحتوى التالي:
{
"compilerOptions": {
"checkJs": false,
"types": ["jest"]
}
}
لاستخدام ESLint في ملفات اختبار Jest، نحتاج أيضاً إلى إضافة eslint-plugin-jest الخاصة بـESLint. لنثبّتها:
npm install eslint-plugin-jest --save-dev
لاستخدام إضافة eslint-plugin-jest، نحتاج إلى تفعيلها في ملف eslint.config.js:
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
const pluginJest = require('eslint-plugin-jest');
module.exports = defineConfig([
expoConfig,
{
ignores: ['dist/*'],
},
{
files: ['**/*.test.js', '**/*.spec.js'],
...pluginJest.configs['flat/recommended'],
},
]);
للتأكد من أن الإعداد يعمل، أنشئ مجلد tests داخل مجلد src وأنشئ في المجلد الذي أنشأته ملفاً باسم example.test.js. وأضف في ذلك الملف هذا الاختبار البسيط:
describe('Example', () => {
it('works', () => {
expect(1).toBe(1);
});
});
الآن، لنشغّل اختبارنا النموذجي بتنفيذ npm test. ينبغي أن تشير مخرجات الأمر إلى أن الاختبار الموجود في ملف src/tests/example.test.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. لاحظ أنه لكي يعثر Jest على ملفات اختبارك، عليك إما وضعها في مجلد tests، أو استخدام اللاحقة .test أو .spec، أو ضبط الأنماط العامة يدوياً.
اختبار المكوّنات
الآن بعد أن نجحنا في إعداد Jest وتشغيل اختبار بسيط جداً، حان وقت معرفة كيفية اختبار المكوّنات. وكما نعلم، يتطلب اختبار المكوّنات وسيلة لتسلسل مخرجات عرض المكوّن ومحاكاة إطلاق أحداث مختلفة الأنواع، مثل الضغط على زر. ولهذه الأغراض توجد عائلة Testing Library التي توفّر مكتبات لاختبار مكوّنات واجهة المستخدم على منصات مختلفة. وتتشارك هذه المكتبات جميعها واجهة برمجية مشابهة لاختبار مكوّنات واجهة المستخدم بطريقة محورها المستخدم.
في الجزء 5 تعرفنا على إحدى هذه المكتبات، وهي React Testing Library. ولسوء الحظ، هذه المكتبة مناسبة فقط لاختبار تطبيقات React للويب. ولحسن الحظ، يوجد نظير لهذه المكتبة خاص بـReact Native، وهو React Native Testing Library. هذه هي المكتبة التي سنستخدمها أثناء اختبار مكوّنات تطبيقنا بـReact Native. والخبر السار أن هاتين المكتبتين تتشاركان واجهة برمجية شديدة التشابه، لذا ليست هناك مفاهيم جديدة كثيرة يجب تعلمها. لنثبّت مكتبة @testing-library/react-native في مشروعنا:
npx expo install @testing-library/react-native --dev
ملاحظة: إذا فشل التثبيت بسبب مشكلات في الاعتماديات النظيرة (peer dependencies)، جرّب تثبيت مكتبة react-test-renderer صراحةً مع الراية
--legacy-peer-deps:
npm install --save-dev --legacy-peer-deps --save-exact react-test-renderer@19.2.0 @testing-library/react-nativeتأكد من أن إصدار react-test-renderer يطابق إصدار React في المشروع في أمر
npm installأعلاه. يمكنك التحقق من إصدار React بتنفيذnpm list react --depth=0.
المفهومان الرئيسيان في React Native Testing Library هما الاستعلامات (queries) وإطلاق الأحداث. تُستخدم الاستعلامات لاستخراج مجموعة من العُقد من المكوّن الذي يُعرض باستخدام دالة render. وتكون الاستعلامات مفيدة في الاختبارات التي نتوقع فيها مثلاً وجود نص ما، مثل اسم مستودع، في المكوّن المعروض. وإليك مثالاً على استخدام استعلام ByText للتحقق من أن عنصر Text في المكوّن يحوي المحتوى النصي الصحيح:
import { Text, View } from 'react-native';
import { render, screen } 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', () => {
render(<Greeting name="Kalle" />);
screen.debug();
expect(screen.getByText('Hello Kalle!')).toBeDefined();
});
});
تستخدم الاختبارات الكائن screen لإجراء الاستعلامات على المكوّن المعروض.
نحصل على عقدة Text التي تحوي نصاً معيناً باستخدام الدالة getByText. ويُستخدم مطابِق Jest toBeDefined للتأكد من أن الاستعلام قد وجد العنصر.
يحتوي توثيق React Native Testing Library على إرشادات جيدة حول كيفية الاستعلام عن أنواع مختلفة من العناصر. وهناك دليل آخر يستحق القراءة، وهو مقال Kent C. Dodds بعنوان Making your UI tests resilient to change.
كما يمتلك الكائن screen دالة مساعدة هي debug تطبع شجرة React المعروضة بصيغة سهلة القراءة. استخدمها إن لم تكن متأكداً من شكل شجرة React التي تعرضها دالة render.
للاطلاع على جميع الاستعلامات المتاحة، راجع توثيق React Native Testing Library. ويمكن العثور على القائمة الكاملة للمطابِقات المتاحة الخاصة بـReact Native في توثيق React Native Testing Library. بينما يحتوي توثيق Jest على كل مطابِق عام في Jest.
المفهوم الثاني المهم جداً في React Native Testing Library هو إطلاق الأحداث. يمكننا إطلاق حدث في عقدة معيّنة باستخدام دوال الكائن fireEvent. وهذا مفيد مثلاً في كتابة نص في حقل إدخال نصي أو الضغط على زر. وإليك مثالاً على كيفية اختبار إرسال نموذج بسيط:
import { useState } from 'react';
import { Text, TextInput, Pressable, View } from 'react-native';
import { render, fireEvent, screen } 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();
render(<Form onSubmit={onSubmit} />);
fireEvent.changeText(screen.getByPlaceholderText('Username'), 'kalle');
fireEvent.changeText(screen.getByPlaceholderText('Password'), 'password');
fireEvent.press(screen.getByText('Submit'));
expect(onSubmit).toHaveBeenCalledTimes(1);
// onSubmit.mock.calls[0][0] يحتوي الوسيط الأول للاستدعاء الأول
expect(onSubmit.mock.calls[0][0]).toEqual({
username: 'kalle',
password: 'password',
});
});
});
في هذا الاختبار، نريد اختبار أنه بعد ملء حقول النموذج باستخدام الدالة fireEvent.changeText والضغط على زر الإرسال باستخدام الدالة fireEvent.press، تُستدعى دالة النداء العكسي onSubmit بشكل صحيح. وللفحص مما إذا كانت دالة onSubmit قد استُدعيت وبأي وسائط، يمكننا استخدام دالة وهمية (mock function). الدوال الوهمية دوال ذات سلوك مبرمج مسبقاً مثل قيمة إرجاع محددة. وإضافة إلى ذلك، يمكننا إنشاء توقعات للدوال الوهمية مثل «توقّع أن تكون الدالة الوهمية قد استُدعيت مرة واحدة». ويمكن العثور على القائمة الكاملة للتوقعات المتاحة في توثيق expect في Jest.
قبل التعمق أكثر في عالم اختبار تطبيقات React Native، جرّب هذه الأمثلة بإضافة ملف اختبار في مجلد tests الذي أنشأناه سابقاً.
التعامل مع الاعتماديات في الاختبارات
المكوّنات في الأمثلة السابقة سهلة الاختبار إلى حد كبير لأنها نقية إلى حدٍّ ما. فالمكوّنات النقية لا تعتمد على آثار جانبية مثل طلبات الشبكة أو استخدام واجهة برمجية أصلية مثل AsyncStorage. ومكوّن Form أقل نقاءً بكثير من مكوّن Greeting لأن تغيّرات حالته يمكن اعتبارها أثراً جانبياً. ومع ذلك، ليس اختباره صعباً للغاية.
بعد ذلك، لنلقِ نظرة على استراتيجية لاختبار المكوّنات ذات الآثار الجانبية. لنأخذ مكوّن RepositoryList من تطبيقنا كمثال. في الوقت الحالي، للمكوّن أثر جانبي واحد هو استعلام GraphQL لجلب المستودعات المُقيَّمة. والتنفيذ الحالي لمكوّن RepositoryList يبدو شيئاً كهذا:
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 على النحو الموضح في توثيق Apollo Client. وهناك طريقة أبسط هي افتراض أن خطاف useRepositories يعمل كما هو مقصود (ويُفضّل ذلك بعد اختباره) واستخراج الشيفرة «النقية» من المكوّن إلى مكوّن آخر مثل مكوّن 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;
الآن، لم يعد مكوّن RepositoryList يحتوي إلا على الآثار الجانبية وتنفيذه بسيط جداً. ويمكننا اختبار مكوّن RepositoryListContainer بتزويده ببيانات مستودعات مقسّمة إلى صفحات عبر خاصية repositories والتحقق من أن المحتوى المعروض يحوي المعلومات الصحيحة.
17. اختبار قائمة المستودعات المُقيَّمة
18. اختبار نموذج تسجيل الدخول
توسيع تطبيقنا
حان وقت الاستفادة القصوى من كل ما تعلمناه حتى الآن والبدء بتوسيع تطبيقنا. ما زال تطبيقنا يفتقر إلى بعض الميزات المهمة مثل تقييم مستودع وتسجيل مستخدم. وستركز التمارين القادمة على هذه الميزات الأساسية.
19. عرض المستودع المفرد
20. قائمة تقييمات المستودع
21. نموذج التقييم
22. نموذج إنشاء الحساب
23. ترتيب قائمة المستودعات المُقيَّمة
24. تصفية قائمة المستودعات المُقيَّمة
25. عرض تقييمات المستخدم
26. إجراءات التقييم
عندما تعيد واجهة برمجية قائمة مرتبة من العناصر من مجموعة ما، فإنها تعيد عادةً مجموعة فرعية من المجموعة الكاملة للعناصر لتقليل عرض النطاق المطلوب وتخفيف استهلاك الذاكرة في تطبيقات العميل. ويمكن تقييد المجموعة الفرعية المرغوبة من العناصر بحيث يستطيع العميل أن يطلب مثلاً أول عشرين عنصراً في القائمة بعد فهرس معيّن. وتُعرف هذه التقنية عموماً باسم ترقيم الصفحات. وعندما يمكن طلب عناصر بعد عنصر معيّن يحدده مؤشر (cursor)، فإننا نتحدث عن ترقيم الصفحات المعتمد على المؤشر.
إذن المؤشر ليس سوى تمثيل متسلسل لعنصر في قائمة مرتبة. لنلقِ نظرة على المستودعات المقسّمة إلى صفحات التي يعيدها استعلام 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 من Relay، التي أصبحت مواصفة ترقيم صفحات شائعة إلى حد كبير واعتُمدت على نطاق واسع مثلاً في واجهة GitHub البرمجية لـGraphQL. في كائن النتيجة، لدينا مصفوفة edges تحتوي عناصر لها خاصيتا node وcursor. وكما نعلم، يحتوي node على المستودع نفسه. أما cursor فهو تمثيل مرمّز بترميز Base64 للعقدة. وفي هذه الحالة، يحتوي على معرّف المستودع وتاريخ إنشاء المستودع على شكل طابع زمني. وهذه هي المعلومات التي نحتاجها للإشارة إلى العنصر عندما تُرتَّب العناصر حسب وقت إنشاء المستودع. ويحتوي pageInfo على معلومات مثل مؤشر العنصر الأول والأخير في المصفوفة.
لنفترض أننا نريد جلب المجموعة التالية من العناصر بعد العنصر الأخير في المجموعة الحالية، وهو المستودع «zeit/swr». يمكننا ضبط الوسيطة after في الاستعلام على قيمة endCursor هكذا:
{
repositories(first: 2, after: "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=") {
totalCount
edges {
node {
id
fullName
createdAt
}
cursor
}
pageInfo {
endCursor
startCursor
hasNextPage
}
}
}
الآن بعد أن حصلنا على العنصرين التاليين، يمكننا مواصلة فعل ذلك حتى تأخذ hasNextPage القيمة false، أي أننا وصلنا إلى نهاية القائمة. للتعمق أكثر في ترقيم الصفحات المعتمد على المؤشر، اقرأ مقال Shopify بعنوان Pagination with Relative Cursors. فهو يقدّم تفاصيل رائعة عن التنفيذ نفسه والفوائد مقارنةً بترقيم الصفحات التقليدي المعتمد على الفهرس.
التمرير اللانهائي
تُنفَّذ القوائم القابلة للتمرير عمودياً في تطبيقات الجوال وسطح المكتب عادةً باستخدام تقنية تسمى التمرير اللانهائي. ومبدأ التمرير اللانهائي بسيط جداً:
- جلب المجموعة الأولية من العناصر
- عندما يصل المستخدم إلى العنصر الأخير، جلب المجموعة التالية من العناصر بعد العنصر الأخير
وتُكرَّر الخطوة الثانية حتى يتعب المستخدم من التمرير أو يُتجاوز حدّ معيّن للتمرير. ويشير اسم «التمرير اللانهائي» إلى الطريقة التي تبدو بها القائمة لانهائية - إذ يمكن للمستخدم أن يواصل التمرير بينما تستمر عناصر جديدة في الظهور في القائمة.
لنلقِ نظرة على كيفية عمل ذلك عملياً باستخدام خطاف useQuery في Apollo Client. لدى Apollo Client توثيق رائع حول تنفيذ ترقيم الصفحات المعتمد على المؤشر. لننفّذ التمرير اللانهائي لقائمة المستودعات المُقيَّمة كمثال.
أولاً، نحتاج إلى معرفة متى وصل المستخدم إلى نهاية القائمة. ولحسن الحظ، يمتلك مكوّن FlatList خاصية onEndReached تستدعي الدالة المزوّدة بمجرد أن يمرّر المستخدم إلى العنصر الأخير في القائمة. ويمكنك تغيير مدى مبكر استدعاء دالة النداء العكسي onEndReached باستخدام خاصية onEndReachedThreshold. عدّل مكوّن FlatList داخل مكوّن RepositoryList بحيث يسجّل رسالة في الطرفية بمجرد الوصول إلى نهاية القائمة:
export const RepositoryListContainer = ({
repositories,
onEndReached,
/* ... */,
}) => {
const repositoryNodes = repositories
? repositories.edges.map((edge) => edge.node)
: [];
return (
<FlatList
data={repositoryNodes}
// ...
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
);
};
const RepositoryList = () => {
// ...
const { repositories } = useRepositories(/* ... */);
return (
<RepositoryListContainer
repositories={repositories}
onEndReached={() => console.log('End of the list reached');}
// ...
/>
);
};
export default RepositoryList;
جرّب التمرير إلى نهاية قائمة المستودعات المُقيَّمة، وينبغي أن ترى الرسالة في السجلات.
بعد ذلك، نحتاج إلى جلب مزيد من المستودعات بمجرد الوصول إلى نهاية القائمة. ويمكن تحقيق ذلك باستخدام دالة fetchMore التي يوفّرها خطاف useQuery. ولوصف كيفية دمج المستودعات الموجودة في الذاكرة المؤقتة مع المجموعة التالية من المستودعات لـApollo Client، يمكننا استخدام سياسة حقل. وبشكل عام، يمكن استخدام سياسات الحقول لتخصيص سلوك الذاكرة المؤقتة أثناء عمليات القراءة والكتابة باستخدام دالتي read وmerge.
لنضف سياسة حقل لاستعلام repositories في ملف apolloClient.js:
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { SetContextLink } from '@apollo/client/link/context';
import { relayStylePagination } from '@apollo/client/utilities';
const httpLink = new HttpLink({
uri: process.env.EXPO_PUBLIC_APOLLO_URI,
});
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
repositories: relayStylePagination(),
},
},
},
});
const createApolloClient = authStorage => {
const authLink = new SetContextLink(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 الفعلية بوسائط مناسبة حتى نتمكن من جلب المجموعة التالية من المستودعات:
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 كما هو موضح في أمثلة ترقيم الصفحات. وستحتاج أيضاً إلى تضمين الوسيطتين after وfirst في الاستعلام.
ستستدعي دالة handleFetchMore دالة fetchMore في Apollo Client إن كانت هناك عناصر أخرى يمكن جلبها، وهو ما تحدده خاصية hasNextPage. ونريد أيضاً منع جلب مزيد من العناصر إذا كانت عملية الجلب جارية بالفعل. وفي هذه الحالة، ستكون loading هي true. وفي دالة fetchMore نزوّد الاستعلام بمتغير after الذي يتلقى أحدث قيمة لـendCursor.
الخطوة الأخيرة هي تمرير دالة fetchMore كقيمة للخاصية onEndReached:
const RepositoryList = () => {
// ...
const { repositories, fetchMore } = useRepositories({
first: 5,
// ...
});
return (
<RepositoryListContainer
repositories={repositories}
onEndReached={fetchMore}
// ...
/>
);
};
export default RepositoryList;
استخدم قيمة صغيرة نسبياً للوسيطة first مثل 5 أثناء تجربة التمرير اللانهائي. وبهذه الطريقة لن تحتاج إلى تقييم عدد كبير جداً من المستودعات. وإذا كانت القائمة تحتوي مستودعات قليلة بحيث تكون نهاية القائمة قريبة بالفعل أو مرئية، فقد تُستدعى fetchMore فوراً عند تحميل العرض أو قد لا تُطلق حتى يمرّر المستخدم. وإذا سبّب ذلك مشكلات أثناء الاختبار، يمكنك تجاوز هذه المسألة بزيادة قيمة الوسيطة first. وبمجرد أن تطمئن إلى أن التمرير اللانهائي يعمل، لا تتردد في استخدام قيمة أكبر للوسيطة first.
27. اختياري: التمرير اللانهائي لقائمة تقييمات المستودع
مشاركة التطبيق برمز QR
حتى الآن، طوّرنا التطبيق محلياً على جهازنا فقط واختبرناه على هاتفنا أو محاكينا. لكن ماذا لو أردنا أن نتيح للآخرين اختبار التطبيق أيضاً، بحيث يستطيع شخص آخر تجربته على هاتفه الخاص؟
يوفّر Expo حلاً جاهزاً لذلك. Expo Application Services (EAS) هي خدمة سحابية من Expo توفّر أدوات لبناء التطبيقات وتحديثها وتوزيعها. وEAS Update خدمة مجانية تتيح لك نشر تطبيقك على خوادم Expo. ويُنشأ رمز QR فريد لكل إصدار منشور، وبه يمكن لأي شخص فتح التطبيق باستخدام تطبيق Expo Go.
لننشر الآن النسخة النهائية من التطبيق على خوادم Expo. ستحتاج إلى حساب Expo لاستخدام الخدمة. وإن لم يكن لديك حساب، أنشئ حساباً على https://expo.dev/signup.
سجّل الدخول إلى حساب Expo من سطر الأوامر:
npx eas-cli@latest login
بعد ذلك، هيّئ المستودع كمشروع EAS:
npx eas-cli@latest update:configure
يربط أمر update:configure المشروع بخدمة EAS من Expo ويضيف الإعداد المطلوب إلى ملف app.json.
اكتمل الإعداد الأولي الآن. غير أن هناك تفصيلاً واحداً لا يزال يحتاج إلى انتباه. لكي يعمل التطبيق، يحتاج إلى Rate Repository API الذي يعمل كواجهة خلفية وقاعدة بيانات للتطبيق. ولأنه تطبيق منفصل تماماً عن واجهة React Native الأمامية، فسيحتاج عادةً إلى نشره في مكان ما بشكل منفصل.
للتمارين النهائية في هذا الجزء، توفّر الدورة واجهة Rate Repository API منشورة مسبقاً على https://rate-repository-api-2.ext.ocp-prod-0.k8s.it.helsinki.fi. وهذه هي واجهة Rate Repository API نفسها التي استخدمناها محلياً في التمارين السابقة. فوظائف الواجهة كما هي، وقد زُوّدت قاعدة البيانات ببيانات أولية تضم بعض المستخدمين والمستودعات وفقاً للتعليمات في README الخاصة بـ Rate Repository API.
تعمل الواجهة الآن في وضع الإنتاج، أي أن متغير البيئة NODE_ENV مضبوط على production. ولذلك بعض النتائج العملية. فمثلاً لا يعرف Apollo Sandbox مخطط الواجهة تلقائياً. كما تُعاد قاعدة البيانات التي تستخدمها Rate Repository API دورياً دون إنذار مسبق، لذا ينبغي أثناء التطوير المحلي استخدام نسخة محلية من Rate Repository API.
أنشئ متغير بيئة باسم EXPO_PUBLIC_APOLLO_URI لمشروع EAS واضبط قيمته على عنوان URL الخاص بواجهة Rate Repository API المنشورة مسبقاً باستخدام الأمر التالي:
npx eas-cli@latest env:create --name EXPO_PUBLIC_APOLLO_URI --value https://rate-repository-api-2.ext.ocp-prod-0.k8s.it.helsinki.fi/ --environment preview --visibility plaintext
- يُنشأ متغير البيئة في بيئة تسمى
preview، وهي مخصّصة تحديداً لهذا النوع من اختبار التطبيقات. - تُضبط رؤية متغير البيئة على
plaintext، لأن القيمة ليست سرية بشكل خاص.
نحن الآن جاهزون لنشر التطبيق على خوادم Expo. ويتم النشر بالأمر التالي:
npx eas-cli@latest update --branch main --environment preview --message "The first deploy"
- يتيح EAS Update تجميع التحديثات في فروع مختلفة. وفي حالتنا، يحدّد الخيار
--branchنشر التحديث في فرع يسمىmain. - بفضل الخيار
--environmentيحصل تطبيقنا على صلاحية الوصول إلى بيئةpreviewوإلى متغير البيئةEXPO_PUBLIC_APOLLO_URIالذي عرّفناه سابقاً. - يضبط الخيار
--messageرسالة اعتباطية للتحديث حتى يمكن تمييزه عن التحديثات الأخرى.
عند نشر التحديث، سيطبع سطر الأوامر أخيراً رابطاً إلى صفحة لوحة EAS الخاصة بعملية النشر. وتضم اللوحة زر Preview يعرض رمز QR يؤدي إلى التطبيق. وعند مسح رمز QR بتطبيق Expo Go، ينبغي أن يفتح التطبيق على الهاتف وأن يعمل مع واجهة Rate Repository API الخارجية. بعبارة أخرى، ينبغي أن تُحمّل قائمة المستودعات، وأن يكون ممكناً إنشاء مستخدمين جدد في التطبيق وتسجيل الدخول ببيانات اعتماد موجودة، وهكذا.
إذا أردت إجراء تغييرات على التطبيق، يكفي تشغيل الأمر npx eas-cli@latest update مرة أخرى لنشر تحديث جديد. لاحظ أن كل تحديث منشور يحصل على رمز QR فريد خاص به.
يُسهّل استخدام EAS Update عرض تقدّم تطوير التطبيق على الآخرين، إذ لا حاجة إلى مشاركة الشيفرة المصدرية أو بناء حزم التثبيت أو نشر أي شيء في متجر تطبيقات. وتبقى عملية النشر بسيطة مع إتاحة اختبار التطبيق على أجهزة حقيقية.
28. نشر التطبيق عبر EAS Publish
موارد إضافية
بينما نقترب من نهاية هذا الجزء، لنأخذ لحظة للنظر في بعض الموارد الإضافية المتعلقة بـReact Native. Awesome React Native قائمة منتقاة شاملة للغاية من موارد React Native مثل المكتبات والدروس والمقالات. ولأن القائمة طويلة بشكل مُستفيض، لنلقِ نظرة أقرب على بعض أبرز معالمها
React Native Paper
Paper مجموعة من المكوّنات القابلة للتخصيص والجاهزة للإنتاج لـReact Native، تتبع إرشادات التصميم Material Design من Google.
React Native Paper هي لـReact Native ما هو Material-UI لتطبيقات React للويب. فهي تقدّم تشكيلة واسعة من مكوّنات واجهة المستخدم عالية الجودة، ودعماً لـالسمات المخصّصة وإعداداً بسيطاً إلى حد كبير لتطبيقات React Native القائمة على Expo.
Styled-components
باستخدام القوالب النصية الموسومة (tagged template literals) وقوة CSS، تتيح لك styled-components كتابة شيفرة CSS فعلية لتنسيق مكوّناتك. كما تُزيل الربط بين المكوّنات والأنماط – فاستخدام المكوّنات كبنية تنسيق منخفضة المستوى لم يكن أسهل من ذلك!
Styled-components مكتبة لتنسيق مكوّنات React باستخدام تقنية CSS-in-JS. وفي React Native اعتدنا بالفعل تعريف أنماط المكوّن ككائن JavaScript، لذا فإن CSS-in-JS ليس أرضاً مجهولة تماماً. غير أن أسلوب styled-components مختلف تماماً عن استخدام الدالة StyleSheet.create والخاصية style.
في styled-components تُعرَّف أنماط المكوّنات مع المكوّن باستخدام ميزة تسمى القالب النصي الموسوم أو كائن JavaScript عادي. وتتيح styled-components تعريف خصائص نمط جديدة للمكوّن بناءً على props الخاصة به وقت التشغيل. وهذا يفتح إمكانات كثيرة، مثل التبديل السلس بين سمة فاتحة وأخرى داكنة. كما أن لديها دعماً كاملاً للسمات. وإليك مثالاً على إنشاء مكوّن Text بتنويعات نمطية بناءً على props:
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 في أسماء الخصائص والوحدات في قيم الخصائص. غير أن الوحدات ليس لها أي تأثير لأن قيم الخصائص تُعامل داخلياً بلا وحدات. لمزيد من المعلومات عن styled-components، توجّه إلى التوثيق.
React-spring
react-spring مكتبة تحريك قائمة على فيزياء النابض (spring-physics) ينبغي أن تغطي معظم احتياجاتك من التحريك المتعلقة بواجهة المستخدم. وهي تمنحك أدوات مرنة بما يكفي لتحويل أفكارك بثقة إلى واجهات متحركة.
React-spring مكتبة توفّر واجهة برمجية نظيفة لتحريك مكوّنات React Native.
React Navigation
التوجيه والتنقل لتطبيقات React Native
React Navigation مكتبة توجيه لـReact Native. وهي تتشارك بعض أوجه الشبه مع مكتبة React Router التي استخدمناها خلال هذا الجزء والأجزاء السابقة. غير أنها، بخلاف React Router، تقدّم ميزات أصلية أكثر مثل الإيماءات والحركات الأصلية للانتقال بين العروض.
كلمة ختامية
هذا كل شيء، تطبيقنا جاهز. عمل رائع! لقد تعلمنا مفاهيم جديدة كثيرة خلال رحلتنا مثل إعداد تطبيق React Native باستخدام Expo، واستخدام المكوّنات الأساسية في React Native وإضافة أنماط إليها، والتواصل مع الخادم، واختبار تطبيقات React Native.
القطعة الأخيرة في اللغز ستكون نشر التطبيق في Apple App Store وGoogle Play Store. وهذا اختياري تماماً. وإذا قررت تجربته، فستحتاج أولاً إلى إنشاء بناءات iOS أو Android باتباع توثيق Expo. ثم يمكنك رفع هذه البناءات إلى Apple App Store أو Google Play Store. ولدى Expo توثيق لهذا أيضاً.
29. مستودع GitHub الخاص بك