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

إنشاء تطبيق حديث باستخدام Django و Vue | الجزء الثاني: ربط الواجهة الخلفية والأمامية


محمد ناولو2

سنوضح في مقال اليوم الخطوات اللازمة لربط الواجهة الخلفية backend مع الواجهة الأمامية frontend من سلسلة مقالات تطوير تطبيق مدونة حديث باستخدام جانغو وفيو.

هذا المقال هو جزء من سلسلة مقالات تتحدث حول كيفية إنشاء تطبيق حديث باستخدام جانغو Django وفيو Vue:

إن الشائع حاليًا في ربط الواجهتين الأمامية والخلفية لتطبيق ما هو استخدام تقنية تسمى REST API. ومصطلح API هو اختصار للواجهة البرمجية للتطبيقات Application Programming Interface، وهي تشير إلى الاتصال بين تطبيقين برمجيين، أما REST فهو اختصار لنقل الحالة التمثيلية Representational State Transfer، وهو يشير إلى بنية محددة يتبعها هذا النوع من الاتصال.

01 الواجهة البرمجية للتطبيقات

يتكون طلب REST API عادةً من نقطة وصول endpoint تتصل مع الخادم، إضافةً إلى طريقة HTTP وترويسة Head وجسم Body. تحتوي الترويسة على عناصر التعريف meta، مثل التخبئة caching واستيثاق المستخدم user authentication واختبار AB، بينما يحتوي الجسم على البيانات التي يريد العميل إرسالها إلى الخادم.

ومع ذلك، فإنه ثمة خلل بسيط في REST API، وهو أنه من المستحيل تصميم واجهات برمجية تجلب البيانات التي يحتاجها العميل بالضبط، إذ من الشائع أن تجلب REST API بيانات زائدة أو ناقصة. ولذلك، طُوّرت لغة الاستعلام غراف كيو إل GraphQL بهدف حل هذه المشكلة، إذ تستخدم المخططات schemas للتأكد من جلب البيانات المحددة فقط لكل طلب، وسنرى كيف يحدث ذلك لاحقًا.

إعداد غراف كيو إل GraphQL باستخدام جانغو Django

لنبدأ بإعداد غراف كيو إل GraphQL في الواجهة الخلفية، ستحتاج إلى تثبيت حزمة جديدة تسمى graphene-django وهي حزمة توفر مجموعة من الأدوات والميزات التي تسهل عملية تطوير واجهات GraphQL داخل تطبيقات Django عبر تفعيل الأمر التالي:

pip install graphene-django

انتقل بعد ذلك إلى مجلد settings.py وابحث عن المتغير INSTALLED_APPS. يجب عليك إضافة graphene-django داخل المجلد لكي يتمكن جانغو من العثور على هذه الحزمة.

INSTALLED_APPS = [
  . . .
  "blog",
  "graphene_django",
]

إعداد graphene-django

ما زال هناك بعض المهام التي يجب عليك إنجازها قبل أن تتمكن من استخدام GraphQL. أولاً، ستحتاج إلى إعداد نمط الرابط URL pattern لخدمة الواجهات البرمجية للتطبيقات APIs في GraphQL، والذي يحدد كيفية تطابق عناوين URL مع عرض معين في تطبيق Django ويستخدم لتحويل طلبات المستخدم إلى العرض المناسب لمعالجتها. للقيام بذلك انتقل إلى الملف urls.py وأضف الشيفرة التالية:

from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

urlpatterns = [
    . . .
    path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

وبعد ذلك، أنشئ المخططات schemas وحدد لجانغو مكان العثور عليها في مجلد settings.py. تتبع مخططات GraphQL نمطًا يسمح لجانغو بترجمة نماذج قاعدة البيانات database إلى GraphQL وبالعكس، ولنأخذ فيما يلي نموذج الموقع Site model كمثال:

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

أنشئ ملف schema.py داخل مجلد المدونة blog.

import graphene
from graphene_django import DjangoObjectType
from blog import models

# Define type
class SiteType(DjangoObjectType):
    class Meta:
        model = models.Site

# The Query class
class Query(graphene.ObjectType):
    site = graphene.Field(types.SiteType)

    def resolve_site(root, info):
        return (
            models.Site.objects.first()
        )

كما هو واضح، يُقسم هذا الملف إلى ثلاثة أجزاء. أولًا، يجب عليك استيراد الحزم والنماذج اللازمة. ثم يُصرّح عن صنف SiteType ويُربط مع نموذج الموقع. وأخيرًا، يستخدم الصنف Query الذي يسمح لك باسترداد المعلومات عبر GraphQL API. أما لإنشاء معلومات أو تحديثها، فستحتاج إلى استخدام صنف آخر يُدعى Mutation والذي سنتحدث عنه بالتفصيل في المقال الثالث من السلسلة.

توجد الدالة Resolve_site داخل الصنف Query وهي تعمل على استرجاع السجل الأول من النموذج Site model (أي تعيد الكائن الأول الموجود في قاعدة البيانات المرتبطة بهذا النموذج). ترتبط هذه الدالة تلقائيًا مع متغير يحمل نفس الاسم site، هذا يعني أنه عند استدعاء الدالة، ستكون النتيجة متاحة مباشرةً من خلال المتغير site دون الحاجة إلى تحديد اسم جديد له، إذ يعمل هذا الجزء تمامًا مثل Django QuerySet لاسترجاع البيانات من قاعدة البيانات. إذَا عندما تستدعي الدالة Resolve_site فإنها سترجع لك السجل الأول من نموذج Site ووتخزنها في المتغير site تلقائيًا، مما يسهل الوصول إلى هذه البيانات لاحقًا.

إنشاء المخططات Schemas

يمكنك الآن تكرار نفس الطريقة لجميع النماذج، ولكي تتجنب الحجم الكبير لملف المخطط، يمكنك فصله إلى ثلاثة ملفات، وهي:

  1. schema.py
  2. types.py
  3. queries.py

ملف schema.py

import graphene
from blog import queries


schema = graphene.Schema(query=queries.Query)

ملف types.py

import graphene
from graphene_django import DjangoObjectType
from blog import models


class SiteType(DjangoObjectType):
    class Meta:
        model = models.Site


class UserType(DjangoObjectType):
    class Meta:
        model = models.User


class CategoryType(DjangoObjectType):
    class Meta:
        model = models.Category


class TagType(DjangoObjectType):
    class Meta:
        model = models.Tag


class PostType(DjangoObjectType):
    class Meta:
        model = models.Post

queries.py

import graphene
from blog import models
from blog import types


# The Query class
class Query(graphene.ObjectType):
    site = graphene.Field(types.SiteType)
    all_posts = graphene.List(types.PostType)
    all_categories = graphene.List(types.CategoryType)
    all_tags = graphene.List(types.TagType)
    posts_by_category = graphene.List(types.PostType, category=graphene.String())
    posts_by_tag = graphene.List(types.PostType, tag=graphene.String())
    post_by_slug = graphene.Field(types.PostType, slug=graphene.String())

    def resolve_site(root, info):
        return (
            models.Site.objects.first()
        )

    def resolve_all_posts(root, info):
        return (
            models.Post.objects.all()
        )

    def resolve_all_categories(root, info):
        return (
            models.Category.objects.all()
        )

    def resolve_all_tags(root, info):
        return (
            models.Tag.objects.all()
        )

    def resolve_posts_by_category(root, info, category):
        return (
            models.Post.objects.filter(category__slug__iexact=category)
        )

    def resolve_posts_by_tag(root, info, tag):
        return (
            models.Post.objects.filter(tag__slug__iexact=tag)
        )

    def resolve_post_by_slug(root, info, slug):
        return (
            models.Post.objects.get(slug__iexact=slug)
        )

والآن، يجب عليك إخبار جانغو بمكان العثور على ملف المخطط، لذا انتقل إلى settings.py وأضف الشيفرة التالية:

# Configure GraphQL
GRAPHENE = {
    "SCHEMA": "blog.schema.schema",
}

ولكي تتأكد من عمل المخططات على النحو الصحيح، افتح المتصفح وانتقل إلى http://127.0.0.1:8000/graphql، يجب أن تشاهد واجهة GraphiQL على النحو التالي:

02 واجهة graphiql

لاحظ كيف نسترجع المعلومات في هذا المثال، هذه لغة GraphQL، وهذه هي الطريقة التي سنسترجع بها البيانات في الواجهة الأمامية، والتي سنشرحها لاحقًا.

إعداد CORS

قبل أن ننتقل إلى الواجهة الأمامية، ثمة شيء يجب الاهتمام به. افتراضيًا، لا يمكن نقل البيانات إلا داخل التطبيق نفسه لأسباب الحماية، ولكننا نحتاج إلى تدفق البيانات بين تطبيقين مختلفين، ولمعالجة هذه المشكلة، يجب تمكين وظيفة CORS، وهي اختصار لـ Cross Origin Resource Sharing والتي تعني مشاركة الموارد ذات الأصول المختلفة.

أولاً، ثبِّت حزمة Django-cors-headers. ومن داخل تطبيق الواجهة الخلفية، فعِّل الأمر التالي:

pip install django-cors-headers

أضف "corsheaders" إلى المتغير INSTALLED_APPS:

INSTALLED_APPS = [
  . . .
  "corsheaders",
]

ثم أضف "corsheaders.middleware.CorsMiddleware" إلى المتغير MIDDLEWARE لتمكين دعم CORS في تطبيق جانغو:

MIDDLEWARE = [
  "corsheaders.middleware.CorsMiddleware",
  . . .
]

وأخيرًا، أضف الشيفرة التالية إلى settings.py:

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = ("http://localhost:8080",) # Matches the port that Vue.js is using

إعداد Apollo باستخدام Vue.js

والآن، حان الوقت للانتقال إلى الواجهة الأمامية. أولًا، يجب تثبيت مكتبة Apollo، إذ تتيح لك استخدام GraphQL في تطبيق Vue. ويمكنك تثبيتها من خلال تفعيل الأمر التالي:

npm install --save graphql graphql-tag @apollo/client

ضمن مجلد src، أنشئ ملف جديد باسم apollo-config.js وأضف له الشيفرة التالية:

import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
} from "@apollo/client/core";

// HTTP connection to the API
const httpLink = createHttpLink({
  uri: "http://127.0.0.1:8000/graphql", // Matches the url and port that Django is using
});

// Cache implementation
const cache = new InMemoryCache();

// Create the apollo client
const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
});

ثم انتقل إلى main.js واستورد apolloClient كي تتيح لتطبيقك القدرة على التفاعل مع واجهات GraphQL واسترجاع البيانات بشكل ديناميكي:

import { apolloClient } from "@/apollo-config";
createApp(App).use(router).use(apolloClient).mount("#app");

أصبح بإمكانك الآن استخدام لغة GraphQL لاسترجاع البيانات من الواجهة الخلفية، ولتطبيق مثال عملي على ذلك، سننتقل إلى App.vue وهناك سنستعيد اسم موقعنا بكتابة الكود التالي:

<template>
  <div class="container mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">
    <div class="flex flex-col justify-between h-screen">
      <header class="flex flex-row items-center justify-between py-10">
        <div class="nav-logo text-2xl font-bold">
          <router-link to="/" v-if="mySite">{{ mySite.name }}</router-link>
        </div>
        . . .
      </header>
      . . .
    </div>
  </div>
</template>

<script>
import gql from "graphql-tag";

export default {
  data() {
    return {
      mySite: null,
    };
  },

  async created() {
    const siteInfo = await this.$apollo.query({
      query: gql`
        query {
          site {
            name
          }
        }
      `,
    });
    this.mySite = siteInfo.data.site;
  },
};
</script>

يُفضِّل البعض إنشاء ملف منفصل لجميع الاستعلامات queries، ومن ثم استيراده إلى ملف فيو كما في الكود التالي:

ملف src/queries.js

import gql from "graphql-tag";

export const SITE_INFO = gql`
  query {
    site {
      name
    }
  }
`;

ملف App.vue

. . .

<script>
import { SITE_INFO } from "@/queries";

export default {
  data() {
    return {
      mySite: null,
    };
  },

  async created() {
    const siteInfo = await this.$apollo.query({
      query: SITE_INFO,
    });
    this.mySite = siteInfo.data.site;
  },
};
</script>

صفحة الصنف Category

تبقى لدينا الآن مشكلة من المقال السابق، عندما نستدعي موجه router، فكيف لهذا الموجه أن يعرف الصفحة التي يجب إعادتها؟ فعلى سبيل المثال، عندما نضغط على رابط Category One يجب إرجاع قائمة المنشورات التي تنتمي إلى الصنف الأول Category One، ولكن كيف يمكن للموجه القيام بذلك؟ دعونا نرى مثالًا.

أولًا، في ملف router/index.js الذي عرفنا فيه جميع الوجهات لدينا، يجب علينا تعيين جزء من نمط الرابط URL pattern كمتغير. ففي المثال التالي، سيتم تعيين الكلمة بعد /category/ كمتغير Category. وسيكون بالإمكان الوصول إلى هذا المتغير في مكون CategoryView.

import { createRouter, createWebHistory } from "vue-router";
. . .

const routes = [
  {
    path: "/",
    name: "Home",
    component: HomeView,
  },
  {
    path: "/category/:category",
    name: "Category",
    component: CategoryView,
  },
  . . .
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

وبعد ذلك، في عرض AllCategories (الذي سيُظهر قائمة بجميع التصنيفات)، سنعمل على تمرير بعض المعلومات إلى المتغير Category.

<template>
  <div class="flex flex-col place-content-center place-items-center">
    <div class="py-8 border-b-2">
      <h1 class="text-5xl font-extrabold">All Categories</h1>
    </div>
    <div class="flex flex-wrap py-8">
      <router-link
        v-for="category in this.allCategories"
        :key="category.name"
        class=". . ."
        :to="`/category/${category.slug}`"
        >{{ category.name }}</router-link
      >
    </div>
  </div>
</template>

وفي عرض Category يمكننا الوصول إلى المتغير Category باستخدام الخاصية this.$route.

<script>
// @ is an alias to /src
import PostList from "@/components/PostList.vue";
import { POSTS_BY_CATEGORY } from "@/queries";

export default {
  components: { PostList },
  name: "CategoryView",

  data() {
    return {
      postsByCategory: null,
    };
  },

  async created() {
    const posts = await this.$apollo.query({
      query: POSTS_BY_CATEGORY,
      variables: {
        category: this.$route.params.category,
      },
    });
    this.postsByCategory = posts.data.postsByCategory;
  },
};
</script>

وأخيرًا، يمكن استرجاع المنشورات باستخدام الاستعلام POSTS_BY_CATEGORY.

export const POSTS_BY_CATEGORY = gql`
  query ($category: String!) {
    postsByCategory(category: $category) {
      title
      slug
      content
      isPublished
      isFeatured
      createdAt
    }
  }
`;

ومع هذا المثال، من المفترض أن تكون قادرًا الآن على إنشاء صفحتي المنشور post التي تعرض محتوى منشو أو مقالة معينة، وصفحة الوسم tag  التي تظهر قائمة بالمنشورات المرتبطة بوسم معين.

إنشاء وتحديث المعلومات باستخدام الطفرات Mutations

ذكرنا في المقال السابق من هذه السلسلة أنه يمكننا استخدام الاستعلامات queries لاسترجاع المعلومات من الواجهة الخلفية وإرسالها إلى الواجهة الأمامية. ومع ذلك، من الشائع في تطبيقات الويب الحديثة أن ترسل المعلومات من الواجهة الأمامية إلى الواجهة الخلفية. وللقيام بذلك، لا بُدّ من الحديث عن مفهوم جديد في GraphQL يُسمى الطفرة mutation.

ملاحظة: تُستخدم الطفرات mutations لتغيير البيانات في الخادم. فإذا أراد المستخدم مثلًا إضافة أو تحديث أو حذف بيانات عليه أن يستخدم mutation لإرسال طلب بالتعديل المطلوب والخادم يقوم بتحديث البيانات وإرجاع النتيجة.

نعود إلى الواجهة الخلفية وننتقل إلى ملف cd في مجلد blog وننشئ ملف باسم Mutations.py. سنكتشف في المثال التالي كيفية نقل البيانات إلى الواجهة الخلفية لإنشاء مستخدم جديد.

import graphene
from blog import models, types


# Mutation sends data to the database
class CreateUser(graphene.Mutation):
    user = graphene.Field(types.UserType)

    class Arguments:
        username = graphene.String(required=True)
        password = graphene.String(required=True)
        email = graphene.String(required=True)

    def mutate(self, info, username, password, email):
        user = models.User(
            username=username,
            email=email,
        )
        user.set_password(password)
        user.save()

        return CreateUser(user=user)

لاحظ في السطر 7 من الكود أعلاه أن UserType مرتبط بنموذج المستخدم User model.

أما في الأسطر من 9 إلى 12، فلاحظ أنه لإنشاء مستخدم جديد، يجب إضافة ثلاث عناصر هي اسم المستخدم وكلمة المرور والبريد الإلكتروني.

أما الأسطر من 15 إلى 18، فيجب أن تكون واضحة لك، فهي ذات الطريقة التي تنشئ من خلالها عنصرًا جديدًا باستخدام Django QuerySet.

السطر 19 مسؤول عن تعيين كلمة المرور، ولأسباب تتعلق الحماية لا يمكنك حفظ كلمة المرور الأصلية للمستخدم في قاعدة البيانات لديك، ويمكن تشفيرها من خلال التابع set_password()‎‎.

والآن، يجب عليك تضمين ملف Mutation.py في مخطط GraphQL، لذا انتقل إلى schema.py:

import graphene
from blog import queries, mutations


schema = graphene.Schema(query=queries.Query, mutation=mutations.Mutation)

ولتتأكد من أنه يعمل على النحو الصحيح، افتح المتصفح وانتقل إلى http://127.0.0.1:8000/graphql للوصول إلى واجهة GraphiQL.

03 واجهة graphiql

mutation {
  createUser(
    username: "testuser2022"
    email: "testuser2022@test.com"
    password: "testuser2022"
  ) {
    user {
      id
      username
    }
  }
}

من المفترض أنك تعرف كيفية استخدام ذلك في الواجهة الأمامية. على سبيل المثال:

<script>
import { USER_SIGNUP } from "@/mutations";

export default {
  name: "SignUpView",

  data() {. . .},

  methods: {
    async userSignUp() {
      // Register user
      const user = await this.$apollo.mutate({
        mutation: USER_SIGNUP,
        variables: {
          username: this.signUpDetails.username,
          email: this.signUpDetails.email,
          password: this.signUpDetails.password,
        },
      });
     // Do something with the variable user
     . . .
    },
  },
};
</script>

src/mutations.js

import gql from "graphql-tag";

export const USER_SIGNUP = gql`
  mutation ($username: String!, $email: String!, $password: String!) {
    createUser(username: $username, email: $email, password: $password) {
      user {
        id
        username
      }
    }
  }
`;

استيثاق المستخدم User authentication مع جانغو وفيو جي إس

والآن بعد أن تعرفت على كيفية إرسال البيانات إلى الواجهة الخلفية، لن تواجه صعوبةً كبيرةً في استيثاق المستخدم User authentication.

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

ومع ذلك، قد تواجه هذه العملية بعض العقبات في الممارسة العملية. فبدايةً، تُعد عملية إرسال كلمة مرور المستخدم ذهابًا وإيابًا عملية غير آمنة تمامًا. لذا ستحتاج إلى طريقة ما لتشفير البيانات. الطريقة الأكثر استخدامًا هي تقنية JWT، وهو اختصار لـ JSON Web Token. إذ تعمل هذه الطريقة على تشفير بيانات بصيغة JSON إلى ترميز بصيغة token. يمكنك رؤية مثال عليها من هنا.

سيُحتفظ بهذا الترميز ضمن التخزين المحلي للمتصفح، وطالما بقي هذا الترميز ضمن ذاكرة المتصفح، سيبقى تسجيل الدخول قائمًا.

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

لذلك ستحتاج إلى مكان مركزي لتخزين هذه المعلومات حين يقوم المستخدم بتسجيل الدخول، ويجب أن تكون جميع المكونات قادرة على قراءة البيانات من ذلك المكان. ولتحقيق بذلك، ستحتاج إلى استخدام Pinia، وهي مكتبة Vue جديدة أُنشئت من خلال Vuex.

دمج JWT في الواجهة الخلفية

في البداية، سندمج JWT مع الواجهة الخلفية لجانغو، لذلك ستحتاج إلى تثبيت حزمة أخرى باسم django-graphql-jwt:

pip install django-graphql-jwt

انتقل الآن إلى ملف settings.py وأضف برنامجًا وسيطًا، وكذلك الاستيثاق في الواجهة الخلفية. ستحل هذه الإعدادات مكان الإعدادات الافتراضية لجانغو، وهو ما يتيح استخدام JWT.

MIDDLEWARE = [
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]

# Configure GraphQL

GRAPHENE = {
    "SCHEMA": "blog.schema.schema",
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}

# Auth Backends

AUTHENTICATION_BACKENDS = [
    'graphql_jwt.backends.JSONWebTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
]

لاستخدام هذه الحزمة، انتقل إلى موقع muts.py وأضف الشيفرة التالية:

import graphql_jwt


class Mutation(graphene.ObjectType):
    token_auth = graphql_jwt.ObtainJSONWebToken.Field()
    verify_token = graphql_jwt.Verify.Field()
    refresh_token = graphql_jwt.Refresh.Field()

يمكننا الآن اختباره في واجهة GraphiQL:

في حال كلمة مرور خاطئة

04 كلمة مرور خاطئة

في حال مستخدم موثوق

05 استيثاق المستخدم

كما هو واضح، فإن متطلبات تسجيل الدخول هي اسم المستخدم وكلمة المرور، وإذا تم التحقق من وثوقية هذا المستخدم بنجاح (أي أدخل اسم مستخدم وكلمة مرور صحيحة) سيُرسل من الواجهة الخلفية ترميز مشفر token. ويمكن حفظ هذا الترميز في ذاكرة المتصفح.

يمكنك أيضًا تخصيص سلوك ObtainJSONWebToken من خلال العودة إلى Mutions.py:

# Customize the ObtainJSONWebToken behavior to include the user info

class ObtainJSONWebToken(graphql_jwt.JSONWebTokenMutation):
    user = graphene.Field(types.UserType)

    @classmethod
    def resolve(cls, root, info, **kwargs):
        return cls(user=info.context.user)

class Mutation(graphene.ObjectType):
    token_auth = ObtainJSONWebToken.Field()

لاحظ أن ObtainJSONWebToken يتوسع إلى JSONWebTokenMutation الافتراضي، ومن ثم في صنف Mutation، ويمكنك استخدام ObtainJSONWebToken عوضًا عن ذلك.

يمكن الآن الحصول على مزيد من المعلومات حول المستخدم عبر GraphQL.

06 تخصيص استيثاق المستخدم

استخدام حزمة بينيا Pinia في الواجهة الأمامية

حان الوقت الآن لحل المشكلة الثانية في الواجهة الأمامية. والخطوة الأولى هي تثبيت بينيا Pinia.

npm install pinia

اذهب الآن إلى main.js وتأكد من أن تطبيقك يستخدم بينيا Pinia.

import { createPinia } from "pinia";

createApp(App).use(createPinia()).use(router).use(apolloProvider).mount("#app");

ارجع الآن إلى مجلد src وأنشئ مجلد باسم stores، وهو المجلد الذي سنخزن فيه جميع بيانات التطبيق. في الوقت الحالي، كل ما تحتاج إليه هو تخزين القيم الخاصة بالمستخدم (بيانات تسجيل الدخول) وسنحافظ على المعلومات فيه حتى بعد تحديث الصفحة، لذا أنشئ ملف باسم user.js من أجل التعامل مع التخزين كما يلي:

import { defineStore } from "pinia";

export const useUserStore = defineStore({
  id: "user",
  state: () => ({
    token: localStorage.getItem("token") || null,
    user: localStorage.getItem("user") || null,
  }),
  getters: {
    getToken: (state) => state.token,
    getUser: (state) => JSON.parse(state.user),
  },
  actions: {
    setToken(token) {
      this.token = token;

      // Save token to local storage
      localStorage.setItem("token", this.token);
    },
    removeToken() {
      this.token = null;

      // Delete token from local storage
      localStorage.removeItem("token");
    },
    setUser(user) {
      this.user = JSON.stringify(user);

      // Save user to local storage
      localStorage.setItem("user", this.user);
    },
    removeUser() {
      this.user = null;

      // Delete user from local storage
      localStorage.removeItem("user");
    },
  },
});

لاحظ أن هذا مخزن البيانات يتكون من ثلاثة أقسام، وهم state و getters و actions. تساعد هذه الأقسام الثلاثة في إدارة حالة التطبيق بطريقة منظمة وسهلة وإذا كنت على معرفة جيدة بكيفية إنشاء تطبيق فيو Vue فيُفترض أن يكون ذلك مفهومًا بالنسبة لك.

تشبه state في Pinia الدالة data() ‎  في مكون فيو Vue، إذ تعمل على تحديد المتغيرات، باستثناء أن يمكن لجميع المكونات الوصول إلى هذه المتغيرات. في مثالنا، سيحاول فيو Vue الحصول على الترميز المشفر في ذاكرة المتصفح، فإذا لم يكن موجودًا، سيأخذ المتغير قيمة null.

أما getters فهي توازي المتغيرات computed، إذ تنفذ إجراءات بسيطة، وعادةً ما يقتصر عملها على إرجاع قيمة state فحسب. وهنا أيضًا يمكن لجميع المكونات والصفحات الوصول إلى المتغيرات.

وأخيرًا، يمكن تشبيه actions بـ methods الموجودة في مكون فيو Vue. كما أنها تؤدي بعض الإجراءات باستخدام الحالات states. وفي هذه الحالة، أنت تعمل على حفظ أو إزالة الترميز المشفر للمستخدم ومعلوماته.

ثمة شيء آخر عليك ملاحظته، وهو أنه لا يمكنك حفظ الكائنات objects في الذاكرة المحلية، وإنما يمكنك حفظ السلاسل النصية strings فقط. لذا يجب عليك استخدام stringify() و parse()‎ لتحويل البيانات إلى سلسلة، ومن ثم إعادتها إلى كائن.

ستحتاج بعد ذلك إلى استخدام هذا المخزن عند تسجيل دخول المستخدم، وقد أنشأنا ملف SignIn.vue مثل هذا:

<script>
import { useUserStore } from "@/stores/user";
import { USER_SIGNIN } from "@/mutations";

export default {
  name: "SignInView",

  setup() {
    const userStore = useUserStore();
    return { userStore };
  },

  data() {
    return {
      signInDetails: {
        username: "",
        password: "",
      },
    };
  },

  methods: {
    async userSignIn() {
      const user = await this.$apollo.mutate({
        mutation: USER_SIGNIN,
        variables: {
          username: this.signInDetails.username,
          password: this.signInDetails.password,
        },
      });
      this.userStore.setToken(user.data.tokenAuth.token);
      this.userStore.setUser(user.data.tokenAuth.user);
    },
  },
};
</script>

نلاحظ في السطر 2 استيراد مخزن المستخدم User store الذي أنشأته.

في السطور 9-12، استدعاء مخزن المستخدم User store في الخطاف setup، وهو ما يجعل التعامل مع بينيا Pinia أسهل دون أي وظائف خريطة إضافية.

في السطرين 32-33، استدعاء الإجراءين setToken()‎ و setUser()‎ اللذيْن أنشأناهما منذ قليل، وهو ما سيحفظ المعلومات داخل الذاكرة المحلية.

هذه هي الطريقة التي يمكنك من خلالها تسجيل دخول المستخدم، ولكن ماذا لو كان المستخدم قد سجل دخوله أساسًا، لنلقي نظرة على هذا المثال:

<script>
import { SITE_INFO } from "@/queries";
import { useUserStore } from "@/stores/user";

export default {
  setup() {
    const userStore = useUserStore();
    return { userStore };
  },

  data() {
    return {
      menuOpen: false,
      mySite: null,
      user: {
        isAuthenticated: false,
        token: this.userStore.getToken || "",
        info: this.userStore.getUser || {},
      },
      dataLoaded: false,
    };
  },

  async created() {
    const siteInfo = await this.$apollo.query({
      query: SITE_INFO,
    });
    this.mySite = siteInfo.data.site;

    if (this.user.token) {
      this.user.isAuthenticated = true;
    }
  },

  methods: {
    userSignOut() {
      this.userStore.removeToken();
      this.userStore.removeUser();
    },
  },
};
</script>

نحاول في السطرين 18- 19 الحصول على الترميز المشفر token ومعلومات المستخدم user info من المخزن Store.

في السطور 31-33، إذا كان الترميز المشفر token موجودًا، فسيُعد المستخدم مستوثقًا authentication .

في السطور 38-41، ستعمل الطريقة userSignOut()‎ على تسجيل خروج المستخدم عند استدعائها.

بعد أن تعرفنا على كيفية استرجاع البيانات باستخدام الاستعلامات queries ووضحنا كيفية إرسال البيانات باستخدام الطفرات mutations، سنشرح في المقال التالي طريقة إنشاء نظام تعليقات وتفاعلات إعجاب لتطبيق المدونة الخاص بنا.

ترجمة -وبتصرّف- للمقال Create a Modern Application with Django and Vue #2.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...