websocketという非同期通信の技術を使用し,簡単なLINEの擬きを作ってみましょう!!
GitHub(デプロイするので必要)
Repositoryを作成しクローンしたディレクトリで作業を行ってください。
djnagoの環境構築
Pythonのインストール
pythonのインストールは下記の公式より各自の環境にあったものをインストールしてください.(Python 3.6以上)
Python公式サイト
仮想環境の作成
仮想環境の作成
python -m venv venv
macはpython3
仮想環境のアクティベート
Mac用
source venv/bin/activate
Windows用
venv\Scripts\activate
ライブラリのインストール
pipのインストール
python -m ensurepip --upgrade
rootにrequirements.txtを作成し下記を記述.
asgiref==3.8.1
autobahn==23.6.2
channels==4.1.0
channels-redis==4.2.0
daphne==4.1.2
Django==5.0.6
Twisted==24.3.0
websockets==12.0
whitenoise==6.0.0
requirements.txt に記述されているパッケージを一括でインストール
pip install -r requirements.txt
プロジェクトの作成
プロジェクトの作成
<project_name>は好きな名前でok
今回、私はlineで作成した.
django-admin startproject <project_name> .
アプリケーションの作成
<yourappname>も好きな名前でok
今回、私はchatappで作成した.
python manage.py startapp <yourappname>
templatesとstatic
rootにtemplates,staticという名前のディレクトリを作成.
settings.py
ALLOWED_HOSTS = [
'localhost', # 追加
'127.0.0.1', # 追加
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'<yourappname>', # 追加
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # 追加
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
migrate
コマンドラインで以下を実行
python manage.py migrate
ここまでの確認
コマンドで下記を実行するとロケットの打ちあがったページが確認できる
python manage.py runserver
ファイル構造はこんな感じ
C:.
| .gitignore
| db.sqlite3
| manage.py
| README.md
| requirements.txt
|
+---chatapp
| | admin.py
| | apps.py
| | models.py
| | tests.py
| | views.py
| | __init__.py
| |
| +---migrations
| | | __init__.py
| | |
| | \---__pycache__
| | __init__.cpython-310.pyc
| |
| \---__pycache__
| admin.cpython-310.pyc
| apps.cpython-310.pyc
| models.cpython-310.pyc
| __init__.cpython-310.pyc
|
+---line
| | asgi.py
| | settings.py
| | urls.py
| | wsgi.py
| | __init__.py
| |
| \---__pycache__
| settings.cpython-310.pyc
| urls.cpython-310.pyc
| wsgi.cpython-310.pyc
| __init__.cpython-310.pyc
|
+---static
+---templates
+---venv
|venvの中は省略
Herokuでデプロイ準備 and Redisのレンタル
Herokuのアカウントは各自で作ってください
herokuでアプリ作成
GitHubを選択しsearchボタンをクリック.今回のリポジトリをconnect
Resourcesに移動しAdd-onsでRedisと検索し,Heroku Data for Redisを選択.
数分したら左下のHeroku Data for Redisをクリック.
Settingの中のView Credentials... をクリックしURIをコピー
Djangoの環境変数設定(localで動かすつもりないなら要らない)
rootに.envファイルを作成し下記を記入
REDIS_URL=rediss://...みたいなの(さっきコピーしたやつね)
settings.py
画面右上のOpen appをクリックしたときに開かれたページのURLを保存する.
これ以降上記のURLの https:// を除いた部分を Heroku_URL と呼ぶ
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1',
'herokuapp.com', # 追加
'Heroku_URL' # 追加
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'<yourappname>',
'channels', # 追加
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # 追加
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # 追加
# 追加----------------------------------------------
import os
# asgiアプリケーションの位置
ASGI_APPLICATION = '<yourappname>.asgi.application'
# チャンネルレイヤーの設定
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [os.getenv('REDIS_URL')], # RedisのURLを環境変数から取得(localの場合は.envファイルから取得)
},
},
}
# --------------------------------------------------
STATIC_URL = 'static/'
# 追加----------------------------------------------
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# --------------------------------------------------
# 追加----------------------------------------------
CSRF_TRUSTED_ORIGINS = [
'https://Heroku_URL',
]
# --------------------------------------------------
Procfileとruntime.txtの作成
rootにProcfile,runtime.txtという名前のファイルを作成
web: daphne <project_name>.asgi:application --port $PORT --bind 0.0.0.0 -v2
pythonのバージョン指定
python-3.10.4
websocketの設定
asgi.py
import os
from django.core.asgi import get_asgi_application
import chatapp.routing
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '<project_name>.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
chatapp.routing.websocket_urlpatterns
)
),
})
routing.py
app内にrouting.pyを作成し下記を記述
from django.urls import re_path
from . import consumers
# WebSocketConsumerは.as_asgi()で呼び出せる。
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)', consumers.ChatConsumer.as_asgi()),
]
consumers.py
app内にconsumers.pyを作成し下記を記述
import hashlib
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{hashlib.md5(self.room_name.encode("utf-8")).hexdigest()}'
print("Room name(consumers):", self.room_name)
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
currentRoomName = text_data_json['room']
user = text_data_json['user']
if currentRoomName is not None:
print("Room name2:", currentRoomName)
else:
print("Room name is not provided in the message.")
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'user': user,
'room': currentRoomName,
'message': message
}
)
def chat_message(self, event):
user = event['user']
message = event['message']
room = event['room']
print(message, room)
self.send(text_data=json.dumps({
'type': 'chat',
'user': user,
'room': room,
'message': message
}))
通常のDjangoの設定
<project_name>/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('<yourappname>.urls')),
]
<yourappname>/urls.py
<yourappname>の中にurls.pyを作成し以下を記述
from django.urls import path
from . import views
app_name = "chat"
urlpatterns = [
path('chat/', views.chat, name="chat_rooms"),
path('', views.loginview, name='login'),
path('create/', views.create_login, name="create"),
]
<yourappname>/views.py
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views import View
# チャット画面を表示するビュー
class ChatView(View):
@method_decorator(login_required)
def get(self,request,*args,**kwargs):
user = request.user
return render(request,"chat.html",{"user":user})
chat = ChatView.as_view()
# ログイン画面を表示するビュー
class LoginView(View):
def get(self,request,*args,**kwargs):
return render(request,"login.html")
def post(self,request,*args,**kwargs):
# ログイン時に表示するページとログイン機能
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
# ユーザー認証
user = authenticate(request, username=username, password=password)
if user is not None:
# ログイン成功
login(request, user)
return redirect('chat:chat_rooms')
else:
# ログイン失敗
messages.error(request, 'ユーザー名またはパスワードが間違っています。')
return render(request,"login.html")
loginview = LoginView.as_view()
# ユーザー作成画面を表示するビュー
class CreateLoginView(View):
def get(self,request,*args,**kwargs):
return render(request,"create_login.html")
def post(self,request,*args,**kwargs):
if request.method == 'POST':
new_username = request.POST.get('new_username')
new_password = request.POST.get('new_password')
try:
# 新しいユーザーオブジェクトを作成し、ユーザー名とパスワードを設定
user = User.objects.create_user(username=new_username, password=new_password)
except Exception as e:
# ユーザ作成失敗
messages.error(request, 'ユーザーの作成に失敗しました。エラー: {}'.format(str(e)))
# ログイン処理
user = authenticate(request, username=new_username, password=new_password)
if user is not None:
# ログイン成功
messages.error(request, 'ログインに成功しました。')
login(request, user)
return redirect('chat:chat_rooms')
else:
# ログイン失敗
messages.error(request, 'ユーザー名またはパスワードが間違っています。ユーザー名: {}'.format(new_password))
return render(request,"create_login.html")
create_login = CreateLoginView.as_view()
templates
login.html
templatesディレクトリにlogin.htmlを追加
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<form method="post" action="{% url 'chat:login' %}">
{% csrf_token %}
<label for="username">ユーザー名:</label>
<input type="text" id="username" name="username">
<br>
<label for="userpassword">パスワード教えろ:</label>
<input type="password" id="userpassword" name="password">
<br>
<button type="submit">ログイン</button>
</form>
<a href="{% url 'chat:create' %}">新規アカウント作成</a>
</body>
</html>
create_login.html
templatesディレクトリにcreate_login.htmlを追加
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<form method="post" action="{% url 'chat:create' %}">
{% csrf_token %}
<label for="username">ユーザー名:</label>
<input type="text" id="username" name="new_username">
<br>
<label for="userpassword">パスワード教えろ:</label>
<input type="password" id="userpassword" name="new_password">
<br>
<button type="submit">ログイン</button>
</form>
</body>
</html>
chat.html
templatesディレクトリにchat.htmlを追加
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Room</title>
<link rel="stylesheet" href="{% static 'chat.css' %}">
</head>
<body>
<div class="chat-container">
<div class="left-panel">
<h1>Let's chat!</h1>
<h2>ユーザー: {{ request.user.username }}</h2>
<input type="hidden" id="username" value="{{ request.user.username }}">
<div class="room-input">
<label for="room-name">Room Name:</label>
<input type="text" id="room-name" placeholder="Enter room name">
<button id="join-room">Join Room</button>
</div>
<div id="rooms" class="room-list"></div>
</div>
<div id="talkroom" class="right-panel">
<form id="form" class="message-form">
<input type="text" name="message" placeholder="Enter message">
</form>
</div>
</div>
<script src="{% static 'chat.js' %}"></script>
</body>
</html>
staticとwebsocketのクライアント側
chat.css
body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
background-color: black;
margin: 0;
height: 100vh;
}
.chat-container {
display: contents;
}
.left-panel, .right-panel {
background-color: antiquewhite;
width: calc(50vw - 1px);
}
.room-input {
display: flex;
align-items: center;
gap: 8px;
}
.room-list {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
margin-top: 8px;
}
.room-list div {
background-color: aquamarine;
padding: 3%;
}
.messages {
padding: 8px;
overflow-y: auto;
height: calc(100vh - 40px - 3rem);
}
.message-form {
position: absolute;
bottom: 0;
width: calc(50vw - 1px - 2%);
margin: 1%;
}
.message-form input {
width: 90%;
padding: 8px;
}
chat.js
const url = wss://Herok_URL.herokuapp.com/ws/chat/${roomName}/
;
だけ注意.
let chatSocket = null;
let currentRoomName = null;
const username = document.getElementById('username').value;
document.getElementById('join-room').addEventListener('click', joinRoom);
function joinRoom() {
const roomName = document.getElementById('room-name').value;
if (roomName) {
const url = `wss://Herok_URL.herokuapp.com/ws/chat/${roomName}/`;
console.log('Join Room', url);
chatSocket = new WebSocket(url);
console.log(username);
currentRoomName = roomName;
console.log('Chat Socket:', chatSocket, 'Room:', currentRoomName);
initChatSocket();
addRoomToList(roomName);
}
}
function initChatSocket() {
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
console.log('Data:', data, 'Room:', currentRoomName);
if (data.type === 'chat') {
console.log('Message:', data.message);
console.log('Room:', currentRoomName);
addMessage(data.message, data.user);
}
};
document.getElementById('form').addEventListener('submit', function(e) {
e.preventDefault();
sendMessage();
});
}
function addRoomToList(roomName) {
const roomDiv = document.getElementById('rooms');
roomDiv.insertAdjacentHTML('beforeend', `
<div id="room_${roomName}">
<p>${roomName}</p>
</div>
`);
// 追加した部屋の要素を取得
let newRoomElement = document.getElementById(`room_${roomName}`);
// クリックイベントリスナーを追加
newRoomElement.addEventListener('click', function() {
changeRoom(roomName);
});
// 全ての chat_talks クラスを持つ要素を取得し、非表示にする
allclose();
// 新たなチャットルームを表示する
const roomChat = document.getElementById('talkroom');
roomChat.insertAdjacentHTML(
'beforeend',
`<div id="Room_${roomName}" class="chat_talks">
<h2>Room: ${roomName}</h2>
<div id="messages_${roomName}" class="messages"></div>
</div>`
);
}
function addMessage(message, username) {
if ( `${message}` != '') {
const messagesDiv = document.getElementById(`messages_${currentRoomName}`);
messagesDiv.insertAdjacentHTML('beforeend', `<div><p>${username}:${message}</p></div>`);
}
}
function sendMessage() {
const form = document.getElementById('form');
const formData = new FormData(form);
const message = formData.get('message');
console.log('Send Message:', message, 'Room:', currentRoomName);
chatSocket.send(JSON.stringify({'message': message, 'room': currentRoomName , 'user': username}));
form.reset();
}
function allclose() {
let chatTalksElements = document.querySelectorAll('.chat_talks');
chatTalksElements.forEach(element => {
element.style.display = 'none';
});
}
function changeRoom(roomName) {
allclose();
currentRoomName = roomName;
let roomElement = document.getElementById(`Room_${roomName}`);
roomElement.style.display = 'block';
}
結果
- Room Nameに参加したいroomを入力しJoin Roomをクリック
- 左側にroomの一覧が表示され,右側にroomのチャット欄が表示される
- 左側のroomの一覧からroomの変更ができる
- 右側にチャット欄が表示され右下の入力欄に文字を入力しEnterで送信ができる.
以上