في هذا الدرس، سنتعلم كيفية إنشاء تطبيق حديث من صفحة واحدة باستخدام Django كواجهة خلفية، وVue كواجهة أمامية، و GraphQL كلغة معالجة API التي تربطهم معًا.
سنستخدم Django فقط للواجهة الخلفية، مما يعني أننا لن نستخدم قالب Django أو العرض، ونستبدلهما بـ Vue.js وGraphQL.
إنشاء مشروع جانغو جديد
شخصياً أحب فصل أدلة backend
و frontend
. هذه هي الطريقة التي أنشأت بها هيكل المشروع:
1 2 3 | blog ├── backend └── frontend |
انتقل إلى مجلد backend
، وقم بإنشاء بيئة افتراضية جديدة. و هي بيئة معزولة مع تثبيت بايثون بدون الحزم المخصصة. فعندما تقوم بتثبيت الحزم داخل هذه البيئة، فلن يؤثر ذلك على بيئة Python الخاصة بنظامك، وهو أمر مهم جدًا إذا كنت تستخدم Linux أو macOS.
1 | python3 -m venv env |
سيقوم هذا الأمر بإنشاء دليل جديد يسمى env، وسيتم إنشاء البيئة الافتراضية بداخله.
و لتفعيل البيئة الافتراضية، استخدم الأمر التالي:
1 | source env/bin/activate |
إذا كنت تستخدم نظام التشغيل Windows، فاستخدم هذا الأمر بدلاً من ذلك.
1 | env/Scripts/activate |
بعد ذلك، سنقوم بإنشاء مشروع Django جديد.
1 2 | python -m pip install Django django-admin startproject backend |
بعد ذلك سنقوم بإنشاء تطبيق جديد:
1 | python manage.py startapp blog |
في النهاية يجب أن يبدو هيكل المشروع كما يلي:
1 2 3 4 5 6 7 | . ├── backend │ ├── backend │ ├── blog │ ├── manage.py │ └── requirements.txt └── frontend |
إنشاء النماذج
تذكر أن النموذج عبارة عن واجهة يمكننا استخدامها للتفاعل مع قاعدة البيانات. وواحدة من أعظم ميزات Django هي أنه يمكنه اكتشاف التغييرات التي أجريتها على النماذج تلقائيًا، وإنشاء ملفات الترحيل والتي يمكننا استخدامها لإجراء تغييرات على بنية قاعدة البيانات.
نموذج Site
لنبدأ بنموذج Site
، الذي يخزن المعلومات الأساسية لموقعك على الويب.
1 2 3 4 5 6 7 8 9 10 11 | class Site(models.Model): name = models.CharField(max_length=200) description = models.TextField() logo = models.ImageField(upload_to='site/logo/') class Meta: verbose_name = 'site' verbose_name_plural = '1. Site' def __str__(self): return self.name |
في السطر 4، يوجد ImageField
الذي يخبر Django بتحميل الصورة إلى دليل 'site/logo/'
.
و لعمل هذا هناك شيئان عليك القيام بهما.
أولاً، يجب عليك تثبيت حزمة Pillow لأن Django يحتاجها لمعالجة الصور.
1 | python -m pip install Pillow |
بعد ذلك عليك أن تخبر Django بالمكان الذي ستخزن فيه ملفات الوسائط وعنوان URL الذي ستستخدمه عند الوصول إلى هذه الملفات.
في ملف settings.py
أضف الكود التالي:
1 2 3 4 5 6 | import os # Media Files MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles') MEDIA_URL = '/media/' |
يعني هذا الإعداد أنه سيتم تخزين ملفات الوسائط داخل دليل /mediafiles
، وسنحتاج إلى استخدام عنوان /media/
للوصول إليها.
على سبيل المثال، سيعمل عنوان http://localhost:3000/media/example.png على استرداد الصورة /mediafiles/example.png
.
نموذج User
يأتي Django مزودًا بنموذج User
مدمج، والذي يوفر وظائف الأذونات والترخيص الأساسية. ومع ذلك، بالنسبة لهذا المشروع، دعونا نجرب شيئًا أكثر تعقيدًا.
يمكنك إضافة الصورة الرمزية للملف الشخصي والسيرة الذاتية وبعض المعلومات الأخرى. للقيام بذلك، تحتاج إلى إنشاء نماذج User
جديدة تمتد إلى فئة AbstractUser
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from django.contrib.auth.models import AbstractUser # New user model class User(AbstractUser): avatar = models.ImageField( upload_to='users/avatars/%Y/%m/%d/', default='users/avatars/default.jpg' ) bio = models.TextField(max_length=500, null=True) location = models.CharField(max_length=30, null=True) website = models.CharField(max_length=100, null=True) joined_date = models.DateField(auto_now_add=True) class Meta: verbose_name = 'user' verbose_name_plural = '2. Users' def __str__(self): return self.username |
تبدو فئة AbstractUser
الخاصة بـ Django كما يلي:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | class AbstractUser(AbstractBaseUser, PermissionsMixin): """ An abstract base class implementing a fully featured User model with admin-compliant permissions. Username and password are required. Other fields are optional. """ username_validator = UnicodeUsernameValidator() username = models.CharField( _('username'), max_length=150, unique=True, help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), validators=[username_validator], error_messages={ 'unique': _("A user with that username already exists."), }, ) first_name = models.CharField(_('first name'), max_length=150, blank=True) last_name = models.CharField(_('last name'), max_length=150, blank=True) email = models.EmailField(_('email address'), blank=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.'), ) is_active = models.BooleanField( _('active'), default=True, help_text=_( 'Designates whether this user should be treated as active. ' 'Unselect this instead of deleting accounts.' ), ) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) objects = UserManager() EMAIL_FIELD = 'email' USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] class Meta: verbose_name = _('user') verbose_name_plural = _('users') abstract = True def clean(self): super().clean() self.email = self.__class__.objects.normalize_email(self.email) def get_full_name(self): """ Return the first_name plus the last_name, with a space in between. """ full_name = '%s %s' % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): """Return the short name for the user.""" return self.first_name def email_user(self, subject, message, from_email=None, **kwargs): """Send an email to this user.""" send_mail(subject, message, from_email, [self.email], **kwargs) |
كما ترون، فهو يوفر بعض الحقول الأساسية مثل first_name
، و last_name
، وما إلى ذلك.
بعد ذلك، تحتاج إلى التأكد من أن Django يستخدم نموذج User
الجديد كنموذج User
الافتراضي، وإلا فلن تعمل المصادقة.
انتقل إلى settings.py
وأضف التوجيه التالي:
1 2 | # Change Default User Model AUTH_USER_MODEL = 'blog.User' |
نموذج Category
وTag
و Post
1 2 3 4 5 6 7 8 9 10 11 | class Category(models.Model): name = models.CharField(max_length=200) slug = models.SlugField() description = models.TextField() class Meta: verbose_name = 'category' verbose_name_plural = '3. Categories' def __str__(self): return self.name |
1 2 3 4 5 6 7 8 9 10 11 | class Tag(models.Model): name = models.CharField(max_length=200) slug = models.SlugField() description = models.TextField() class Meta: verbose_name = 'tag' verbose_name_plural = '4. Tags' def __str__(self): return self.name |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class Post(models.Model): title = models.CharField(max_length=200) slug = models.SlugField() content = RichTextField() featured_image = models.ImageField( upload_to='posts/featured_images/%Y/%m/%d/') is_published = models.BooleanField(default=False) is_featured = models.BooleanField(default=False) created_at = models.DateField(auto_now_add=True) modified_at = models.DateField(auto_now=True) # Each post can receive likes from multiple users, and each user can like multiple posts likes = models.ManyToManyField(User, related_name='post_like') # Each post belong to one user and one category. # Each post has many tags, and each tag has many posts. category = models.ForeignKey( Category, on_delete=models.SET_NULL, null=True) tag = models.ManyToManyField(Tag) user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) class Meta: verbose_name = 'post' verbose_name_plural = '5. Posts' def __str__(self): return self.title def get_number_of_likes(self): return self.likes.count() |
نموذج Comment
هذه المرة، دعونا نخطو خطوة أخرى إلى الأمام، وننشئ قسمًا للتعليقات لهذا التطبيق.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Comment(models.Model): content = models.TextField(max_length=1000) created_at = models.DateField(auto_now_add=True) is_approved = models.BooleanField(default=False) # Each comment can receive likes from multiple users, and each user can like multiple comments likes = models.ManyToManyField(User, related_name='comment_like') # Each comment belongs to one user and one post user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True) class Meta: verbose_name = 'comment' verbose_name_plural = '6. Comments' def __str__(self): if len(self.content) > 50: comment = self.content[:50] + '...' else: comment = self.content return comment def get_number_of_likes(self): return self.likes.count() |
إعداد لوحة إدارة Django
أخيرًا، حان الوقت لإعداد مسؤول Django. افتح ملف admin.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from django.contrib import admin from .models import * # Register your models here. class UserAdmin(admin.ModelAdmin): list_display = ('username', 'first_name', 'last_name', 'email', 'date_joined') class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('name',)} class TagAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('name',)} class PostAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('title',)} list_display = ('title', 'is_published', 'is_featured', 'created_at') class CommentAdmin(admin.ModelAdmin): list_display = ('__str__', 'is_approved', 'created_at') admin.site.register(Site) admin.site.register(User, UserAdmin) admin.site.register(Category, CategoryAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(Post, PostAdmin) admin.site.register(Comment, CommentAdmin) |
بالنسبة إلى CommentAdmin
، و __str__
يشير إلى التابع __str__()
في نموذج التعليق.
والذي سيعيد أول 50 حرفًا متسلسل بـ “…”.
الآن، قم بتشغيل خادم التطوير ومعرفة ما إذا كان كل شيء يعمل:
1 | python manage.py runserver |
نبذة مختصرة عن Vue.js
الآن بعد أن انتهيت من الواجهة الخلفية، فقد حان الوقت للتركيز على الواجهة الأمامية في هذا الجزء من هذه المقالة، دعونا نستخدم Vue.js لإنشاء تطبيق الواجهة الأمامية.
Vue.js هو إطار عمل JavaScript للواجهة الأمامية يوفر لك نظامًا بسيطًا قائمًا على المكونات، والذي يسمح لك بإنشاء واجهات مستخدم تفاعلية.
“الاعتماد على المكونات” يعني أن المكون الجذر (App.vue
) يمكنه استيراد مكونات أخرى (الملفات ذات الامتداد vue
)، ويمكن لهذه المكونات استيراد المزيد من المكونات، مما يسمح لك بإنشاء أنظمة معقدة للغاية.
يحتوي ملف
النموذجي على ثلاثة أقسام: .vue
<template>
يتضمن رموز HTML، والقسم <script>
يتضمن رموز JavaScript، والقسم <style>
يتضمن رموز CSS.
في قسم <script>
يمكنك الإعلان عن روابط جديدة داخل نموذج data()
. يمكن بعد ذلك عرض هذه الارتباطات داخل قسم <template>
باستخدام صيغة الأقواس المزدوجة المتعرجة ({{ binding }}
).
سيتم تغليف الارتباطات المعلنة داخل طريقة data()
تلقائيًا داخل نظام تفاعل Vue. وهذا يعني أنه عندما تتغير قيمة الارتباط، سيتم إعادة عرض المكون المقابل تلقائيًا، دون الحاجة إلى تحديث الصفحة.
يمكن أن يحتوي القسم <script>
توابع أخرى غير data()
، مثل computed
و props
و methods
وما إلى ذلك. ويتيح لنا <template>
لنا أيضًا بربط البيانات باستخدام توجيهات مثل v-bind
و v-on
و v-model
.
إنشاء مشروع Vue.js جديد
سنستخدم أداة إنشاء الواجهة الأمامية التي تسمى Vite والتي تم إنشاؤها بواسطة نفس المؤلف الذي أنشأ Vue.js.
انتقل إلى مجلد الواجهة الأمامية، وقم بتشغيل الأمر التالي:
1 | npm init vue@latest |
ستتم مطالبتك بخيارات متعددة، لهذا المشروع، ما عليك سوى إضافة Vue Router:
1 2 3 4 5 6 7 8 9 10 11 12 | Project name: … <your_project_name> Add TypeScript? … No / Yes Add JSX Support? … No / Yes Add Vue Router for Single Page Application development? … No / Yes Add Pinia for state management? … No / Yes Add Vitest for Unit testing? … No / Yes Add Cypress for both Unit and End-to-End testing? … No / Yes Add ESLint for code quality? … No / Yes Add Prettier for code formating? … No / Yes Scaffolding project in ./<your_project_name>. . . Done. |
في دليل المشروع الخاص بك، تحتاج إلى تثبيت هذه الحزم داخل مشروعك.
1 2 3 | cd <your_project_name> npm install npm run dev |
شيء آخر قبل أن نبدأ في إنشاء تطبيق الواجهة الأمامية. حيث سنستخدم إطار عمل CSS يسمى TailwindCSS في هذا المشروع. و لتثبيته قم بتشغيل الأمر التالي:
1 2 | npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p |
سيؤدي هذا إلى إنشاء ملفين، tailwind.config.js
و postcss.config.js
انتقل إلى tailwind.config.js
، وأضف المسار إلى جميع ملفات القالب الخاصة بك:
1 2 3 4 5 6 7 | module.exports = { content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; |
أنشئ ملف
وأضف توجيهات ./src/index.css
@tailwind
لكل طبقة من طبقات Tailwind.
1 2 3 | @tailwind base; @tailwind components; @tailwind utilities; |
قم باستيراد ملف ./src/index.css
الذي تم إنشاؤه حديثًا إلى ملف ./src/main.js
الخاص بك.
1 2 3 4 5 6 7 8 9 10 | import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import "./index.css"; const app = createApp(App); app.use(router); app.mount("#app"); |
الآن يجب أن تكون قادرًا على استخدام Tailwind داخل ملفات .vue. دعونا نختبر ذلك.
1 2 3 4 5 6 7 8 9 10 11 | <template> <header> . . . <div class="wrapper"> <HelloWorld msg="You did it!" /> <h1 class="text-3xl font-bold underline">Hello world!</h1> . . . </div> </header> . . . </template> |
أضفنا عنوان <h1>
بعد <HelloWorld>
، ويستخدم العنوان فئات Tailwind.
Vue router
لاحظ أن دليل المشروع مختلف قليلاً هذه المرة.
داخل دليل src
يوجد router
ومجلد views
. يحتوي دليل router
على ملف index.js
. و هو المكان الذي يمكنك فيه تحديد طرق مختلفة.
سيشير كل مسار إلى مكون عرض موجود داخل دليل views
، ويمكن أن يمتد العرض بعد ذلك إلى مكونات أخرى داخل دليل components
و لقد زودتنا Vue بمثال على ملف index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import { createRouter, createWebHistory } from "vue-router"; import HomeView from "../views/HomeView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "home", component: HomeView, }, { path: "/about", name: "about", // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import("../views/AboutView.vue"), }, ], }); export default router; |
و لاستدعاء router محدد، انظر داخل ملف App.vue
و بدلاً من العلامة <a>
نستخدم <RouterLink>
الذي تم استيراده من حزمة vue-router
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <script setup> import { RouterLink, RouterView } from "vue-router"; . . . </script> <template> <header> . . . <div class="wrapper"> . . . <nav> <RouterLink to="/">Home</RouterLink> <RouterLink to="/about">About</RouterLink> </nav> </div> </header> <RouterView /> </template> |
عند عرض الصفحة سيتم استبدال علامة <RouterView />
بالعرض المقابل. أما إذا كنت لا ترغب في استيراد هذه المكونات، فما عليك سوى استخدام علامتي <router-link to="">
و <router-view>
بدلاً من ذلك. أنا شخصياً أفضل هذه الطريقة لأنني دائماً أنسى استيرادها.
إنشاء مسارات باستخدام Vue router
بالنسبة لتطبيق المدونة الخاص بنا، نحتاج إلى إنشاء 6 صفحات على الأقل. نحتاج إلى صفحة رئيسية تعرض قائمة بالصفحات الأخيرة، وصفحة التصنيفات/الوسوم تعرض جميع التصنيفات/الوسوم، وصفحة التصنيف/الوسوم تعرض قائمة بالمقالات التي تنتمي إلى الصنف/الوسم، وأخيرًا، صفحة المنشورات التي تعرض محتوى المنشور بالإضافة إلى التعليقات.
و هذه هي المسارات التي قمت بإنشائها. يقوم @
بتعيين دليل src
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import { createRouter, createWebHistory } from "vue-router"; import HomeView from "@/views/main/Home.vue"; import PostView from "@/views/main/Post.vue"; import CategoryView from "@/views/main/Category.vue"; import TagView from "@/views/main/Tag.vue"; import AllCategoriesView from "@/views/main/AllCategories.vue"; import AllTagsView from "@/views/main/AllTags.vue"; const routes = [ { path: "/", name: "Home", component: HomeView, }, { path: "/category", name: "Category", component: CategoryView, }, { path: "/tag", name: "Tag", component: TagView, }, { path: "/post", name: "Post", component: PostView, }, { path: "/categories", name: "Categories", component: AllCategoriesView, }, { path: "/tags", name: "Tags", component: AllTagsView, }, ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, }); export default router; |
في هذه المقالة نقوم بإنشاء الواجهة الأمامية فقط، ولن نتعامل مع نقل البيانات الآن، لذلك لا تقلق بشأن كيفية العثور على المنشور/التصنيف/العلامة الصحيحة الآن.
إنشاء العروض والصفحات والمكونات
هذه هي واجهة المستخدم الأمامية التي قمت بإنشائها لهذا المشروع.
ليست هناك تعليقات:
إرسال تعليق