見出し画像

レンタルサーバでWebアプリ作り:複数ページを操作

ロリポップレンタルサーバではできることが限られていたので、ホスティングサービスを使ってWebアプリを公開するまでの道のりを記録します。

データ分析を仕事としているのでパソコンやITのことは多少は詳しいですが、インフラまわりは全くの素人です。そんな人が見て参考になる情報をまとめていきたいと思います。


背景

そろそろテストではなくちゃんとしたWebアプリを作ろうと思い、複数ページを動かすテストアプリを作りました。(やっぱりテスト・・・)

GPTを使う予定なのでSocket通信前提のものですが、これがしんどい。。。最終的には、「1つのサーバで複数のWebアプリを動かす」ことをしたいのですが、どういうフォルダ構成にしたらいいのかとかすらわかってない。。

ということで、2段構えで進めました。
1.1つのWebアプリを立ち上げ、複数ページでそれぞれSocket通信させる
2.2つのWebアプリを立ち上げる。


ゴール1=ページを複数作る

複数ページを作るとき、htmlは複数できると思いますが、pyは複数いるの?どう切り分けるの??という基本的なことも分かっていないので、まずはいつものGPT先生に泣きつきました。

そのGPT先生のアドバイスをちょっとずつ改良していきます。

実際のコーディング

GPT先生への質問

以下のような構成でWebアプリを構築したい。フォルダ構成、およびそれぞれのプログラムの中身をサンプルで教えてください。

# トップページ(index.html)
link1.htmlへのリンクが書かれている。
PythonとSocket通信する。

リンク1:音楽生成ページ
link1.htmlで、中身は「Hello World」と記載されている。

GPT先生への質問

こう聞いてみたところ、Pythonファイルはserver.pyの1種類のみ。いったんそれでちゃんと動くのを確認。この段階でうそを教えている可能性もあるので、いったん確認するという作業は結構重要です。

挙動確認した後、追加で以下の質問。

server.pyを、各ページの機能ごとにファイル分割することは可能ですか?

GPT先生への追加質問

ここでくれた案が動かず、かなりはまりました。複数のファイルが関係するものはGPT先生は弱そうです。一話完結型はめっぽう強いのですが。

以下は、GPT先生案から頑張って改良した最終型です。

フォルダ構成

オブジェクトの橋渡し的な「socket_config.py」を作っているのがミソです。

webapp/
│
├── app.py            # メインアプリケーション
├── socket_config.py  # Socketイベントハンドラ
├── index.py          # トップページのルート
├── link1.py          # サブページのルート
│
└── templates/        # HTMLファイル
    ├── index.html
    ├── link1.html
    └── link2.html

HTMLファイル

【 index.html 】
GPT先生が教えてくれたものに、過去の記事にあるSocket通信のコードを流用し、テケテケ表示がちゃんとできるか確認します。

<!DOCTYPE html>
<html>
<head>
    <title>トップページ</title>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
    <script type="text/javascript">
        document.addEventListener('DOMContentLoaded', function() {
            var socketUrl;
            var options = {
                transports: ['websocket'] // WebSocketのみを使用
            };

            if (location.protocol === 'https:') {
                // DigitalOceanの設定(HTTPS)
                socketUrl = 'https://' + document.domain;
            } else {
                // ローカル環境の設定(HTTP、ポート5000)
                socketUrl = 'http://' + document.domain + ':5000';
            }

            var socket = io.connect(socketUrl, options);


            socket.on('response', function(msg) {
                var logElement = document.getElementById('log');
                logElement.textContent = 'カウント: ' + msg.count;
            });

            document.getElementById('startButton').addEventListener('click', function() {
                socket.emit('start_count', {count: 10});
            });
        });
    </script>
</head>
<body>
    <h1>トップページ</h1>
    <a href="/link1">リンク1:音楽生成ページ</a>
    <br>
    <a href="/link2">リンク2:文章生成ページ</a>
    <br>
    <button id="startButton">Start Counting</button>
    <div id="log"></div>
</body>
</html>

【link1.html】

<!DOCTYPE html>
<html>
<head>
    <title>音楽生成ページ</title>
</head>
<body>
    <h1>Hello World</h1>
</body>
</html>

【app.py】
※ .env ファイルも必要になります。(過去の記事をご参照ください)
橋渡しの「socket_config.py」から空のsocketioを読み込み、そこにappを埋め込んでいます。
その後に各イベントハンドラーを読み込んでいるのがミソです( import index の部分)

import os, sys
from flask import Flask, render_template
from socket_config import socketio

import os
from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY') #環境変数から設定
socketio.init_app(app)

#--- ルーティング -----------------------------#
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/link1')
def link1():
    return render_template('link1.html')

# SocketIOイベントハンドラをインポート
import index
import link1

if __name__ == '__main__':
    socketio.run(app, debug=True)

【socket_config.py】
単なる橋渡しなので、とてもシンプルです。

from flask_socketio import SocketIO
socketio = SocketIO()

【index.py】
過去の記事のものからちょっとだけ変更していますが、やっていることは同じです。
変えたところ=background_task に渡す引数に、socketio と eventname (どのHTMLに返すか)を追加。

from socket_config import socketio

#--- ソケットイベントハンドラーで使うローカル関数群 ------------#
def background_task(n, sio:socketio, eventname:str):
    """バックグラウンドで数を数えるタスク"""
    count = 0
    for i in range(n):
        count += 1
        sio.sleep(0.1)  # 0.1秒ごとに更新
        sio.emit(eventname, {'count': count})
    sio.emit(eventname, {'count': '完了'})

#--- ソケットイベントのハンドラーを定義 -----------------------#
@socketio.on('start_count')
def start_count(message):
    n = int(message['count'])
    socketio.start_background_task(background_task, n, socketio, 'response')

これでテケテケ表示されれば成功です。試しにlink1.htmlにもテケテケさせてみてください。link1.pyを、index.pyとほぼ同じ内容にすればOKです。

このアプリは、app.pyがすべて差配しています。html側からの受信も送信も、app.pyを通ります。なので、app1.htmlとapp2.htmlとで同じイベント名は使えまえん。

ということで、複数のWebアプリがそれぞれ独立してSocket通信するように改良します。

ゴール2:複数のWebアプリを立ち上げる

いよいよ本丸です。GPT先生にいろいろ助けてもらったのですが、紆余曲折があったので成功したもののみ掲載します。
ちなみに、「複数のWebページがそれぞれ独立してSocket通信するようなWebアプリを1つ立ち上げる」という言い方のほうが正確かも。。

フォルダ構成

一番のポイントは、「LyricsMakerとmyapp2のhtml名は同じじゃだめ!Flaskが混乱するから」です。なので、myapp2のほうは、「myapp2_index.html」にしています。

myapp/
│
├── app/
│   ├── __init__.py
│   ├── socket_config.py  # Socketイベントハンドラ
│   │
│   ├── LyricsMaker/
│   │   ├── __init__.py
│   │   ├── LyricsMaker.py
│   │   └── templates/
│   │       └── index.html
│   │
│   ├── myapp2/
│   │   ├── __init__.py
│   │   ├── myapp2.py
│   │   └── templates/
│   │       └── myapp2_index.html
│   │
├── Procfile
├── requirements.txt
└── run.py


run.py

gunicornはこいつを叩いて起動します。

from app import create_app
from app.socket_config import socketio

app = create_app()

if __name__ == '__main__':
    socketio.run(app, debug=True)


app/socket_config.py

ゴール1に記載したものと全く同じ。

from flask_socketio import SocketIO
socketio = SocketIO()

app/__init__.py

ココが一番のキモ。ブループリントなるものでそれぞれ登録することで、それぞれ独立してSocket通信できます。

import os
from flask import Flask
from app.LyricsMaker    import lyrics_maker
from app.myapp2         import my_appli2
from app.socket_config  import socketio

def create_app():
    app = Flask(__name__)
    app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY') #環境変数から設定

    # Blueprintsの登録
    app.register_blueprint(lyrics_maker, url_prefix='/lyricsmaker')
    app.register_blueprint(my_appli2   , url_prefix='/myapp2'     )

    socketio.init_app(app, cors_allowed_origins="*")

    return app


app/LyricsMaker/__init__.py

ここから先は、ゴール1の内容とほぼ同じです。

from flask import Blueprint
from app.socket_config import socketio

my_appli2 = Blueprint('my_appli2', __name__, template_folder='templates')
from . import myapp2

app/LyricsMaker/LyricsMaker.py

ゴール1から変わったところが、「namespace」なるもの。サブフォルダーみたいなものですね。

from flask import render_template
from . import lyrics_maker, socketio

@lyrics_maker.route('/')
def index():
    return render_template('index.html')


#-----------------------------------------------------------#
#--- ソケットイベントハンドラーで使うローカル関数群 ------------#
#-----------------------------------------------------------#
def background_task(n, sio:socketio, eventname:str):
    """バックグラウンドで数を数えるタスク"""
    count = 0
    for i in range(n):
        count += 1
        sio.sleep(0.1)  # 0.1秒ごとに更新
        sio.emit(eventname, {'count': count}, namespace='/lyricsmaker')
    sio.emit(eventname, {'count': '完了'}, namespace='/lyricsmaker')


#-----------------------------------------------------------#
#--- ソケットイベントのハンドラーを定義 -----------------------#
#-----------------------------------------------------------#
@socketio.on('start_count', namespace='/lyricsmaker')
def handle_start_count(message):
    n = int(message['count'])
    print("(L)Received count:", n)
    socketio.start_background_task(background_task, n, socketio, 'response')


app/LyricsMaker/templates/index.html

socketのアドレスにnamespace(/lyricsmaker)を加えています。

<!DOCTYPE html>
<html>
<head>
    <title>トップページ</title>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
    <script type="text/javascript">
        document.addEventListener('DOMContentLoaded', function() {
            var socketUrl;
            var options = { transports: ['websocket'] }; // WebSocketのみを使用

            if (location.protocol === 'https:') {
                socketUrl = 'https://' + document.domain;
            } else {
                socketUrl = 'http://' + document.domain + ':5000';
            }

            var socket = io.connect(socketUrl + '/lyricsmaker', options);


            socket.on('response', function(msg) {
                var logElement = document.getElementById('log');
                logElement.textContent = 'カウント: ' + msg.count;
            });

            document.getElementById('startButton').addEventListener('click', function() {
                socket.emit('start_count', {count: 10});
            });
        });
    </script>
</head>

<body>
    <h1>トップページ</h1>
    <button id="startButton">Start Counting</button>
    <div id="log"></div>
</body>
</html>


app/myapp2/__init__.py

from flask import Blueprint
from app.socket_config import socketio

my_appli2 = Blueprint('my_appli2', __name__, template_folder='templates')
from . import myapp2

app/myapp2/myapp2.py

namespace で切り分けられるので、イベント名はLyricsMakerと同じで大丈夫です。background_taskは共通化できますが、今回は割愛。

from flask import render_template
from . import my_appli2, socketio

@my_appli2.route('/')
def index():
    return render_template('myapp2_index.html')

#-----------------------------------------------------------#
#--- ソケットイベントハンドラーで使うローカル関数群 ------------#
#-----------------------------------------------------------#
def background_task(n, sio:socketio, eventname:str):
    """バックグラウンドで数を数えるタスク"""
    count = 0
    for i in range(n):
        count += 1
        sio.sleep(0.1)  # 0.1秒ごとに更新
        sio.emit(eventname, {'count': count}, namespace='/myapp2')
    sio.emit(eventname, {'count': '完了'}, namespace='/myapp2')


#-----------------------------------------------------------#
#--- ソケットイベントのハンドラーを定義 -----------------------#
#-----------------------------------------------------------#
@socketio.on('start_count', namespace='/myapp2')
def handle_start_count(message):
    n = int(message['count'])
    print("(2)Received count:", n)
    socketio.start_background_task(background_task, n, socketio, 'response')


app/myapp2/templates/myapp2_index.html

namespaceの部分だけ変えて、あとはLyricsMakerのものと一緒。
(見た目を変えるため、ボタン名とカウント数も変えています)

また、ファイル名は変えましょう!!最初、index.htmlでやっていたところ、/myapp2に接続しても/lyricsmakerの画面が出てきて超絶混乱。。ファイル名が一緒の場合、どうやらFlaskも混乱するようです。

<!DOCTYPE html>
<html>
<head>
    <title>トップページ</title>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
    <script type="text/javascript">
        document.addEventListener('DOMContentLoaded', function() {
            var socketUrl;
            var options = { transports: ['websocket'] }; // WebSocketのみを使用

            if (location.protocol === 'https:') {
                socketUrl = 'https://' + document.domain;
            } else {
                socketUrl = 'http://' + document.domain + ':5000';
            }

            var socket = io.connect(socketUrl + '/myapp2', options);


            socket.on('response', function(msg) {
                var logElement = document.getElementById('log');
                logElement.textContent = 'カウント: ' + msg.count;
            });

            document.getElementById('startButton').addEventListener('click', function() {
                socket.emit('start_count', {count: 20});
            });
        });
    </script>
</head>

<body>
    <h1>トップページ</h1>
    <button id="startButton">スタート</button>
    <div id="log"></div>
</body>
</html>


実演

以下のように、それぞれのページでテケテケできていれば大成功です!



最後まで見ていただきありがとうございました!

なんとなくFlaskアプリの構造が分かってきたことと、GPT先生との付き合い方もちょっとうまくなってきた気がします♪ GPT先生に忖度して質問投げたりしてる気がするw


DigitalOceanのアカウント登録
(200ドル分チケット付き)

以下は紹介リンクですが、ここから手続きを進めてもらえると200ドル分の無料チケット(有効期間2ヶ月)がもらえるようですので、ぜひご活用ください。
※ 2023/12/29時点

紹介リンク : https://m.do.co/c/a8b31ed34b75

サポート問い合わせ先

DigitalOceanのサポート問い合わせリンクがなかなか見つからないので、リンクを載せておきます。

https://cloudsupport.digitalocean.com/s/

場所は、トップページの右下にある「Ask a question」に行き、そのページの一番下(欄外っぽいところ)にひっそりと「Support」というリンクがあります(Contact内)。そのページの一番最後に「Contact Support」ボタンがあります。


この記事が気に入ったらサポートをしてみませんか?