Что такое Django Channels?

Введение

Приложения для чата повсюду!

Если вы, как и я, провели хоть какое-то исследование, чтобы ответить на этот вопрос, вы обнаружили: вы можете создать приложение для чата. Существует 100, если не больше, руководств, которые помогут вам создать следующий Slack. Существует не так много учебников, блогов, документации или других ресурсов, которые помогут вам сделать многое другое с Django Channels.

Я хотел бы рассказать о некоторых своих открытиях в работе с Django Channels и посмотреть, смогу ли я помочь объяснить некоторые функциональные возможности его частей, методы и классы, которые он и некоторые из его зависимых библиотек предоставляют. И сделать это вне рамок простого приложения для чата.

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

Что такое Django Channels?

Django Channels расширяет встроенные возможности Django за пределы только HTTP, используя преимущества "духовного наследника" WSGI (Web Server Gateway Interface), ASGI (Asynchronous Server Gateway Interface). Если WSGI предоставлял синхронный стандарт для веб-приложений Python, то ASGI предоставляет как синхронные, так и асинхронные стандарты.

В двух словах это каналы Django. Я дам ссылку ниже, чтобы вы могли прочитать больше о Django Channels, ASGI и WSGI. Если вы еще не создали чат-приложение с использованием Django Channels, то я рекомендую сделать это; потому что эта статья предполагает, что вы это сделали, и, следовательно, у вас уже есть некоторые рабочие знания терминологии и ссылок: Django Channels Chat Tutorial

За пределами чат-приложений

После того как вы создали несколько приложений для чата, вы, скорее всего, захотите создать что-то еще. В Lofty Labs мы создаем много приложений для приборных панелей, чтобы наши клиенты могли просматривать свои данные и взаимодействовать с ними осмысленным образом. Еще один инструмент, который мы добавляем в свой арсенал: веб-сокеты. Django Channels - логичный выбор, поскольку мы уже в значительной степени полагаемся на Django.

Я столкнулся с несколькими препятствиями, выходящими за рамки приложения для чата, где мы действительно хотим просто передавать изменения состояния в несколько браузеров по событию, вызванному пользователем, без перезагрузки страницы. Как насчет запуска сообщения веб-сокета на основе события на стороне сервера?

Объявление слоев канала

Для того чтобы вызвать сообщение на основе, скажем, HTTP POST запроса. Нам необходимо получить доступ к канальному уровню.

Что это?

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

Помните этого парня из учебника по чату? Здесь мы определяем уровни каналов, здесь мы просто объявляем уровень по умолчанию, который использует Redis для хранения сообщений веб-сокета. Мы можем использовать Redis для этого, потому что мы не хотим хранить эти сообщения неопределенно долго, но нам нужно место для чтения и записи этих сообщений. Это и есть наш канальный уровень. Можно настроить более одного канального уровня, но пока нам нужен только канальный уровень по умолчанию.

Но как нам получить доступ к этому слою вне Django Channels Consumer?

Ну, Django Channels предоставляет метод для доступа к слою канала вне Consumer.

from channels.layers import get_channel_layer

Замечательно! Тогда я могу просто вызвать это из rest_framework APIView, верно? Но я не смог понять, как заставить это работать со стандартным Django Channels SyncConsumer. Я перепробовал множество способов, основанных на документации Django Channels, исходном коде Django Channels и Stack Overflow, но ничего не помогло. И самое неприятное, что это было беззвучно.

Нет ошибок.

Ни одно новое сообщение веб-сокета не достигло браузера.

AsyncJsonWebsocketConsumer

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

class IndexConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        await self.accept()
        await self.channel_layer.group_add('index', self.channel_name)

    async def disconnect(self, code):
        print('disconnected')
        await self.channel_layer.group_discard('index', self.channel_name)
        print(f'removed {self.channel_name}')

    async def websocket_receive(self, message):
        return super().websocket_receive(message)

    async def websocket_ingest(self, event):
        await self.send_json(event)

В этом потребителе происходят некоторые вещи, о которых стоит упомянуть. Во-первых, мы должны быть более явными с нашими методами на этом потребителе.

В методе connect класса IndexConsumer, после того как мы явно принимаем новые соединения, мы также добавляем это соединение в новую группу, 'index'. Она может быть названа как угодно. Общая идея заключается в том, что по мере появления новых соединений, так как, возможно, больше браузеров подключаются к серверу, все они получают доступ к одним и тем же сообщениям в этом канальном слое, которые организованы в эту группу.

Обратите внимание, что наши пользовательские методы дополнены символом websocket_. Это ссылка на маршрут, который мы объявили в маршрутизаторе каналов.

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(URLRouter([
        path('index/', IndexConsumer)
    ]))
})

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

Итак, идем по кругу. Как мы можем вызвать/отправить событие вне нашего объявленного потребителя?

Применение слоев канала

Я упоминал о методе, который входит в состав Django Channels для доступа к канальному уровню. Давайте посмотрим на этот метод в действии.

from rest_framework.views import APIView
from rest_framework.response import Response
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync


def random_data():
    return [random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) for i in range(7)]


class IngestView(APIView):
    def post(self, request):
        channel_layer = get_channel_layer()
        text = random_data()

        async_to_sync(channel_layer.group_send)(
            'index', {
                'type': 'websocket.ingest',
                'text': text
                }
        )
        return Response([])

    def get(self, request):
        return Response([])

IngestView подключен так, как вы ожидаете в urls.py, поэтому там нет ничего нового. Я написал простую функцию, которая будет возвращать список случайных целых чисел, значения которых варьируются от 1 до 10. Мы будем использовать ее для имитации недельного временного ряда данных.

Мы можем использовать get_channel_layer изнутри rest_framework APIView, но для этого мы должны использовать другую волшебную функцию, которая включена в библиотеку asgiref: async_to_sync. Вы можете получить лучшее понимание библиотеки asgiref в Django github repo: asgiref Метод asgiref.async_to_sync позволяет нам взаимодействовать с нашим асинхронным потребителем внутри синхронного представления rest_framework. Согласно исходному коду asgiref на github: "Утилитарный класс, который превращает awaitable, работающий только в потоке с циклом событий, в синхронный callable, работающий в подпотоке". Это из docstring в классе, который async_to_sync использует для объявления экземпляра класса AsyncToSync.

Итак, мы объявляем объект channel_layer, не передавая никаких аргументов - мы просто используем канальный слой по умолчанию, который мы объявили в нашем файле настроек, и передаем его в служебную функцию async_to_sync. Теперь синтаксис становится немного странным. Не знаю, как вы, но я не видел ничего подобного в Python. Это похоже на Javascript IIFE (Immediately Invoked Function Expression). После просмотра исходного кода, похоже, что так оно и есть, возможно, благодаря преимуществу переопределения метода __call__ для класса AsyncToSync.

Я указываю на это, потому что я боролся с тем, чтобы мой метод POST вызвал что-либо на этом этапе, и это было просто из-за пары синтаксических ошибок.

Успех!

На этом этапе я затаил дыхание, я просто хотел подключить APIView, получить к нему доступ из просматриваемого API, который поставляется с rest_framework, и иметь возможность видеть изменения в пользовательском интерфейсе без перезагрузки страницы. И тут это заработало! Я не был так счастлив нажать на кнопку на веб-странице со времен моего первого вызова AJAX. Конечно, это мгновение стало возможным благодаря Vue. Vue отлично подходит для Django, и я предпочитаю его React; потому что можно использовать объект Vue внутри шаблона Django без необходимости создавать отдельный Javascript-проект для фронтенда - я бы, вероятно, так и сделал, но это лишь доказательство концепции. Итак, быстрый взгляд на код фронтенда:

        var LineChart = Vue.component('line-chart', {
          extends: VueChartJs.Line,
          props: ['dataSet'],
          watch: {
            dataSet() {
              this.loadData()
            }
          },        
          mounted () {
            this.loadData()
          },
          methods: {
            loadData() {
              var self = this;
                this.renderChart({
                  labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
                  datasets: [{
                    label: 'Weekly Data',
                    backgroundColor: 'rgb(255, 99, 132)',
                    borderColor: 'rgb(255, 99, 132)',
                    data: self.dataSet
                  }]
              })
            }
          }    
        })      

        var app = new Vue({
          el: '#app',
          components: {LineChart},
          data: {
            message: 'Hello!',
            dataSet: [6, 3, 10, 2, 10, 1, 10],
            connected: false,
          },
          mounted() {
              console.log()
              var socket = new WebSocket(
                'ws://' + window.location.host +
                '/index/')

              socket.onopen = event => {
                  console.log('connected')
                  this.connected = true
                  socket.send({})
              }

              socket.onmessage = event => {
                json_data = JSON.parse(event.data)
                this.dataSet = json_data.text
                console.log(json_data.text)
              }

              socket.onclose = event => {
                this.connected = false
              }

            }          
        })

Я использую готовый компонент Vue, который хорошо интегрируется с Chart.js. В основном объекте Vue я настраиваю веб-сокеты на стороне клиента внутри метода mounted. Это позволяет нам обновлять DOM (Document Object Model) по мере поступления данных через веб-сокет. В этом случае мы можем перерисовать диаграмму на основе нового набора данных, который приходит через сокет, и сделать это, когда он приходит, без перезагрузки всей страницы.

Заключение

Итак, вот и все! Я надеюсь, что у нас есть лучшее понимание того, чего мы можем достичь с помощью Django Channels. По крайней мере, я надеюсь, что у нас есть понимание того, как мы можем вызывать сообщения Web Socket из событий на стороне сервера. Я рассматриваю Django Channels как, в некоторых случаях, усовершенствование уже полезной библиотеки django.contrib.messages. Flash-сообщения - это отличный способ вставлять сообщение о состоянии, например, о статусе отправки HTML-формы при перезагрузке страницы, но тот факт, что flash-сообщения зависят от перезагрузки страницы, может быть довольно большим ограничением для современных отзывчивых веб-приложений

Вернуться на верх