本文主要參考 Flask和Vue.js構建全棧單頁面web應用【通過Flask開發RESTful API】的前部分,英文原文在這里 Developing a Single Page App with Flask and Vue.js。
開發過程中我們可以保持 Flask 和 Vue.js 為單獨的兩個項目,並啟動各自的服務,比如 Flask 是 http://localhost:5000, Vue.js 項目通過 npm run serve 啟動在 http://localhost:8080,借助於 node js 的功能,修改 Vue.js 項目的內容能夠自動刷新網頁。要是開發中把靜態文件全放在 Flask 項目中,那么任何對靜態文件的修改都必須重啟 Flask 服務。雖然 Debug 模式啟動的 Flask 在看到它的目錄中有任何修改時也能自動重啟,但對靜態文件的修改重啟 Flask 沒這個必要性。
但部署時需進一步整合,最終只需要啟動 Flask 服務,而無須兩個,方便部署。如果是以 Docker 容器的方式發布,使用 docker-compose 來編排兩個容器來發布也還算不錯。更專業的部署方式應該是 Vue.js 的靜態內容放到專門的 Web 服務器,如 Apache/Nginx 中,Flask 也通過 wsgi 與 Web 服務器集成起來。
介於原文中所用的 Vue CLI 稍稍顯老,所以實踐中也有些區別,先注明本文寫作時所依賴的各主要組件版本
- Vue v2.6.11
- Vue CLI v4.6.6
- Node v14.4.0
- npm v6.14.4
- Flask v1.1.2
- Python v3.7
創建 Flask 項目
創建項目目錄
$ mkdir flask-vue-app
$ cd flask-vue-app
接下來創建 Python 虛擬環境
$ python3.7 -m venv .venv
$ source .venv/bin/activate
安裝 Flask 和 Flask-CORS 擴展,前面說過,由於開發中啟動了兩個服務,需要跨域訪問服務,所以要用到 Flask-CORS
(.venv) $ pip install flask-cors
Flask 本身會被自動安裝,當前日期為 2020-06-30, 所安裝的 flask-cors 版本為 3.0.8, Fask 為 1.1.2。也可以鎖定版本來安裝擴展,如 pip install flask-cors==3.0.8。現在查看下所有的第三方依賴
$ pip freeze
click==7.1.2
Flask==1.1.2
Flask-Cors==3.0.8
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
six==1.15.0
Werkzeug==1.0.1
有需要的話,保存為 requirements.txt 放到版本服務器上
現在在 flask-vue-app 下創建一個 backend 目錄,並在其中創建文件 app.py, 內容為
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
from flask import Flask, jsonify
from flask_cors import CORS
DEBUG = True
app = Flask(__name__)
app.config.from_object(__name__)
CORS(app, resources={r'/*': {'origins': '*'}})
@app.route('/api/ping', methods=['GET'])
def ping_pong():
return jsonify('pong!')
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.route('/<path:fallback>')
def fallback(fallback): # Vue Router 的 mode 為 'hash' 時可移除該方法
if fallback.startswith('css/') or fallback.startswith('js/')\
or fallback.startswith('img/') or fallback == 'favicon.ico':
return app.send_static_file(fallback)
else:
return app.send_static_file('index.html')
if __name__ == '__main__':
app.run()
|
簡單說明一下上面的代碼
- CORS(app, resources={r'/*': {'origins': '*'}}) 允許來自於 Vue 的跨域訪問請求
- 定義以
/api/* 開頭的 Flask 的路由,由 Flask 來處理 /請求直接發送一個靜態文件/index.html,由於不會用到 Flask 的模板系統,所以也就無需調用render_template()方法去渲染。- 后面會將到在
backend目錄中會建立一個到 Vue.js 項目打包后的 dist 目錄的符號鏈接static, 所以其中有index.html等 - @app.rout('/<path:fallback>') 里是個關鍵,凡是 Flask 未定義的路由都會落到這里來。如果訪問的是
static(dis)中的 css, js, img 或 favicon.ico 文件,直接送出內容,其他的請求轉到 Vue 的入口index.html, 最后將由 Vue 中定義的路由來處理 - 如果 Vue 的 Router 工作在 hash 模式的話,fallback 方法可以不要,因為
/#/home到/#/about的切換本身不產生 HTTP 請求,Flask 只需要/一個路由進入 Vue 入口頁面
運行 Flask
(.venv) $ python backend/app.py
Flask 會在 localhost:5000 中啟動服務,用 curl 命令驗證
$ curl http://localhost:5000/api/ping
"pong!"
創建 Vue 項目
開始轉到 Vue 項目來,將使用 Vue CLI 工具來生成它,首先是安裝 Vue CLI
$ npm install -g @vue/cli
當前日期 2020-06-30, 安裝后用 vue --version 看到的版本是 @vue/cli 4.4.6。安裝時欲鎖定版本用命令 npm install -g @vue/cli@4.4.6
正式創建項目 frontend,在 flask-vue-app 目錄下運行
$ vue create frontend # 選擇 Manually select features, 接下回答幾個問題
啟動 Vue 服務
$ cd frontend
$ npm run serve
打開瀏覽器訪問 http://localhost:8080 會有一個 "Wellcome to Your Vue.js App" 的界面。后面對 frontend 項目的修改會自動刷新網頁。
下面是如何在 Vue.js(8080) 中調用到 Flask(5000) 的 /api/ping 服務,當前在 frontend 目錄中
創建 src/components/Ping.vue 文件,內容為
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<template>
<div>
<p>{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'Ping',
data() {
return {
msg: 'Hello!',
};
},
};
</script>
|
編輯 src/router/index.js 文件,高亮行為新加的內容
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import Ping from '../components/Ping.vue';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
{
path: '/ping', # 用來調用 Flask 的 "/api/ping" API
name: 'Ping',
component: Ping,
},
{
path: '/ping_xyz', # 這個用來測試,非 Flask 中定義的路由,可被 Vue 進行處理
name: 'Ping',
component: Ping,
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
export default router;
|
對 src/App.vue 的 <template> 中的導航部分刪除,內容變為
|
1
2
3
4
5
|
<template>
<div id="app">
<router-view/>
</div>
</template>
|
瀏覽器中訪問 http://localhost:8080/ping, "Hello!" 顯示的還是 src/components/Ping.vue 中 data 的內容
現在開始將 Ping.vue 與 Flask 的 /api/ping API 進行連接,Vue 中要用 Ajax 來訪問,先要安裝 axios,命令如下
$ npm install axios --save
目前安裝的是 axios@0.19.2, 安裝后可在 package.json 里看到 dependencies 中的 "axios": "^0.19.2"
編輯 src/components/Ping.vue 文件,修改為
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<script>
import axios from 'axios';
export default {
name: 'Ping',
data() {
return {
msg: '',
};
},
methods: {
getMessage() {
const path = 'http://localhost:5000/ping';
axios.get(path)
.then((res) => {
this.msg = res.data;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
},
created() {
this.getMessage();
},
};
</script>
|
高亮行為新加的代碼, 保存后 http://localhost:8080/ping 窗口中的內容自動刷新為
pong! 消息是來自於 Flask 的 /api/ping API 的響應。由於我們前面是以 Debug 模式啟動的 Flask backend 應用, 所以在控制台也能夠看到一個對 /api/ping 的請求
127.0.0.1 - - [01/Jul/2020 02:53:06] "GET /api/ping HTTP/1.1" 200 -
訪問 http://localhost:8080/ping_xyz 指向了同一個 Vue 組件,所以效果上與 http://localhost:8080/ping 是一樣的。
Flask 與 Vue.js 整合
開發的時候啟動兩個服務很方面,但我們希望在部署后只啟動一個 Flask 服務,那么可以這樣做
首先用 npm 對 fronend 中的靜態內容打包
$ npm run build
將會在 frontend 下生成 dist 目錄,其下內容為
css favicon.ico img index.html js
綠色為目錄
這時修只要在 backend 中創建一個符號鏈接
$ ln -s ../front/dist static
創建后在 backend 目錄中的內容為
-rw-r--r-- 1 yanbin root 690 Jul 1 01:01 app.py
lrwxr-xr-x 1 yanbin root 16 Jun 30 22:33 static -> ../frontend/dist
因為 Flask 是以 Debug 模式啟動的,對 Flask 項目 backend 的改動也可能會觸發 Flask 的重新啟動,需要的話手動重啟 Flask (CTRL+C 退出再重啟)
$ python backend/app.py
現在 Vue.js 那個服務可以停止了,不管是 Flask 還是 Vue.js 的路由都能夠通過 http://localhost:5000 來訪問了
http://localhost:5000/ping
http://localhost:5000/ping_xyz
Flask + Vue 對 http://localhost:5000/ping 和 http://localhost:5000/ping_xyz 的處理過程是
- 對 localhost:5000 的請求發往 Flask, Flask 的
@app.route('/<path:fallback>')進行處理 - 不是 css/js/img 和 favicon.iso 的請求,交由 Vue.js 的入口
index.html處理 - Vue.js 在自己的路由表中找到了
/ping和/ping_xyz, 進它們進行渲染 - 如隨意一個 http://localhost:5000/abc,也會轉給 Vue.js 的入口
index.html,但 Vue.js 未定/abc路由,頁面得不到渲染,一面空白
最后,Flask 與 Vue.js 這樣整合后,Vue.js 路由中訪問 Flask API 要與 Flask 實際啟動的 IP 端口保持一致,因為只有一個服務也就不存在跨域訪問的問題,允許跨域相關的 Python 代碼也就可以移除掉了。
本文演示的是一個 Vue.js 多頁面程序,如果是單頁面程序(用 /#/abc) 導引的,在 Flask 中處理起來還稍微簡單些,只要 "/" 請求交給 Vue.js 的入口 index.html, 其他全當是靜態文件,Flask 的 API 還是最好約定為 /api/* 的形式。
VueRouter 的 history 和 hash 模式
如果 VueRouter 使用 hash 模式,在服務端可以更簡單的些,前面說過在 app.py 中的 fallback() 方法可以不需要了。Vue 默認的模式是 hash, 只是用 vue 命令生成的項目設置成了 history 模式,重新啟用 hash 模式的方法是修改 src/router/index.js 文件中,把 mode 值改為 hash 或去掉 mode 行
|
1
2
3
4
5
|
const router = new VueRouter({
// mode: 'history', 或改為 mode: 'hash', 默認為 'hash'
base: process.env.BASE_URL,
routes,
});
|
這時候打開 http://localhost:8080 會自動跳轉到 http://localhost:8080/#/, 其他的路由也加上了 #, 如 /#/ping
瀏覽時看到原來的 localhost:8080/ping 變成了 localhost:8080/#/ping, 使用 hash 的好處是每次 Vue 的路由跳轉其時是一個錨點鏈接(anchor),它相當於當前頁的位置跳轉,不會重新刷新整個頁面,且本身不會產生與服務端的 HTTP 請求,所以可減少許多的因 Vue 跳轉而產生的交互,雖然前也簡單的跳轉回 Vue 的入口文件 index.html,但怎么着也是省了不少來回。
接下來將在 Vue.js 中試驗 Bootstrap 和 BootstrapVue 的集成。
本實例代碼已推送到了 github, 倉庫地址為 https://github.com/yabqiu/flask-vue-app.git,姓沒變,歡迎檢閱
相關鏈接:






