基於 flask-socketio 的 CRUD 操作初探
Flask 作為一個全棧架構,如果你只會 python,而不懂 javascript 的前端知識,似乎是無法支撐起你的 web 夢想的,比如,一個簡單的頁面 局部刷新 功能,你就需要用到 ajax 的知識,當然,你還可以使用 HTML5 的新特性 —— websocket功能,好在 flask 還提供了一個 flask-socketio 插件,本文我們就探討一下這個 flask-scoketio插件的用法。
理解 websocket 協議
- HTTP 協議只能通過客戶端發起請求來與客戶端進行通訊 —— 這是一個缺陷。
- 通過websocket 協議,伺服器可以主動向客戶端推送信息,客戶端也可以主動向伺服器發送信息,是真正的雙向平等對話,屬於伺服器推送技術的一種。
websocket 協議特性
- 建立在 TCP 協議之上,伺服器端的實現比較容易。
- 與 HTTP 協議有著良好的兼容性。默認埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理伺服器。
- 數據格式比較輕量,性能開銷小,通信高效。
- 可以發送文本,也可以發送二進位數據。
- 沒有同源限制,客戶端可以與任意伺服器通信。
- 協議標識符是ws(如果加密,則為wss),伺服器網址就是 URL。
使用 flask-socketio
安裝插件
pip install flask-socketio
項目結構
本文是在 《基於 flask 的 CRUD 操作》 的基礎上增加了 webscoket 的功能,使用的是 init_app() 的形式載入 flask-socketio 插件,和網上的大多數教程稍有不同。
flask-wtf-crud/
|-- env/
|--
|-- app/ <項目的模塊名稱>
|-- crud/ <前端藍圖>
|-- __init__.py
|-- views.py <路由和視圖函數文件>
|-- forms.py <表單類文件, wtforms插件必須項>
|-- templates
|-- static <靜態文件夾>
|-- js
|-- crud.js # 非同步請求的程序主要在此添加
|-- XXXXXX/ <其它藍圖>
|-- __init__.py
|-- models.py <資料庫模型文件>
|-- migrations/ <資料庫表關係文件夾,Flask-Migrate遷移資料庫時使用>
|-- config.py <項目的配置文件>
|-- manage.py <用於啟動程序以及其它程序任務>
將 flask-socketio 引入項目
修改 manage.py 內容
1 # -*- coding:utf-8 -*-
2 __author__ = 東方鶚
3 __blog__ = uhttp://www.os373.cn
4
5 import os
6 from app import create_app, db, socketio
7 from app.models import User
8 from flask_script import Manager, Shell
9 from flask_migrate import Migrate, MigrateCommand
10
11
12 app = create_app(os.getenv(FLASK_CONFIG) or default)
13 manager = Manager(app=app)
14 migrate = Migrate(app=app, db=db)
15
16 def make_shell_context():
17 return dict(app=app, db=db, User=User)
18
19
20 manager.add_command("shell", Shell(make_context=make_shell_context))
21 manager.add_command(db, MigrateCommand)
22 manager.add_command(run, socketio.run(app=app, host=0.0.0.0, port=5001)) # 新加入的內容
23
24
25 if __name__ == __main__:
26 manager.run()
修改 app/__init__.py 內容
1 # -*- coding:utf-8 -*-
2 __author__ = 東方鶚
3 __blog__ = uhttp://www.os373.cn
4
5 from flask import Flask
6 from flask_sqlalchemy import SQLAlchemy
7 from config import config
8 from flask_socketio import SocketIO # 新加入的內容
9 db = SQLAlchemy()
10
11 async_mode = None
12 socketio = SocketIO()
13
14
15 def create_app(config_name):
16 """ 使用工廠函數初始化程序實例"""
17 app = Flask(__name__)
18 app.config.from_object(config[config_name])
19 config[config_name].init_app(app=app)
20
21 db.init_app(app=app)
22
23 socketio.init_app(app=app, async_mode=async_mode) # 新加入的內容
24
25 # 註冊藍本crud
26 from .crud import crud as crud_blueprint
27 app.register_blueprint(crud_blueprint, url_prefix=/crud)
28
29 return app
當前藍圖的 views.py
1 # -*- coding:utf-8 -*-
2 __author__ = 東方鶚
3 __blog__ = uhttp://www.os373.cn
4
5 from flask import render_template, redirect, request, current_app, url_for, flash, json
6 from . import crud
7 from ..models import User
8 from .forms import AddUserForm, DeleteUserForm, EditUserForm
9 from ..import db
10 from threading import Lock
11 from app import socketio # 新加入的內容
12 from flask_socketio import emit # 新加入的內容
13
14 # 新加入的內容-開始
15 thread = None
16 thread_lock = Lock()
17
18 def background_thread(users_to_json):
19 """Example of how to send server generated events to clients."""
20 while True:
21 socketio.sleep(5) \ 每五秒發送一次
22
23 socketio.emit(user_response, {data: users_to_json}, namespace=/websocket/user_refresh)
24 # 新加入的內容-結束
25
26 @crud.route(/, methods=[GET, POST])
27 def index():
28
29 return render_template(index.html)
30
31
32 @crud.route(/websocket, methods=[GET, POST])
33 def websocket():
34 add_user_form = AddUserForm(prefix=add_user)
35 delete_user_form = DeleteUserForm(prefix=delete_user)
36 if add_user_form.validate_on_submit():
37 if add_user_form.role.data == uTrue:
38 role = True
39 else:
40 role = False
41 if add_user_form.status.data == uTrue:
42 status = True
43 else:
44 status = False
45 u = User(username=add_user_form.username.data.strip(), email=add_user_form.email.data.strip(),
46 role=role, status=status)
47 db.session.add(u)
48 flash({success: u添加用戶<%s>成功! % add_user_form.username.data.strip()})
49 if delete_user_form.validate_on_submit():
50 u = User.query.get_or_404(int(delete_user_form.user_id.data.strip()))
51 db.session.delete(u)
52 flash({success: u刪除用戶<%s>成功! % u.username})
53
54 users = User.query.all()
55
56 return render_template(websocket.html, users=users, addUserForm=add_user_form, deleteUserForm=delete_user_form)
57
58
59 @crud.route(/websocket-edit/<user_id>, methods=[GET, POST])
60 def user_edit(user_id):
61 user = User.query.get_or_404(user_id)
62 edit_user_form = EditUserForm(prefix=edit_user, obj=user)
63 if edit_user_form.validate_on_submit():
64 user.username = edit_user_form.username.data.strip()
65 user.email = edit_user_form.email.data.strip()
66 if edit_user_form.role.data == uTrue:
67 user.role = True
68 else:
69 user.role = False
70 if edit_user_form.status.data == uTrue:
71 user.status = True
72 else:
73 user.status = False
74 flash({success: u用戶資料已修改成功!})
75 return redirect(url_for(.basic))
76
77 return render_template(edit_websocket.html, editUserForm=edit_user_form, user=user)
78
79 # 新加入的內容-開始
80 @socketio.on(connect, namespace=/websocket/user_refresh)
81 def connect():
82 """ 服務端自動發送通信請求 """
83 global thread
84 with thread_lock:
85 users = User.query.all()
86 users_to_json = [user.to_json() for user in users]
87
88 if thread is None:
89 thread = socketio.start_background_task(background_thread, (users_to_json, ))
90 emit(server_response, {data: 試圖連接客戶端!})
91
92
93 @socketio.on(connect_event, namespace=/websocket/user_refresh)
94 def refresh_message(message):
95 """ 服務端接受客戶端發送的通信請求 """
96
97 emit(server_response, {data: message[data]})
98 # 新加入的內容-結束
---------- 以上內容是後端的內容,以下內容是將是前段的內容 ----------
crud.js 內容
1 $(document).ready(function () {
2 namespace=/websocket/user_refresh;
3 var socket = io.connect(location.protocol + // + document.domain + : + location.port + namespace);
4 $("#url_show").text("websocket URL: " + location.protocol + // + document.domain + : + location.port + namespace);
5
6 socket.on(connect, function() { // 發送到伺服器的通信內容
7 socket.emit(connect_event, {data: 我已連接上服務端!});
8 });
9
10 socket.on(server_response, function(msg) {
11 \ 顯示接受到的通信內容,包括伺服器端直接發送的內容和反饋給客戶端的內容
12 $(#log).append(<br> + $(<div/>).text(接收 : + msg.data).html());
13 });
14 socket.on(user_response, function(msg) {
15 //console.log(eval(msg.data[0]));
16 //$(#users_show).append(<br> + $(<div/>).text(接收 : + msg.data).html());
17 var tbody = "";
18 var obj = eval(msg.data[0]);
19 $.each(obj, function (n, value) {
20 var role = "";
21 if (value.role===true){
22 role = "管理員";
23 }else {
24 role = "一般用戶";
25 }
26 var status = "";
27 if (value.status===true){
28 status = "正常";
29 }else {
30 status = "註銷";
31 }
32 edit_url = "<a href=" + location.protocol + // + document.domain + : + location.port + "/crud/websocket-edit/" + value.id + "> 修改</a>";
33 delete_url = "<a href="javascript:delete_user_" + value.id + "()">刪除</a>";
34 var trs = "";
35 trs += "<tr><th>" + (n+1) + "</th><td>" + value.username + "</td><td>" + value.email + "</td><td>" + role + "</td><td>" + status + "</td><td>" + edit_url + " | " + delete_url +"</td></tr>";
36 tbody += trs;
37 })
38 $(#users_show).empty();
39 $(#users_show).append(tbody);
40 });
41 });
顯示結果

每次打開網頁,會顯示服務端發送的內容——「試圖連接客戶端!」,其後,客戶端返回給服務端——「我已連接上服務端!」,而後又被服務端返回給客戶端顯示。
以下的表格內容顯示數據局裡的內容,每 5 秒局部刷新一次表格內容。
伺服器後端 log 日誌內容如下:

總結
- 由於 flask 架構具有上下文的限制,在資料庫里 增加刪改 內容的時候,表格的內容沒有變化——儘管局部已經進行了刷新。要想顯示變化後的資料庫內容,必須得重新啟動一下 flask 服務。
- 就整體的部署來說,在 flask 項目里添加 websocket 協議,顯得項目較重,實現一個局部刷新的功能還是用 ajax 比較簡單。
- 歡迎大俠能夠給我的項目提出修改意見,先行感謝!!!
源碼下載
參考
- 基於 flask 的 CRUD 操作
- WebSocket 教程 —— 阮一峰
推薦閱讀:
