What is Django Channels?
Introduction
Chat apps are everywhere!
If, like me, you've done any research to answer this question, you've found that you can build a chat app. There are 100 if not more guides out there to help you build your next Slack. There aren't many tutorials, blogs, documentation, or other resources to help you do much more with Django Channels.
I'd like to share some of my findings with Django Channels and see if I can help explain some of the functionality of its parts, the methods and classes that it and some of its dependent libraries provide. And do it outside of a simple chat app.
If you haven't spent time with Django Channels yet, then by all means create a chat app. There is one such application in the Django Channels documentation, and there are many other similar tutorials. It's a good place to start, and that's where I started. This is not one of those tutorials.
What are Django Channels?
Django Channels extends Django's built-in capabilities beyond just HTTP, taking advantage of the "spiritual successor" WSGI (Web Server Gateway Interface), ASGI (Asynchronous Server Gateway Interface). While WSGI provided a synchronous standard for Python web applications, ASGI provides both synchronous and asynchronous standards.
In a nutshell, these are Django channels. I will link below so you can read more about Django Channels, ASGI and WSGI. If you haven't created a chat app using Django Channels yet, then I recommend doing so; because this article assumes you've done it, and therefore you already have some working knowledge of terminology and references: Django Channels Chat Tutorial
Outside chat apps
Declaring channel layers
To trigger a message based on, say, an HTTP POST request. We need to access the link layer.
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
But how do we access this layer outside of the Django Channels Consumer?
Well, Django Channels provides a method to access the channel layer outside of the Consumer.
from channels.layers import get_channel_layer
No new web socket message has reached the browser.
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)
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(URLRouter([
path('index/', IndexConsumer)
]))
})
So, let's go in a circle. How can we trigger/dispatch an event outside of our declared consumer?
Apply channel layers
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([])
We can use get_channel_layer from within the rest_framework APIView, but to do this we must use another magic function that is included in the asgiref library: async_to_sync. You can get a better understanding of the asgiref library in Django github repo: asgiref The asgiref.async_to_sync method allows us to interact with our async consumer inside a synchronous rest_framework views. According to the asgiref source code on github: "A utility class that turns an awaitable that runs only on an event loop thread into a synchronous callable that runs on a subthread." This is from the docstring in the class that async_to_sync uses to declare an instance of the AsyncToSync class.
So we declare the channel_layer object without passing any arguments - we just use the default channel layer we declared in our settings file and pass it to the async_to_sync utility function. Now the syntax is getting a little weird. I don't know about you, but I haven't seen anything like this in Python. It is similar to Javascript IIFE (Immediately Invoked Function Expression). After looking at the source code, this appears to be the case, perhaps due to the benefit of overriding the __call__ method for the AsyncToSync class.
I'm pointing this out because I was struggling with my POST method calling anything at this point, and it was just a couple of syntax errors.
Success!
I'm holding my breath at this point, I just wanted to attach an APIView, access it from the viewable API that comes with rest_framework, and be able to see changes in the UI without reloading the page. And then it worked! I haven't been this happy to click on a button on a web page since my first AJAX call. Of course, this moment is made possible by Vue. Vue is great for Django and I prefer it to React; because it's possible to use a Vue object inside a Django template without having to create a separate Javascript project for the frontend - I'd probably do that, but it's just a proof of concept. So, a quick look at the frontend code:
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
}
}
})
I'm using a prebuilt Vue component that integrates well with Chart.js. In the main Vue object, I'm setting up client-side web sockets inside the mounted method. This allows us to update the DOM (Document Object Model) as data arrives via the web socket. In this case, we can redraw the chart based on the new dataset that comes through the socket, and do it when it arrives, without reloading the whole page.
Conclusion
So, that's it! I hope we have a better understanding of what we can achieve with Django Channels. At least I hope we have an understanding of how we can call Web Socket messages from server side events. I see Django Channels as, in some cases, an improvement on an already useful library django.contrib.messages . Flash messages are a great way to insert a status message, such as the submission status of an HTML form on page reload, but the fact that flash messages depend on page reload can be quite a limitation for modern responsive web applications
Back to Top