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

بناء تطبيق ويب لإدارة معلومات العملاء باستخدام جانغو Django وريآكت React


Mostafa Almahmoud

يستخدم الناس أنواعًا مختلفة من الأجهزة للاتصال بالإنترنت وتصفح الويب، ولهذا تحتاج إلى تمكين المستخدمين من الوصول إلى التطبيقات مهما كان موقعهم؛ ففي حالة المواقع التقليدية، يكفي غالبًا امتلاك واجهة مستخدم متجاوبة؛ بينما تحتاج في حالة التطبيقات الأعقد إلى استخدام تقنيات ومعماريات أخرى، منها امتلاك تطبيقات ريست REST مستقلة للواجهة الأمامية والواجهة الخلفية والتي يمكن تنفيذها مثل تطبيقات ويب من جهة العميل أو تطبيقات ويب تقدّميّة Progressive Web Apps -اختصارًا PWA- أو تطبيقات جوال أصلية.

نذكر فيما يلي بعض الأدوات الممكن استخدامها لبناء تطبيقات أعقد:

ستبني في هذه المقالة تطبيق ويب حديث مع واجهة REST API خلفية منفصلة وواجهة أمامية مستخدمًا لهذا الغرض ريآكت وجانغو وإطار عمل جانغو ريست، إذ يمكنك باستخدام ريآكت وجانغو معًا الاستفادة من أحدث الترقيات في جافا سكريبت وتطوير الواجهات الأمامية، وبدلًا من بناء تطبيق جانغو يستخدم محرك قوالب داخلي، ستستخدم ريآكت مثل مكتبة واجهة مستخدم UI library مستفيدًا من نموذج كائن الوثيقة Document Object Model الافتراضي -أو اختصارًا DOM-، والطريقة التصريحية declarative والمكونات التي تصيّر render سريعًا التغيرات على البيانات.

يخزن تطبيق الويب الذي ستبنيه سجلات العملاء في قاعدة بيانات، ويمكنك استخدامها بمثابة نقطة بداية لتطبيق إدارة علاقات العملاء CRM، وعند انتهائك ستكون قادرًا على إنشاء وقراءة وتحديث وحذف السجلات باستخدام واجهة ريآكت المُنسقة مستخدمًا بوتستراب 4.

المتطلبات الأساسية

ستحتاج لإتمام هذه المقالة وتطبيق ما فيها إلى ما يلي:

الخطوة الأولى - إنشاء بيئة بايثون افتراضية وتثبيت الاعتماديات

ستنشئ في هذه الخطوة بيئةً افتراضيةً وتثبت الاعتمادات اللازمة لتطبيقك بما فيها جانغو وإطار عمل جانغو ريست وحزمة django-cors-headers.

سيستخدم تطبيقنا خادمي تطوير مختلفين لجانغو وريآكت، وسيعملان على منفذين مختلفين وبمثابة نطاقين منفصلين، ولهذا نحتاج تفعيل سياسة تعدد الموارد cross-origin resource sharing -أو اختصارًا CORS- لإرسال طلبات من نوع HTTP من ريآكت إلى جانغو دون التعرض للحظر على يدي المتصفح.

انتقل إلى المجلد الأساسي وأنشئ بيئة افتراضية باستخدام وحدة venv في بايثون 3:

$ cd ~
$ python3 -m venv ./env

فعّل البيئة الافتراضية التي أنشأتها مستخدمًا الأمر source:

$ source env/bin/activate

ثم ثبّت اعتماديات المشروع مستخدمًا pip، وهذه الاعتمادات هي:

  • جانغو: وهو إطار عمل ويب المشروع.
  • إطار عمل جانغو ريست: تطبيق جانبي (منشأ من طرف ثالث) يبني واجهات برمجة التطبيقات لريست بواسطة جانغو.
  • حزمة django-cors-headers: وهي حزمة تفعّل سياسة تعدد الموارد CORS.

ثبّت إطار عمل جانغو بالأمر التالي:

(env) ali@ubuntu:$ pip install django djangorestframework django-cors-headers

والآن أصبح بإمكانك بعد أن ثبتّت اعتمادات المشروع أن تنشئ مشروع جانغو وواجهة ريآكت الأمامية.

الخطوة الثانية - إنشاء مشروع جانغو

سنولّد في هذه الخطوة مشروع جانغو باستخدام التعليمات والأدوات التالية:

  • أداة django-admin startproject project-name، إذ تُعد django-admin أداةً تعمل في بيئة سطر الأوامر وتُستخدم لإتمام المهام بواسطة جانغو. ينشئ الأمرstartproject مشروع جانغو جديد.
  • أداة سكريبت python manage.py startapp myapp، إذ يُعد manage.py سكريبت برمجي يضاف تلقائيًا لكل مشروع جانغو وهو يؤدي عددًا من المهام الإدارية، مثل إنشاء تطبيقات جديدة وتهجير ملفات قاعدة البيانات وخدمة مشروع جانغو محلي. ينشئ الأمر startapp التابع لهذا السكريبت تطبيق جانغو داخل مشروع جانغو. يُقصد بالمصطلح "تطبيق application" في جانغو حزمة بايثون توفّر بعض المزايا في المشروع.

ولكي تبدأ، أنشئ مشروع جانغو باستخدام الأمر django-admin startproject، وسنطلق على التطبيق اسم "djangoreactproject":

(env) ali@ubuntu:$ django-admin startproject djangoreactproject

سننظر إلى بنية المجلد لمشروع جانغو الذي نعمل عليه باستخدام الأمر tree قبل المتابعة.

نصيحة: الأمر tree هو أمرٌ مفيد لعرض بُنى الملفات والمجلدات من مستوى سطر الأوامر، يمكنك تثبيته مستخدمًا الأمر التالي:

$ sudo apt-get install tree

ولكي تستخدمه انتقل إلى المجلد الذي تريد رؤية بنيته واكتب tree أو اكتب المسار كاملًا وصولًا إلى نقطة البداية "tree /home/ali/ali-project".

انتقل إلى المجلد djangoreactproject ضمن جذر مشروعك ونفذ الأمر tree:

(env) ali@ubuntu:$ cd ~/djangoreactproject
(env) ali@ubuntu:$ tree

سيظهر لك الخرج التالي:

├── djangoreactproject
   ├── __init__.py
   ├── settings.py
   ├── urls.py
   └── wsgi.py
└── manage.py

يمثّل المجلد "djangoreactproject/~" جذر المشروع، وتوجد داخله عدة ملفات مهمة لمشروعك، هي:

  • "manage.py": وهو سكريبت خدمي يؤدي عددًا من المهام الإدارية.
  • "settings.py": هو ملف التهيئة الرئيسي لمشروع جانغو، حيث يمكنك تعديل إعدادات المشروع، إذ تتضمن هذه الإعدادات متحولاتٍ، مثل INSTALLED_APPS الذي يحدد قائمة التطبيقات النشطة في مشروعك. ويوجد في توثيق جانغو معلومات أعمق عن الإعدادات المتاحة.
  • "urls.py": يحتوي هذا الملف على قائمة بنماذج محددات الموارد الموحدة URL والعروض المرتبطة بها، إذ يُنظِّم كل نموذج اتصالًا بين عنوان محدد موارد موحد URL والدالة التي يجب أن تُستدعى لذلك العنوان URL.

أولى خطواتنا للعمل في المشروع هي ضبط الحزم التي ثبتناها في الخطوة السابقة ومن بينها إطار عمل جانغو ريست وحزمة جانغو CORS، وذلك بإضافتهما إلى الملف "settings.py". افتح الملف مستخدمًا الأمر nano أو محرر النصوص المفضل لديك:

(env) ali@ubuntu:$ nano ~/djangoreactproject/djangoreactproject/settings.py

انتقل إلى الإعداد INSTALLED_APPS وأضف التطبيقين "rest_framework" و "corsheaders" أسفل القائمة:

...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders'
]

ثم أضف البرمجية الوسيطة corsheaders.middleware.CorsMiddleware من حزمة سياسة تعدد الموارد CORS التي ثبتناها سابقًا إلى الإعداد MIDDLEWARE، الذي يمثّل قائمةً بالبرمجيات الوسيطة middlewares؛ وهو صنف بايثون يحتوي على شيفرة تُعالج في كل مرة يتعامل تطبيق الويب مع طلب أو استجابة:

...

MIDDLEWARE = [
...
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware'
]

يمكنك بعد ذلك تفعيل CORS، إذ يحدّد الإعداد CORS_ORIGIN_ALLOW_ALL ما إذا كنت ستسمح بسياسة تعدد الموارد لكل النطاقات أم لا، والإعداد CORS_ORIGIN_WHITELIST هو صف بايثون يحتوي على عناوين URL المسموحة.

سيعمل خادم تطوير ريآكت على العنوان "http://localhost:3000"، لذلك سنضيف الإعدادين التاليين إلى ملف الإعدادات "settings.py":

CORS_ORIGIN_ALLOW_ALL = False
(,'CORS_ORIGIN_WHITELIST('localhost:3000

حيث يمكنك إضافة هذين الإعدادين في أي مكان تشاء من هذا الملف:

...
CORS_ORIGIN_ALLOW_ALL = False

CORS_ORIGIN_WHITELIST = (
       'localhost:3000',
)

ويمكنك العثور على المزيد من خيارات التهيئة في وثائق django-cors-headers.

احفظ الملف واخرج من محرر النصوص عندما تنتهي.

نحتاج إلى إنشاء تطبيق جانغو جديد داخل المجلد "djangoreactproject/~" اسمه "customers":

(env) ali@ubuntu:$ python manage.py startapp customers

سيحتوي هذا التطبيق على النماذج models والعروض views لإدارة العملاء؛ إذ تعرّف النماذجُ الحقولَ والسلوكات لبيانات التطبيق؛ بينما تمكّن العروض تطبيقنا من التعامل مع الطلبات على النحو المناسب وإعادة الاستجابات المطلوبة على النحو المناسب أيضًا.

بعد ذلك، أضف التطبيق إلى قائمة التطبيقات المثبتة في ملف المشروع "settings.py" لكي يتعرف جانغو عليه على أنه جزءٌ من المشروع. افتح الملف "settings.py" مرةً أخرى:

(env) ali@ubuntu:$ nano ~/djangoreactproject/djangoreactproject/settings.py

أضف التطبيق "customers":

...
INSTALLED_APPS = [
    ...
    'rest_framework',
    'corsheaders',
    'customers'
]

بعد ذلك، هجّر قاعدة البيانات وشغّل خادم التطوير المحلي، إذ تُعد التهجيرات طريقة جانغو في نشر التغييرات التي تجريها على نماذجك إلى مخطط قاعدة بياناتك، ويمكن أن تتضمن هذه التغييرات أشياء مختلفة، مثل إضافة حقل، أو حذف نموذج.

هجّر قاعدة البيانات كما يلي:

(env) ali@ubuntu:$ python manage.py migrate

وشغّل خادم التطوير المحلي:

(env) ali@ubuntu:$ python manage.py runserver

سترى خرجًا يشابه التالي:

Performing system checks...

System check identified no issues (0 silenced).
October 22, 2018 - 15:14:50
Django version 2.1.2, using settings 'djangoreactproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

سيعمل تطبيق الويب من خلال العنوان "http://127.0.0.1:8000"، وإذا انتقلت إلى هذا العنوان في متصفحك فيفترض أن ترى الصفحة التالية:

نتيجة عمل تطبيق الويب من خلال العنوان "http://127.0.0.1:8000"

دع التطبيق يعمل وافتح نافذة طرفية جديدة لمتابعة تطوير المشروع.

الخطوة الثالثة - إنشاء الواجهة الأمامية لريآكت

سننشئ في هذا القسم تطبيق الواجهة الأمامية لمشروعنا مستخدمين ريآكت؛ إذ يوجد لدى ريآكت برنامج خدمي رسمي يسمح لك بتوليد مشاريع ريآكت بسرعة دون الحاجة إلى ضبط مجمع الوحدات Webpack، التي تمثّل مجمع وحدات module bundler يُستخدم لتجميع أصول الويب، مثل شيفرة جافا سكريبت و CSS والصور. قبل أن تتمكن من استخدام Webpack، ستحتاج عادةً إلى تحديد خيارات ضبط متعددة، لكن بفضل الأداة create-react-app لن تحتاج للتعامل مع Webpack مباشرةً حتى تقرر أو عندما تكون بحاجةٍ لمزيدٍ من التحكم. يمكنك استخدام أداة npx (وهي أداة تنفّذ الملفات الثنائية لحزم npm) لتشغيل create-react-app.

تأكد في نافذة الطرفية الثانية أنك موجود داخل مجلد المشروع:

$ cd ~/djangoreactproject

أنشئ مشروع ريآكت اسمه "frontend" باستخدام create-react-app و npx:

$ npx create-react-app frontend

ثم انتقل إلى داخل تطبيق ريآكت وشغّل خادم التطوير:

$ cd ~/djangoreactproject/frontend
$ npm start

سيعمل تطبيقك باستخدام العنوان "/http://localhost:3000":

عمل التطبيق باستخدام العنوان "/http://localhost:3000"

دع خادم تطوير ريآكت يعمل وافتح نافذة طرفية جديدة للمتابعة.

ولرؤية بنية مجلدات كامل المشروع عند هذه النقطة، انتقل إلى المجلد الجذر ونفّذ الأمر tree مرةً أخرى:

$ cd ~/djangoreactproject
$ tree

سترى بنيةً تشبه التالية:

├── customers
   ├── admin.py
   ├── apps.py
   ├── __init__.py
   ├── migrations
      └── __init__.py
   ├── models.py
   ├── tests.py
   └── views.py
├── djangoreactproject
   ├── __init__.py
   ├── __pycache__
   ├── settings.py
   ├── urls.py
   └── wsgi.py
├── frontend
   ├── package.json
   ├── public
      ├── favicon.ico
      ├── index.html
      └── manifest.json
   ├── README.md
   ├── src
      ├── App.css
      ├── App.js
      ├── App.test.js
      ├── index.css
      ├── index.js
      ├── logo.svg
      └── registerServiceWorker.js
   └── yarn.lock
└── manage.py

سيستخدم تطبيقنا بوتستراب 4 لتحيدد تنسيق style واجهة ريآكت، لذلك سنضمّنه في الملف "frontend/src/App.css" الذي يدير إعدادات CSS. افتح الملف:

$ nano ~/djangoreactproject/frontend/src/App.css

أضف تعليمة الاستيراد import إلى بداية الملف. يمكن حذف المحتوى الحالي للملف رغم أن هذا غير مطلوب:

@import  'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css';

تعليمة import@ هي تعليمة CSS تُستخدم لاستيراد قواعد التنسيق style rules من صفحات تنسيق أخرى.

الآن بعد أن انتهينا من إنشاء تطبيق الواجهة الأمامية والواجهة الخلفية، دعنا ننشئ نموذج العميل Customer وبعض البيانات التجريبية.

الخطوة الرابعة - إنشاء نموذج العميل وبيانات التهيئة

بعد أن انتهينا من إنشاء تطبيق جانغو والواجهة الأمامية لريآكت، ستكون خطوتنا التالية هي إنشاء نموذج العميل Customer، الذي يمثّل جدول قاعدة البيانات الذي سيخزن معلومات العملاء. لن تحتاج إلى أي أوامر SQL، إذ سيتولى الرابط العلائقي للكائنات في جانغو Object Relational Mapper -أو اختصارًا ORM- أمر العمليات التي تجري على قاعدة البيانات من خلال إسقاط أصناف بايثون ومتحولاته على جداول SQL وأعمدته، وبهذه الطريقة يكون الرابط العلائقي للكائنات في جانغو قد جرّد تفاعلات SQL مع قاعدة البيانات من خلال واجهة بايثون.

فعّل البيئة الافتراضية من جديد:

$ cd ~
$ source env/bin/activate

انتقل إلى المجلد "customers" وافتح الملف "models.py"، وهو ملف بايثون يخزن كافة نماذج تطبيقك:

(env) ali@ubuntu:$ cd ~/djangoreactproject/customers/
(env) ali@ubuntu:$ nano models.py

سيحتوي الملف على المحتوى التالي:

from django.db import models
# Create your models here.

نشير إلى أنه قد استوردت واجهة برمجة التطبيقات لنموذج العملاء سلفًا ضمن الملف بفضل تعليمة الاستيرادfrom django.db import models. ستضيف الآن الصنف Customer المشتق من الصنف models.Model. كل نموذج في جانغو هو صنف بايثون مشتق من django.db.models.Model.

سيكون في نموذج العميل "Customer" حقول قاعدة البيانات التالية:

  • first_name: الاسم الأول للعميل.
  • last_name: اسم العائلة للعميل.
  • email: البريد الإلكتروني للعميل.
  • phone: رقم هاتف العميل.
  • address: عنوان العميل.
  • description: وصف العميل.
  • createdAt: تاريخ إضافة العميل.

سنضيف أيضًا الدالة ()__str__ التي تعرّف طريقة عرض النموذج، والذي سيكون في حالتنا مع الاسم الأول للعميل. ولمزيدٍ من التفاصيل عن إنشاء الأصناف وتعريف الكائنات يرجى الاطلاع على المقالة كيف تبني الأصناف وتعرف الكائنات في بايثون 3.

أضف الشيفرة التالية إلى الملف "models.py":

from django.db import models

class Customer(models.Model):
    first_name = models.CharField("First name", max_length=255)
    last_name = models.CharField("Last name", max_length=255)
    email = models.EmailField()
    phone = models.CharField(max_length=20)
    address =  models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    createdAt = models.DateTimeField("Created At", auto_now_add=True)

    def __str__(self):
        return self.first_name

ثم هجّر قاعدة البيانات لإنشاء جداول قاعدة البيانات. ينشئ الأمر makemigrations ملفات التهجير التي تضاف فيها التغييرات على النموذج، في حين يطبّق migrate التغييرات في ملفات التهجير إلى قاعدة البيانات.

عُد الآن إلى المجلد الجذر للمشروع:

(env) ali@ubuntu:$ cd ~/djangoreactproject

ونفذ ما يلي من أجل إنشاء ملفات التهجير:

(env) ali@ubuntu:$ python manage.py makemigrations

ستحصل على خرج يشبه التالي:

customers/migrations/0001_initial.py
    - Create model Customer

طبّق هذه التغييرات على قاعدة البيانات:

(env) ali@ubuntu:$ python manage.py migrate

وسترى خرجًا يشير إلى نجاح عملية التهجير:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, customers, sessions
Running migrations:
  Applying customers.0001_initial... OK

ستستخدم الآن ملف تهجير البيانات لإنشاء بيانات العميل المبدئية،إن أن ملف تهجير البيانات هو تهجير يضيف البيانات أو يعدّلها في قاعدة البيانات. أنشئ ملف تهجير بيانات فارغ للتطبيق customers:

(env) ali@ubuntu:$ python manage.py makemigrations --empty --name customers customers

سترى التأكيد التالي مع اسم ملف التهجير:

Migrations for 'customers':
  customers/migrations/0002_customers.py

لاحظ أن اسم ملف التهجير هو "customers.py‏_0002".

انتقل بعد ذلك إلى داخل مجلد التهجيرات للتطبيق customers.

(env) ali@ubuntu:$ cd ~/djangoreactproject/customers/migrations

افتح ملف التهجير المنشأ للتو:

(env) ali@ubuntu:$ nano 0002_customers.py

وهذا هو محتوى التهيئة للملف:

from django.db import migrations

class Migration(migrations.Migration):
    dependencies = [
        ('customers', '0001_initial'),
    ]
    operations = [
    ]

تستورد تعليمة الاستيراد واجهة برمجة التطبيقات للملف "migrations" وواجهة برمجة التطبيقات لجانغو من أجل إنشاء التهجيرات من الملف "django.db" والتي هي حزمة مبنية مسبقًا built-in تحتوي على الأصناف اللازمة للعمل مع قواعد البيانات.

الصنف Migration هو صنف بايثون يصف العمليات التي تُنفذ عند تهجير قواعد البيانات، وهو امتداد للصنف migrations.Migration، ويملك قائمتين:

  • dependencies: تحتوي على التهجيرات المعتمدة.
  • operations: تحتوي على العمليات التي ستُنفذ عندما نطبّق التهجير.

الآن، أضف تابعًا لإنشاء بيانات تجريبية للعملاء قبل تعريف الصنف Migration:

...
def create_data(apps, schema_editor):
    Customer = apps.get_model('customers', 'Customer')
    Customer(first_name="Customer 001", last_name="Customer 001", email="customer001@email.com", phone="00000000", address="Customer 000 Address", description= "Customer 001 description").save()

...

أمسكنا grab في هذا التابع الصنف Customer التابع للتطبيق customers وأنشأنا عميلًا تجريبيًّا لإدخاله إلى قاعدة البيانات.

للحصول على الصنف Customer، الذي سيمكّن من إنشاء عملاء جدد، نستخدم التابع ()get_model للكائن apps، الذي يمثّل مسجلًا registry للتطبيقات المثبتة ونماذج قواعد البيانات الخاصة بها.

سيمرّر الكائن apps من التابع ()RunPython عندما نستخدمه لتشغيل ()create_data. أضف التابع ()migrations.RunPython إلى القائمة الفارغة operations:

...
    operations = [
        migrations.RunPython(create_data),
    ]

يُعد التابع ()RunPython جزءًا من واجهة برمجة التطبيقات للتهجيرات التي تسمح لك بتشغيل شيفرة بايثون معينة في تهجير ما. تحدد القائمة operations أن هذا التابع سيُنفّذ عندما نطبق التهجير.

وفيما يلي الملف كاملًا:

from django.db import migrations

def create_data(apps, schema_editor):
    Customer = apps.get_model('customers', 'Customer')
    Customer(first_name="Customer 001", last_name="Customer 001", email="customer001@email.com", phone="00000000", address="Customer 000 Address", description= "Customer 001 description").save()

class Migration(migrations.Migration):
    dependencies = [
        ('customers', '0001_initial'),
    ]
    operations = [
        migrations.RunPython(create_data),
    ]

ولمزيدٍ من المعلومات عن تهجيرات البيانات، راجع التوثيق تهجير البيانات في جانغو.

الآن لتهجير قاعدة بياناتك، عُد أولًا إلى مجلد الجذر لمشروعك:

(env) ali@ubuntu:$ cd ~/djangoreactproject

هجّر قاعدة بياناتك لتنشئ البيانات التجريبية:

(env) ali@ubuntu:$ python manage.py migrate

سنرى خرجًا يؤكد على نجاح عملية التهجير:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, customers, sessions
Running migrations:
  Applying customers.0002_customers... OK

ولمزيدٍ من المعلومات عن العملية، راجع مقالة كيف تنشئ نماذج جانغو.

وهكذا بعد إنشاء نموذج العميل والبيانات التوضيحية، يمكننا المتابعة إلى بناء واجهة برمجة التطبيقات لريست REST API.

الخطوة الخامسة - إنشاء واجهة برمجة التطبيقات لريست REST API

سننشئ في هذه الخطوة واجهة برمجة التطبيقات لريست باستخدام إطار عمل جانغو ريست، إذ سننشئ عدة عروض مختلفة لواجهة برمجة التطبيقات؛ إذ يُقصد بعرض واجهة برمجة التطبيقات API view دالة تعالج طلب واجهة برمجة تطبيقات أو استدعاء واجهة برمجة التطبيقات؛ في حين أن نقطة نهاية واجهة برمجة التطبيقات هي عنوان URL متفرد يمثّل نقطة تماس مع نظام ريست. على سبيل المثال، عندما يرسل المستخدم طلب من نوع GET إلى نقطة نهاية واجهة برمجة التطبيقات، يستدعي جانغو الدالة المقابلة، أو عرض واجهة برمجة التطبيقات لمعالجة الطلب وإعادة أي نتائج محتملة.

سنستفيد أيضًا من المسلسِلات Serializers؛ إذ يسمح المسلسِل في إطار عمل جانغو بأن تُحوّل نسخ النماذج المعقدة ومجموعات الاستعلام QuerySets إلى صيغة محتوى JSON من أجل استهلاك واجهة برمجة التطبيقات، ويمكن أن يعمل صنف المسلسِل في الاتجاه الآخر أيضًا، إذ يزوِّد بآليات لتحليل وإلغاء سَلسَلة البيانات إلى نماذج جانغو ومجموعات استعلامه.

ستتضمن نقاط نهاية واجهة برمجة التطبيقات ما يلي:

  • "api/customers": تُستخدم نقطة النهاية هذه لإنشاء عملاء وإعادة مجموعات العملاء المرقمة.
  • "<api/customers/<pk": تُستخدم نقطة النهاية هذه للحصول على عملاء منفردين وتحديثهم وحذفهم بالاستعانة بمفتاح أساسي أو معرّف id.

سننشئ أيضًا عناوين URL في الملف "urls.py" التابع للمشروع من أجل نقاط النهاية المقابلة، أي "api/customers" و "<api/customers/<pk".

دعنا نبدأ بإنشاء الصنف serializer من أجل نموذجنا Customer.

إضافة صنف المسسلسل Serializer

يُعد إنشاء صنف مسلسِل لنموذج "Customer" ضروري لتحويل نسخ العملاء ومجموعات الاستعلام QuerySets من وإلى JSON، ولإنشاء الصنف المسلسِل، أنشئ أولًا الملف "serializers.py" داخل التطبيق "customers":

(env) ali@ubuntu:$ cd ~/djangoreactproject/customers/
(env) ali@ubuntu:$ nano serializers.py

أضف الشيفرة التالية لاستيراد واجهة برمجة التطيبقات للمسلسِلات ونموذج العميل Customer:

from rest_framework import serializers
from .models import Customer

ثم أنشئ صنف مسلسِل مُمتد عن الصنف serializers.ModelSerializer ويحدد الحقول التي ستُسلسَل:

...
class CustomerSerializer(serializers.ModelSerializer):

    class Meta:
        model = Customer 
        fields = ('pk','first_name', 'last_name', 'email', 'phone','address','description')

يحدد الصنف Meta النموذج والحقول التي ستُسلسَل، وهي:

  • pk
  • first_name
  • last_name
  • email
  • phone
  • address
  • description

وهذا هو المحتوى الكامل لملف serializers.py:

from rest_framework import serializers
from .models import Customer

class CustomerSerializer(serializers.ModelSerializer):

    class Meta:
        model = Customer 
        fields = ('pk','first_name', 'last_name', 'email', 'phone','address','description')

الآن وبعد أن انتهينا من إنشاء صنف المسلسِل، يمكننا إضافة عروض واجهة برمجة التطبيقات.

إضافة عروض واجهة برمجة التطبيقات

سننشئ في هذا القسم عروض API لتطبيقنا الذي سيستدعيه جانغو عندما يزور المستخدمُ نقطةَ النهاية المقابلة لدالة العرض.

افتح الملف التالي:

(env) ali@ubuntu:$ nano ~/djangoreactproject/customers/views.py

احذف محتوى الملف وأضف أوامر الاستيراد التالية:

from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import Customer 
from .serializers import *

نحن نستورد المسلسِل الذي أنشأناه إلى جانب نموذج العميل Customer وجانغو وواجهات برمجة التطبيقات لجانغو ريست.

بعد ذلك، أضف العرض الذي سيتولى معالجة طلبات من نوع POST و GET HTTP:

...

@api_view(['GET', 'POST'])
def customers_list(request):
    """
 List  customers, or create a new customer.
 """
    if request.method == 'GET':
        data = []
        nextPage = 1
        previousPage = 1
        customers = Customer.objects.all()
        page = request.GET.get('page', 1)
        paginator = Paginator(customers, 10)
        try:
            data = paginator.page(page)
        except PageNotAnInteger:
            data = paginator.page(1)
        except EmptyPage:
            data = paginator.page(paginator.num_pages)

        serializer = CustomerSerializer(data,context={'request': request} ,many=True)
        if data.has_next():
            nextPage = data.next_page_number()
        if data.has_previous():
            previousPage = data.previous_page_number()

        return Response({'data': serializer.data , 'count': paginator.count, 'numpages' : paginator.num_pages, 'nextlink': '/api/customers/?page=' + str(nextPage), 'prevlink': '/api/customers/?page=' + str(previousPage)})

    elif request.method == 'POST':
        serializer = CustomerSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

نستخدم أولًا المزخرِف (['api_view(['GET', 'POST@ لإنشاء عرض واجهة برمجة تطبيقات يمكنه قبول طلبات GET و POST، إذ يُعد المزخرِف decorator دالة تأخذ دالةً أخرى وتمدّدها extend تلقائيًا.

نستخدم المتغيّر request.method داخل متن التابع لفحص تابع HTTP الحالي وتنفيذ المنطق المقابل اعتمادًا على نوع الطلب:

  • إذا كان الطلب من النوع GET، يرقّم التابع البيانات باستخدام مرقّم جانغو Django Paginator ويعيد الصفحة الأولى من البيانات بعد السَّلسَلَة، وعدد العملاء المتوفرين وعدد الصفحات المتوفرة والروابط إلى الصفحات السابقة واللاحقة. المرقّم paginator هو صنف جانغو داخلي يقسّم قوائم البيانات في عدة صفحات ويزوّد بتوابع للوصول إلى عناصر كل صفحة.
  • إذا كان الطلب من النوع POST، يسلسِل التابع بيانات العميل المستلمة ثم يستدعي التابع ()save لكائن المسلسِل، ثم يعيد كائن استجابة Response وهو نسخة عن استجابة http أو HttpResponse مع رمز الحالة 201. كل عرض تنشؤه مسؤول عن إعادة كائن HttpResponse. يحفظ التابع ()save البيانات المسلسَلة في قاعدة البيانات.

أضف الآن عرض واجهة برمجة التطبيقات الذي سيكون مسؤولًا عن معالجة طلبات GET و PUT و DELETE من أجل جلب وتحديث وحذف العملاء اعتمادًا على المفتاح الأساسي pk:

...
@api_view(['GET', 'PUT', 'DELETE'])
def customers_detail(request, pk):
 """
 Retrieve, update or delete a customer by id/pk.
 """
    try:
        customer = Customer.objects.get(pk=pk)
    except Customer.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = CustomerSerializer(customer,context={'request': request})
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = CustomerSerializer(customer, data=request.data,context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        customer.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

زُخرِف التابعُ باستخدام (['api_view(['GET', 'PUT', 'DELETE@ للإشارة إلى أنه عرض واجهة برمجة تطبيقات API view يمكنه قبول طلبات من النوع GET و PUT و DELETE.

التحقق الموجود في الحقل request.method يتحقق من تابع الطلب واعتمادًا على قيمته يستدعي المنطق المناسب:

  • إذا كان الطلب من النوع GET، تُسَلسَلُ بياناتُ العميل وترسلُ باستخدام كائن استجابة Response.
  • إذا كان الطلب من النوع PUT، ينشئ التابع مسلسِلًا لبيانات العميل الجديد، ثم يستدعي التابع ()save لكائن المسلسَل المُنشأ، وأخيرًا يرسل كائن الاستجابة Response مع العميل المحدّث.
  • إذا كان الطلب من النوع DELETE، يستدعي التابعُ التابعَ ()delete لكائن العميل لحذفه، ثم يعيد كائن استجابة دون بيانات.

يبدو ملف "views.py" المكتمل كما يلي:

from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import Customer 
from .serializers import *


@api_view(['GET', 'POST'])
def customers_list(request):
    """
 List  customers, or create a new customer.
 """
    if request.method == 'GET':
        data = []
        nextPage = 1
        previousPage = 1
        customers = Customer.objects.all()
        page = request.GET.get('page', 1)
        paginator = Paginator(customers, 5)
        try:
            data = paginator.page(page)
        except PageNotAnInteger:
            data = paginator.page(1)
        except EmptyPage:
            data = paginator.page(paginator.num_pages)

        serializer = CustomerSerializer(data,context={'request': request} ,many=True)
        if data.has_next():
            nextPage = data.next_page_number()
        if data.has_previous():
            previousPage = data.previous_page_number()

        return Response({'data': serializer.data , 'count': paginator.count, 'numpages' : paginator.num_pages, 'nextlink': '/api/customers/?page=' + str(nextPage), 'prevlink': '/api/customers/?page=' + str(previousPage)})

    elif request.method == 'POST':
        serializer = CustomerSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(['GET', 'PUT', 'DELETE'])
def customers_detail(request, pk):
    """
 Retrieve, update or delete a customer by id/pk.
 """
    try:
        customer = Customer.objects.get(pk=pk)
    except Customer.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = CustomerSerializer(customer,context={'request': request})
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = CustomerSerializer(customer, data=request.data,context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        customer.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

يمكننا الآن المتابعة إلى إنشاء نقاط النهاية.

إضافة نقاط النهاية لواجهة برمجة التطبيقات

سننشئ الآن نقطتي النهاية لواجهة برمجة التطبيقات، الأولى /‎api/customers من أجل الاستعلام عن العملاء وإنشائهم، والثانية <api/customers/<pk من أجل جلب وتحديث وحذف العملاء المنفردين اعتمادًا على المفاتيح الأساسية لكل منهم.

افتح الملف التالي:

(env) ali@ubuntu:$ nano ~/djangoreactproject/djangoreactproject/urls.py

دع المحتويات كما هي ولكن أضف الاستيراد إلى عروض customers في أعلى الملف:

from django.contrib import admin
from django.urls import path
from customers import views
from django.conf.urls import url

ثم أضف عنواني URL لكلٍ من /api/customers و <api/customers/<pk إلى قائمة نماذج عناوين URL urlpatterns التي تحتوي على عناوين URLs للتطبيق:

...

urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^api/customers/$', views.customers_list),
    url(r'^api/customers/(?P<pk>[0-9]+)$', views.customers_detail),
]

الآن بعد أن انتهينا من إنشاء نقاط النهاية لريست، لنرى كيف نستهلكها.

الخطوة السادسة - استهلاك واجهة برمجة التطبيقات لريست باستخدام اكسيوس Axios

سنثبت في هذه الخطوة Axios، عميل HTTP الذي سنستخدمه لإنشاء استدعاءات واجهة برمجة التطبيقات. سننشئ أيضًا صنفًا لاستهلاك نقاط النهاية لواجهة برمجة التطبيقات التي أنشأناها.

نبدأ بإيقاف تنشيط البيئة الافتراضية:

(env) ali@ubuntu:$ deactivate

ثم ننتقل إلى مجلد الواجهة الأمامية "frontend":

(env) ali@ubuntu:$ cd ~/djangoreactproject/frontend

ثبت اكسيوس axios من npm باستخدام الأمر التالي:

(env) ali@ubuntu:$ npm install axios --save

يضيف الخيار save– اعتمادية axios إلى ملف التطبيق "package.json".

ثم أنشئ ملف جافا سكريبت اسمه "CustomersService.js" الذي سيحتوي على الشيفرة التي ستستدعي واجهات برمجة التطبيقات لريست. سنفعّل هذا داخل مجلد "src"، حيث توجد شيفرة التطبيق لمشروعنا:

$ cd src
$ nano CustomersService.js

أضف الشيفرة التالية التي تحتوي على توابع ستتصل بواجهات برمجة التطبيقات لجانغو ريست:

import axios from 'axios';
const API_URL = 'http://localhost:8000';

export default class CustomersService{

    constructor(){}


    getCustomers() {
        const url = `${API_URL}/api/customers/`;
        return axios.get(url).then(response => response.data);
    }  
    getCustomersByURL(link){
        const url = `${API_URL}${link}`;
        return axios.get(url).then(response => response.data);
    }
    getCustomer(pk) {
        const url = `${API_URL}/api/customers/${pk}`;
        return axios.get(url).then(response => response.data);
    }
    deleteCustomer(customer){
        const url = `${API_URL}/api/customers/${customer.pk}`;
        return axios.delete(url);
    }
    createCustomer(customer){
        const url = `${API_URL}/api/customers/`;
        return axios.post(url,customer);
    }
    updateCustomer(customer){
        const url = `${API_URL}/api/customers/${customer.pk}`;
        return axios.put(url,customer);
    }
}

سيستدعي الصنف CustomersService توابع اكسيوس Axios التالية:

  • ()getCustomers: يحصل على الصفحة الأولى من العملاء.
  • ()getCustomersByURL: يحصل على العملاء بواسطة عنوان URL، ويتيح هذا بدوره الحصول على الصفحات التالية من العملاء بتمرير روابط، مثل "api/customers/?page=2/".
  • ()getCustomer: يحصل على عميل بواسطة مفتاحه الأساسي.
  • ()createCustomer: ينشئ عميلًا.
  • ()updateCustomer: يحدّث عميلًا.
  • ()deleteCustomer: يحذف عميلًا.

نستطيع الآن عرض البيانات من واجهة برمجة التطبيقات الخاصة بنا في واجهة المستخدم لريآكت بإنشاء مكوّن CustomersList.

الخطوة السابعة - عرض البيانات من واجهة برمجة التطبيقات في تطبيق ريآكت

سننشئ في هذه الخطوة مكون ريآكت اسمه: CustomersList، الذي يمثّل جزءًا من واجهة المستخدم، ويمكنك أيضًا من تقسيم واجهة المستخدم إلى أقسام منفصلة وقابلة للاستخدام مجددًا.

أنشئ أولًا الملف "CustomersList.js" في المجلد "frontend/src":

$ nano ~/djangoreactproject/frontend/src/CustomersList.js

ثم ابدأ في استيراد React و Component لإنشاء مكون ريآكت:

import  React, { Component } from  'react';

ثم استورد واستنسخ الوحدة CustomersService التي أنشأتها في الخطوة السابقة، والتي توفر توابع تُربط مع الواجهة الخلفية لواجهة برمجة تطبيقات ريست REST API:

...
import  CustomersService  from  './CustomersService';

const  customersService  =  new  CustomersService();

ثم أنشئ المكون CustomersList المُمتد عن الصنف Component، الذي سيستدعي واجهة برمجة التطبيقات لريست، إذ يجب أن يمتد مكون ريآكت عن الصنف Component أو أن يكون صنفًا فرعيًا عنه.

أضف الشيفرة التالية إلى الملف "CustomersList.js" لإنشاء مكون ريآكت الذي يمتدّ عن المكوّن react.Component:

...
class  CustomersList  extends  Component {

    constructor(props) {
        super(props);
        this.state  = {
            customers: [],
            nextPageURL:  ''
        };
        this.nextPage  =  this.nextPage.bind(this);
        this.handleDelete  =  this.handleDelete.bind(this);
    }
}
export  default  CustomersList;

هيأنا في الشيفرة كائن الحالة state داخل الباني constructor؛ إذ يحتفظ هذا الكائن بحالة متغيرات مكوّننا باستخدام مصفوفة عملاء فارغة. ستحتفظ هذه المصفوفة بالعملاء والمتغير nextPageURL الذي يحتفظ بعنوان URL للصفحة التالية التي ستستعيدها من واجهة برمجة تطبيقات الواجهة الخلفية.

ربطنا أيضًا التابعين ()nextPage و ()handleDelete بالمؤشر this بحيث يستطاع الوصول إليهما من شيفرة HTML، ثم أضفنا التابع ()componentDidMount واستدعاءً للتابع ()getCustomers ضمن الصنف CustomersList قبل قوس الإغلاق.

يُعد التابع ()componentDidMount تابع دورة حياة المكوّن ويُستدعى عند إنشاء المكوّن وإدخاله إلى نموذج كائن الوثيقة DOM. يستدعي التابع ()getCustomers كائن خدمة العملاء Customers Service للحصول على صفحة البيانات الأولى ورابط الصفحة التالية من الواجهة الخلفية لجانغو:

...
componentDidMount() {
    var  self  =  this;
    customersService.getCustomers().then(function (result) {
        self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
    });
}

أضف تحت التابع ()componentDidMount بالضبط التابع ()handleDelete، الذي يتولى مهمة حذف عميل:

...
handleDelete(e,pk){
    var  self  =  this;
    customersService.deleteCustomer({pk :  pk}).then(()=>{
        var  newArr  =  self.state.customers.filter(function(obj) {
            return  obj.pk  !==  pk;
        });
        self.setState({customers:  newArr})
    });
}

يستدعي التابعُ ()handleDelete التابعَ ()deleteCustomer لحذف عميل باستخدام مفتاحه الأساسي، وإذا كانت العملية ناجحة، ستُرشّح filtered المصفوفة لتعكس آخر حالة بعد حذف ذلك العميل.

بعد ذلك، أضف التابع ()nextPage لتحصل على بيانات الصفحة التالية وحدّث رابط الصفحة التالية:

...
nextPage(){
    var  self  =  this;
    customersService.getCustomersByURL(this.state.nextPageURL).then((result) => {
        self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
    });
}

يستدعي التابعُ ()nextPage التابعَ ()getCustomersByURL، الذي يأخذ عنوان URL للصفحة التالية من كائن الحالة this.state.nextPageURL ويحدّث مصفوفة العملاء customers بالبيانات المعادة.

وأخيرًا، أضف التابع ()render، الذي يصيّر جدولًا للعملاء من حالة المكوّن:

...
render() {

    return (
    <div  className="customers--list">
        <table  className="table">
            <thead  key="thead">
            <tr>
                <th>#</th>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Phone</th>
                <th>Email</th>
                <th>Address</th>
                <th>Description</th>
                <th>Actions</th>
            </tr>
            </thead>
            <tbody>
                {this.state.customers.map( c  =>
                <tr  key={c.pk}>
                    <td>{c.pk}  </td>
                    <td>{c.first_name}</td>
                    <td>{c.last_name}</td>
                    <td>{c.phone}</td>
                    <td>{c.email}</td>
                    <td>{c.address}</td>
                    <td>{c.description}</td>
                    <td>
                    <button  onClick={(e)=>  this.handleDelete(e,c.pk) }> Delete</button>
                    <a  href={"/customer/" + c.pk}> Update</a>
                    </td>
                </tr>)}
            </tbody>
        </table>
        <button  className="btn btn-primary"  onClick=  {  this.nextPage  }>Next</button>
    </div>
    );
}

وفيما يلي يظهر المحتوى الكامل للملف "CustomersList.js":

import  React, { Component } from  'react';
import  CustomersService  from  './CustomersService';

const  customersService  =  new  CustomersService();

class  CustomersList  extends  Component {

constructor(props) {
    super(props);
    this.state  = {
        customers: [],
        nextPageURL:  ''
    };
    this.nextPage  =  this.nextPage.bind(this);
    this.handleDelete  =  this.handleDelete.bind(this);
}

componentDidMount() {
    var  self  =  this;
    customersService.getCustomers().then(function (result) {
        console.log(result);
        self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
    });
}
handleDelete(e,pk){
    var  self  =  this;
    customersService.deleteCustomer({pk :  pk}).then(()=>{
        var  newArr  =  self.state.customers.filter(function(obj) {
            return  obj.pk  !==  pk;
        });

        self.setState({customers:  newArr})
    });
}

nextPage(){
    var  self  =  this;
    console.log(this.state.nextPageURL);        
    customersService.getCustomersByURL(this.state.nextPageURL).then((result) => {
        self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
    });
}
render() {

    return (
        <div  className="customers--list">
            <table  className="table">
            <thead  key="thead">
            <tr>
                <th>#</th>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Phone</th>
                <th>Email</th>
                <th>Address</th>
                <th>Description</th>
                <th>Actions</th>
            </tr>
            </thead>
            <tbody>
            {this.state.customers.map( c  =>
                <tr  key={c.pk}>
                <td>{c.pk}  </td>
                <td>{c.first_name}</td>
                <td>{c.last_name}</td>
                <td>{c.phone}</td>
                <td>{c.email}</td>
                <td>{c.address}</td>
                <td>{c.description}</td>
                <td>
                <button  onClick={(e)=>  this.handleDelete(e,c.pk) }> Delete</button>
                <a  href={"/customer/" + c.pk}> Update</a>
                </td>
            </tr>)}
            </tbody>
            </table>
            <button  className="btn btn-primary"  onClick=  {  this.nextPage  }>Next</button>
        </div>
        );
  }
}
export  default  CustomersList;

الآن وبعد أنشأنا المكون CustomersList المسؤول عن عرض قائمة العملاء يمكننا الآن إضافة المكون الذي يتولى مهمة إنشاء العملاء وتحديثهم.

الخطوة الثامنة - إضافة مكون ريآكت لإنشاء العملاء وتحديثهم

سننشئ في هذه الخطوة المكون CustomerCreateUpdate الذي سيتولى مهمة إنشاء وتحديث العملاء، وذلك بإتاحة نموذج تعبئة form للمستخدمين لملئها ببيانات عميل جديد أو تحديث بيانات عميل موجود.

أنشئ الملف "CustomerCreateUpdate.js" داخل المجلد "frontend/src":

$ nano ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

أضف الشيفرة التالية لإنشاء مكون ريآكت الذي يستورد React و Component:

import  React, { Component } from  'react';

يمكنك أيضًا استيراد واستنساخ الصنف CustomersService الذي أنشأناه في الخطوة السابقة والذي يزود بتوابع توصل مع الواجهة الخلفية لواجهة برمجة تطبيقات ريست:

...
import  CustomersService  from  './CustomersService';

const  customersService  =  new  CustomersService();

ثم أنشئ المكون CustomerCreateUpdate المُمتدّ عن Component لإنشاء العملاء وتحديثهم:

...
class  CustomerCreateUpdate  extends  Component {

    constructor(props) {
        super(props);
    }

}
export default CustomerCreateUpdate;

أضف ضمن تعريف الصنف تابعَ المكوّن ()render الذي يصيّر نموذج HTML الذي يجمع معلومات عن العميل.

...
render() {
        return (
          <form onSubmit={this.handleSubmit}>
          <div className="form-group">
            <label>
              First Name:</label>
              <input className="form-control" type="text" ref='firstName' />

            <label>
              Last Name:</label>
              <input className="form-control" type="text" ref='lastName'/>

            <label>
              Phone:</label>
              <input className="form-control" type="text" ref='phone' />

            <label>
              Email:</label>
              <input className="form-control" type="text" ref='email' />

            <label>
              Address:</label>
              <input className="form-control" type="text" ref='address' />

            <label>
              Description:</label>
              <textarea className="form-control" ref='description' ></textarea>


            <input className="btn btn-primary" type="submit" value="Submit" />
            </div>
          </form>
        );
  }

يضيف هذا التابع الخاصية ref للوصول إلى عنصر النموذج التعبئة ووضع قيمة له وذلك من أجل كل عنصر إدخال ضمن نموذج التعبئة.

الآن، عرّف التابعَ (handleSubmit(event فوق التابع ()render لكي تمتلك الوظائف المناسبة عندما ينقر عميل على زر الإرسال submit:

...
handleSubmit(event) {
    const { match: { params } } =  this.props;
    if(params  &&  params.pk){
        this.handleUpdate(params.pk);
    }
    else
    {
        this.handleCreate();
    }
    event.preventDefault();
}

يعالج التابع (handleSubmit(event إرسال النموذج ويستدعي -اعتمادًا على المسار- إما التابع (handleUpdate(pk لتحديث العميل بالمفتاح الأساسي الممرر، أو التابع ()handleCreate لإنشاء عميل جديد. سنعرّف هذين التابعين باختصار.

عد إلى باني المكون، واربط التابع ()handleSubmit بالمؤشر this لكي تتمكن من الوصول إليه في نموذجك:

...
class CustomerCreateUpdate extends Component {

constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
}
...

ثم عرّف التابع ()handleCreate لإنشاء عميل من بيانات نموذج التعبئة، وأضف الشيفرة التالية فوق التابع (handleSubmit(event:

...
handleCreate(){
    customersService.createCustomer(
        {
        "first_name":  this.refs.firstName.value,
        "last_name":  this.refs.lastName.value,
        "email":  this.refs.email.value,
        "phone":  this.refs.phone.value,
        "address":  this.refs.address.value,
        "description":  this.refs.description.value
        }).then((result)=>{
                alert("Customer created!");
        }).catch(()=>{
                alert('There was an error! Please re-check your form.');
        });
}

سوف يُستخدَم التابع ()handleCreate من أجل إنشاء عميل من البيانات المُدخلة، إذ يستدعي التابع المقابل ()CustomersService.createCustomer الذي يجري استدعاءً لواجهة برمجة التطبيقات الفعلية للواجهة الخلفية لينشئ عميلًا جديدًا.

بعد ذلك عرّف التابع (handleUpdate(pk الذي يجري التحديثات أسفل التابع ()handleCreate:

...
handleUpdate(pk){
customersService.updateCustomer(
    {
    "pk":  pk,
    "first_name":  this.refs.firstName.value,
    "last_name":  this.refs.lastName.value,
    "email":  this.refs.email.value,
    "phone":  this.refs.phone.value,
    "address":  this.refs.address.value,
    "description":  this.refs.description.value
    }
    ).then((result)=>{

        alert("Customer updated!");
    }).catch(()=>{
        alert('There was an error! Please re-check your form.');
    });
}

سيحدّث التابع ()updateCustomer العميل ذا المفتاح الأساسي pk بالمعلومات الجديدة التي جلبها من نموذج معلومات العميل، ويستدعي التابع ()customersService.updateCustomer.

الآن، أضف التابع ()componentDidMount، وبالتالي إذا زار المستخدم الوجهة "customer/:pk"، فإننا نريد تعبئة النموذج بمعلومات مرتبطة بالعميل باستخدام المفتاح الأساسي من عنوان URL، ولفعل ذلك يمكننا إضافة التابع (getCustomer(pk بعد أن يُثبّت المكون في حدث دورة الحياة للتابع ()componentDidMount. أضف الشيفرة التالية أسفل باني المكون لإضافة هذا التابع:

...
componentDidMount(){
    const { match: { params } } =  this.props;
    if(params  &&  params.pk)
    {
        customersService.getCustomer(params.pk).then((c)=>{
            this.refs.firstName.value  =  c.first_name;
            this.refs.lastName.value  =  c.last_name;
            this.refs.email.value  =  c.email;
            this.refs.phone.value  =  c.phone;
            this.refs.address.value  =  c.address;
            this.refs.description.value  =  c.description;
        })
    }
}

وفيما يلي المحتوى الكامل للملف "CustomerCreateUpdate.js":

import React, { Component } from 'react';
import CustomersService from './CustomersService';

const customersService = new CustomersService();

class CustomerCreateUpdate extends Component {
    constructor(props) {
        super(props);

        this.handleSubmit = this.handleSubmit.bind(this);
      }

      componentDidMount(){
        const { match: { params } } = this.props;
        if(params && params.pk)
        {
          customersService.getCustomer(params.pk).then((c)=>{
            this.refs.firstName.value = c.first_name;
            this.refs.lastName.value = c.last_name;
            this.refs.email.value = c.email;
            this.refs.phone.value = c.phone;
            this.refs.address.value = c.address;
            this.refs.description.value = c.description;
          })
        }
      }

      handleCreate(){
        customersService.createCustomer(
          {
            "first_name": this.refs.firstName.value,
            "last_name": this.refs.lastName.value,
            "email": this.refs.email.value,
            "phone": this.refs.phone.value,
            "address": this.refs.address.value,
            "description": this.refs.description.value
        }          
        ).then((result)=>{
          alert("Customer created!");
        }).catch(()=>{
          alert('There was an error! Please re-check your form.');
        });
      }
      handleUpdate(pk){
        customersService.updateCustomer(
          {
            "pk": pk,
            "first_name": this.refs.firstName.value,
            "last_name": this.refs.lastName.value,
            "email": this.refs.email.value,
            "phone": this.refs.phone.value,
            "address": this.refs.address.value,
            "description": this.refs.description.value
        }          
        ).then((result)=>{
          console.log(result);
          alert("Customer updated!");
        }).catch(()=>{
          alert('There was an error! Please re-check your form.');
        });
      }
      handleSubmit(event) {
        const { match: { params } } = this.props;

        if(params && params.pk){
          this.handleUpdate(params.pk);
        }
        else
        {
          this.handleCreate();
        }

        event.preventDefault();
      }

      render() {
        return (
          <form onSubmit={this.handleSubmit}>
          <div className="form-group">
            <label>
              First Name:</label>
              <input className="form-control" type="text" ref='firstName' />

            <label>
              Last Name:</label>
              <input className="form-control" type="text" ref='lastName'/>

            <label>
              Phone:</label>
              <input className="form-control" type="text" ref='phone' />

            <label>
              Email:</label>
              <input className="form-control" type="text" ref='email' />

            <label>
              Address:</label>
              <input className="form-control" type="text" ref='address' />

            <label>
              Description:</label>
              <textarea className="form-control" ref='description' ></textarea>


            <input className="btn btn-primary" type="submit" value="Submit" />
            </div>
          </form>
        );
      }  
}

export default CustomerCreateUpdate;

وهكذا بعد إنشاء المكوّن CustomerCreateUpdate، أصبح بإمكاننا تحديث المكوّن App الرئيسي لإضافة روابط للمكونات المختلفة التي أنشأناها.

الخطوة التاسعة - تحديث مكون التطبيق الرئيسي

سنحدّث في هذا القسم المكوّن App من تطبيقنا لإنشاء روابط إلى المكونات التي أنشأناها في الخطوات السابقة.

نفذ الأمر التالي من المجلد "frontend"، لتثبيت موجه ريآكت الذي يمكّنك من إضافة المسارات والتنقلات بين مكونات ريآكت المتعددة.

$ cd ~/djangoreactproject/frontend
$ npm install --save react-router-dom

والآن افتح الملف "djangoreactproject/frontend/src/App.js/~":

$ nano ~/djangoreactproject/frontend/src/App.js

احذف كل شيء هناك وأضف الشيفرة التالية لاستيراد الأصناف اللازمة لإضافة التوجيه، إذ تتضمن هذه الأصناف الصنفَ BrowserRouter الذي ينشئ مكوّن الموجّه والصنفَ Route الذي ينشئ مكوّن الوُجهة route:

import  React, { Component } from  'react';
import { BrowserRouter } from  'react-router-dom'
import { Route, Link } from  'react-router-dom'
import  CustomersList  from  './CustomersList'
import  CustomerCreateUpdate  from  './CustomerCreateUpdate'
import  './App.css';

ملاحظة: يحافظ BrowserRouter على التزامن بين واجهة المستخدم وعنوان URL باستخدام واجهة برمجة تطبيقات تاريخ HTML5.

أنشئ نمطًا أساسيًّا ليزودنا بالمكون الأساسي الذي سيُغلِّفُه المكون BrowserRouter.

...

const  BaseLayout  = () => (
<div  className="container-fluid">
    <nav  className="navbar navbar-expand-lg navbar-light bg-light">
        <a  className="navbar-brand"  href="#">Django React Demo</a>
        <button  className="navbar-toggler"  type="button"  data-toggle="collapse"  data-target="#navbarNavAltMarkup"  aria-controls="navbarNavAltMarkup"  aria-expanded="false"  aria-label="Toggle navigation">
        <span  className="navbar-toggler-icon"></span>
    </button>
    <div  className="collapse navbar-collapse"  id="navbarNavAltMarkup">
        <div  className="navbar-nav">
            <a  className="nav-item nav-link"  href="/">CUSTOMERS</a>
            <a  className="nav-item nav-link"  href="/customer">CREATE CUSTOMER</a>
        </div>
    </div>
    </nav>
    <div  className="content">
        <Route  path="/"  exact  component={CustomersList}  />
        <Route  path="/customer/:pk"  component={CustomerCreateUpdate}  />
        <Route  path="/customer/"  exact  component={CustomerCreateUpdate}  />
    </div>
</div>
)

استخدمنا بالشيفرة أعلاه المكون Route لتعريف وجهات تطبيقنا، وهو المكون الذي يتعيّن على الموجّه تحميله عندما يجد تطابقًا. تحتاج كل وجهة إلى مسار path لتحديد المسار الذي سيجري مطابَقته، وإلى مكوّن component لتحديد المكون الذي سيحمّله. تخبر الخاصّيّة exact الموجّه أن يطابق المسار الصحيح.

وأخيرًا أنشئ المكون App المكوّن الجذر أو مكوّن مستوى القمة في تطبيق ريآكت:

...

class  App  extends  Component {

render() {
    return (
    <BrowserRouter>
        <BaseLayout/>
    </BrowserRouter>
    );
}
}
export  default  App;

وبما أننا نريد أن يعمل التطبيق في المتصفح، فقد غلّفنا المكوّن BaseLayout بالمكوّن BrowserRouter.

الآن يبدو ملف "App.js" المكتمل شبيهًا بما يلي:

import React, { Component } from 'react';
import { BrowserRouter } from 'react-router-dom'
import { Route, Link } from 'react-router-dom'

import  CustomersList from './CustomersList'
import  CustomerCreateUpdate  from './CustomerCreateUpdate'
import './App.css';

const BaseLayout = () => (
  <div className="container-fluid">
<nav className="navbar navbar-expand-lg navbar-light bg-light">
  <a className="navbar-brand" href="#">Django React Demo</a>
  <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
    <span className="navbar-toggler-icon"></span>
  </button>
  <div className="collapse navbar-collapse" id="navbarNavAltMarkup">
    <div className="navbar-nav">
      <a className="nav-item nav-link" href="/">CUSTOMERS</a>
      <a className="nav-item nav-link" href="/customer">CREATE CUSTOMER</a>

    </div>
  </div>
</nav>  

    <div className="content">
      <Route path="/" exact component={CustomersList} />
      <Route path="/customer/:pk"  component={CustomerCreateUpdate} />
      <Route path="/customer/" exact component={CustomerCreateUpdate} />

    </div>

  </div>
)

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <BaseLayout/>
      </BrowserRouter>
    );
  }
}

export default App;

بعد إضافة التوجيه إلى تطبيقنا، أصبحنا الآن جاهزين لاختبار التطبيق. انتقل إلى العنوان "http://localhost:3000"، ويفترض أن ترى الصفحة الأولى من التطبيق:

الصفحة الأولى للتطبيق بعد إدخال العنوان "http://localhost:3000"

وبهذا يكتمل التطبيق، وأصبح لديك الآن الأساس لتطبيق إدارة علاقات العملاء CRM.

الخاتمة

أنشأت في هذا المقال تطبيقًا تجريبيًّا باستخدام جانغو وريآكت، واستخدمت إطار عمل جانغو ريست لبناء واجهة برمجة التطبيقات لريست REST واكسيوس Axios لاستهلاك واجهة برمجة التطبيقات وبوتستراب 4 لتنسيق CSS.

يمكنك العثور على الشيفرة المصدرية لهذا المشروع في مستودع غيت هب GitHub.

ترجمة - وبتصرف - للمقالة How To Build a Modern Web Application to Manage Customer Information with Django and React on Ubuntu 18.04 لصاحبها Ahmed Bouchefra.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...