إنشاء تطبيق ويب باستخدام React و Redux و Sanity.io
نشرت: 2022-03-10وضع التطور السريع للمنصات الرقمية قيودًا خطيرة على أنظمة إدارة المحتوى التقليدية مثل Wordpress. هذه المنصات مقترنة وغير مرنة وتركز على المشروع بدلاً من المنتج. لحسن الحظ ، تم تطوير العديد من أنظمة إدارة المحتوى مقطوعة الرأس لمواجهة هذه التحديات وغيرها الكثير.
على عكس نظام إدارة المحتوى التقليدي ، يمكن استخدام نظام إدارة المحتوى بدون رأس ، والذي يمكن وصفه بأنه برنامج كخدمة (SaaS) ، لتطوير مواقع الويب وتطبيقات الأجهزة المحمولة وشاشات العرض الرقمية وغير ذلك الكثير. يمكن استخدامها على منصات لا حدود لها. إذا كنت تبحث عن CMS مستقل عن النظام الأساسي ، ومطور أولاً ، ويقدم دعمًا عبر الأنظمة الأساسية ، فلن تحتاج إلى البحث بعيدًا عن CMS مقطوعة الرأس.
إن نظام CMS مقطوع الرأس هو ببساطة نظام CMS بدون رأس. يشير head
هنا إلى الواجهة الأمامية أو طبقة العرض بينما يشير body
إلى الواجهة الخلفية أو مستودع المحتوى. هذا يقدم الكثير من الفوائد المثيرة للاهتمام. على سبيل المثال ، يسمح للمطور باختيار أي واجهة أمامية من اختياره ويمكنك أيضًا تصميم طبقة العرض التقديمي كما تريد.
هناك الكثير من CMS مقطوعة الرأس ، وبعض أشهرها تشمل Strapi ، و Contentful ، و Contentstack ، و Sanity ، و Butter CMS ، و Prismic ، و Storyblok ، و Directus ، وما إلى ذلك. على سبيل المثال ، CMS مثل Sanity و Strapi و Contentful و Storyblok مجانية للمشاريع الصغيرة.
تعتمد أنظمة CMS مقطوعة الرأس هذه على مجموعات تقنية مختلفة أيضًا. بينما يعتمد Sanity.io على React.js ، يستند Storyblok إلى Vue.js. بصفتي مطورًا لـ React ، فإن هذا هو السبب الرئيسي الذي جعلني سريعًا في اختيار الاهتمام بـ Sanity. ومع ذلك ، نظرًا لكونها CMS مقطوعة الرأس ، يمكن توصيل كل من هذه الأنظمة الأساسية بأي واجهة أمامية ، سواء كانت Angular أو Vue أو React.
كل من هذه CMS مقطوعة الرأس لديها خطط مجانية ومدفوعة على حد سواء والتي تمثل قفزة كبيرة في الأسعار. على الرغم من أن هذه الخطط المدفوعة توفر المزيد من الميزات ، إلا أنك لن ترغب في دفع كل هذا المبلغ مقابل مشروع صغير إلى متوسط الحجم. يحاول Sanity حل هذه المشكلة من خلال تقديم خيارات الدفع أولاً بأول. باستخدام هذه الخيارات ، ستتمكن من الدفع مقابل ما تستخدمه وتجنب ارتفاع الأسعار.
سبب آخر لاختيار Sanity.io هو لغتهم GROQ. بالنسبة لي ، تبرز Sanity من بين الحشود من خلال تقديم هذه الأداة. تعمل استعلامات الكائنات الرسومية العلائقية (GROQ) على تقليل وقت التطوير ، وتساعدك في الحصول على المحتوى الذي تحتاجه بالشكل الذي تحتاجه ، كما تساعد المطور أيضًا على إنشاء مستند بنموذج محتوى جديد دون تغيير التعليمات البرمجية.
علاوة على ذلك ، لا يقيد المطورون بلغة GROQ. يمكنك أيضًا استخدام fetch
أو حتى المحاور التقليدية والجلب في تطبيق axios
للاستعلام عن الواجهة الخلفية. مثل معظم أنظمة إدارة المحتوى بدون رؤوس أخرى ، فإن Sanity لديها وثائق شاملة تحتوي على نصائح مفيدة للبناء على النظام الأساسي.
ملاحظة: تتطلب هذه المقالة فهمًا أساسيًا لكل من React و Redux و CSS.
الشروع في العمل مع Sanity.io
لاستخدام Sanity في جهازك ، ستحتاج إلى تثبيت أداة Sanity CLI. بينما يمكن تثبيته محليًا على مشروعك ، فمن الأفضل تثبيته عالميًا لجعله في متناول أي تطبيقات مستقبلية.
للقيام بذلك ، أدخل الأوامر التالية في جهازك.
npm install -g @sanity/cli
تتيح العلامة -g
في الأمر أعلاه إمكانية التثبيت العام.
بعد ذلك ، نحتاج إلى تهيئة Sanity في تطبيقنا. على الرغم من أنه يمكن تثبيته كمشروع منفصل ، فمن الأفضل عادةً تثبيته داخل تطبيق الواجهة الأمامية (في هذه الحالة React).
في مدونتها ، شرحت Kapehe بالتفصيل كيفية دمج Sanity مع React. سيكون من المفيد الاطلاع على المقالة قبل متابعة هذا البرنامج التعليمي.
أدخل الأوامر التالية لتهيئة Sanity في تطبيق React الخاص بك.
sanity init
يصبح الأمر sanity
متاحًا لنا عندما نقوم بتثبيت أداة Sanity CLI. يمكنك عرض قائمة بأوامر Sanity المتاحة عن طريق كتابة sanity
sanity help
في جهازك الطرفي.
عند إعداد مشروعك أو تهيئته ، ستحتاج إلى اتباع المطالبات لتخصيصه. سيُطلب منك أيضًا إنشاء مجموعة بيانات ويمكنك حتى اختيار مجموعة البيانات المخصصة الخاصة بهم والمليئة بالبيانات. بالنسبة لتطبيق القائمة هذا ، سنستخدم مجموعة بيانات أفلام الخيال العلمي المخصصة من Sanity. هذا سيوفر لنا من إدخال البيانات بأنفسنا.
لعرض مجموعة البيانات الخاصة بك وتحريرها ، اضغط على دليل cd
الفرعي في الجهاز الطرفي وأدخل sanity start
. يعمل هذا عادةً على https://localhost:3333/
. قد يُطلب منك تسجيل الدخول للوصول إلى الواجهة (تأكد من تسجيل الدخول بنفس الحساب الذي استخدمته عند بدء المشروع). يتم عرض لقطة شاشة للبيئة أدناه.
التواصل ثنائي الاتجاه
يحتاج العقل وفاعلية إلى التواصل مع بعضهما البعض من أجل تطبيق يعمل بكامل طاقته.
إعداد أصول CORS في مدير الصحة
سنقوم أولاً بتوصيل تطبيق React الخاص بنا بـ Sanity. للقيام بذلك ، قم بتسجيل الدخول إلى https://manage.sanity.io/
وحدد موقع CORS origins
ضمن API Settings
في علامة التبويب " Settings
". هنا ، ستحتاج إلى ربط أصل الواجهة الأمامية بخلفية Sanity. يعمل تطبيق React الخاص بنا على https://localhost:3000/
افتراضيًا ، لذلك نحتاج إلى إضافة ذلك إلى CORS.
هذا هو مبين في الشكل أدناه.
ربط العقل بالرد
يربط Sanity project ID
مشروع بكل مشروع تقوم بإنشائه. هذا المعرف مطلوب عند توصيله بتطبيق الواجهة الأمامية الخاص بك. يمكنك العثور على معرّف المشروع في مدير Sanity الخاص بك.
تتواصل الواجهة الخلفية مع React باستخدام مكتبة تُعرف باسم sanity client
. تحتاج إلى تثبيت هذه المكتبة في مشروع Sanity الخاص بك عن طريق إدخال الأوامر التالية.
npm install @sanity/client
قم بإنشاء ملف sanitySetup.js
(لا يهم اسم الملف) ، في مجلد src
الخاص بالمشروع وأدخل رموز React التالية لإعداد اتصال بين Sanity و React.
import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });
لقد مررنا useCdn
المشروع dataset name
المنطقي projectId
إلى مثيل عميل العقل الذي تم استيراده من @sanity/client
. يعمل هذا السحر ويربط تطبيقنا بالواجهة الخلفية.
الآن وقد أكملنا الاتصال ثنائي الاتجاه ، فلننتقل مباشرةً لبناء مشروعنا.
إعداد وتوصيل Redux بتطبيقنا
سنحتاج إلى بعض التبعيات للعمل مع Redux في تطبيق React الخاص بنا. افتح المحطة الطرفية في بيئة React الخاصة بك وأدخل أوامر bash التالية.
npm install redux react-redux redux-thunk
Redux هي مكتبة إدارة حالة عالمية يمكن استخدامها مع معظم أطر عمل ومكتبات الواجهة الأمامية مثل React. ومع ذلك ، فنحن بحاجة إلى أداة وسيطة react-redux
وإعادة الإرسال لتمكين الاتصال بين متجر Redux الخاص بنا وتطبيق React الخاص بنا. سيساعدنا Redux thunk على إرجاع دالة بدلاً من كائن عمل من Redux.
بينما يمكننا كتابة سير عمل Redux بالكامل في ملف واحد ، غالبًا ما يكون من الأفضل فصل مخاوفنا. لهذا ، سنقسم سير العمل لدينا إلى ثلاثة ملفات ، وهي actions
، reducers
، ثم store
. ومع ذلك ، نحتاج أيضًا إلى ملف منفصل لتخزين action types
، والمعروفة أيضًا باسم constants
.
انشاء المتجر
المخزن هو أهم ملف في Redux. يقوم بتنظيم وتعبئة الدول وشحنها إلى تطبيق React الخاص بنا.
هذا هو الإعداد الأولي لمتجر Redux الخاص بنا المطلوب لتوصيل سير عمل Redux الخاص بنا.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );
تأخذ وظيفة createStore
في هذا الملف ثلاث معاملات: reducer
(مطلوب) ، والحالة الأولية والمحسن (عادةً ما يكون برنامج وسيط ، في هذه الحالة ، thunk
الموفر من خلال applyMiddleware
). سيتم تخزين أدوات التخفيض الخاصة بنا في مجلد reducers
وسنقوم بدمجها وتصديرها في ملف index.js
في مجلد reducers
. هذا هو الملف الذي قمنا باستيراده في الكود أعلاه. سنعود إلى هذا الملف لاحقًا.
مقدمة إلى لغة GROQ في Sanity
يأخذ Sanity الاستعلام عن بيانات JSON خطوة إلى الأمام من خلال تقديم GROQ. GROQ لتقف على استعلامات كائن العلاقة بالرسم البياني. وفقًا لـ Sanity.io ، GROQ هي لغة استعلام تعريفية مصممة للاستعلام عن مجموعات من مستندات JSON التي لا تحتوي على مخطط إلى حد كبير.
يوفر Sanity أيضًا ملعب GROQ لمساعدة المطورين على التعرف على اللغة. ومع ذلك ، للوصول إلى الملعب ، تحتاج إلى تثبيت رؤية العقل . قم بتشغيل sanity install @sanity/vision
على جهازك الطرفي لتثبيته.
يحتوي GROQ على بناء جملة مماثل لـ GraphQL ولكنه أكثر تكثيفًا وأسهل في القراءة. علاوة على ذلك ، على عكس GraphQL ، يمكن استخدام GROQ للاستعلام عن بيانات JSON.
على سبيل المثال ، لاسترداد كل عنصر في مستند الفيلم الخاص بنا ، سنستخدم صيغة GROQ التالية.
*[_type == "movie"]
ومع ذلك ، إذا كنا نرغب في استرداد _ids
وأعضاء crewMembers
فقط في مستند الفيلم الخاص بنا. نحن بحاجة إلى تحديد تلك المجالات على النحو التالي.
`*[_type == 'movie']{ _id, crewMembers }
هنا ، استخدمنا *
لإخبار GROQ أننا نريد كل مستند من فيلم _type
. _type
هي سمة ضمن مجموعة الفيلم. يمكننا أيضًا إرجاع النوع كما فعلنا مع _id
وأعضاء crewMembers
على النحو التالي:
*[_type == 'movie']{ _id, _type, crewMembers }
سنعمل أكثر على GROQ من خلال تنفيذه في إجراءات Redux الخاصة بنا ولكن يمكنك التحقق من وثائق Sanity.io لـ GROQ لمعرفة المزيد عنها. توفر ورقة الغش لاستعلام GROQ الكثير من الأمثلة لمساعدتك على إتقان لغة الاستعلام.
إنشاء الثوابت
نحتاج إلى ثوابت لتتبع أنواع الإجراءات في كل مرحلة من مراحل سير عمل Redux. تساعد الثوابت في تحديد نوع الإجراء المرسل في كل نقطة زمنية. على سبيل المثال ، يمكننا تتبع وقت تحميل واجهة برمجة التطبيقات وتحميلها بالكامل وعند حدوث خطأ.
لا نحتاج بالضرورة إلى تعريف الثوابت في ملف منفصل ولكن من أجل البساطة والوضوح ، عادةً ما يكون هذا هو أفضل ممارسة في Redux.
حسب الاصطلاح ، يتم تعريف الثوابت في جافا سكريبت بأحرف كبيرة. سنتبع أفضل الممارسات هنا لتحديد ثوابتنا. فيما يلي مثال على ثابت للإشارة إلى طلبات نقل الفيلم.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";
هنا ، أنشأنا MOVIE_FETCH_REQUEST
ثابتًا يشير إلى نوع الإجراء MOVIE_FETCH_REQUEST
. يساعدنا هذا في استدعاء نوع الإجراء هذا بسهولة دون استخدام strings
وتجنب الأخطاء. قمنا أيضًا بتصدير الثابت ليكون متاحًا في أي مكان في مشروعنا.
وبالمثل ، يمكننا إنشاء ثوابت أخرى لجلب أنواع الإجراءات للإشارة إلى نجاح الطلب أو فشله. يوجد رمز كامل لـ movieConstants.js
في الكود أدناه.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";
هنا قمنا بتعريف عدة ثوابت لجلب فيلم أو قائمة أفلام وفرز وجلب الأفلام الأكثر شهرة. لاحظ أننا قمنا بتعيين ثوابت لتحديد وقت loading
الطلب successful
failed
.
وبالمثل ، يوجد ملف personConstants.js
بنا أدناه:
export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST"; export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS"; export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL"; export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST"; export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS"; export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL"; export const PERSONS_COUNT = "PERSONS_COUNT";
مثل movieConstants.js
، وضعنا قائمة بالثوابت لجلب شخص أو أشخاص. وضعنا أيضًا ثابتًا لعد الأشخاص. تتبع الثوابت الاتفاقية الموصوفة لـ movieConstants.js
وقمنا أيضًا بتصديرها لتكون في متناول أجزاء أخرى من تطبيقنا.
أخيرًا ، سنقوم بتنفيذ الوضع الفاتح والظلام في التطبيق ، وبالتالي لدينا ملف ثوابت آخر globalConstants.js
. دعونا نلقي نظرة عليه.
export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";
هنا نضع ثوابت لتحديد وقت إرسال الوضع الفاتح أو المظلم. تحدد SET_LIGHT_THEME
متى ينتقل المستخدم إلى سمة الضوء ويحدد SET_DARK_THEME
متى يتم تحديد السمة الداكنة. قمنا أيضًا بتصدير ثوابتنا كما هو موضح.
إعداد الإجراءات
وفقًا للاتفاقية ، يتم تخزين إجراءاتنا في مجلد منفصل. يتم تجميع الإجراءات وفقًا لأنواعها. على سبيل المثال ، يتم تخزين إجراءات الفيلم الخاصة بنا في movieActions.js
بينما يتم تخزين إجراءاتنا الشخصية في ملف personActions.js
.
لدينا أيضًا globalActions.js
السمة من الوضع الفاتح إلى الوضع المظلم.
لنجلب جميع الأفلام الموجودة في moviesActions.js
.
import sanityAPI from "../../sanitySetup"; import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster": poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } };
هل تتذكر عندما أنشأنا ملف sanitySetup.js
لربط React بخلفية Sanity؟ هنا ، قمنا باستيراد الإعداد لتمكيننا من الاستعلام عن الخلفية الصحية الخاصة بنا باستخدام GROQ. قمنا أيضًا باستيراد بعض الثوابت المصدرة من ملف movieConstants.js
في مجلد constants
.
بعد ذلك ، أنشأنا وظيفة الحركة fetchAllMovies
لجلب كل فيلم في مجموعتنا. تستخدم معظم تطبيقات axios
التقليدية المحاور أو fetch
لجلب البيانات من الواجهة الخلفية. ولكن بينما يمكننا استخدام أي منها هنا ، فإننا نستخدم GROQ
من Sanity. للدخول إلى وضع GROQ
، نحتاج إلى استدعاء وظيفة sanityAPI.fetch()
كما هو موضح في الكود أعلاه. هنا ، sanityAPI
هو اتصال React-Sanity الذي أنشأناه سابقًا. يؤدي هذا إلى إرجاع Promise
ولذا يجب استدعاؤه بشكل غير متزامن. لقد استخدمنا هنا بناء الجملة async-await
، ولكن يمكننا أيضًا استخدام .then
الجملة.
نظرًا لأننا نستخدم thunk
في تطبيقنا ، يمكننا إرجاع دالة بدلاً من كائن Action. ومع ذلك ، اخترنا تمرير بيان الإرجاع في سطر واحد.
const fetchAllMovies = () => async (dispatch) => { ... }
لاحظ أنه يمكننا أيضًا كتابة الوظيفة بهذه الطريقة:
const fetchAllMovies = () => { return async (dispatch)=>{ ... } }
بشكل عام ، لجلب جميع الأفلام ، أرسلنا أولاً نوع إجراء يتتبع وقت استمرار تحميل الطلب. ثم استخدمنا صيغة Sanity's GROQ للاستعلام عن مستند الفيلم بشكل غير متزامن. _id
url الخاص ببيانات الفيلم. ثم أعدنا حمولة تحتوي على البيانات التي تم الحصول عليها من واجهة برمجة التطبيقات.
وبالمثل ، يمكننا استرداد الأفلام من خلال _id
، وفرز الأفلام ، والحصول على الأفلام الأكثر شهرة.
يمكننا أيضًا جلب الأفلام التي تطابق مرجع شخص معين. لقد فعلنا ذلك في دالة fetchMoviesByRef
.
const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title } ` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } };
تأخذ هذه الوظيفة وسيطة وتتحقق مما إذا كان person._ref
في castMembers
أو crewMembers
يطابق الوسيطة التي تم تمريرها. نعيد الفيلم _id
poster url
title
جنبًا إلى جنب. نرسل أيضًا إجراءً من النوع MOVIES_REF_FETCH_SUCCESS
، مع إرفاق حمولة البيانات التي تم إرجاعها ، وفي حالة حدوث خطأ ، نرسل إجراءً من النوع MOVIE_REF_FETCH_FAIL
، مع إرفاق حمولة من رسالة الخطأ ، وذلك بفضل غلاف try-catch
.
في وظيفة fetchMovieById
، استخدمنا GROQ
لاسترداد فيلم يطابق id
معينًا تم تمريره إلى الوظيفة.
يتم عرض صيغة GROQ
للوظيفة أدناه.
const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` );
مثل إجراء fetchAllMovies
، بدأنا بتحديد جميع مستندات نوع movie
ولكننا ذهبنا إلى أبعد من ذلك لتحديد فقط تلك التي تحتوي على معرف مزود للوظيفة. نظرًا لأننا نعتزم عرض الكثير من التفاصيل للفيلم ، فقد حددنا مجموعة من السمات لاستردادها.
استرجعنا id
الفيلم وأيضًا بعض السمات في مصفوفة castMembers
وهي ref
و characterName
واسم الشخص وصورة الشخص. قمنا أيضًا بتغيير الاسم المستعار من castMembers
إلى cast
.
مثل أعضاء فريق العمل ، castMembers
بعض السمات من مجموعة crewMembers
، وهي ref
department
job
واسم الشخص وصورة الشخص. قمنا أيضًا بتغيير الاسم المستعار من crewMembers
الطاقم إلى crew
.
وبنفس الطريقة ، اخترنا نص العرض العام والشهرة وعنوان URL لملصق الفيلم وتاريخ إصدار الفيلم وعنوانه.
تتيح لنا لغة GROQ من Sanity أيضًا فرز المستند. لفرز عنصر ، نقوم بتمرير الطلب بجانب مشغل الأنابيب .
على سبيل المثال ، إذا كنا نرغب في فرز الأفلام حسب releaseDate
بترتيب تصاعدي ، فيمكننا القيام بما يلي.
const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );
استخدمنا هذه الفكرة في دالة sortMoviesBy
للفرز إما بترتيب تصاعدي أو تنازلي.
دعنا نلقي نظرة على هذه الوظيفة أدناه.
const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } };
بدأنا بإرسال إجراء من النوع MOVIES_SORT_REQUEST
لتحديد وقت تحميل الطلب. ثم استخدمنا صيغة GROQ
لفرز وجلب البيانات من مجموعة movie
. يتم توفير العنصر المطلوب الفرز وفقًا له في item
المتغير ويتم توفير وضع الفرز (تصاعديًا أو تنازليًا) في type
المتغير. وبالتالي ، قمنا بإرجاع id
وعنوان url والملصق والعنوان. بمجرد إرجاع البيانات ، أرسلنا إجراءً من النوع MOVIES_SORT_SUCCESS
وإذا فشل ، نرسل إجراءً من النوع MOVIES_SORT_FAIL
.
ينطبق مفهوم GROQ
مماثل على دالة getMostPopular
. يتم عرض صيغة GROQ
أدناه.
const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` );
الاختلاف الوحيد هنا هو أننا قمنا بفرز الأفلام حسب الشعبية بترتيب تنازلي ثم اخترنا الأفلام الثلاثة الأولى فقط. يتم إرجاع العناصر في فهرس قائم على الصفر ، وبالتالي فإن العناصر الثلاثة الأولى هي العناصر 0 و 1 و 2. إذا أردنا استرداد العناصر العشرة الأولى ، فيمكننا تمرير [0..9]
إلى الوظيفة.
هذا هو الكود الكامل لإجراءات الفيلم في ملف movieActions.js
.
import sanityAPI from "../../sanitySetup"; import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL, MOVIES_REF_FETCH_REQUEST } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } }; const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title }` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } }; const fetchMovieById = (id) => async (dispatch) => { try { dispatch({ type: MOVIE_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` ); dispatch({ type: MOVIE_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIE_FETCH_FAIL, payload: error.message }); } }; const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_MOST_POPULAR_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } }; const getMostPopular = () => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` ); dispatch({ type: MOVIES_MOST_POPULAR_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_MOST_POPULAR_FAIL, payload: error.message }); } }; export { fetchAllMovies, fetchMovieById, sortMoviesBy, getMostPopular, fetchMoviesByRef };
إعداد المخفضات
المخفضات هي واحدة من أهم المفاهيم في Redux. يأخذون الحالة السابقة ويقررون تغييرات الحالة.
عادة ، سنستخدم تعليمة التبديل لتنفيذ شرط لكل نوع من أنواع الإجراءات. على سبيل المثال ، يمكننا إعادة loading
عندما يشير نوع الإجراء إلى التحميل ، ثم الحمولة عندما تشير إلى النجاح أو الخطأ. من المتوقع أن تتخذ في initial state
action
كحجج.
يحتوي ملف movieReducers.js
الخاص بنا على مخفضات مختلفة لمطابقة الإجراءات المحددة في ملف movieActions.js
. ومع ذلك ، فإن كل من المخفضات له نفس التركيب والهيكل. الاختلافات الوحيدة هي constants
التي يسمونها والقيم التي يرجعونها.
لنبدأ بإلقاء نظرة على fetchAllMoviesReducer
في ملف movieReducers.js
.
import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } };
مثل جميع المخفضات ، يأخذ fetchAllMoviesReducer
كائن الحالة الأولية ( state
) وكائن action
كوسائط. استخدمنا بيان التبديل للتحقق من أنواع الإجراءات في كل نقطة زمنية. إذا كان يتوافق مع MOVIES_FETCH_REQUEST
، فإننا نعيد التحميل على النحو الصحيح لتمكيننا من إظهار مؤشر التحميل للمستخدم.
إذا كان يتوافق مع MOVIES_FETCH_SUCCESS
، فإننا نوقف تشغيل مؤشر التحميل ثم نعيد حمولة الإجراء في movies
متغيرة. ولكن إذا كان MOVIES_FETCH_FAIL
، فإننا نوقف التحميل أيضًا ثم نعيد الخطأ. نريد أيضًا خيار إعادة تعيين أفلامنا. سيمكننا ذلك من تصفية الدول عندما نحتاج إلى القيام بذلك.
لدينا نفس الهيكل لمخفضات أخرى. يتم عرض movieReducers.js
الكاملة أدناه.
import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_FETCH_RESET, MOVIES_REF_FETCH_REQUEST, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } }; const fetchMoviesByRefReducer = (state = {}, action) => { switch (action.type) { case MOVIES_REF_FETCH_REQUEST: return { loading: true }; case MOVIES_REF_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_REF_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const fetchMovieByIdReducer = (state = {}, action) => { switch (action.type) { case MOVIE_FETCH_REQUEST: return { loading: true }; case MOVIE_FETCH_SUCCESS: return { loading: false, movie: action.payload }; case MOVIE_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const sortMoviesByReducer = (state = {}, action) => { switch (action.type) { case MOVIES_SORT_REQUEST: return { loading: true }; case MOVIES_SORT_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_SORT_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const getMostPopularReducer = (state = {}, action) => { switch (action.type) { case MOVIES_MOST_POPULAR_REQUEST: return { loading: true }; case MOVIES_MOST_POPULAR_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_MOST_POPULAR_FAIL: return { loading: false, error: action.payload }; default: return state; } }; export { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer };
لقد اتبعنا أيضًا نفس البنية بالضبط لـ personReducers.js
. على سبيل المثال ، تحدد الدالة fetchAllPersonsReducer
حالات جلب جميع الأشخاص في قاعدة البيانات.
ويرد هذا في الكود أدناه.
import { PERSONS_FETCH_FAIL, PERSONS_FETCH_REQUEST, PERSONS_FETCH_SUCCESS, } from "../constants/personConstants"; const fetchAllPersonsReducer = (state = {}, action) => { switch (action.type) { case PERSONS_FETCH_REQUEST: return { loading: true }; case PERSONS_FETCH_SUCCESS: return { loading: false, persons: action.payload }; case PERSONS_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } };
تمامًا مثل fetchAllMoviesReducer
، قمنا بتعريف fetchAllPersonsReducer
state
action
كوسائط. هذه إعدادات قياسية لمخفضات Redux. استخدمنا بعد ذلك عبارة switch للتحقق من أنواع الإجراءات وإذا كانت من النوع PERSONS_FETCH_REQUEST
، فسنعيد التحميل على أنه صحيح. إذا كانت PERSONS_FETCH_SUCCESS
، فإننا نوقف التحميل ونعيد الحمولة ، وإذا كانت PERSONS_FETCH_FAIL
، فإننا نعيد الخطأ.
الجمع بين المخفضات
تتيح لنا وظيفة Redux combineReducers
دمج أكثر من مخفض واحد وتمريره إلى المتجر. سنقوم بدمج أفلامنا ومخفضات الأشخاص في ملف index.js
داخل مجلد reducers
.
دعونا نلقي نظرة عليه.
import { combineReducers } from "redux"; import { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer } from "./movieReducers"; import { fetchAllPersonsReducer, fetchPersonByIdReducer, countPersonsReducer } from "./personReducers"; import { toggleTheme } from "./globalReducers"; export default combineReducers({ fetchAllMoviesReducer, fetchMovieByIdReducer, fetchAllPersonsReducer, fetchPersonByIdReducer, sortMoviesByReducer, getMostPopularReducer, countPersonsReducer, fetchMoviesByRefReducer, toggleTheme });
هنا قمنا باستيراد جميع المخفضات من ملف الأفلام والأشخاص والمخفضات العالمية وقمنا بتمريرها لدمج وظيفة combineReducers
. تأخذ وظيفة المخفضات شيئًا يسمح لنا بتمرير جميع combineReducers
. يمكننا حتى إضافة اسم مستعار إلى الحجج في هذه العملية.
سنعمل على globalReducers
لاحقًا.
يمكننا الآن تمرير المخفضات في ملف Redux store.js
. هذا موضح أدناه.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));
بعد إعداد سير عمل Redux ، فلنقم بإعداد تطبيق React الخاص بنا.
إعداد تطبيق React الخاص بنا
سيقوم تطبيق رد الفعل الخاص بنا بسرد الأفلام والممثلين وأعضاء الطاقم المطابقين. react-router-dom
للتوجيه styled-components
لتصميم التطبيق. سنستخدم أيضًا واجهة المستخدم المادية للأيقونات وبعض مكونات واجهة المستخدم.
أدخل الأمر bash
التالي لتثبيت التبعيات.
npm install react-router-dom @material-ui/core @material-ui/icons query-string
هذا ما سنبنيه:
ربط Redux بتطبيق React الخاص بنا
React-redux
بوظيفة المزود التي تتيح لنا توصيل تطبيقنا بمتجر Redux. للقيام بذلك ، يتعين علينا تمرير مثيل المتجر إلى المزود. يمكننا القيام بذلك إما في ملف index.js
أو App.js
.
هذا هو ملف index.js الخاص بنا.
import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
هنا ، قمنا باستيراد Provider
من react-redux
والتخزين من store
Redux الخاص بنا. ثم قمنا بلف شجرة مكوناتنا بالكامل مع الموفر ، ونقلنا المتجر إليها.
بعد ذلك ، نحتاج إلى react-router-dom
للتوجيه في تطبيق React الخاص بنا. يأتي React react-router-dom
مع BrowserRouter
و Switch
و Route
الذي يمكن استخدامه لتحديد مسارنا وطرقنا.
نقوم بذلك في ملف App.js
بنا. هذا موضح أدناه.
import React from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import MoviesList from "./pages/MoviesListPage"; import PersonsList from "./pages/PersonsListPage"; function App() { return ( <Router> <main className="contentwrap"> <Header /> <Switch> <Route path="/persons/"> <PersonsList /> </Route> <Route path="/" exact> <MoviesList /> </Route> </Switch> </main> <Footer /> </Router> ); } export default App;
هذا إعداد قياسي للتوجيه باستخدام جهاز التوجيه التفاعلي دوم. يمكنك التحقق من ذلك في وثائقهم. قمنا باستيراد PersonsList
من Header
Footer
وقائمة الأشخاص وقائمة MovieList
. نقوم بعد ذلك بإعداد react-router-dom
عن طريق تغليف كل شيء في Router
Switch
.
نظرًا لأننا نريد أن تشارك صفحاتنا نفس الرأس والتذييل ، فقد كان علينا تمرير مكون <Header />
و <Footer />
قبل تغليف الهيكل باستخدام Switch
. لقد فعلنا أيضًا شيئًا مشابهًا مع العنصر main
لأننا نريده أن يلف التطبيق بأكمله.
مررنا كل مكون إلى المسار باستخدام Route
من react-router-dom
.
تحديد صفحاتنا ومكوناتنا
طلبنا منظم بطريقة منظمة. يتم تخزين المكونات القابلة لإعادة الاستخدام في مجلد components
بينما يتم تخزين Pages في مجلد pages
.
تشتمل pages
على movieListPage.js
و moviePage.js
و PersonListPage.js
و PersonPage.js
. يسرد MovieListPage.js
جميع الأفلام الموجودة في الواجهة الخلفية لـ Sanity.io بالإضافة إلى الأفلام الأكثر شهرة.
لسرد جميع الأفلام ، نقوم ببساطة dispatch
الإجراء fetchAllMovies
المحدد في ملف movieAction.js
بنا. نظرًا لأننا نحتاج إلى جلب القائمة بمجرد تحميل الصفحة ، يتعين علينا تحديدها في useEffect
. هذا موضح أدناه.
import React, { useEffect } from "react"; import { fetchAllMovies } from "../redux/actions/movieActions"; import { useDispatch, useSelector } from "react-redux"; const MoviesListPage = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchAllMovies()); }, [dispatch]); const { loading, error, movies } = useSelector( (state) => state.fetchAllMoviesReducer ); return ( ... ) }; export default MoviesListPage;
بفضل useDispatch
و useSelector
Hooks ، يمكننا إرسال إجراءات Redux وتحديد الحالات المناسبة من متجر Redux. لاحظ أن حالات loading
error
movies
تم تعريفها في وظائف Reducer الخاصة بنا وتم تحديدها هنا باستخدام الخطاف useSelector
من React Redux. تصبح هذه الحالات ، وهي loading
error
movies
متاحة على الفور ، قمنا بإرسال إجراءات fetchAllMovies()
.
بمجرد حصولنا على قائمة الأفلام ، يمكننا عرضها في تطبيقنا باستخدام وظيفة map
أو كيفما نرغب.
هذا هو الكود الكامل لملف moviesListPage.js
.
import React, {useState, useEffect} from 'react' import {fetchAllMovies, getMostPopular, sortMoviesBy} from "../redux/actions/movieActions" import {useDispatch, useSelector} from "react-redux" import Loader from "../components/BackdropLoader" import {MovieListContainer} from "../styles/MovieStyles.js" import SortIcon from '@material-ui/icons/Sort'; import SortModal from "../components/Modal" import {useLocation, Link} from "react-router-dom" import queryString from "query-string" import {MOVIES_FETCH_RESET} from "../redux/constants/movieConstants" const MoviesListPage = () => { const location = useLocation() const dispatch = useDispatch() const [openSort, setOpenSort] = useState(false) useEffect(()=>{ dispatch(getMostPopular()) const {order, type} = queryString.parse(location.search) if(order && type){ dispatch({ type: MOVIES_FETCH_RESET }) dispatch(sortMoviesBy(order, type)) }else{ dispatch(fetchAllMovies()) } }, [dispatch, location.search]) const {loading: popularLoading, error: popularError, movies: popularMovies } = useSelector(state => state.getMostPopularReducer) const { loading: moviesLoading, error: moviesError, movies } = useSelector(state => state.fetchAllMoviesReducer) const { loading: sortLoading, error: sortError, movies: sortMovies } = useSelector(state => state.sortMoviesByReducer) return ( <MovieListContainer> <div className="mostpopular"> { popularLoading ? <Loader /> : popularError ? popularError : popularMovies && popularMovies.map(movie => ( <Link to={`/movie?id=${movie._id}`} className="popular" key={movie._id} style={{backgroundImage: `url(${movie.poster})`}}> <div className="content"> <h2>{movie.title}</h2> <p>{movie.overview.text.substring(0, 50)}…</p> </div> </Link> )) } </div> <div className="moviespanel"> <div className="top"> <h2>All Movies</h2> <SortIcon onClick={()=> setOpenSort(true)} /> </div> <div className="movieslist"> { moviesLoading ? <Loader /> : moviesError ? moviesError : movies && movies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) } { ( sortLoading ? !movies && <Loader /> : sortError ? sortError : sortMovies && sortMovies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) ) } </div> </div> <SortModal open={openSort} setOpen={setOpenSort} /> </MovieListContainer> ) } export default MoviesListPage
بدأنا بإرسال حركة أفلام getMostPopular
(يحدد هذا الإجراء الأفلام ذات الشعبية الأكبر) في خطاف useEffect
. هذا يسمح لنا باسترداد الأفلام الأكثر شعبية بمجرد تحميل الصفحة. بالإضافة إلى ذلك ، سمحنا للمستخدمين بفرز الأفلام حسب releaseDate
popularity
. يتم التعامل مع هذا عن طريق sortMoviesBy
الإجراء المرسل في الكود أعلاه. علاوة على ذلك ، fetchAllMovies
اعتمادًا على معاملات الاستعلام.
أيضًا ، استخدمنا خطاف useSelector
لتحديد المخفضات المقابلة لكل من هذه الإجراءات. اخترنا حالات loading
error
movies
لكل من مخفضات السرعة.
بعد الحصول على movies
من علبة التروس ، يمكننا الآن عرضها على المستخدم. هنا ، استخدمنا وظيفة map
ES6 للقيام بذلك. عرضنا أولاً أداة تحميل عندما يتم تحميل كل حالة من حالات الفيلم وإذا كان هناك خطأ ، فإننا نعرض رسالة الخطأ. أخيرًا ، إذا حصلنا على فيلم ، فإننا نعرض صورة الفيلم للمستخدم باستخدام وظيفة map
. قمنا بلف المكون بأكمله في مكون MovieListContainer
.
<MovieListContainer> … </MovieListContainer>
هي div
معرفة باستخدام مكونات ذات نمط. سنلقي نظرة سريعة على ذلك قريبًا.
تصميم تطبيقنا بمكونات مصممة
تسمح لنا المكونات المصممة بتصميم صفحاتنا ومكوناتنا على أساس فردي. كما أنه يقدم بعض الميزات المثيرة للاهتمام مثل inheritance
، Theming
، passing of props
، وما إلى ذلك.
على الرغم من أننا نريد دائمًا تصميم صفحاتنا على أساس فردي ، فقد يكون التصميم العالمي مرغوبًا في بعض الأحيان. ومن المثير للاهتمام أن المكونات المصممة توفر طريقة للقيام بذلك بفضل وظيفة createGlobalStyle
.
لاستخدام المكونات المصممة في تطبيقنا ، نحتاج إلى تثبيته. افتح المحطة الطرفية في مشروع رد الفعل الخاص بك وأدخل الأمر bash
التالي.
npm install styled-components
بعد تثبيت المكونات المصممة ، دعنا نبدأ مع الأنماط العالمية الخاصة بنا.
لنقم بإنشاء مجلد منفصل في دليل src
الخاص بنا باسم styles
. هذا سوف يخزن كل ما لدينا من أنماط. لنقم أيضًا بإنشاء ملف globalStyles.js
داخل مجلد الأنماط. لإنشاء نمط عالمي في المكونات المصممة ، نحتاج إلى استيراد createGlobalStyle
.
import { createGlobalStyle } from "styled-components";
يمكننا بعد ذلك تحديد أنماطنا على النحو التالي:
export const GlobalStyle = createGlobalStyle` ... `
تستفيد المكونات المصممة من النموذج الحرفي لتعريف الدعائم. ضمن هذه الحرفية ، يمكننا كتابة أكواد CSS
التقليدية الخاصة بنا.
لقد قمنا أيضًا باستيراد deviceWidth
المحدد في ملف يسمى definition.js
. يحتوي deviceWidth
على تعريف نقاط التوقف لتعيين استعلامات الوسائط الخاصة بنا.
import { deviceWidth } from "./definition";
وضعنا الفائض على المخفي للتحكم في تدفق تطبيقنا.
html, body{ overflow-x: hidden; }
حددنا أيضًا نمط الرأس باستخدام محدد نمط .header
.
.header{ z-index: 5; background-color: ${(props)=>props.theme.midDarkBlue}; display:flex; align-items:center; padding: 0 20px; height:50px; justify-content:space-between; position:fixed; top:0; width:100%; @media ${deviceWidth.laptop_lg} { width:97%; } ... }
هنا ، يتم تحديد أنماط مختلفة مثل لون الخلفية ، والفهرس z ، والحشو ، والكثير من خصائص CSS التقليدية الأخرى.
لقد استخدمنا props
المكونات المصممة لتعيين لون الخلفية. هذا يسمح لنا بتعيين المتغيرات الديناميكية التي يمكن تمريرها من المكون الخاص بنا. علاوة على ذلك ، مررنا أيضًا متغير السمة لتمكيننا من تحقيق أقصى استفادة من تبديل السمة.
يمكن وضع التصميم هنا لأننا قمنا بلف تطبيقنا بالكامل باستخدام ThemeProvider
من المكونات المصممة. سنتحدث عن هذا في لحظة. علاوة على ذلك ، استخدمنا CSS flexbox
بشكل صحيح وضبط الموضع على fixed
للتأكد من أنه يظل ثابتًا فيما يتعلق بالمتصفح. لقد حددنا أيضًا نقاط التوقف لجعل الرؤوس متوافقة مع الأجهزة المحمولة.
هذا هو الكود الكامل لملف globalStyles.js
الخاص بنا.
import { createGlobalStyle } from "styled-components"; import { deviceWidth } from "./definition"; export const GlobalStyle = createGlobalStyle` html{ overflow-x: hidden; } body{ background-color: ${(props) => props.theme.lighter}; overflow-x: hidden; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } #root{ display: grid; flex-direction: column; } h1,h2,h3, label{ font-family: 'Aclonica', sans-serif; } h1, h2, h3, p, span:not(.MuiIconButton-label), div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){ color: ${(props) => props.theme.bodyText} } p, span, div, input{ font-family: 'Jost', sans-serif; } .paginate button{ color: ${(props) => props.theme.bodyText} } .header{ z-index: 5; background-color: ${(props) => props.theme.midDarkBlue}; display: flex; align-items: center; padding: 0 20px; height: 50px; justify-content: space-between; position: fixed; top: 0; width: 100%; @media ${deviceWidth.laptop_lg}{ width: 97%; } @media ${deviceWidth.tablet}{ width: 100%; justify-content: space-around; } a{ text-decoration: none; } label{ cursor: pointer; color: ${(props) => props.theme.goldish}; font-size: 1.5rem; } .hamburger{ cursor: pointer; color: ${(props) => props.theme.white}; @media ${deviceWidth.desktop}{ display: none; } @media ${deviceWidth.tablet}{ display: block; } } } .mobileHeader{ z-index: 5; background-color: ${(props) => props.theme.darkBlue}; color: ${(props) => props.theme.white}; display: grid; place-items: center; width: 100%; @media ${deviceWidth.tablet}{ width: 100%; } height: calc(100% - 50px); transition: all 0.5s ease-in-out; position: fixed; right: 0; top: 50px; .menuitems{ display: flex; box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme}; flex-direction: column; align-items: center; justify-content: space-around; height: 60%; width: 40%; a{ display: flex; flex-direction: column; align-items:center; cursor: pointer; color: ${(props) => props.theme.white}; text-decoration: none; &:hover{ border-bottom: 2px solid ${(props) => props.theme.goldish}; .MuiSvgIcon-root{ color: ${(props) => props.theme.lightred} } } } } } footer{ min-height: 30px; margin-top: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.875rem; background-color: ${(props) => props.theme.midDarkBlue}; color: ${(props) => props.theme.white}; } `;
لاحظ أننا كتبنا كود CSS خالصًا داخل الحرف الحرفي ولكن هناك بعض الاستثناءات. تسمح لنا المكونات المصممة بتمرير الدعائم. يمكنك معرفة المزيد عن هذا في الوثائق.
بصرف النظر عن تحديد الأنماط العامة ، يمكننا تحديد أنماط للصفحات الفردية.
على سبيل المثال ، هذا هو نمط PersonListPage.js
المحدد في PersonStyle.js
في مجلد styles
.
import styled from "styled-components"; import { deviceWidth, colors } from "./definition"; export const PersonsListContainer = styled.div` margin: 50px 80px; @media ${deviceWidth.tablet} { margin: 50px 10px; } a { text-decoration: none; } .top { display: flex; justify-content: flex-end; padding: 5px; .MuiSvgIcon-root { cursor: pointer; &:hover { color: ${colors.darkred}; } } } .personslist { margin-top: 20px; display: grid; place-items: center; grid-template-columns: repeat(5, 1fr); @media ${deviceWidth.laptop} { grid-template-columns: repeat(4, 1fr); } @media ${deviceWidth.tablet} { grid-template-columns: repeat(3, 1fr); } @media ${deviceWidth.tablet_md} { grid-template-columns: repeat(2, 1fr); } @media ${deviceWidth.mobile_lg} { grid-template-columns: repeat(1, 1fr); } grid-gap: 30px; .person { width: 200px; position: relative; img { width: 100%; } .content { position: absolute; bottom: 0; left: 8px; border-right: 2px solid ${colors.goldish}; border-left: 2px solid ${colors.goldish}; border-radius: 10px; width: 80%; margin: 20px auto; padding: 8px 10px; background-color: ${colors.transparentWhite}; color: ${colors.darkBlue}; h2 { font-size: 1.2rem; } } } } `;
styled
أولاً نمطًا من styled-components
deviceWidth
من ملف definition
. ثم PersonsListContainer
على أنه div
لإبقاء أنماطنا. باستخدام استعلامات الوسائط ونقاط التوقف المحددة ، جعلنا الصفحة مناسبة للجوّال من خلال تعيين نقاط توقف مختلفة.
هنا ، استخدمنا فقط نقاط توقف المتصفح القياسية للشاشات الصغيرة والكبيرة والكبيرة جدًا. لقد حققنا أيضًا أقصى استفادة من Flexbox والشبكة CSS لتصميم وعرض المحتوى الخاص بنا على الصفحة بشكل صحيح.
لاستخدام هذا النمط في ملف PersonListPage.js
بنا ، قمنا ببساطة باستيراده وإضافته إلى صفحتنا على النحو التالي.
import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;
سينتج الغلاف div
لأننا قمنا بتعريفه على أنه div في أنماطنا.
إضافة الموضوعات والتفافها
إنها دائمًا ميزة رائعة لإضافة سمات إلى تطبيقنا. لهذا نحتاج إلى ما يلي:
- تم تحديد سماتنا المخصصة في ملف منفصل (في ملف
definition.js
الحالة الخاص بنا). - المنطق المحدد في إجراءات Redux ومخفضاتنا.
- استدعاء السمة الخاصة بنا في تطبيقنا وتمريرها عبر شجرة المكونات.
دعنا نتحقق من هذا.
ها هو theme
موضوعنا في ملف definition.js
.
export const theme = { light: { dark: "#0B0C10", darkBlue: "#253858", midDarkBlue: "#42526e", lightBlue: "#0065ff", normal: "#dcdcdd", lighter: "#F4F5F7", white: "#FFFFFF", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "#0B0C10", lightshadowtheme: "rgba(0, 0, 0, 0.1)" }, dark: { dark: "white", darkBlue: "#06090F", midDarkBlue: "#161B22", normal: "#dcdcdd", lighter: "#06090F", white: "white", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "white", lightshadowtheme: "rgba(255, 255, 255, 0.9)" } };
لقد أضفنا خصائص ألوان مختلفة للسمات الفاتحة والداكنة. يتم اختيار الألوان بعناية لإتاحة الرؤية في كل من الوضع الفاتح والداكن. يمكنك تحديد السمات الخاصة بك كما تريد. هذه ليست قاعدة صارمة وسريعة.
بعد ذلك ، دعنا نضيف الوظيفة إلى Redux.
لقد أنشأنا globalActions.js
في مجلد إجراءات Redux وأضفنا الأكواد التالية.
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; import { theme } from "../../styles/definition"; export const switchToLightTheme = () => (dispatch) => { dispatch({ type: SET_LIGHT_THEME, payload: theme.light }); localStorage.setItem("theme", JSON.stringify(theme.light)); localStorage.setItem("light", JSON.stringify(true)); }; export const switchToDarkTheme = () => (dispatch) => { dispatch({ type: SET_DARK_THEME, payload: theme.dark }); localStorage.setItem("theme", JSON.stringify(theme.dark)); localStorage.setItem("light", JSON.stringify(false)); };
هنا ، قمنا ببساطة باستيراد السمات المحددة الخاصة بنا. أرسل الإجراءات المقابلة ، وتمرير حمولة الموضوعات التي نحتاجها. يتم تخزين نتائج الحمولة في التخزين المحلي باستخدام نفس المفاتيح لكل من السمات الفاتحة والداكنة. هذا يمكننا من الاستمرار في الحالات في المتصفح.
نحتاج أيضًا إلى تحديد مخفضنا للموضوعات.
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; export const toggleTheme = (state = {}, action) => { switch (action.type) { case SET_LIGHT_THEME: return { theme: action.payload, light: true }; case SET_DARK_THEME: return { theme: action.payload, light: false }; default: return state; } };
هذا مشابه جدًا لما كنا نفعله. استخدمنا عبارة switch
للتحقق من نوع الإجراء ثم أعدنا payload
المناسبة. قمنا أيضًا بإعادة light
الحالة الذي يحدد ما إذا كان المستخدم قد تم تحديد السمة الفاتحة أو الداكنة. سنستخدم هذا في مكوناتنا.
نحتاج أيضًا إلى إضافته إلى مخفض الجذر والمخزن. هذا هو الرمز الكامل store.js
.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { theme as initialTheme } from "../styles/definition"; import reducers from "./reducers/index"; const theme = localStorage.getItem("theme") ? JSON.parse(localStorage.getItem("theme")) : initialTheme.light; const light = localStorage.getItem("light") ? JSON.parse(localStorage.getItem("light")) : true; const initialState = { toggleTheme: { light, theme } }; export default createStore(reducers, initialState, applyMiddleware(thunk));
نظرًا لأننا احتجنا إلى استمرار الموضوع عند تحديث المستخدم ، فقد كان علينا الحصول عليه من التخزين المحلي باستخدام localStorage.getItem()
إلى حالتنا الأولية.
إضافة الوظيفة إلى تطبيق React الخاص بنا
تزودنا المكونات المصممة بـ ThemeProvider
الذي يسمح لنا بتمرير السمات من خلال تطبيقنا. يمكننا تعديل ملف App.js لإضافة هذه الوظيفة.
دعونا نلقي نظرة عليه.
import React from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { useSelector } from "react-redux"; import { ThemeProvider } from "styled-components"; function App() { const { theme } = useSelector((state) => state.toggleTheme); let Theme = theme ? theme : {}; return ( <ThemeProvider theme={Theme}> <Router> ... </Router> </ThemeProvider> ); } export default App;
من خلال تمرير السمات عبر ThemeProvider
، يمكننا بسهولة استخدام دعائم السمات في أنماطنا.
على سبيل المثال ، يمكننا ضبط اللون على bodyText
المخصص للنص الأساسي على النحو التالي.
color: ${(props) => props.theme.bodyText};
يمكننا استخدام السمات المخصصة في أي مكان نحتاج فيه إلى اللون في تطبيقنا.
على سبيل المثال ، لتحديد border-bottom
، نقوم بما يلي.
border-bottom: 2px solid ${(props) => props.theme.goldish};
خاتمة
بدأنا بالخوض في Sanity.io وإعداده وربطه بتطبيق React الخاص بنا. ثم قمنا بإعداد Redux واستخدمنا لغة GROQ للاستعلام عن واجهة برمجة التطبيقات الخاصة بنا. لقد رأينا كيفية الاتصال واستخدام Redux لتطبيقنا React باستخدام react-redux
واستخدام المكونات المصممة والتخصيص.
ومع ذلك ، فقد خدشنا السطح فقط على ما هو ممكن باستخدام هذه التقنيات. أشجعك على الاطلاع على نماذج التعليمات البرمجية في GitHub repo الخاص بي وتجربة يديك في مشروع مختلف تمامًا باستخدام هذه التقنيات لتعلمها وإتقانها.
موارد
- وثائق السلامة الصحية
- كيفية إنشاء مدونة باستخدام Sanity.io بواسطة Kapehe
- إعادة التوثيق
- توثيق المكونات المصممة
- ورقة الغش GROQ
- وثائق واجهة المستخدم المادية
- برنامج Redux Middleware و SideEffects
- إعادة توثيق Thunk