سنبني في هذه المقالة تطبيق المهام To-Do application باستخدام جانغو Django وريآكت React.
ريآكت هي مكتبة مبنية بلغة جافا سكربت، وتُستخدم لتطوير تطبيقات الصفحة الواحدة Single-page applications -أو اختصارًا SPA. تتمتع ريآكت بتوثيق قوي ومنظومة بيئية حيَّة؛ أما جانغو فهو إطار عمل ويب مبني بلغة بايثون طُوّر لتبسيط الممارسات الشائعة المتبعة لتطوير الويب، وهو إطار عمل موثوق ويتمتع هو الآخر بمنظومة بيئية حيَّة من المكتبات البرمجية المستقرة التي تدعم احتياجات التطوير الشهيرة.
ستلعب ريآكت في التطبيق الذي سنطوّره دور الواجهة الأمامية frontend، أو ما يسمى "إطار عمل من جهة العميل"، إذ ستتولى مسؤولية التعامل مع واجهة المستخدم إلى جانب جلب ووضع قيم البيانات من خلال الطلبات المُرسلة إلى الواجهة الخلفية لجانغو، التي تمثّل واجهة برمجة تطبيقات API بُنِيَت باستخدام إطار عمل جانغو ريست Django REST -أو اختصارًا DRF.
ستحصل في نهاية هذا المقال على تطبيق فعال ونظامي.
ملاحظة: إذا أردت الحصول على الشيفرة المصدرية لهذا المقال، فستجدها هنا.
تنبيه: الشيفرة الواردة هي للأغراض التعليمية وليست للاستخدام المهني للشركات.
سيسمح التطبيق لمستخدميه بإنشاء قائمة بالمهام التي يودون إنجازها، وتُنجز كل مهمة بوضع علامة "مكتمل complete" عليها، وفي حال عدم إنجازها تُوضع عليها علامة "غير مكتمل incomplete".
المتطلبات الأساسية
سنحتاج لكي نتمكن من تنفيذ ما في هذه المقالة إلى ما يلي:
- تثبيت بايثون 3 وإعداد بيئة برمجية محلية له.
- تثبيت Node.js وإنشاء بيئة تطوير محلية.
إضافةً إلى الاعتماديات التالية:
- Python v3.9.1
- pip v20.2.4
- Django v3.1.6
- djangorestframework v3.12.2
- django-cors-headers v3.7.0
- Node v15.8.0
- npm v7.5.4
- React v17.0.1
- axios v0.21.0
الخطوة الأولى - إعداد الواجهة الخلفية
سننشئ الآن مجلد مشروع جديد ونثبّت جانغو وفق ما يلي:
افتح نافذة طرفية جديدة terminal، ونفّذ الأمر التالي الذي سينشئ مجلد مشروع جديدًا:
$ mkdir django-todo-react
ثم انتقل إلى داخل المجلد الذي أنشأته:
$ cd django-todo-react
الآن ثبّت pipenv
مستخدمًا الأمر التالي:
$ pip install pipenv
ملاحظة: قد تحتاج إلى استخدام pip3
بدلًا من pip
وهذا يعتمد على نوع التثبيت الذي تستخدمه.
فعّل الآن بيئة افتراضية جديدة:
$ pipenv shell
وثبّت جانغو باستخدام pipenv
:
$ pipenv install django
ثم أنشئ مشروع جانغو جديد وسمِّه backend
:
$ django-admin startproject backend
ثم انتقل إلى داخل ذلك المجلد:
$ cd backend
وافتتح تطبيقًا جديدًا وسمِّه todo
:
python manage.py startapp todo
نفّذ عمليات التهجير migrations لكي تُطبق التغييرات التي أجريت للتو على مخطط قاعدة البيانات:
python manage.py migrate
وشغّل الخادم:
python manage.py runserver
اذهب إلى العنوان "http://localhost:8000" في متصفح الويب الذي تستخدمه، وستظهر لك الصفحة التالية:
سيظهر لك في هذه النقطة نسخةً من تطبيق جانغو في وضعية التشغيل. يمكنك الآن بعد أن انتهيت إيقاف الخادم باستخدام المفتاحين التاليين: "CONTROL+C"، أو "CTRL+C".
تسجيل تطبيق المهام todo
يمكنك الآن بعد أن انتهيت من إعداد الواجهة الخلفية أن تبدأ في تسجيل تطبيق المهام todo، ونعني بذلك أن تذكره مع التطبيقات المثبّتة ليتمكن جانغو من التعرُّف عليه.
افتح الملف "backend/settings.py" بمحرر الشيفرة الذي تستخدمه وأضف التطبيق "todo" إلى قائمة التطبيقات المثبّتة "INSTALLED_APPS":
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'todo', ]
ثم احفظ التغييرات التي أجريتها على الملف.
تعريف نموذج تطبيق المهام Todo
لننشئ الآن نموذجًا نتمكن به من تعريف كيفية تخزين عناصر "Todo" في قاعدة البيانات.
افتح الملف "todo/models.py" بمحرر الشيفرة وأضف السطور التالية:
from django.db import models # Create your models here. class Todo(models.Model): title = models.CharField(max_length=120) description = models.TextField() completed = models.BooleanField(default=False) def _str_(self): return self.title
تعرّف الشيفرة السابقة ثلاث خاصيّات ضمن النموذج Todo وهي:
-
title
-
description
-
completed
تدل الخاصية completed
على حالة المهمة في لحظة ما، فهي إما مكتملة أو غير مكتملة. ستحتاج الآن إلى إنشاء ملف تهجير بما أنك أنشأت نموذج "Todo":
$ python manage.py makemigrations todo
ثم طبّق التغييرات على قاعدة البيانات:
$ python manage.py migrate todo
يمكنك التأكد من إمكانية تنفيذ عمليات الإنشاء والقراءة والتحديث والحذف Create Read Update Delete - أو اختصارًا CRUD- على نموذج "Todo" باستخدام واجهة الإدارة التي يوفرها جانغو لمستخدميه.
ولفعل ذلك، افتح الملف "todo/admin.py" بمحرر الشيفرة، وأضف الأسطر التالية:
from django.contrib import admin from .models import Todo class TodoAdmin(admin.ModelAdmin): list_display = ('title', 'description', 'completed') # Register your models here. admin.site.register(Todo, TodoAdmin)
ثم احفظ التغييرات.
ستحتاج إلى حساب مستخدم مميز superuser لكي تتمكن من الدخول إلى واجهة الإدارة، لذا نفذ الأمر التالي في نافذة الطرفية لديك:
$ python manage.py createsuperuser
سيُطلبُ منك إدخال اسم المستخدم والبريد الإلكتروني وكلمة المرور لحساب المستخدم المميز. ولأنك ستحتاجها دومًا للدخول إلى لوحة تحكم الإدارة، ننصحك بأن تختار تفاصيل يسهل عليك تذكرها.
شغّل الخادم مرةً أخرى:
$ python manage.py runserver
والآن انتقل إلى العنوان "http://localhost:8000/admin" في متصفح الويب الذي تستخدمه، وسجِّل الدخول مستعملًا اسم المستخدم وكلمة المرور اللذين أنشأتهما للتو.
يمكنك إنشاء وتحرير وحذف عناصر "Todo" باستخدام هذه الواجهة:
بعد إجراء تجاربك على هذه الواجهة، يمكنك إيقاف الخادم باستخدام المفتاحين "CONTROL+C" أو "CTRL+C".
الخطوة الثانية - إعداد واجهة برمجة التطبيقات API
سننشئ هنا واجهة برمجة تطبيقات مستعينين بإطار عمل جانغو ريست، وسنثبّت لتحقيق ذلك كلًّا من djangorestframework
و django-cors-headers
مستخدمين pipenv
:
pipenv install djangorestframework django-cors-headers
يجب أن تضيف اسمي التطبيقين "rest_framework" و "corsheaders" إلى قائمة التطبيقات المثبّتة ضمن ملف الإعدادات. لذلك، افتح ملف الإعدادات "backend/settings.py" بمحرر الشيفرة، وحدّث قسمي التطبيقات المثبّتة "INSTALLED_APPS"، والبرمجيات الوسيطة "MIDDLEWARE" كما يلي:
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders', 'rest_framework', 'todo', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', ]
ثم ألحق أسطر الشيفرة التالية بنهاية الملف "backend/settings.py":
CORS_ORIGIN_WHITELIST = [ 'http://localhost:3000' ]
تُعد مكتبة "django-cors-headers" مكتبة بايثون تمنع من حدوث الأخطاء التي تنتج عادةً عن استخدام بنود سياسة تعدد الموارد CORS، وهذا ما جعلك تضيف "localhost:3000" ضمن القائمة البيضاء "CORS_ORIGIN_WHITELIST"، لأنك تريد أن تمكِّن الواجهة الأمامية للتطبيق -التي تستخدم ذلك المنفذ- من التفاعل مع واجهة برمجة التطبيقات.
إنشاء المسلسلات serializers
ستحتاج للمسلسِلات لتحويل نسخ النموذج إلى محتوى من النوع JSON، وذلك لكي تتمكن الواجهة الأمامية من العمل مع البيانات التي تستلمها.
أنشئ الملف "todo/serializers.py" باستخدام محرر الشيفرة، ثم افتحه وأضف إليه أسطر الشيفرة التالية:
from rest_framework import serializers from .models import Todo class TodoSerializer(serializers.ModelSerializer): class Meta: model = Todo fields = ('id', 'title', 'description', 'completed')
تحدد الشيفرة السابقة النموذج الذي سيجري العمل عليه إلى جانب الحقول التي ستُحوّل إلى محتوى من النوع JSON.
إنشاء العرض
ستحتاج إلى إنشاء الصنف TodoView
في الملف "todo/views.py".
افتح الملف "todo/views.py" بمحرر الشيفرة الذي تستخدمه وأضف أسطر الشيفرة التالية:
from django.shortcuts import render from rest_framework import viewsets from .serializers import TodoSerializer from .models import Todo # Create your views here. class TodoView(viewsets.ModelViewSet): serializer_class = TodoSerializer queryset = Todo.objects.all()
يوفِّر الصنف الأساسي viewsets
تنفيذًا لعمليات CRUD افتراضيًا. تحدد الشيفرة السابقة صنف المسلسِل serializer_class
و مجموعة الاستعلام queryset
.
افتح الملف "backend/urls.py" بمحرر الشيفرة وبدّل محتوياته بالأسطر التالية:
from django.contrib import admin from django.urls import path, include from rest_framework import routers from todo import views router = routers.DefaultRouter() router.register(r'todos', views.TodoView, 'todo') urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), ]
تحدد الشيفرة السابقة مسار محدّد الموارد الموحد URL لواجهة برمجة التطبيقات API، وبهذه الخطوة نكون قد انتهينا من بناء واجهة برمجة التطبيقات.
أصبح بإمكانك الآن إنجاز عمليات CRUD على نموذج المهام "Todo"، إذ يتيح لك صنف الموجِّه router class إجراء الاستعلامات التالية:
-
/todos/
: يعيد قائمةً بكافة عناصر Todo، ويمكن تنفيذ عمليتي الإنشاء CREATE والقراءة READ هنا. -
todos/id/
- يعيد عنصر Todo واحدًا اعتمادًا على المفتاح الأساسيid
، ويمكن تنفيذ عمليتي التحديث UPDATE والحذف DELETE هنا.
دعنا الآن نعيد تشغيل الخادم:
$ python manage.py runserver
اذهب إلى العنوان "http://localhost:8000/api/todos" في متصفح الويب الذي تستخدمه.
يمكنك إنشاء عنصر مهام Todo جديد بهذه الواجهة باستخدام العملية CREATE كما يلي:
إذا كانت عناصر قائمة المهام Todo قد أنشئت بنجاح، فستحصل على استجابة ناجحة.
يمكنك أيضًا تنفيذ عمليتي الحذف DELETE والتحديث UPDATE على بعض عناصر Todo بناءً على المفاتيح الأساسية id التي تزودها. استخدم بنية العنوان "/api/todos/{id}" وحدد المفتاح الأساسي "id" الذي تريد تطبيق هذه العمليات عليه.
أضف "1" إلى نهاية عنوان محدد الموارد الموحد URL لمعرفة عنصر Todo الذي يحمل مفتاحه الأساسي القيمة "1"، ثم انتقل إلى "http://localhost:8000/api/todos/1" في متصفح الويب الذي تستخدمه. تبين الشاشة التالية النتيجة التي حصلنا عليها:
إذا ظهرت لك هذه الشاشة فذلك يعني أنك انتهيت من بناء الواجهة الخلفية للتطبيق.
الخطوة الثالثة - إعداد الواجهة الأمامية
بعد أن انتهيت من إعداد الواجهة الخلفية، حان وقت تجهيز الواجهة الأمامية وتمكينها من التواصل مع الواجهة الخلفية عبر الواجهة التي أنشأتها.
افتح نافذة طرفية واذهب إلى مجلد المشروع "django-todo-react".
سنعتمد في إعداد الواجهة الأمامية على التطبيق Create React الذي تتنوع طرق استخدامه، ومن أشهرها استخدام مشغّل npx لتشغيل الحزمة وإنشاء المشروع:
$ npx create-react-app frontend
وللاطلاع أكثر على هذه الطريقة يرجى مراجعة بدء العمل مع مكتبة ريآكت.
يمكنك الآن بعد أن أنشأت المشروع الانتقال إلى المجلد الذي أنشأته للتو "frontend":
$ cd frontend
الآن، شغّل التطبيق:
$ npm start
سيفتح متصفحك الرابط "http://localhost:3000" وستظهر لك الشاشة الابتدائية لتطبيق "Create React".
ثم ثبّت بوتستراب bootstrap وريآكتستراب reactstrap لتصبح أدوات واجهة المستخدم في حوزتك:
$ npm install bootstrap@4.6.0 reactstrap@8.9.0 --legacy-peer-deps
ملاحظة: ربما تتلقى رسائل خطأ مثل "unable to resolve dependency tree" بحسب إصدارات كل من ريآكت وبوتستراب وريآكتستراب.
أصبحت النسخة الأحدث للملف "popper.js" في لحظة كتابة هذه المقالة مهملة وتتعارض مع الإصدار 17 وما بعده من ريآكت، وهذه مشكلة معروفة، ويمكن استخدام الخيار legacy-peer-deps--
عند التثبيت.
افتح الملف "index.js" في محرر الشيفرة الذي تستخدمه وأضف bootstrap.min.css
:
import React from 'react'; import ReactDOM from 'react-dom'; import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
إذا واجهت أي صعوبة أثناء هذه الخطوة، فننصحك بالاستعانة بموقع التوثيق الرسمي لإضافة بوتستراب.
افتح الملف "App.js" بمحرر الشيفرة وأضف أسطر الشيفرة التالية:
import React, { Component } from "react"; const todoItems = [ { id: 1, title: "Go to Market", description: "Buy ingredients to prepare dinner", completed: true, }, { id: 2, title: "Study", description: "Read Algebra and History textbook for the upcoming test", completed: false, }, { id: 3, title: "Sammy's books", description: "Go to library to return Sammy's books", completed: true, }, { id: 4, title: "Article", description: "Write article on how to use Django with React", completed: false, }, ]; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: todoItems, }; } displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span className={this.state.viewCompleted ? "nav-link active" : "nav-link"} onClick={() => this.displayCompleted(true)} > Complete </span> <span className={this.state.viewCompleted ? "nav-link" : "nav-link active"} onClick={() => this.displayCompleted(false)} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed == viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" > Edit </button> <button className="btn btn-danger" > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> </main> ); } } export default App;
تحتوي هذه الشيفرة على بعض المتوفرة مسبقًا hardcoded لأربعة عناصر، وستبقى هذه القيم إلى حين إحضار العناصر من الواجهة الخلفية.
تعرض الدالة ()renderTabList
امتدادين spans يظهِر كل منهما عند الضغط عليه مجموعةً من المهام، إذ يظهر الضغط على الامتداد Completed المهام المكتملة، ويظهر الامتداد Incomplete المهام غير المكتملة في المقابل.
احفظ الآن التغييرات التي أجريتها وتأمل النتيجة في متصفحك:
هناك أفعال معينة داخل تطبيق المهام، مثل إضافة المهام وتحريرها، ولكي تتمكن من التعامل معها ستحتاج إلى إنشاء مكوّن شرطي modal component.
أولًا، أنشئ مجلد المكوّنات "components" داخل المجلد "src":
$ mkdir src/components
ثم أنشئ الملف "Modal.js" وافتحه بمحرر الشيفرة وأضف إليه الأسطر التالية:
import React, { Component } from "react"; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Input, Label, } from "reactstrap"; export default class CustomModal extends Component { constructor(props) { super(props); this.state = { activeItem: this.props.activeItem, }; } handleChange = (e) => { let { name, value } = e.target; if (e.target.type === "checkbox") { value = e.target.checked; } const activeItem = { ...this.state.activeItem, [name]: value }; this.setState({ activeItem }); }; render() { const { toggle, onSave } = this.props; return ( <Modal isOpen={true} toggle={toggle}> <ModalHeader toggle={toggle}>Todo Item</ModalHeader> <ModalBody> <Form> <FormGroup> <Label for="todo-title">Title</Label> <Input type="text" id="todo-title" name="title" value={this.state.activeItem.title} onChange={this.handleChange} placeholder="Enter Todo Title" /> </FormGroup> <FormGroup> <Label for="todo-description">Description</Label> <Input type="text" id="todo-description" name="description" value={this.state.activeItem.description} onChange={this.handleChange} placeholder="Enter Todo description" /> </FormGroup> <FormGroup check> <Label check> <Input type="checkbox" name="completed" checked={this.state.activeItem.completed} onChange={this.handleChange} /> Completed </Label> </FormGroup> </Form> </ModalBody> <ModalFooter> <Button color="success" onClick={() => onSave(this.state.activeItem)} > Save </Button> </ModalFooter> </Modal> ); } }
تنشئ الشيفرة السابقة الصنف CustomModal
وهو يتداخل مع المكون Modal
المشتق من المكتبة reactstrap
.
وقد عرَّفت هذه الشيفرة أيضًا ثلاثة حقول ضمن نموذج التعبئة form هي:
-
title
-
description
-
completed
هذه الحقول هي نفسها التي عرفناها مثل خصائص في نموذج قائمة المهام "Todo" عند إعداد الواجهة الخلفية.
يستقبل CustomModal
القيم activeItem
و toggle
و onSave
مثل خاصيّات، بحيث يمثِّل:
-
activeItem
عنصر قائمة المهام الذي سيُحرّر. -
toggle
الدالة التي ستُستخدم للتحكم في حالة المكونModal
، أي فتحه أو غلقه. -
onSave
دالةً تُستدعى لحفظ القيم التي حرِّرت لعنصر قائمة المهام.
الآن، سنستورد المكون CustomModal
إلى داخل الملف "App.js".
عد إلى الملف "src/App.js" وافتحه بمحرر الشيفرة واستبدل محتوياته بأسطر الشيفرة التالية:
import React, { Component } from "react"; import Modal from "./components/Modal"; const todoItems = [ { id: 1, title: "Go to Market", description: "Buy ingredients to prepare dinner", completed: true, }, { id: 2, title: "Study", description: "Read Algebra and History textbook for the upcoming test", completed: false, }, { id: 3, title: "Sammy's books", description: "Go to library to return Sammy's books", completed: true, }, { id: 4, title: "Article", description: "Write article on how to use Django with React", completed: false, }, ]; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: todoItems, modal: false, activeItem: { title: "", description: "", completed: false, }, }; } toggle = () => { this.setState({ modal: !this.state.modal }); }; handleSubmit = (item) => { this.toggle(); alert("save" + JSON.stringify(item)); }; handleDelete = (item) => { alert("delete" + JSON.stringify(item)); }; createItem = () => { const item = { title: "", description: "", completed: false }; this.setState({ activeItem: item, modal: !this.state.modal }); }; editItem = (item) => { this.setState({ activeItem: item, modal: !this.state.modal }); }; displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span className={this.state.viewCompleted ? "nav-link active" : "nav-link"} onClick={() => this.displayCompleted(true)} > Complete </span> <span className={this.state.viewCompleted ? "nav-link" : "nav-link active"} onClick={() => this.displayCompleted(false)} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed === viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" onClick={() => this.editItem(item)} > Edit </button> <button className="btn btn-danger" onClick={() => this.handleDelete(item)} > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" onClick={this.createItem} > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> {this.state.modal ? ( <Modal activeItem={this.state.activeItem} toggle={this.toggle} onSave={this.handleSubmit} /> ) : null} </main> ); } } export default App;
احفظ التغييرات وتأمل ما حصل للتطبيق في متصفحك:
إذا حاولت تحرير وحفظ عنصر من عناصر قائمة المهام Todo، سيظهر لك تنبيه يُظهِرُ الكائن المرتبط بعنصر المهام Todo. سيؤدي النقر على حفظ Save أو حذف Delete إلى تنفيذ الفعل المقابل على هذا العنصر.
ملاحظة: قد تواجه أخطاءً في واجهة الطرفية وذلك بحسب نسختي ريآكت وريآكتستراب اللتين تستخدمهما، وأثناء عمل هذه المراجعة، كان الخطآن التاليان من بين الأخطاء الشائعة في ريآكتستراب:
Warning: Legacy context API has been detected within a strict-mode tree Warning: findDOMNode is deprecated in StrictMode
ستعدل الآن التطبيق ليتمكن من التفاعل مع واجهة برمجة التطبيقات لجانغو التي بنيتها في القسم السابق. أعد فتح نافذة الطرفية الأولى وتأكد من أن الخادم يعمل، وإذا لم يكن يعمل، استخدم الأمر التالي:
$ python manage.py runserver
ملاحظة: إذا كنت قد أغلقت نافذة الطرفية هذه، تذكر أنك ستحتاج للذهاب إلى المجلد "backend" واستخدام صدفة Pipenv الافتراضية.
لإنشاء طلبات توجه إلى نقاط النهاية لواجهة برمجة التطبيقات في خادم الواجهة الخلفية، سنثبّت واحدةً من مكتبات جافا سكريبت المهمة وهي مكتبة "axios".
في نافذة الطرفية الثانية، تأكد أنك موجود داخل المجلد "frontend"، ثم ثبّت مكتبة axios
:
$ npm install axios@0.21.1
ثم افتح الملف "frontend/package.json" بمحرر الشيفرة وأضف خادمًا وسيطًا proxy كما يلي:
[...] "name": "frontend", "version": "0.1.0", "private": true, "proxy": "http://localhost:8000", "dependencies": { "axios": "^0.18.0", "bootstrap": "^4.1.3", "react": "^16.5.2", "react-dom": "^16.5.2", "react-scripts": "2.0.5", "reactstrap": "^6.5.0" }, [...]
تتركز مهمة الخادم الوسيط في المساعدة في إنشاء نفق tunneling لطلبات واجهة برمجة التطبيقات إلى شبكة أخرى وإرسالها إلى العنوان "http://localhost:8000"، ليستلمها تطبيق جانغو هناك ويعالجها، وبدون هذا الخادم الوسيط، ستضطر إلى تحديد المسارات الكاملة كما يلي:
axios.get("http://localhost:8000/api/todos/")
أما مع استخدام الخادم الوسيط، فستحتاج للتزويد بمسارات نسبية فقط:
axios.get("/api/todos/")
ملاحظة: ربما تحتاج إلى إعادة تشغيل خادم التطوير لكي يسجل الخادم الوسيط في التطبيق.
عد إلى الملف "frontend/src/App.js" وافتحه بمحرر الشيفرة، إذ ستحذف في هذه الخطوة عناصر قائمة المهام todoItems ذات القيم الموفرة hardcoded وتستخدم البيانات من الطلبات الموجهة إلى خادم الواجهة الخلفية مستخدمًا handleSubmit
و handleDelete
.
افتح الملف App.js واستبدل محتوياته بالنسخة النهائية التالية:
import React, { Component } from "react"; import Modal from "./components/Modal"; import axios from "axios"; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: [], modal: false, activeItem: { title: "", description: "", completed: false, }, }; } componentDidMount() { this.refreshList(); } refreshList = () => { axios .get("/api/todos/") .then((res) => this.setState({ todoList: res.data })) .catch((err) => console.log(err)); }; toggle = () => { this.setState({ modal: !this.state.modal }); }; handleSubmit = (item) => { this.toggle(); if (item.id) { axios .put(`/api/todos/${item.id}/`, item) .then((res) => this.refreshList()); return; } axios .post("/api/todos/", item) .then((res) => this.refreshList()); }; handleDelete = (item) => { axios .delete(`/api/todos/${item.id}/`) .then((res) => this.refreshList()); }; createItem = () => { const item = { title: "", description: "", completed: false }; this.setState({ activeItem: item, modal: !this.state.modal }); }; editItem = (item) => { this.setState({ activeItem: item, modal: !this.state.modal }); }; displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span onClick={() => this.displayCompleted(true)} className={this.state.viewCompleted ? "nav-link active" : "nav-link"} > Complete </span> <span onClick={() => this.displayCompleted(false)} className={this.state.viewCompleted ? "nav-link" : "nav-link active"} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed === viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" onClick={() => this.editItem(item)} > Edit </button> <button className="btn btn-danger" onClick={() => this.handleDelete(item)} > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" onClick={this.createItem} > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> {this.state.modal ? ( <Modal activeItem={this.state.activeItem} toggle={this.toggle} onSave={this.handleSubmit} /> ) : null} </main> ); } } export default App;
الدالة ()refreshList
معدّةٌ ليُعاد استخدامها المرة تلو المرة، وتُستدعى بعد كل مرة يكتمل فيها طلب واجهة برمجة تطبيقات، ومهمتها تحديث قائمة المهام لتعرِض دومًا أحدث حالة لها؛ أما الدالة ()handleSubmit
فهي مسؤولة عن عمليتي الإنشاء والتحديث، فإذا لم يمتلك العنصر الممرّر مثل معامل parameter مفتاحًا أساسيًّا، فالأرجح عندها أنه لم يُنشأ، لذا يتولى مهمة إنشائه.
تأكد الآن من أن خادم الواجهة الخلفية لديك يعمل في أول نافذة طرفية استخدمتها.
$ python manage.py runserver
ملاحظة: إذا أغلقت نافذة الطرفية هذه، فستحتاج للذهاب إلى المجلد "backend" واستخدام صدفة Pipenv الافتراضية.
وتأكد في نافذة الطرفية الثانية أنك في مجلد الواجهة الأمامية "frontend" وشغّل تطبيق الواجهة الأمامية:
$ npm start
الآن عندما تذهب في متصفحك إلى العنوان "http://localhost:3000"، سيسمح لك التطبيق بإنشاء المهام وقراءتها وتعديلها وحذفها.
وبهذا تكتمل الواجهة الأمامية والواجهة الخلفية لتطبيق المهام.
خاتمة
بنينا في هذه المقالة تطبيق مهام باستخدام جانغو وريآكت، وقد حققنا هذا بالاستعانة بالمكتبات: djangorestframework و django-cors-headers و axios و bootstrap و reactstrap.
إذا أردت تعلم المزيد عن جانغو ننصحك بمراجعة صفحة مواضيع جانغو في أكاديمية حسوب.
وإذا أردت تعلم المزيد عن ريآكت ننصحك بزيارة دلبل ريآكت في موسوعة حسوب.
ترجمة -وبتصرف- للمقالة How To Build a To-Do application Using Django and React لصاحبها Jordan Irabor.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.