LoginSignup
5
7

簡易的なLINEのwebを作る(Django + Heroku + Redis + SQLite)

Last updated at Posted at 2024-05-31

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を作成し下記を記述.

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

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>', # 追加
]
settings.py
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 

image.png

ファイル構造はこんな感じ

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でアプリ作成

左上のNewをクリックしCreate new appを選択
image.png

お好みのアプリ名を決め,Create app
image.png

GitHubを選択しsearchボタンをクリック.今回のリポジトリをconnect
image.png

Resourcesに移動しAdd-onsでRedisと検索し,Heroku Data for Redisを選択.
image.png

最安のプランでSubmit Order Form
image.png

数分したら左下のHeroku Data for Redisをクリック.
image.png

Settingの中のView Credentials... をクリックしURIをコピー
image.png

Djangoの環境変数設定(localで動かすつもりないなら要らない)

rootに.envファイルを作成し下記を記入

REDIS_URL=rediss://...みたいなの(さっきコピーしたやつね)

settings.py

画面右上のOpen appをクリックしたときに開かれたページのURLを保存する.

これ以降上記のURLの https:// を除いた部分を Heroku_URL と呼ぶ

image.png

settings.py
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' # 追加
settings.py
# 追加----------------------------------------------
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ファイルから取得) 
        },
    },
}
# --------------------------------------------------
settings.py
STATIC_URL = 'static/'

# 追加----------------------------------------------
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
    BASE_DIR / 'static',
]
# --------------------------------------------------
settings.py
# 追加----------------------------------------------
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のバージョン指定

runtime.txt
python-3.10.4

websocketの設定

asgi.py

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を作成し下記を記述

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を作成し下記を記述

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

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を作成し以下を記述

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

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を追加

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を追加

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を追加

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

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}/;
だけ注意.

chat.js
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';
}

結果

  1. Room Nameに参加したいroomを入力しJoin Roomをクリック
  2. 左側にroomの一覧が表示され,右側にroomのチャット欄が表示される
  3. 左側のroomの一覧からroomの変更ができる
  4. 右側にチャット欄が表示され右下の入力欄に文字を入力しEnterで送信ができる.

image.png

以上

5
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7