I den här handledningen kommer vi att använda Django Channels för att skapa en realtidsapplikation som uppdaterar en lista över användare när de loggar in och ut.
Med WebSockets (via Django Channels) som hanterar kommunikationen mellan klienten och servern, närhelst en användare autentiseras, kommer en händelse att sändas till alla andra anslutna användare. Varje användares skärm ändras automatiskt, utan att de behöver ladda om sina webbläsare.
OBS: Vi rekommenderar att du har lite erfarenhet av Django innan du börjar med den här handledningen. Du bör också vara bekant med konceptet WebSockets.
Gratis bonus: Klicka här för att få tillgång till en gratis guide för Django Learning Resources (PDF) som visar dig tips och tricks samt vanliga fallgropar att undvika när du bygger Python + Django webbapplikationer.
Vår applikation använder:
- Python (v3.6.0)
- Django (v1.10.5)
- Django-kanaler (v1.0.3)
- Redis (v3.2.8)
Mål
I slutet av denna handledning kommer du att kunna...
- Lägg till stöd för webbsockets till ett Django-projekt via Django-kanaler
- Sätt upp en enkel anslutning mellan Django och en Redis-server
- Implementera grundläggande användarautentisering
- Utnyttja Django-signaler för att vidta åtgärder när en användare loggar in eller ut
Komma igång
Skapa först en ny virtuell miljö för att isolera vårt projekts beroenden:
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
Installera Django, Django Channels och ASGI Redis och skapa sedan ett nytt Django-projekt och en ny app:
(env)$ pip install django==1.10.5 channels==1.0.2 asgi_redis==1.0.0
(env)$ django-admin.py startproject example_channels
(env)$ cd example_channels
(env)$ python manage.py startapp example
(env)$ python manage.py migrate
OBS: Under den här handledningen kommer vi att skapa en mängd olika filer och mappar. Se mappstrukturen från projektets arkiv om du fastnar.
Ladda sedan ner och installera Redis. Om du använder en Mac rekommenderar vi att du använder Homebrew:
$ brew install redis
Starta Redis-servern i ett nytt terminalfönster och se till att den körs på sin standardport, 6379. Portnumret kommer att vara viktigt när vi berättar för Django hur man kommunicerar med Redis.
Slutför installationen genom att uppdatera INSTALLED_APPS
i projektets settings.py fil:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'example',
]
Konfigurera sedan CHANNEL_LAYERS
genom att ställa in en standardbackend och routing:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
'ROUTING': 'example_channels.routing.channel_routing',
}
}
Detta använder en Redis-backend som också behövs i produktionen.
WebSockets 101
Normalt använder Django HTTP för att kommunicera mellan klienten och servern:
- Klienten skickar en HTTP-begäran till servern.
- Django analyserar begäran, extraherar en URL och matchar den sedan med en vy.
- Vyn behandlar begäran och returnerar ett HTTP-svar till klienten.
Till skillnad från HTTP tillåter WebSockets-protokollet dubbelriktad kommunikation, vilket innebär att servern kan skicka data till klienten utan att bli tillfrågad av användaren. Med HTTP är det bara klienten som gjorde en begäran som får ett svar. Med WebSockets kan servern kommunicera med flera klienter samtidigt. Som vi kommer att se senare i denna handledning skickar vi WebSockets-meddelanden med ws://
prefix, i motsats till http://
.
OBS: Läs snabbt igenom kanalkonceptdokumentationen innan du dyker in.
Konsumenter och grupper
Låt oss skapa vår första konsument, som hanterar de grundläggande kopplingarna mellan klienten och servern. Skapa en ny fil som heter example_channels/example/consumers.py :
from channels import Group
def ws_connect(message):
Group('users').add(message.reply_channel)
def ws_disconnect(message):
Group('users').discard(message.reply_channel)
Konsumenterna är motsvarigheten till Django-vyerna. Alla användare som ansluter till vår app kommer att läggas till i gruppen "användare" och kommer att få meddelanden skickade av servern. När klienten kopplar från vår app tas kanalen bort från gruppen och användaren kommer att sluta ta emot meddelanden.
Låt oss sedan ställa in rutter, som fungerar på nästan samma sätt som Django URL-konfiguration, genom att lägga till följande kod i en ny fil som heter example_channels/routing.py :
from channels.routing import route
from example.consumers import ws_connect, ws_disconnect
channel_routing = [
route('websocket.connect', ws_connect),
route('websocket.disconnect', ws_disconnect),
]
Så vi definierade channel_routing
istället för urlpatterns
och route()
istället för url()
. Observera att vi länkade våra konsumentfunktioner till WebSockets.
Mallar
Låt oss skriva upp lite HTML som kan kommunicera med vår server via en WebSocket. Skapa en "mallar"-mapp i "exempel" och lägg sedan till en "exempel"-mapp inom "mallar" - "exempel_kanaler/exempel/mallar/exempel".
Lägg till en _base.html fil:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<title>Example Channels</title>
</head>
<body>
<div class="container">
<br>
{% block content %}{% endblock content %}
</div>
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
{% block script %}{% endblock script %}
</body>
</html>
Och user_list.html :
{% extends 'example/_base.html' %}
{% block content %}{% endblock content %}
{% block script %}
<script>
var socket = new WebSocket('ws://' + window.location.host + '/users/');
socket.onopen = function open() {
console.log('WebSockets connection created.');
};
if (socket.readyState == WebSocket.OPEN) {
socket.onopen();
}
</script>
{% endblock script %}
Nu, när klienten lyckas öppna en anslutning med servern med hjälp av en WebSocket, kommer vi att se ett bekräftelsemeddelande skrivas ut till konsolen.
Visningar
Konfigurera en stödjande Django-vy för att återge vår mall inom example_channels/example/views.py :
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
Lägg till webbadressen till example_channels/example/urls.py :
from django.conf.urls import url
from example.views import user_list
urlpatterns = [
url(r'^$', user_list, name='user_list'),
]
Uppdatera även projektets webbadress i example_channels/example_channels/urls.py :
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('example.urls', namespace='example')),
]
Testa
Är du redo att testa?
(env)$ python manage.py runserver
OBS: Du kan alternativt köra
python manage.py runserver --noworker
ochpython manage.py runworker
i två olika terminaler för att testa gränssnittet och arbetarservrarna som två separata processer. Båda metoderna fungerar!
När du besöker http://localhost:8000/ bör du se anslutningsmeddelandet skrivas ut till terminalen:
[2017/02/19 23:24:57] HTTP GET / 200 [0.02, 127.0.0.1:52757]
[2017/02/19 23:24:58] WebSocket HANDSHAKING /users/ [127.0.0.1:52789]
[2017/02/19 23:25:03] WebSocket DISCONNECT /users/ [127.0.0.1:52789]
Användarautentisering
Nu när vi har bevisat att vi kan öppna en anslutning är vårt nästa steg att hantera användarautentisering. Kom ihåg:Vi vill att en användare ska kunna logga in i vår app och se en lista över alla andra användare som prenumererar på den användarens grupp. Först behöver vi ett sätt för användare att skapa konton och logga in. Börja med att skapa en enkel inloggningssida som gör det möjligt för en användare att autentisera sig med ett användarnamn och lösenord.
Skapa en ny fil som heter log_in.html inom “example_channels/example/templates/example”:
{% extends 'example/_base.html' %}
{% block content %}
<form action="{% url 'example:log_in' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">Log in</button>
</form>
<p>Don't have an account? <a href="{% url 'example:sign_up' %}">Sign up!</a></p>
{% endblock content %}
Uppdatera sedan example_channels/example/views.py som så:
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
def user_list(request):
return render(request, 'example/user_list.html')
def log_in(request):
form = AuthenticationForm()
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.get_user())
return redirect(reverse('example:user_list'))
else:
print(form.errors)
return render(request, 'example/log_in.html', {'form': form})
def log_out(request):
logout(request)
return redirect(reverse('example:log_in'))
Django kommer med formulär som stöder vanliga autentiseringsfunktioner. Vi kan använda AuthenticationForm
för att hantera användarinloggning. Detta formulär kontrollerar det angivna användarnamnet och lösenordet och returnerar sedan en Användare
objekt om en validerad användare hittas. Vi loggar in den validerade användaren och omdirigerar dem till vår hemsida. En användare bör också ha möjlighet att logga ut från applikationen, så vi skapar en utloggningsvy som tillhandahåller den funktionen och sedan tar användaren tillbaka till inloggningsskärmen.
Uppdatera sedan example_channels/example/urls.py :
from django.conf.urls import url
from example.views import log_in, log_out, user_list
urlpatterns = [
url(r'^log_in/$', log_in, name='log_in'),
url(r'^log_out/$', log_out, name='log_out'),
url(r'^$', user_list, name='user_list')
]
Vi behöver också ett sätt att skapa nya användare. Skapa en registreringssida på samma sätt som inloggningen genom att lägga till en ny fil som heter sign_up.html till “example_channels/example/templates/example”:
{% extends 'example/_base.html' %}
{% block content %}
<form action="{% url 'example:sign_up' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">Sign up</button>
<p>Already have an account? <a href="{% url 'example:log_in' %}">Log in!</a></p>
</form>
{% endblock content %}
Observera att inloggningssidan har en länk till registreringssidan och att registreringssidan har en länk tillbaka till inloggningen.
Lägg till följande funktion i vyerna:
def sign_up(request):
form = UserCreationForm()
if request.method == 'POST':
form = UserCreationForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('example:log_in'))
else:
print(form.errors)
return render(request, 'example/sign_up.html', {'form': form})
Vi använder en annan inbyggd form för att skapa användare. Efter framgångsrik formulärvalidering omdirigerar vi till inloggningssidan.
Se till att importera formuläret:
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
Uppdatera example_channels/example/urls.py igen:
from django.conf.urls import url
from example.views import log_in, log_out, sign_up, user_list
urlpatterns = [
url(r'^log_in/$', log_in, name='log_in'),
url(r'^log_out/$', log_out, name='log_out'),
url(r'^sign_up/$', sign_up, name='sign_up'),
url(r'^$', user_list, name='user_list')
]
Vid det här laget måste vi skapa en användare. Kör servern och besök http://localhost:8000/sign_up/
i din webbläsare. Fyll i formuläret med ett giltigt användarnamn och lösenord och skicka in det för att skapa vår första användare.
OBS: Testa att använda
michael
som användarnamn ochjohnson123
som lösenord.
sign_up
view omdirigerar oss till log_in
visa, och därifrån kan vi autentisera vår nyskapade användare.
När vi har loggat in kan vi testa våra nya autentiseringsvyer.
Använd registreringsformuläret för att skapa flera nya användare som förberedelse för nästa avsnitt.
Inloggningsvarningar
Vi har grundläggande användarautentisering som fungerar, men vi behöver fortfarande visa en lista över användare och vi behöver servern för att tala om för gruppen när en användare loggar in och ut. Vi måste redigera våra konsumentfunktioner så att de skickar ett meddelande direkt efter en klient ansluter och precis innan en klient kopplar ur. Meddelandedata kommer att innehålla användarens användarnamn och anslutningsstatus.
Uppdatera example_channels/example/consumers.py som så:
import json
from channels import Group
from channels.auth import channel_session_user, channel_session_user_from_http
@channel_session_user_from_http
def ws_connect(message):
Group('users').add(message.reply_channel)
Group('users').send({
'text': json.dumps({
'username': message.user.username,
'is_logged_in': True
})
})
@channel_session_user
def ws_disconnect(message):
Group('users').send({
'text': json.dumps({
'username': message.user.username,
'is_logged_in': False
})
})
Group('users').discard(message.reply_channel)
Lägg märke till att vi har lagt till dekoratörer till funktionerna för att få användaren från Django-sessionen. Alla meddelanden måste dessutom vara JSON-serialiserbara, så vi dumpar vår data i en JSON-sträng.
Uppdatera sedan example_channels/example/templates/example/user_list.html :
{% extends 'example/_base.html' %}
{% block content %}
<a href="{% url 'example:log_out' %}">Log out</a>
<br>
<ul>
{% for user in users %}
<!-- NOTE: We escape HTML to prevent XSS attacks. -->
<li data-username="{{ user.username|escape }}">
{{ user.username|escape }}: {{ user.status|default:'Offline' }}
</li>
{% endfor %}
</ul>
{% endblock content %}
{% block script %}
<script>
var socket = new WebSocket('ws://' + window.location.host + '/users/');
socket.onopen = function open() {
console.log('WebSockets connection created.');
};
socket.onmessage = function message(event) {
var data = JSON.parse(event.data);
// NOTE: We escape JavaScript to prevent XSS attacks.
var username = encodeURI(data['username']);
var user = $('li').filter(function () {
return $(this).data('username') == username;
});
if (data['is_logged_in']) {
user.html(username + ': Online');
}
else {
user.html(username + ': Offline');
}
};
if (socket.readyState == WebSocket.OPEN) {
socket.onopen();
}
</script>
{% endblock script %}
På vår hemsida utökar vi vår användarlista för att visa en lista över användare. Vi lagrar varje användares användarnamn som ett dataattribut för att göra det enkelt att hitta användarobjektet i DOM. Vi lägger även till en händelseavlyssnare till vår WebSocket som kan hantera meddelanden från servern. När vi får ett meddelande analyserar vi JSON-data, hittar
Django spårar inte om en användare är inloggad, så vi måste skapa en enkel modell för att göra det åt oss. Skapa en LoggedInUser
modell med en en-till-en-anslutning till vår Användare
modell i example_channels/example/models.py :
from django.conf import settings
from django.db import models
class LoggedInUser(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, related_name='logged_in_user')
Vår app kommer att skapa en LoggedInUser
instans när en användare loggar in, och appen tar bort instansen när användaren loggar ut.
Gör schemamigreringen och migrera sedan vår databas för att tillämpa ändringarna.
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
Uppdatera sedan vår användarlistvy iexample_channels/example/views.py , för att hämta en lista över användare att rendera:
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
User = get_user_model()
@login_required(login_url='/log_in/')
def user_list(request):
"""
NOTE: This is fine for demonstration purposes, but this should be
refactored before we deploy this app to production.
Imagine how 100,000 users logging in and out of our app would affect
the performance of this code!
"""
users = User.objects.select_related('logged_in_user')
for user in users:
user.status = 'Online' if hasattr(user, 'logged_in_user') else 'Offline'
return render(request, 'example/user_list.html', {'users': users})
def log_in(request):
form = AuthenticationForm()
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.get_user())
return redirect(reverse('example:user_list'))
else:
print(form.errors)
return render(request, 'example/log_in.html', {'form': form})
@login_required(login_url='/log_in/')
def log_out(request):
logout(request)
return redirect(reverse('example:log_in'))
def sign_up(request):
form = UserCreationForm()
if request.method == 'POST':
form = UserCreationForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('example:log_in'))
else:
print(form.errors)
return render(request, 'example/sign_up.html', {'form': form})
Om en användare har en associerad LoggedInUser
, då registrerar vi användarens status som "Online", och om inte är användaren "Offline". Vi lägger också till en @login_required
dekoratör till både vår användarlista och utloggningsvyer för att begränsa åtkomsten endast till registrerade användare.
Lägg till importerna också:
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
Vid det här laget kan användare logga in och ut, vilket kommer att trigga servern att skicka meddelanden till klienten, men vi har inget sätt att veta vilka användare som är inloggade när användaren loggar in först. Användaren ser bara uppdateringar när en annan användares statusändringar. Det är här LoggedInUser
spelar in, men vi behöver ett sätt att skapa en LoggedInUser
instans när en användare loggar in, och sedan ta bort den när den användaren loggar ut.
Django-biblioteket innehåller en funktion som kallas signaler som sänder meddelanden när vissa åtgärder inträffar. Appar kan lyssna efter dessa meddelanden och sedan agera på dem. Vi kan utnyttja två hjälpsamma, inbyggda signaler (user_logged_in
och user_logged_out
) för att hantera vår LoggedInUser
beteende.
Inom "example_channels/example" lägg till en ny fil som heter signals.py :
from django.contrib.auth import user_logged_in, user_logged_out
from django.dispatch import receiver
from example.models import LoggedInUser
@receiver(user_logged_in)
def on_user_login(sender, **kwargs):
LoggedInUser.objects.get_or_create(user=kwargs.get('user'))
@receiver(user_logged_out)
def on_user_logout(sender, **kwargs):
LoggedInUser.objects.filter(user=kwargs.get('user')).delete()
Vi måste göra signalerna tillgängliga i vår appkonfiguration, example_channels/example/apps.py :
from django.apps import AppConfig
class ExampleConfig(AppConfig):
name = 'example'
def ready(self):
import example.signals
Uppdatera example_channels/example/__init__.py likaså:
default_app_config = 'example.apps.ExampleConfig'
Syndhetskontroll
Nu är vi klara med kodningen och är redo att ansluta till vår server med flera användare för att testa vår app.
Kör Django-servern, logga in som användare och besök hemsidan. Vi bör se en lista över alla användare i vår app, var och en med statusen "Offline". Öppna sedan ett nytt inkognitofönster och logga in som en annan användare och titta på båda skärmarna. Direkt när vi loggar in uppdaterar den vanliga webbläsaren användarstatusen till "Online". Från vårt inkognitofönster ser vi att användaren som är inloggad också har statusen "Online". Vi kan testa WebSockets genom att logga in och ut på våra olika enheter med olika användare.
Genom att observera utvecklarkonsolen på klienten och serveraktiviteten i vår terminal kan vi bekräfta att WebSocket-anslutningar bildas när en användare loggar in och förstörs när en användare loggar ut.
[2017/02/20 00:15:23] HTTP POST /log_in/ 302 [0.07, 127.0.0.1:55393]
[2017/02/20 00:15:23] HTTP GET / 200 [0.04, 127.0.0.1:55393]
[2017/02/20 00:15:23] WebSocket HANDSHAKING /users/ [127.0.0.1:55414]
[2017/02/20 00:15:23] WebSocket CONNECT /users/ [127.0.0.1:55414]
[2017/02/20 00:15:25] HTTP GET /log_out/ 302 [0.01, 127.0.0.1:55393]
[2017/02/20 00:15:26] HTTP GET /log_in/ 200 [0.02, 127.0.0.1:55393]
[2017/02/20 00:15:26] WebSocket DISCONNECT /users/ [127.0.0.1:55414]
OBS :Du kan också använda ngrok för att exponera den lokala servern för internet säkert. Genom att göra detta kan du träffa den lokala servern från olika enheter som din telefon eller surfplatta.
Avslutande tankar
Vi täckte mycket i den här handledningen - Django-kanaler, WebSockets, användarautentisering, signaler och viss front-end-utveckling. Det viktigaste är detta:Channels utökar funktionaliteten hos en traditionell Django-app genom att låta oss skicka meddelanden från servern till grupper av användare via WebSockets.
Det här är kraftfulla grejer!
Tänk på några av applikationerna. Vi kan skapa chattrum, spel för flera spelare och samarbetsappar som tillåter användare att kommunicera i realtid. Även vardagliga uppgifter förbättras med WebSockets. Till exempel, istället för att regelbundet fråga servern för att se om en långvarig uppgift har slutförts, kan servern skicka en statusuppdatering till klienten när den är klar.
Den här handledningen skrapar bara på ytan av vad vi kan göra med Django Channels också. Utforska Django Channels-dokumentationen och se vad mer du kan skapa.
Gratis bonus: Klicka här för att få tillgång till en gratis guide för Django Learning Resources (PDF) som visar dig tips och tricks samt vanliga fallgropar att undvika när du bygger Python + Django webbapplikationer.
Ta den sista koden från django-example-channels repo. Skål!