ستختلف التمارين قليلًا في القسم السابع من منهاج full_stack_101 عن ما سبقها. فستجد في هذا الفصل وفي الفصل الذي يليه تمارين تتعلق بالأفكار التي سنقدمها في هذا الفصل، بالإضافة إلى سلسلة من التمارين التي سنراجع من خلالها ما تعلمناه خلال تقدمنا في مادة المنهاج، وذلك بتوسيع التطبيق Bloglist الذي عملنا عليه في القسمين 4 و5.
هيكلية التنقل ضمن التطبيق
سنعود حاليًا إلى React دون استخدام Redux.
من الشائع أن تحتوي التطبيقات إلى شريط تنقل يساعد على تغيير الصفحات التي يعرضها التطبيق. فيمكن أن يتضمن تطبيقنا صفحة رئيسية:
وصفحات منفصلة لعرض معلومات عن الملاحظات والمستخدمين:
تتغير الصفحات التي يحتويها تطبيق وفقًا للمدرسة القديمة باستخدام طلب HTTP-GET يرسله المتصفح إلى الخادم ومن ثم يصيّر شيفرة HTML التي تعرض الصفحة المُعادة.
لكننا سنبقى عمليًا في نفس الصفحة في التطبيقات وحيدة الصفحة. حيث يوحي تنفيذ شيفرة JavaScript ضمن المتصفح بوجود عدة صفحات. فلو أرسل طلب HTTP عند تغيير الواجهة المعروضة، فهو فقط لإحضار البيانات بصيغة JSON والتي قد يحتاجها التطبيق لعرض الواجهة الجديدة.
من السهل جدًا إضافة شريط التنقل أو عرض تطبيق بعدة واجهات استخدام React.
تمثل الشيفرة التالية إحدى الطرق:
import React, { useState } from 'react' import ReactDOM from 'react-dom' const Home = () => ( <div> <h2>TKTL notes app</h2> </div> ) const Notes = () => ( <div> <h2>Notes</h2> </div> ) const Users = () => ( <div> <h2>Users</h2> </div> ) const App = () => { const [page, setPage] = useState('home') const toPage = (page) => (event) => { event.preventDefault() setPage(page) } const content = () => { if (page === 'home') { return <Home /> } else if (page === 'notes') { return <Notes /> } else if (page === 'users') { return <Users /> } } const padding = { padding: 5 } return ( <div> <div> <a href="" onClick={toPage('home')} style={padding}> home </a> <a href="" onClick={toPage('notes')} style={padding}> notes </a> <a href="" onClick={toPage('users')} style={padding}> users </a> </div> {content()} </div> ) } ReactDOM.render(<App />, document.getElementById('root'))
تضاف كل واجهة عرض على هيئة مكوّن خاص. وتُخزّن معلومات المكوّن في حالة التطبيق التي تحمل الاسم page
. تخبرنا هذه المعلومات عن المكوّن الذي يمثل الواجهة التي ستعرض أسفل شريط القوائم.
لكن هذا الأسلوب ليس الأسلوب الأمثل لتنفيذ ذلك. حيث يمكن أن نلاحظ من الصور التي عرضناها أن العنوان سيبقى نفسه للواجهات المختلفة. ومن المفضل أن تحمل كل واجهة عرض عنوانها الخاص، لتسهيل إنشاء اختصارات لها ضمن المتصفح على سبيل المثال. لن يعمل الزر "back" كما هو متوقع في التطبيق، أي أنه لن ينقلك إلى الواجهة السابقة بل إلى مكان مختلف كليًا. فإن توسع التطبيق وهذا ما نريده، عند إنشاء واجهات منفصلة لكل مستخدم أو ملاحظة فسيغدو هذا الأسلوب الذي اتبعناه في التوجه، والذي يدير آلية التنقل في التطبيق معقدًا بشكل كبير.
لحسن الحظ تؤمن لنا المكتبة React router حلًا ممتازًا لإدارة التنقل بين الواجهات في تطبيقات React.
لنغيّر التطبيق السابق بحيث يستخدم React-router. إذًا علينا أولًا تثبيت المكتبة بتنفيذ الأمر:
npm install react-router-dom
تُفعَّل آلية التنقل التي تؤمنها المكتبة السابقة كالتالي:
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom" const App = () => { const padding = { padding: 5 } return ( <Router> <div> <Link style={padding} to="/">home</Link> <Link style={padding} to="/notes">notes</Link> <Link style={padding} to="/users">users</Link> </div> <Switch> <Route path="/notes"> <Notes /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch> <div> <i>Note app, Department of Computer Science 2020</i> </div> </Router> ) }
تستخدم آلية التنقل أو التصيير الشرطي للمكوّنات المسؤولة عن عرض الواجهات بناء على عنوانها الظاهر على المتصفح، بوضع هذه المكوّنات كأبناء للمكوّن Router
، أي داخل المُعِّرف <Router>
.
لاحظ أنه على الرغم من أنّ اسم المكوّن Router
، فإننا نتحدث في الواقع عن موجِّه المتصفح BrowserRouter حيث غيرنا اسم الكائن عندما أدرجناه:
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"
وفقًا لدليل استخدام المكتبة:
اقتباسموجّه المتصفح هو موجّه يستخدم واجهة المحفوظات البرمجية المتوافقة مع HTML5 (الأحداث pushState وreplaceState وpopState) لتبقى واجهة المستخدم متزامنة مع العنوان.
يحمّل المتصفح عادة صفحة جديدة عندما يتغير العنوان ضمن شريط العناوين. لكن سيمكننا مكوّن موجّه المتصفح BrowserRouter
بالاستفادة من الواجهة البرمجية HTML5 history API من استخدام العنوان الموجود في شريط العناوين للتنقل الداخلي ضمن الواجهات التي يعرضها تطبيق React.فحتى لو تغيّر العنوان ضمن شريط عناوين المتصفح سيتم التلاعب بمحتوى الصفحة باستخدام شيفرة JavaScript ولن يحمّل المتصفح محتوًى جديدًا من الخادم.
وسيكون استخدام أفعال التراجع أو التقدم وحفظ الاختصارات منطقيًا كأي صفحة ويب تقليدية.
نعرّف داخل الموجّه روابط link
لتعديل شريط العناوين بمساعدة المكوّن Link. تنشئ الشيفرة التالية على سبيل المثال:
<Link to="/notes">notes</Link>
رابطًا داخل التطبيق له النص "notes" يغيّر بالنقر عليه العنوان في شريط عناوين المتصفح إلى العنوان "notes/".
تٌعرّف المكوِّنات التي يجري تصييرها وفقًا للعنوان على المتصفح باستخدام المكوّن Route. فوظيفة الشيفرة التالية على سبيل المثال:
<Route path="/notes"> <Notes /> </Route>
هو تصيير المكوّن Note
إن كان العنوان المحدد في المتصفح هو "notes/". سنغلف المكونات التي ستُصيّر بناء على عنوان المتصفح داخل المكوّن Switch:
<Switch> <Route path="/notes"> <Notes /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch>
يصير المكوّن switch
أول مكوّن داخله يتطابق مساره مع العنوان الموجود ضمن شريط عناوين المتصفح.
وانتبه إلى أهمية ترتيب المكوّنات. فإن أردنا وضع المكوّن Home
ذو المسار"/"=path
أولًا، فلن يصير أي مكوّن آخر لأن المسار "/" غير موجود أصلًا فهو بداية كل المسارات:
<Switch> <Route path="/"> <Home /> </Route> <Route path="/notes"> <Notes /> </Route> // ... </Switch>
إسناد معاملات إلى الموجه
لنتفحص النسخة المعدّلة من المثال السابق. يمكنك أن تجد الشيفرة الكاملة للمثال على Github.
يعرض التطبيق الآن خمس واجهات مختلفة يتحكم الموجّه بآلية عرضها. فبالإضافة إلى المكونات Home
وUser
وNotes
من المثال السابق سنجد المكوّن Login
الذي يعرض واجهة لتسجيل الدخول والمكوّن Note
الذي يعرض ملاحظة واحدة.
لم نغير المكونين Home
وUsers
، لكن Notes
أعقد قليلًا لانها تصير قائمة الملاحظات التي تُمرّر إليها كخاصية بطريقة تمكننا من النقر على اسم كل ملاحظة:
تأتي إمكانية النقر على اسم الملاحظة من المكوّن Link
، فالنقر على اسم الملاحظة التي تحمل المعرّف 3 سيسبب وقوع الحدث الذي يغيّر العنوان في المتصفح إلى "notes/3":
const Notes = ({notes}) => ( <div> <h2>Notes</h2> <ul> {notes.map(note => <li key={note.id}> <Link to={`/notes/${note.id}`}>{note.content}</Link> </li> )} </ul> </div> )
نعرّف العناوين ذات المعاملات في الموجّه ضمن المكوّن App
كالتالي:
<Router> <div> <div> <Link style={padding} to="/">home</Link> <Link style={padding} to="/notes">notes</Link> <Link style={padding} to="/users">users</Link> </div> <Switch> <Route path="/notes/:id"> <Note notes={notes} /> </Route> <Route path="/notes"> <Notes notes={notes} /> </Route> <Route path="/"> <Home /> </Route> </Switch> </Router>
نعرّف الموجّه الذي يصير ملاحظة محددة " بتنسيق express" بتعليم المعامل بالوسم "id:":
<Route path="/notes/:id">
فعندما ينتقل المتصفح إلى عنوان الملاحظة المحددة، "notes/3/" على سبيل المثال، يُصّير المكوّن Note
:
import { // ... useParams} from "react-router-dom" const Note = ({ notes }) => { const id = useParams().id const note = notes.find(n => n.id === Number(id)) return ( <div> <h2>{note.content}</h2> <div>{note.user}</div> <div><strong>{note.important ? 'important' : ''}</strong></div> </div> ) }
يتلقى المكوّن Notes
كل الملاحظات ضمن الخاصيّة notes
، ويمكنه بعدها الوصول إلى معامل العنوان (معرّف الملاحظة التي ستُعرض) باستخدام الدالة useParams العائدة للمكتبة react-router.
استخدام الدالة useHistory
أضفنا أيضًا طريقة بسيطة لتسجيل الدخول في تطبيقنا. فإن سجّل المستخدم دخوله ستُخزّن معلومات تسجيل الدخول في الحقل user
من حالة المكوّن App
.
يُصيّر خيار الانتقال إلى واجهة تسجيل الدخول شرطيًا في القائمة:
<Router> <div> <Link style={padding} to="/">home</Link> <Link style={padding} to="/notes">notes</Link> <Link style={padding} to="/users">users</Link> {user ? <em>{user} logged in</em> : <Link style={padding} to="/login">login</Link> } </div> // ... </Router>
فلو سجلّ المستخدم دخوله للتو، فسيُظهر التطبيق اسم المستخدم بدلًا من الانتقال إلى واجهة تسجيل الدخول:
تعطي الشيفرة التالية وظيفة تسجيل الدخول لتطبيقنا:
import { // ... useHistory} from 'react-router-dom' const Login = (props) => { const history = useHistory() const onSubmit = (event) => { event.preventDefault() props.onLogin('mluukkai') history.push('/') } return ( <div> <h2>login</h2> <form onSubmit={onSubmit}> <div> username: <input /> </div> <div> password: <input type='password' /> </div> <button type="submit">login</button> </form> </div> ) }
إن ما يلفت الانتباه في هذا المكوّن هو استخدامه الدالة useHistory. حيث يمكن للمكوّن الولوج إلى كائن محفوظات history باستخدام تلك الدالة. ويستخدم كائن المحفوظات لتغيير عنوان المتصفح برمجيًا.
فعند تسجيل الدخول، يُستدعى التابع ('/')history.push
العائد لكائن المحفوظات والذي يسبب تغييرًا في عنوان المتصفح إلى "/" ويصيِّر بعدها التطبيق المكوّن Home
.
تمثل كلا الدالتين useParams وuseHistory دوال خطافات تمامًا كالدوال useState
وuseEffect
والتي استخدمناها عدة مرات سابقًا. وكما أسلفنا في القسم 1 فهنالك الكثير من القواعد لاستخدام دوال الخطافات. وقد هيئت تطبيقات Creat-react-app لتنبيهك إن أغفلت تلك القواعد، كاستدعاء دوال الخطافات من داخل العبارات الشرطية.
إعادة التوجيه
يبقى هناك تفصيل مهم يتعلق بالمسار Users
:
<Route path="/users" render={() => user ? <Users /> : <Redirect to="/login" /> } />
إن لم يسجل المستخدم دخوله، فلن يصيّر المكوّن Users
. وبدلًا من ذلك سيعاد توجيه المستخدم إلى واجهة تسجيل الدخول باستخدام المكوّن Redirect
.
<Redirect to="/login" />
من الأفضل في الواقع أن لا نظهر الروابط التي تحتاج إلى تسجيل الدخول في شريط التنقل إن لم يسجل المستخدم دخوله إلى التطبيق.
تمثل الشيفرة التالية المكوّن App
بشكله الكامل:
const App = () => { const [notes, setNotes] = useState([ // ... ]) const [user, setUser] = useState(null) const login = (user) => { setUser(user) } const padding = { padding: 5 } return ( <div> <Router> <div> <Link style={padding} to="/">home</Link> <Link style={padding} to="/notes">notes</Link> <Link style={padding} to="/users">users</Link> {user ? <em>{user} logged in</em> : <Link style={padding} to="/login">login</Link> } </div> <Switch> <Route path="/notes/:id"> <Note notes={notes} /> </Route> <Route path="/notes"> <Notes notes={notes} /> </Route> <Route path="/users"> {user ? <Users /> : <Redirect to="/login" />} </Route> <Route path="/login"> <Login onLogin={login} /> </Route> <Route path="/"> <Home /> </Route> </Switch> </Router> <div> <br /> <em>Note app, Department of Computer Science 2020</em> </div> </div> ) }
نعرّف في الشيفرة السابقة مكوّنًا شائع الاستخدام في تطبيقات الويب الحديثة ويدعى footer
والذي يعرّف الجزء السفلي من شاشة عرض التطبيق خارج نطاق المكوّن Router
، وبالتالي سيظهر دائمًا بغض النظر عن المكوّن الذي سيُعرض.
مرور آخر على إسناد المعاملات إلى الموجه
لاتزال هناك ثغرة في تطبيقنا. حيث يتلقى المكوّن Note
كل الملاحظات على الرغم من أنه سيعرض الملاحظة التي يتطابق معرّفها مع معامل العنوان:
const Note = ({ notes }) => { const id = useParams().id const note = notes.find(n => n.id === Number(id)) // ... }
هل يمكن أن تعدّل التطبيق لكي يتلقى المكون Note
المكوّن الذي سيُعرض فقط؟
const Note = ({ note }) => { return ( <div> <h2>{note.content}</h2> <div>{note.user}</div> <div><strong>{note.important ? 'important' : ''}</strong></div> </div> ) }
تعتمد إحدى الطرق المتبعة في تنفيذ ذلك على استخدام الخطاف useRouteMatch لتحديد معرّف الملاحظة التي ستُعرض ضمن المكوّن App
.
من غير الممكن أن نستخدم الخطاف useRouteMatch
في المكوّن الذي يعرّف الشيفرة المسؤولة عن التنقل. لننقل إذًا المكوّن Router
خارج المكوّن App
:
ReactDOM.render( <Router> <App /> </Router>, document.getElementById('root') )
سيصبح المكون App
كالتالي:
import { // ... useRouteMatch} from "react-router-dom" const App = () => { // ... const match = useRouteMatch('/notes/:id') const note = match ? notes.find(note => note.id === Number(match.params.id)) : null return ( <div> <div> <Link style={padding} to="/">home</Link> // ... </div> <Switch> <Route path="/notes/:id"> <Note note={note} /> </Route> <Route path="/notes"> <Notes notes={notes} /> </Route> // ... </Switch> <div> <em>Note app, Department of Computer Science 2020</em> </div> </div> ) }
في كل مرة ستغير فيها العنوان على المتصفح سيجري تنفيذ الأمر التالي:
const match = useRouteMatch('/notes/:id')
إن تطابق العنوان مع القيمة "notes/:id/"، فسيُسند إلى متغير التطابق كائن يمكنه الولوج إلى القسم الذي يحوي المعامل من مسار العنوان وهو معرّف الملاحظة التي ستُعرض، وبالتالي يمكن إحضار الملاحظة المطلوبة وعرضها.
const note = match ? notes.find(note => note.id === Number(match.params.id)) : null
ستجد الشيفرة كاملة على GitHub.
التمارين 7.1 - 7.3
سنعود إلى العمل في تطبيق الطرائف. استخدم نسخة التطبيق التي لا تعتمد على Redux والموجودة على GitHub كنقطة انطلاق للتمارين.
إن نسخت الشيفرة ووضعتها في مستودع git موجود أصلًا، لاتنس حذف ملف تهيئة git لنسختك من التطبيق:
Exercises 7.1.-7.3.
cd routed-anecdotes // توجه أوّلًا إلى المستودع الذي يحوي نسختك من التطبيق rm -rf .git
شغّل التطبيق بالطريقة الاعتيادية، لكن عليك أوّلًا تثبيت الاعتماديات اللازمة:
npm install npm start
7.1 تطبيق الطرائف بشريط للتنقل: الخطوة 1
أضف موجّه React إلى التطبيق بحيث يمكن تغيير الواجهة المعروضة عند النقر على الروابط في المكوّن Menu
. أظهر قائمة الطرائف عند الوصول إلى جذر التطبيق (الموقع الذي يحمل المسار "/")
ينبغي أن يظهر المكوّن Footer
بشكل دائم في أسفل الشاشة.
كما ينبغي إنشاء الطرفة الجديدة في مسار خاص، ضمن المسار "create" على سبيل المثال:
7.2 تطبيق الطرائف بشريط للتنقل: الخطوة 2
أضف إمكانية عرض طرفة واحدة:
انتقل إلى الصفحة التي تعرض طرفة واحدة بالنقر على اسم الطرفة:
7.3 تطبيق الطرائف بشريط للتنقل: الخطوة 3
ستغدو الوظيفة الافتراضية لنموذج إنشاء طرفة جديدة مربكة قليلًا لأن شيئًا لن يحدث عند إنشاء طرفة جديدة باستخدام النموذج.
حسّن الأمر بحيث ينتقل التطبيق تلقائيًا إلى إظهار كل الطرائف عند إنشاء الطرفة الجديدة، ويظهر تنبيه للمستخدم مدته عشر ثوان، لإبلاغه بإضافة طرفة جديدة
ترجمة -وبتصرف- للفصل React-Router من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.