1. 程式人生 > >Flask & Vue 構建前後端分離的應用

Flask & Vue 構建前後端分離的應用

Flask & Vue 構建前後端分離的應用

最近在使用 Flask 製作基於 HTML5 的桌面應用,前面寫過《用 Python 構建 web 應用》,藉助於完善的 Flask 框架,可以輕鬆的構建一個網站應用。服務端的路由管理和前端模板頁面的渲染都使用 Flask 提供的 API 即可,並且由於 werkzuge 提供了強大的開發功能,可以在執行時自動重新載入整個應用。如果使用 gevent 提供的 WSGIServer 作為伺服器閘道器,在使用時需要進行一定的配置。此時仍然是由 Python 負責前後端的處理。

儘管 Jinja2 為介面渲染提供了諸多便利的方法,但修改模板中的 HTML 檔案後都需要手動重新整理 Chrome 瀏覽器以便觀察變化。如果能給將介面的渲染從服務端分離出來,服務端只需要提供資料或相應的 API,介面由其他框架負責處理,那麼將給程式開發帶來極大的便利,可以考慮採用 Vue+Flask 的模式構建應用。

Vue 的使用非常靈活,既可以將其應用在現有網站的部分頁面中(可相容已經完成的網站專案),又可以將其作為一個單獨的完整前端專案進行開發。由於我所構建的網站較小,而且使用 Flask 模板開發介面並不方便,最終我選擇了將前端介面作為一個獨立於服務端的專案進行開發,後端的資料或驗證以 api 的形式開放給前端呼叫。

前後端分離的好處是,介面上的複雜的東西可以輕鬆的使用 Vue 框架處理,由 webpackdev server 監聽檔案事件,介面改動後自動重新整理瀏覽器,同時可以利用 Vue Devtoools 可以很方便的檢視介面中的相應變數。另外 Vue 的文件比較全面(含有官方中文文件),並且入門門檻較低容易上手。

環境

需要注意的是,環境對應用開發有一定的影響,有些文章中 Vue-cli 版本如果和你使用的不一樣,將會有一些配置上的區別。

  • Python 3.6.7
  • 服務端依賴項:flask 等,具體見檔案
  • Vue 2.5.17
  • Vue 3.2.0
  • 前端其他依賴項見檔案

如果環境和你的有所不同,參照相應的官方文件進行操作。

Flask 後端

儘管前端分離成的一個單獨的專案,但是在生產環境中還是需要 Flask 提供路由訪問生成好的 html 介面檔案。不過訪問頁面的路由可以做的比較簡單。
與其他的 Flask 應用沒有區別,首先例項化一個 Flask 應用:

from flask import Flask, Blueprint
app = Flask(__name__, 
    template_folder='templates', 
    static_folder='templates/static',
    static_url_path='/static')
  
@app.route('/', methods=['GET'])
def app_index():
    if 'user' in session:
        return redirect('/user')
    return redirect('/home')

home = Blueprint( 'home', __name__,
    template_folder='vtemplates', 
    static_folder='vtemplates/vstatic',
    static_url_path='/vstatic' )

@home.route('/home', defaults={'path': ''}, methods=['GET'])
@home.route('/home/<path:path>', methods=['GET'])
def home_index(path):
    return render_template('home.html')

app.register_blueprint(home)

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

傳入 Flask 的引數中 template_folderstatic_folderstatic_url_path 都是可以指定的,如果你需要相容舊版本的應用,可以使用藍圖(Blueprint)併為其指定不同的模板路徑和靜態檔案路徑。在這裡我用到了藍圖,例項化了 home 藍圖,併為其指定了一個不同的模板和靜態檔案路徑(假設這個資料夾是我們稍後會用 Vue 構建出來的),這樣的話就可以避免藍圖和應用的模板相互影響。

另一個要注意的地方是,必須在定義 home 藍圖的所有路由後再呼叫 app.register_blueprint(home), 否則將會出現找不到相應路由的錯誤提示。

我們這裡將會構建單頁應用,所以對於 home 的路由訪問全部渲染到 home.html 頁面上。

我們在專案根目錄下面新建一個 templates 資料夾,在裡面新建名為 home.html 的檔案,新增以下內容:

<!DOCTYPE html>
<html>
  <head>
    <title>Home Page with Jinja2 Template Engine</title>
  </head>
  <body>
    Hello, this is a home page rendered by Jinja2 Template Engine.
  </body>
</html>

現在執行這個 Python 指令碼:

python app.py

伺服器程式預設執行在 127.0.0.1:5000 地址上,訪問 http://127.0.0.1:5000,我們能夠在瀏覽器介面上看到 "Hello, this is a home page rendered by Jinja2 Template Engine."。注意這個位置有一個隱藏的坑:儘管我們設定了 home 藍圖的 template_folder 路徑為 vtemplates(注意我們這個時候還沒有建立這個資料夾),但是在訪問 /home 路徑時,渲染的檔案卻是 templates/home.html,看上去似乎不錯,這讓我們可以在藍圖和應用間共享模板,但是卻會帶來另一個問題。

接下來我們手動建立另一個資料夾 vtemplates,在裡面新建名為 home.html 的檔案,新增以下內容(稍後會使用 Vue-cli 自動構建 vtemplates 資料夾):

<!DOCTYPE html>
<html>
  <head>
    <title>Home Page</title>
  </head>
  <body>
    Hello, this is a home page (it will be built by vue-cli commands).
  </body>
</html>

開啟瀏覽器,訪問 http://127.0.0.1:5000 這個地址,它會重定向至 http://127.0.0.1:5000/home,但是這裡顯示的介面仍然是 ./templates/home.html 檔案的內容,而非 ./vtemplates/home.html。如果藍圖要訪問的模板檔案與應用中的重名了,那麼 Flask 渲染模板的順序可能和你所想的不同。在 github 的 issue 中有一些相關的討論:https://github.com/pallets/flask/issues/2664,基本上是討論模板的渲染順序問題。為了防止渲染錯誤的頁面,我們直接將 templates 路徑下的重名檔案刪除,再次訪問 http://127.0.0.1:5000/home,出現的內容是 “Hello, this is a home page (it will be built by vue-cli commands).”。

好了,一個基本的 Flask 後端程式就完成了(目前僅僅提供 HTML 檔案的渲染)。前端將會由 Vue 構建的專案處理。

Vue 前端

建立一個 Vue 專案比較簡單,Vue 的官方文件也比較詳細,就不過多介紹了。在專案根目錄下建立一個名為 frontend 的子專案:

vue create frontend

如果沒有什麼要定製的話,回車使用預設配置即可。完成後會在專案根目錄下面看到 frontend 資料夾。進入該資料夾,便是前端專案了。

在 frontend 資料夾中,輸入 yarn serve 會開啟一個開發用的伺服器,根據專案原始碼改動情況自動重新載入伺服器;輸入 yarn build 會在 /frontend 資料夾中構建用於生產環境的 dist 資料夾。前面說過,我們想讓 home 藍圖的模板路徑為 /vtemplates,因此我們需要對 Vue-cli 做一些配置。

/frontend 資料夾中新建一個名為 vue.config.js 的檔案,並新增以下內容:

module.exports = {
    chainWebpack: config => {
        config.module.rules.delete('eslint');
    },
    pages: {
        home: {
            entry: 'src/home/main.js',
            template: 'public/index.html',
            filename: 'home.html',
            title: 'Home Page',
            chunks: ['chunk-vendors', 'chunk-common', 'home']
        },
        user: {
            entry: 'src/user/main.js',
            template: 'public/index.html',
            filename: 'user.html',
            title: 'User Page',
            chunks: ['chunk-vendors', 'chunk-common', 'user']
        }
    },
    assetsDir: 'vstatic',
    configureWebpack: {
        devtool: 'source-map',
    },
    devServer: {
        index: 'home.html',
        proxy: {
            '/api': {
                target: 'http://127.0.0.1:5000/api/',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': ''
                }
            },
            '/user': {
                target: 'http://127.0.0.1:8080/user.html/',
                changeOrigin: false,
                pathRewrite: {
                    '^/user': ''
                }
            }
        }
    },
    outputDir: '../vtemplates'
}

在這個檔案中,配置了將會輸出兩個 html 檔案:home.htmluser.html。並且將輸出目錄放在了根目錄下的 vuetempletas 資料夾中,將靜態檔案路徑設為了 vstatic

我想讓 home 作為一個 SPA(single page app 單頁應用),user 作為另一個 SPA。你可以按照自己喜歡的方式組織程式碼。

/frontend/src 目錄下新建一個 home 資料夾,用於放置 home 應用的程式碼,程式碼簡略結構圖如下:

/  # 專案根目錄
  |- frontend  # 前端子專案
    |- ...
    |- src
      |- home
  |- venv  # python virtualenv
  |- templates # 用 Jinja2 語法編碼的模板
  -- ...
  -- app.py   # 後端應用

/frontend/src/home 中新增 home.js,現在的程式碼很簡單,只用匯入 Vue 依賴和 App.vue 檔案就好。如果想要做成一個複雜的單頁應用,那麼你還需要使用路由,如 vue-router,官網上對單頁應用有相應的示例 可供參考。:

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
Vue.config.productionTip = false

import Index from './pages/Index.vue'
const routes = [
  { 
    path: '/', 
    name: 'index', 
    component: Index,
    alias: ['/home', '/index'],
  },
];

const router = new VueRouter({
  routes,
  mode: 'hash'
});

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

注意這裡要呼叫 Vue.use(VueRouter) 載入 VueRouter 外掛,否則不會顯示相應的子介面。

使用 yarn serve 啟動開發伺服器,在瀏覽器中輸入 localhost:8080/home.html 就可以看到如下帶有 Vue Logo 和 “Hello, this is Home App” 的介面了。

注意在上面 vue.config.js 配置檔案中,我將 devServer 的 index 欄位設為了 'home.html',因此直接訪問 localhost:8080 和訪問 localhost:8080/home.html 的效果是一樣的。

前後端結合

有時候在開發過程中,我們想要通過類似 localhost:8080/home 的方式而不用在路徑末尾加上 .html 字尾的方式訪問路由。比如現有的伺服器路由就是不帶字尾名的,那麼我們可以通過修改 devServer 的配置,使得在開發前端介面時保持多頁面的路徑統一。

我在 webpack dev server 的配置中找了一下,如果前端的路由採用的是 history 模式,也就是傳統的 url 模式,那麼可以在 devServer 中加入以下內容,重寫路徑:

devServer: {
  historyApiFallback: true,
  historyApiFallback: {
    rewrites: [
      { from: '/^home', to: 'home.html'},
      { from: '/^user', to: 'user.html'},
    ]
  },
}

如果前端路由採用的 hash 模式,那麼上面的方法就不奏效了,沒有找到其他比較好的方法,但是我們可以修改 devServer 的 proxy 表來改變路由:

proxy: {
    '/user': {
        target: 'http://127.0.0.1:8080/user.html',
        changeOrigin: false,
        pathRewrite: {
            '^/user': ''
        }
    }
}

現在我們在開發伺服器中訪問 http://127.0.0.1:8080/user 也就訪問到了相應的介面。這樣做就使得服務端和前端的多頁面路由跳轉是一致的。

當前端開發完成後,使用 yarn build 命令將會在根目錄的 vtemplates 目錄下建立前端要用到的介面檔案和 JS 程式碼。只需使用 python app.py 啟動伺服器即可。

完成了訪問頁面的路由統一,接下來只需要處理前後端通訊的 API 即可。

我們在 app.py 檔案中新增一個用於處理前後端通訊的藍圖 api:

# app.py
api = Blueprint( 'api', __name__ )

@api.route('/home/signin', methods=['POST'])
def home_signin():
    username = request.form.get('username')
    password = request.form.get('password')
    resp = { 'status': 'success' }
    if username == 'test' and password == '1234':
        session['user'] = username
    else:
        resp['status'] = 'fail'

    return jsonify(resp)

app.register_blueprint(api, url_prefix='/api')

定義一個路由,以便可以響應相應的 POST 操作。

然後在前端專案 frontend 中新增一個用於通訊的 src/api.js,內容如下:

import $ from 'jquery'

export function fetchPost(url, params = {}) {
    return new Promise((resolve, reject) => {
        $.post(url, params).then( resp => {
            resolve( resp );
        }).catch( error => {
            reject( error );
        });
    });
}

export default {
    fetchPost: fetchPost
}

由於在 devServer 中我們已經定義了 api 地址的跨域訪問,因此可以使用 JQuery,當然如果你更熟悉 axios,那麼你可以引入 axios 替換掉 jquery。

然後我們在 /frontend/src/home/ 路徑下再新增一個 api.js 檔案,負責處理前後端的 api 路由:

# home/api.js
import {fetchPost} from '../api.js'

export const singin = function(params) {
    return fetchPost('/api/home/signin', params);
}

最後修改 /frontend/src/home/pages/Index.vue 檔案,新增兩個輸入框和按鈕,並且新增相應的資料,以下為該檔案中的內容:

<template>
  <div>
    <hr />
    This is Index Page In Home SPA.
    <form class="m-1">
      <div class="m-1">
        Username: <input type="text" v-model="username"/>
      </div>
      <div class="m-1">
        Password: <input type="text" v-model="password"/>
      </div>
      <div class="m-1">
        <button type="button" @click="toSignin()">SignIn</button>
      </div>
    </form>
  </div>
</template>

<script>
import {signin} from '../api.js'

export default {
  name: 'homeIndex',
  data() {
    return {
      username: null,
      password: null,
    }
  },
  methods: {
    toSignin: function() {
      signin({
        username: this.username,
        password: this.password
      }).then( resp => {
        if( resp.status === 'success' ) {
          window.location = '/user'
        } else {
          alert('Username or password is wrong.')
        }
      })
    }
  }
}
</script>

<style>
.m-1 {
  margin: 5px;
}
</style>

在該介面中輸入一些錯誤的使用者名稱或密碼,將會在瀏覽器中彈出警告框,輸入正確的使用者名稱(test)和密碼(1234)後,前端頁面自動跳轉到 /user 路徑下。這樣前後端結合的工作就完成了。我們還做了一個非常簡陋的登入示例。最後,我們將寫好的前端程式碼打包到相應目錄下,在瀏覽器中輸入 localhost:5000 訪問我們的網站,可以正常的顯示和跳轉,和訪問前端的開發伺服器一樣,只是所有服務都由 Flask 提供了。

拓展:利用 PyQt5 製作桌面應用

既然使用 Python Flask 和 Vue 製作了一個前後端分離的網站應用,那麼我們實際上可以考慮新增 PyQt5 元件,利用現有的程式碼製作一個基於 HTML5 的桌面應用,當然也可以直接通過在瀏覽器中輸入 IP + 地址的方式訪問這個桌面應用。

我們在專案根目錄下新建一個 deskapp.py,內容如下:

from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl

def startWeb():
    from app import app
    app.run()

def main(argv):
    qtapp = QApplication(argv)
    from threading import Thread
    webapp = Thread(target=startWeb)
    webapp.daemon = True
    webapp.start()
    view = QWebEngineView()
    view.setWindowTitle('DeskApp')
    port = 5000
    view.setUrl( QUrl("http://localhost:{}/".format(port)))
    view.show()

    return qtapp.exec_()

import sys
if __name__ == '__main__':
    main(sys.argv)

使用 python deskapp.py 執行程式,就會顯示一個桌面應用,在我們的網站應用規模較小時這樣做沒什麼問題,但是最終應用的生產環境的 web app 可能使用的是 gevent.pywsgi.WSGIServer,並且後臺可能需要處理的事情較多,這時有可能會出現介面閃爍的情況,如果出現了這種情況,可以參考 PyFladesk 這個專案使用的方式:使用 QThread 包裝我們的 web 應用。由於 Python 中有 GIL 全域性鎖,所以它的多執行緒不是真正意義上的多執行緒,但是 QThread 是 Qt 提供的多執行緒機制,執行緒之間不會相互影響。

總結

如果你只想在部分頁面中使用 Vue,並且要在 Flask 的模板中使用 Vue,那麼你需要讓 Vue 使用不同的定界符,詳見 specify delimiters for a vuejs component

最開始我的專案中的前後端的通訊部分都是分散在各個 Vue 檔案中,我在檢視 xmall-front 前端專案 的原始碼時發現了將前後端的通訊操作集中到一個檔案,以 API 的形式開放給各個 Vue 頁面更利於聚合程式碼,因此在介紹【前後端結合】這一節中採用了這種方式。

總的來說,使用 Flask 構建一個 web 應用並不困難,使用 Flask + Vue 構建一個前後端分離的 web 應用也比較簡單,我們可以用 Flask + Vue 構建一個複雜的網站應用,但前後端分離使得開發過程並不會太複雜。另外,我們可以嘗試使用 QWebEngineView 構建一個基於 HTML5 的桌面應用,既能夠用瀏覽器訪問,也可以打包成一個 .exe 可執行檔案。總之,使用 HTML5 開發可以給我們帶來很多便利。

所有相關的程式碼存放在 github 上。

參考

  1. developing-a-single-page-app-with-flask-and-vuejs
  2. 使用 Vue.js 和 Flask 來構建一個單頁的App
  3. specify delimiters for a vuejs component
  4. xmall-front 前端專案
  5. https://stackoverflow.com/questions/43838135/vue-app-doesnt-load-when-served-through-python-flask-server
  6. https://forum.vuejs.org/t/routes-not-working-after-npm-build/34261
  7. https://router.vuejs.org/guide/essentials/history-mode.html
  8. https://codeburst.io/full-stack-single-page-application-with-vue-js-and-flask-b1e036315532
  9. https://blog.csdn.net/MRblackLu/article/details/71263276
  10. https://github.com/vuejs-templates/webpack/issues/450
  11. https://stackoverflow.com/questions/31945763/how-to-tell-webpack-dev-server-to-serve-index-html-for-any-route