🔩 Полный фуллстек: пишем сайт на Django, Vue и GraphQL

Шаг за шагом пишем сайт с бэкендом на Django, фронтендом на Vue и связкой между ними на GraphQL. Для всех любителей Python и современной веб-разработки.

Публикация представляет собой незначительно сокращенное пособие Дэйна Хилларда Build a Blog Using Django, Vue, and GraphQL.

***

Это руководство проведет вас через процесс создания серверной части на Django и клиентской части на Vue со связкой между ними в виде GraphQL. Это большой пошаговый проект, делайте перерывы по мере необходимости.

Из руководства вы узнаете:

  • Как транслировать модели Django в GraphQL API.
  • Как одновременно запустить сервер Django и приложение на Vue.
  • Как администрировать Django-проект.
  • Как использовать GraphQL API для отображения данных в браузере с помощью Vue.
Обратите внимание
Весь исходный код туториала доступен в GitHub-репозитории.

Суть проекта на Django, Vue и GraphQL

Традиционно стартовым проектом для веба явлется блог — такие проекты включают все стандартные CRUD-операции: создание, чтение, обновление и удаление.

Мы разработаем небольшое приложение для ведения блога с некоторыми элементарными функциями. Авторы могут писать множество сообщений, сообщения могут иметь множество тегов, могут публиковаться или сохраняться в черновиках.

Бэкенд блога мы сделаем на Django, затем реализуем передачу контента GraphQL API и, наконец, воспользуемся Vue для отображения данных в браузере. Вот наши шаги:

  1. Настроить блог Django.
  2. Создать администратора блога Django.
  3. Настроить Graphene-Django.
  4. Настроить django-cors-headers.
  5. Настроить Vue.js.
  6. Настроить Vue Router.
  7. Создать компоненты Vue.
  8. Получить и отобразить даные.

Предварительные знания

Руководство будет легче воспринять, если вы уже знакомы с концепцией веб-приложений: как работают HTTP-запросы и API. В этом плане будет полезно прочитать нашу публикацию про Python и API.

Поскольку мы будем использовать для создания серверной части блога Django, хорошо бы ознакомиться с процедурой запуска проекта на Django. Возможно, стоит для начала попробовать создать проект на чистом Django.

Поскольку мы будем использовать для интерфейса пользователя Vue, будет полезен опыт работы с реактивным JavaScript. Если в прошлом вы манипулировали DOM-элементами только с помощью jQuery, знакомство с Vue станет хорошим продолжением.

Запросы GraphQL похожи на JSON-объекты и возвращают данные в формате JSON. Поэтому полезно разобраться, что это такое. Позже в этом руководстве вам потребуется установить Node.js, прочитайте наше руководство для новичков.

Шаг 1. Настраиваем Django

Создадим каталог, в котором будем хранить код проекта. Назовем его dvg, сокращенно от Django-Vue-GraphQL:

mkdir dvg/
cd dvg/

Мы будем разделять фронтенд и бэкенд-код, так что неплохо сразу создать подкаталоги для бэкенда:

mkdir backend/
cd backend/

Весь код Django мы поместим в каталог backend, полностью изолировав его от кода Vue.

Устанавливаем Django

Чтобы отделить зависимости проекта от других ваших проектов, создадим виртуальное окружение. Далее в руководстве предполагается, что вы запускате команды, связанные с Python и Django, в активированном виртуальном окружении. Для установки зависимостей создадим в директории backend файл requirements.txt:

Django==3.1.7

Устанавливаем Django в виртуальном окружении:

(venv) $ python -m pip install -r requirements.txt

Django установлен, инициализируем проект:

(venv) $ django-admin startproject backend .

Команда создаст в каталоге backend модуль manage.py и внутренний пакет backend. Структура каталогов теперь выглядит так:

dvg
└── backend
    ├── manage.py
    ├── requirements.txt
    └── backend
        ├── __init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

Мы создали новый проект, а значит, нужно создать базу данных и осуществить миграцию:

(venv) $ python manage.py migrate

Вы увидите список миграций:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

Это создаст файл базы данных SQLite с именем db.sqlite3, в котором будут храниться данные нашего проекта.

SQL
Подробнее о SQL, различных базах данных и о том как подружить Python и базы данных, читайте в нашем руководстве о работе в Python с SQL.

Есть база данных — можем создать суперпользователя:

(venv) $ python manage.py createsuperuser

Итак, мы установили Django, создали проект, провели миграции и добавили суперпользователя. В итоге у нас есть полностью функционирующее (пусть пока и пустое) приложение. Запускается оно так:

(venv) $ python manage.py runserver

Результат можно посмотреть по адресу http://localhost:8000/. Вы увидите стартовую страницу пустого Django-приложения. Кроме того, станет доступна страница http://localhost:8000/admin, с помощью которой можно администрировать проект. Чтобы попасть внутрь, используйте логин и пароль суперпользователя.

Шаг 2. Создаем приложение

Проект на Django может содержать множество различных приложений. Обычно одно приложение соответствует одному смысловому блоку сайта, например, ленте новостей, магазину товаров или корзине. Создадим приложение блога:

(venv) $ python manage.py startapp blog

Будет создана директория blog с несколькими шаблонными файлами:

blog
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

Позже в руководстве мы их дополним.

Вновь созданное приложение не добавляется по умолчанию в проект. Чтобы фреймворк знал, что приложение является частью проекта, дополняем список приложений в файле настроек проекта backend/settings.py:

INSTALLED_APPS = [
  ...
  "blog",
]

Это поможет Django найти информацию о приложении: модели данных и шаблоны URL-адресов.

Создаем модели данных для блога

Создадим три следующие модели данных:

  • Profile хранит информацию о пользователях блога.
  • Tag содержит данные о категориях, по которым группируются записи блога.
  • Post используется для хранения контента и метаданных о каждом посте блога.

Все модели добавляются в соответствующий файл приложения blog/models.py. Каждая модель наследуется от стандартных моделей Django:

from django.db import models

Модель Profile

Модель профиля содержит несколько полей:

  • user — связь с пользователем Django (связь один-к-одному).
  • website — опциональный URL, по которому можно узнать больше о пользователе.
  • bio — опциональное небольшое био («о себе»).

Импортируем из Django модуль настроек settings и опишем класс для нашей новой модели:

from django.conf import settings

class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
    )
    website = models.URLField(blank=True)
    bio = models.CharField(max_length=240, blank=True)

    def __str__(self):
        return self.user.get_username()

Метод __str__ сделает удобнее отображение профилей в панели администратора .

Модель Tag

В модели Tag будет единственное поле, короткое имя тега:

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

Модель Post

Модель Post — самая сложная, содержит множество полей: заголовок (title), подзаголовок (subtitle), слаг (slug, уникальная часть URL для нашего поста), контент поста (body) и т.д.

class Post(models.Model):
    class Meta:
        ordering = ["-publish_date"]

    title = models.CharField(max_length=255, unique=True)
    subtitle = models.CharField(max_length=255, blank=True)
    slug = models.SlugField(max_length=255, unique=True)
    body = models.TextField()
    meta_description = models.CharField(max_length=150, blank=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    publish_date = models.DateTimeField(blank=True, null=True)
    published = models.BooleanField(default=False)

    author = models.ForeignKey(Profile, on_delete=models.PROTECT)
    tags = models.ManyToManyField(Tag, blank=True)

Некоторые пояснения:

  1. В подклассе Meta мы указываем порядок сортировки постов (ordering) по дате публикации.
  2. Аргумент on_delete = models.PROTECT для поля author гарантирует, что при удалении постов мы случайно не удалим автора.
  3. Каждый тег может быть связан со многими сообщениями, поэтому для поля tags используется отношение ManyToManyField.

Конфигурируем панель администратора

Для того, чтобы определить, как будут отображаться записи в панели администрирования блога, переходим в blog/admin.py и импортируем созданные модели:

from django.contrib import admin

from blog.models import Profile, Post, Tag

Создаем и регистрируем классы моделей:

@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    model = Profile

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    model = Tag

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    model = Post

    list_display = (
        "id",
        "title",
        "subtitle",
        "slug",
        "publish_date",
        "published",
    )
    list_filter = (
        "published",
        "publish_date",
    )
    list_editable = (
        "title",
        "subtitle",
        "slug",
        "publish_date",
        "published",
    )
    search_fields = (
        "title",
        "subtitle",
        "slug",
        "body",
    )
    prepopulated_fields = {
        "slug": (
            "title",
            "subtitle",
        )
    }
    date_hierarchy = "publish_date"
    save_on_top = True

У постов мы не показываем все поля подряд, а только необходимые для администрирования. К ним мы добавили возможности фильтрации, редактирования и поиска. Подробно эти настройки рассмотрены в статье Customize the Django Admin With Python.

Создаем миграции модели

В Django есть вся информация, необходимая для управления содержимым сайта, но сначала нужно обновить базу данных. Ранее в этом руководстве мы запускали миграции Django для встроенных моделей. Теперь создадим и запустим миграции уже для наших моделей:

(venv) $ python manage.py makemigrations
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Tag
    - Create model Profile
    - Create model Post

Это создаст миграцию с именем по умолчанию 0001_initial.py. Запустим миграцию с помощью команды управления migrate:

(venv) $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

Теперь у нас есть модели данных и мы настроили админпанель Django, чтобы добавлять и редактировать эти модели.

Запустите или перезапустите сервер разработки Django и зайдите в панель по адресу http://localhost:8000/admin посмотреть, что изменилось. Вы увидите ссылки на списки тегов, профилей и сообщений, а также ссылки для добавления и редактирования каждого из них. Добавьте и отредактируйте несколько из них, чтобы увидеть, как отреагирует интерфейс администратора.

Шаг 3. Настройка Graphene-Django

В результате предыдущего этапа мы завершили основную работу над бэкендом. Далее можно было бы использовать механизмы маршрутизации URL и шаблонов Django для создания страниц, которые будут показывать читателям контент. Но вместо этого мы обернем созданную нами серверную часть в GraphQL API. За счет этого мы обеспечим более удобную работу на стороне клиента.

GraphQL позволяет получать только те данные, которые нам действительно нужны, что выгодно отличает эту технологию от RESTful API. GraphQL обеспечивает гибкость при проектировании данных, за счет чего мы можем получать новые структуры данных, не изменяя логику службы, предоставляющей GraphQL API.

Устанавливаем Graphene-Django

Для интеграции Django и GraphQL мы используем библиотеку Graphene-Django. Для установки библиотеки дополняем файл requirements.txt:

graphene-django==2.14.0

Запускаем установку через менеджер пакетов pip:

(venv) $ python -m pip install -r requirements.txt

Теперь нужно добавить приложение "graphene_django" в список INSTALLED_APPS в модуле settings.py:

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

Настраиваем Graphene-Django

Параметр GRAPHENE в файле settings.py указывает Graphene-Django расположение схемы GraphQL. Для нашего примера этот путь соответствует blog.schema.schema (саму схему мы вскоре создадим):

GRAPHENE = {
  "SCHEMA": "blog.schema.schema",
}

Добавляем шаблон URL для GraphQL и GraphiQL. Чтобы позволить Django обслуживать конечную точку GraphQL и интерфейс GraphiQL, добавим новый шаблон URL в backend/urls.py. Поскольку мы не используем функции защиты от подделки межсайтовых запросов (CSRF) шаблонизатора Django, нам необходимо импортировать декоратор Django csrf_exempt, чтобы пометить представление, как свободное от CSRF-защиты:

backend/urls.py
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

Добавляем паттерн в список переменной urlpatterns:

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

Аргумент graphiql = True указывает Graphene-Django сделать доступным GraphiQL-интерфейс .

Создаем GraphQL-схему. Теперь мы создадим схему GraphQL, похожую по своей логике на конфигурацию панели администратора. Схема состоит из нескольких классов, каждый из которых связан с определенной моделью Django, а также ещё одного класса, который показывает, как разрешать несколько важных типов запросов, которые понадобятся нам во внешнем интерфейсе.

В каталоге blog/ создадим новый модуль schema.py . Импортируем из Graphene-Django DjangoObjectType, модели блога и модель пользователя Django:

blog/schema.py
from django.conf import settings
from graphene_django import DjangoObjectType

from blog import models

Создадим класс для каждой из наших моделей и модели User. Имя каждого класса должно заканчиваться на Type, потому что каждое из них соответствует типу GraphQL. Классы должны выглядеть следующим образом:

blog/schema.py
class UserType(DjangoObjectType):
    class Meta:
        model = settings.AUTH_USER_MODEL

class AuthorType(DjangoObjectType):
    class Meta:
        model = models.Profile

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

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

Ещё нам нужно создать класс Query, наследуемый от graphene.ObjectType. Этот класс объединит все созданные нами классы типов, и мы добавим к нему методы, указывающие способы запроса моделей. Сначала импортируем модуль graphene:

import graphene

Класс Query требует ряда атрибутов, которые являются либо graphene.List, (если запрос возращает несколько элементов), либо graphene.Field (если запрос возвращает один элемент).

Для каждого из атрибутов мы создадим метод решения запроса. Мы разрешаем запрос, беря информацию, предоставленную в запросе, и возвращая в ответ соответствующий запрос Django. Метод каждого преобразователя должен начинаться с resolve_, а остальная часть имени должна соответствовать атрибуту. Например, метод разрешения запросов для атрибута all_posts должен называться resolve_all_posts.

В итоге получается следующий сниппет:

class Query(graphene.ObjectType):
    all_posts = graphene.List(PostType)
    author_by_username = graphene.Field(AuthorType, username=graphene.String())
    post_by_slug = graphene.Field(PostType, slug=graphene.String())
    posts_by_author = graphene.List(PostType, username=graphene.String())
    posts_by_tag = graphene.List(PostType, tag=graphene.String())

    def resolve_all_posts(root, info):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .all()
        )

    def resolve_author_by_username(root, info, username):
        return models.Profile.objects.select_related("user").get(
            user__username=username
        )

    def resolve_post_by_slug(root, info, slug):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .get(slug=slug)
        )

    def resolve_posts_by_author(root, info, username):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .filter(author__user__username=username)
        )

    def resolve_posts_by_tag(root, info, tag):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .filter(tags__name__iexact=tag)
        )

Теперь у нас есть все типы и преобразователи для нашей схемы. Но помним, что переменная GRAPHENE указывает на blog.schema.schema. Создаем переменную схемы, которая обертывает класс Query в graphene.Schema, чтобы связать все это вместе:

schema = graphene.Schema(query=Query)

В результатае переменная соответствует значению blog.schema.schema, которое мы настроили для Graphene-Django ранее в этом руководстве.

Итак, мы обернули модель данных с помощью Graphene-Django, чтобы использовать эти данные в GraphQL API. Запустите сервер разработки Django и посетите страницу http://localhost:8000/graphql. Вы должны увидеть интерфейс GraphiQL с некоторыми комментариями, объясняющими, как использовать инструмент.

Разверните раздел Docs в правом верхнем углу экрана и щелкните по query:Query. Вы должны увидеть каждый из запросов и типов, которые мы настроили в схеме.

Если вы еще не наполняли блог тестовыми данными, сделайте это сейчас. Попробуйте выполнить следующий запрос. Он должен вернуть список всех созданных сообщений:

{
  allPosts {
    title
    subtitle
    author {
      user {
        username
      }
    }
    tags {
      name
    }
  }
}

Ответ должен вернуть список постов. Структура каждого поста должна соответствовать форме запроса, как в следующем примере:

{
  "data": {
    "allPosts": [
      {
        "title": "The Great Coney Island Debate",
        "subtitle": "American or Lafayette?",
        "author": {
          "user": {
            "username": "coney15land"
          }
        },
        "tags": [
          {
            "name": "food"
          },
          {
            "name": "coney island"
          }
        ]
      }
    ]
  }
}

Если вы сохранили несколько постов и видите их в ответе, значит, можно продолжать.

Шаг 4. Настраиваем django-cors-headers

Чтобы считать работу над бэкендом завершенной, сделаем еще один шаг. Серверная часть и интерфейс будут запускаются на разных портах, а на практике так и вообще могут запускаться на разных доменах. Поэтому важное значение принимает вопрос совместного использования ресурсов (CORS). Без поддержки CORS запросы от фронтенда к бэкенду обычно блокируются браузером.

Библиотека django-cors-headers делает работу с CORS довольно безболезненной. Мы будем использовать эту библиотеку, чтобы указать Django отвечать на запросы, даже если они исходят из другого источника. Это позволит фронтенду правильно взаимодействовать с GraphQL API.

Установка. Добавляем название модуля в зависимости (requirements.txt):

django-cors-headers==3.6.0

Устанавливаем:

(venv) $ python -m pip install -r requirements.txt

Добавляем в список приложений INSTALLED_APPS в файле settings.py:

settings.py
INSTALLED_APPS = [
  ...
  "corsheaders",
]

Теперь нужно подключить библиотеку в качестве промежуточного обработчика в переменной MIDDLEWARE:

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

Документация django-cors-headers советует ставить эту строку как можно выше в списке обработчиков.

CORS существует не просто так. Мы не хотим, чтобы наше приложение было доступно для использования из любого места в Интернете. Чтобы этого избежать, мы используем две настройки, чтобы определить, насколько мы хотим открыть GraphQL API:

  1. CORS_ORIGIN_ALLOW_ALL определяет, должен ли Django быть полностью открыт или полностью закрыт по умолчанию.
  2. CORS_ORIGIN_WHITELIST определяет, для каких доменов приложение Django будет разрешать запросы.

Соответственно добавляем в settings.py две следующие строки:

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = ["http://localhost:8080"]

Такие настройки разрешат запросы только от нашего фронтенда, которые в конечном итоге мы будем запускать локально на порту 8080.

Бэкенд готов! У нас есть модель данных, интерфейс администратора, GraphQL API на базе GraphiQL и возможность запрашивать API из внешнего интерфейса, который мы создадим дальше. Отличный момент, чтобы передохнуть, если вы ещё этого не делали ⛱️.

Шаг 5. Настраиваем Vue.js

В качестве фронтенд-фреймворка мы будем использовать Vue. Как и Django, Vue предоставляет интерфейс для создания проекта. Используя этот подход, нам не придется устанавливать вручную множество отдельных зависимостей, необходимых для запуска проекта на Vue. Достаточно использовать npx:

$ cd путь_к_директории_с_проектом
$ npx @vue/cli create frontend --default
...
🎉  Successfully created project frontend.
...
$ cd frontend/

Указанная комана создаст подкаталог frontend/ рядом с уже существующим каталогом backend/, установит ряд зависимостей JavaScript и создаст некоторые каркасные файлы для нашего будущего фронтенд-приложения.

Установим плагины Vue

Чтобы правильно выполнять маршрутизацию и взаимодействовать с GraphQL API, нам понадобятся плагины Vue Router и Vue Apollo. При появлении запросов выбирайте параметры по умолчанию:

$ npx @vue/cli add router
$ npx @vue/cli add apollo

Этим командам потребуется время для установки зависимостей, они добавят или изменят некоторые файлы в проекте. Теперь мы можем запустить сервер разработки:

$ npm run serve

Итак, у нас есть приложение Django, работающее по адресу http://localhost:8000, и приложение Vue, которое запускается по адресу http://localhost:8080.

Шаг 6. Настраиваем Vue Router

Важной частью клиентских приложений является обработка маршрутизации без необходимости делать новые запросы к серверу. Распространенным решением в Vue является плагин Vue Router, который мы установили ранее.

В каталоге src/ создадим модуль router.js . Этот файл будет содержать настройки сопоставления URL-адресов и компонентов Vue. Начнем с импорта Vue и Vue Router:

import Vue from 'vue'
import VueRouter from 'vue-router'

Каждый из следующих элементов импорта соответствует нашим будущим компонентам:

import Post from '@/components/Post'
import Author from '@/components/Author'
import PostsByTag from '@/components/PostsByTag'
import AllPosts from '@/components/AllPosts'

Регистрируем плагин:

Vue.use(VueRouter)

Теперь создадим список маршрутов. Каждый маршрут имеет два параметра:

  1. path — URL-шаблон, похожий по смыслу на URL-шаблоны Django.
  2. component — компонент Vue, соответствующий пути.

Создадим константу routes:

const routes = [
  { path: '/author/:username', component: Author },
  { path: '/post/:slug', component: Post },
  { path: '/tag/:tag', component: PostsByTag },
  { path: '/', component: AllPosts },
]

Создадим новый экземпляр VueRouter и экспортируем его из модуля router.js, чтобы другие модули могли его использовать:

const router = new VueRouter({
  routes: routes,
  mode: 'history',
})
export default router

Далее в начале файла src/main.js импортируем router :

import router from '@/router'

Передаем маршрут в экземпляр Vue:

new Vue({
  router,
  ...
})

На этом настройка Vue Router завершена. Мы создали маршруты для внешнего интерфейса, которые сопоставляют шаблон URL-адреса с отображаемым компонентом. Сами маршруты пока не работают, потому как указывают на компоненты, которые еще не созданы.

Шаг 7. Создаем компоненты Vue

Теперь Vue умеет работать с маршрутами, пора создать компоненты, которые будут отображать данные из конечной точки GraphQL:

  • AuthorLink – ссылка на страницу автора (используется в Post и PostList).
  • PostList – список постов в блоге (используется в AllPosts, Author и PostsByTag).
  • AllPosts – список постов, начиная с самых последних.
  • PostsByTag – список постов, связанных с заданным тегом, начиная с самых недавних.
  • Post – метаданные и контент публикации.
  • Author – информация об авторе и список написанных им постов.

Компонент AuthorLink

Первый компонент, который мы создадим, отображает ссылку на автора. В каталоге src/components/ создадим файл AuthorLink.vue. Этот файл представляет собой однофайловый компонент (single file component, SFC) Vue. SFC в одном файле хранит HTML, JavaScript и CSS, необходимые для визуализации компонента.

AuthorLink принимает prop-объект author, структура которого соответствует данным об авторах в GraphQL API. Компонент должен отображать имя и фамилию пользователя, если они указаны, в противном случае — имя пользователя.

Файл AuthorLink.vue должен выглядеть следующим образом:

AuthorLink.vue
<template>
  <router-link
      :to="`/author/${author.user.username}`"
  >{{ displayName }}</router-link>
</template>

<script>
export default {
  name: 'AuthorLink',
  props: {
    author: {
      type: Object,
      required: true,
    },
  },
  computed: {
    displayName () {
      return (
        this.author.user.firstName &&
        this.author.user.lastName &&
        `${this.author.user.firstName} ${this.author.user.lastName}`
      ) || `${this.author.user.username}`
    },
  },
}
</script>

Этот компонент не будет использовать GraphQL напрямую. Вместо этого другие компоненты передают информацию об авторе, используя свойство author.

Компонент PostList

Компонент PostList принимает prop-объект posts, структура которого соответствует данным о сообщениях в нашем GraphQL API. Компонент отображает следующие вещи:

  • Заголовок и подзаголовок поста, слинкованный с самой страницей поста.
  • Ссылка на автора поста через AuthorLink (если логическая переменная showAuthor равна true).
  • Дата публикации поста.
  • Мета-описание поста.
  • Список тегов.

Создайте PostList.vue в каталоге src/components/. Шаблон компонента должен выглядеть следующим образом:

PostList.vue
<template>
  <div>
    <ol class="post-list">
      <li class="post" v-for="post in publishedPosts" :key="post.title">
          <span class="post__title">
            <router-link
              :to="`/post/${post.slug}`"
            >{{ post.title }}: {{ post.subtitle }}</router-link>
          </span>
          <span v-if="showAuthor">
            by <AuthorLink :author="post.author" />
          </span>
          <div class="post__date">{{ displayableDate(post.publishDate) }}</div>
        <p class="post__description">{{ post.metaDescription }}</p>
        <ul>
          <li class="post__tags" v-for="tag in post.tags" :key="tag.name">
            <router-link :to="`/tag/${tag.name}`">#{{ tag.name }}</router-link>
          </li>
        </ul>
      </li>
    </ol>
  </div>
</template>

Код JavaScript должен выглядеть так:

PostList.vue
<script>
import AuthorLink from '@/components/AuthorLink'

export default {
  name: 'PostList',
  components: {
    AuthorLink,
  },
  props: {
    posts: {
      type: Array,
      required: true,
    },
    showAuthor: {
      type: Boolean,
      required: false,
      default: true,
    },
  },
  computed: {
    publishedPosts () {
      return this.posts.filter(post => post.published)
    }
  },
  methods: {
    displayableDate (date) {
      return new Intl.DateTimeFormat(
        'en-US',
        { dateStyle: 'full' },
      ).format(new Date(date))
    }
  },
}
</script>

Компонент PostList получает данные через prop вместо прямого использования GraphQL.

В том же файле можно добавить несколько дополнительных стилей CSS, чтобы сделать список постов удобнее для чтения:

PostList.vue
<style>
.post-list {
  list-style: none;
}

.post {
  border-bottom: 1px solid #ccc;
  padding-bottom: 1rem;
}

.post__title {
  font-size: 1.25rem;
}

.post__description {
  color: #777;
  font-style: italic;
}

.post__tags {
  list-style: none;
  font-weight: bold;
  font-size: 0.8125rem;
}
</style>

Компонент AllPosts

Следующий компонент, который мы создадим, — список постов в блоге. Он должен отображать две вещи:

  • Заголовок, например "Недавние сообщения" или "Recent Posts".
  • Список постов с помощью PostList.

Создаём AllPosts.vue в каталоге src/components/. Должно получиться так:

AllPosts.vue
<template>
  <div>
    <h2>Recent posts</h2>
    <PostList v-if="allPosts" :posts="allPosts" />
  </div>
</template>

<script>
import PostList from '@/components/PostList'

export default {
  name: 'AllPosts',
  components: {
    PostList,
  },
  data () {
    return {
        allPosts: null,
    }
  },
}
</script>

Позже мы заполним переменную allPosts динамически с помощью GraphQL-запроса.

Компонент PostsByTag

Компонент PostsByTag очень похож на компонент AllPosts:

PostsByTag.vue
<template>
  <div>
    <h2>Posts in #{{ $route.params.tag }}</h2>
    <PostList :posts="posts" v-if="posts" />
  </div>
</template>

<script>
import PostList from '@/components/PostList'

export default {
  name: 'PostsByTag',
  components: {
    PostList,
  },
  data () {
    return {
      posts: null,
    }
  },
}
</script>

Компонент Author

Компонент Author действует, как страница профиля автора. То есть компонент должен отображать следующее:

  • Заголовок с именем автора.
  • Ссылка на сайт автора (если указана).
  • "О себе" автора (если предоставлено).
  • Список постов автора.
Author.vue
<template>
  <div v-if="author">
    <h2>{{ displayName }}</h2>
    <a
      :href="author.website"
      target="_blank"
      rel="noopener noreferrer"
    >Website</a>
    <p>{{ author.bio }}</p>

    <h3>Posts by {{ displayName }}</h3>
    <PostList :posts="author.postSet" :showAuthor="false" />
  </div>
</template>

<script>
import PostList from '@/components/PostList'

export default {
  name: 'Author',
  components: {
    PostList,
  },
  data () {
    return {
      author: null,
    }
  },
  computed: {
    displayName () {
      return (
        this.author.user.firstName &&
        this.author.user.lastName &&
        `${this.author.user.firstName} ${this.author.user.lastName}`
      ) || `${this.author.user.username}`
    },
  },
}
</script>

Компонент Post

Компонент Post является наиболее интересным, поскольку отвечает за отображение всей информации о публикации:

  1. Заголовок и подзаголовок.
  2. Автор (с помощью ссылки AuthorLink).
  3. Дата публикации.
  4. Мета-описание.
  5. Содержание ("тело" поста).
  6. Список связанных тегов в виде ссылок.

За счет используемой модели данных и компонентной архитектуры нам потребуется совсем немного кода:

<template>
  <div class="post" v-if="post">
      <h2>{{ post.title }}: {{ post.subtitle }}</h2>
      By <AuthorLink :author="post.author" />
      <div>{{ displayableDate(post.publishDate) }}</div>
    <p class="post__description">{{ post.metaDescription }}</p>
    <article>
      {{ post.body }}
    </article>
    <ul>
      <li class="post__tags" v-for="tag in post.tags" :key="tag.name">
        <router-link :to="`/tag/${tag.name}`">#{{ tag.name }}</router-link>
      </li>
    </ul>
  </div>
</template>

<script>
import AuthorLink from '@/components/AuthorLink'

export default {
  name: 'Post',
  components: {
    AuthorLink,
  },
  data () {
    return {
      post: null,
    }
  },
  methods: {
    displayableDate (date) {
      return new Intl.DateTimeFormat(
        'en-US',
        { dateStyle: 'full' },
      ).format(new Date(date))
    }
  },
}
</script>

Компонент App

Прежде чем увидеть результаты работы, необходимо обновить компонент App, созданный при первоначальной настройке Vue. Вместо отображения страницы-заставки Vue должен отображаться компонент AllPosts.

В каталоге src/ откроем App.vue и заменим содержимое следующим кодом:

<template>
    <div id="app">
        <header>
          <router-link to="/">
            <h1>Awesome Blog</h1>
          </router-link>
        </header>
        <router-view />
    </div>
</template>

<script>
export default {
  name: 'App',
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
}

body {
  margin: 0;
  padding: 1.5rem;
}

* + * {
  margin-top: 1.5rem;
}

#app {
  margin: 0;
  padding: 0;
}
</style>

Здесь описан заголовок с названием блога, который ведет на главную страницу, а также компонент Vue Router, который отображает компонент для текущего маршрута.

Итак, мы подошли к концу седьмого (предпоследнего) шага. Если вы раньше не использовали Vue, этот шаг, возможно, был трудоемким. Однако мы достигли важной вехи — работающего приложения Vue с маршрутами и представлениями, готовыми для отображения данных.

Запустите сервер разработки Vue и перейдите по адресу http://localhost:8080. Вы должны увидеть заголовок блога и заголовки недавних публикаций. На последнем шаге мы воспользуемся Apollo для обращений к GraphQL API, чтобы соединить между собой интерфейс и серверную часть.

Шаг 8. Собираем данные

Пора получить данные из GraphQL API. Плагин Vue Apollo, который мы установили ранее, интегрирует Apollo во Vue и делает удобнее процедуру выполнения запросов к GraphQL API.

Vue Apollo уже настроен «из коробки», но нужно указать правильную конечную точку запроса. Мы также можем отключить WebSocket-соединение, которое плагин пытается использовать по умолчанию, поскольку это создает лишний шум на вкладках «Сеть» и «Консоль» в средствах разработки в браузере. Отредактируем определение apolloProvider в модуле src/main.js, указав свойства httpEndpoint и wsEndpoint:

new Vue({
  ...
  apolloProvider: createProvider({
    httpEndpoint: 'http://localhost:8000/graphql',
    wsEndpoint: null,
  }),
  ...
})

Теперь мы можем добавить запросы для заполнения страниц. Мы сделаем это, добавив функцию created() в несколько наших SFC. Эта функция — специальный хук жизненного цикла Vue, выполняемый в момент, когда компонент готовится к рендерингу на странице. Функцию можно использовать для запроса данных, которые мы хотим визуализировать. Мы создадим запросы для следующих компонентов:

  • Post
  • Author
  • PostByTag
  • AllPosts

Запрос для получения информации о посте (Post)

Запрос для отдельного поста принимает slug нужного сообщения и возвращает необходимую информацию для отображения публикации. Для создания запроса в функции created() мы используем функции-помощники $apollo.query и gql . Функция created() будет выглядеть следующим образом:

<script>
  ...
  async created () {
    const post = await this.$apollo.query({
        query: gql`query ($slug: String!) {
          postBySlug(slug: $slug) {
            title
            subtitle
            publishDate
            metaDescription
            slug
            body
            author {
              user {
                username
                firstName
                lastName
              }
            }
            tags {
              name
            }
          }
        }`,
        variables: {
          slug: this.$route.params.slug,
        },
    })
    this.post = post.data.postBySlug
  },
  ...
</script>

Запрос извлекает большую часть данных о публикации, авторе и тегах. Обратите внимание, что в запросе используется заполнитель $slug, для заполнения которого применяется свойство variables, передаваемое в $apollo.query. Свойство slug соответствует имени заполнителя $slug. Мы встретим такой же шаблон в следующих запросах.

Запрос для Author

Запрос Author принимает username автора и возвращает информацию, необходимую для отображения автора и его списка постов. Должно получиться так:

<script>
  ...
  async created () {
    const user = await this.$apollo.query({
      query: gql`query ($username: String!) {
        authorByUsername(username: $username) {
          website
          bio
          user {
            firstName
            lastName
            username
          }
          postSet {
            title
            subtitle
            publishDate
            published
            metaDescription
            slug
            tags {
              name
            }
          }
        }
      }`,
      variables: {
        username: this.$route.params.username,
      },
    })
    this.author = user.data.authorByUsername
  },
  ...
</script>

В этом запросе используется postSet, который может показаться знакомым по нашей модели данных Django. Название «post set» происходит от связи, которую Django создает для поля внешнего ключа. Graphene-Django автоматически предоставляет postSet в GraphQL API.

Запрос для PostByTag

Запрос для PostsByTag очень похож на предыдущие. Он принимает желаемый тег и возвращает список подходящих постов:

<script>
  ...
  async created () {
    const posts = await this.$apollo.query({
      query: gql`query ($tag: String!) {
        postsByTag(tag: $tag) {
          title
          subtitle
          publishDate
          published
          metaDescription
          slug
          author {
            user {
              username
              firstName
              lastName
            }
          }
          tags {
            name
          }
        }
      }`,
      variables: {
        tag: this.$route.params.tag,
      },
    })
    this.posts = posts.data.postsByTag
  },
  ...
</script>

Вы уже могли заметить, что некоторые части запросов очень похожи друг на друга. Чтобы уменьшить дублирование кода, обратите внимание на концепцию GraphQL fragments.

Чистый код
О том, чем плохо дублирование кода, читайте в нашей популярной статье «Как написать код, который полюбят все».

Запрос для AllPosts

Запрос AllPosts не требует ввода информации и возвращает тот же набор данных, что и запрос PostsByTag. Должно получиться так:

<script>
  ...
  async created () {
    const posts = await this.$apollo.query({
      query: gql`query {
        allPosts {
          title
          subtitle
          publishDate
          published
          metaDescription
          slug
          author {
            user {
              username
              firstName
              lastName
            }
          }
          tags {
            name
          }
        }
      }`,
    })
    this.allPosts = posts.data.allPosts
  },
  ...
</script>

Ура! Это последний запрос. Теперь каждый компонент получает данные, необходимые для отображения, а мы получили работающий блог. Запустите сервер разработки Django и сервер разработки Vue. Откройте http://localhost:8080 и посмотрите на результат.

Возможные следующие шаги

Мы начали с создания серверной части блога Django для администрирования, сохранения и обслуживания данных блога. Затем создали интерфейс Vue для использования и отображения этих данных. Наконец, научили их общаться с GraphQL, используя Graphene и Apollo.

Чтобы еще раз убедиться, что блог работает должным образом, можно попробовать следующее:

  • Добавить пользователей и посты, чтобы увидеть разделение по авторам.
  • Создать несколько сообщений без публикации, чтобы убедиться, что они не отображаются в блоге, а сохраняются в форме черновика.

Заключение

Итак, мы узнали, как использовать GraphQL для создания гибких типизированных представлений данных. Вы можете применять эти методы как в уже созданных приложениях Django, так и в тех, что вы только планируете создать. Как и другие API, этот подход применим для большинства современных фронтенд-фреймворков. Надеемся, что эта концепция пригодится вам ещё не раз.

Вот ещё несколько материалов о GraphQL:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
28 июня 2018

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...