
レンタルサーバで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」と記載されている。
こう聞いてみたところ、Pythonファイルはserver.pyの1種類のみ。いったんそれでちゃんと動くのを確認。この段階でうそを教えている可能性もあるので、いったん確認するという作業は結構重要です。
挙動確認した後、追加で以下の質問。
server.pyを、各ページの機能ごとにファイル分割することは可能ですか?
ここでくれた案が動かず、かなりはまりました。複数のファイルが関係するものは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」ボタンがあります。