إنشاء تطبيق حديث باستخدام Django وVue

0

 في هذا الدرس، سنتعلم كيفية إنشاء تطبيق حديث من صفحة واحدة باستخدام 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

Vue router

لاحظ أن دليل المشروع مختلف قليلاً هذه المرة.

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;

في هذه المقالة نقوم بإنشاء الواجهة الأمامية فقط، ولن نتعامل مع نقل البيانات الآن، لذلك لا تقلق بشأن كيفية العثور على المنشور/التصنيف/العلامة الصحيحة الآن.

إنشاء العروض والصفحات والمكونات

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

لا يوجد تعليقات

أضف تعليق