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

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

最近在使用 Flask 製做基於 HTML5 的桌面應用,前面寫過《用 Python 構建 web 應用》,藉助於完善的 Flask 框架,能夠輕鬆的構建一個網站應用。服務端的路由管理和前端模板頁面的渲染都使用 Flask 提供的 API 便可,而且因爲 werkzuge 提供了強大的開發功能,能夠在運行時自動從新加載整個應用。若是使用 gevent 提供的 WSGIServer 做爲服務器網關,在使用時須要進行必定的配置。此時仍然是由 Python 負責先後端的處理。html

儘管 Jinja2 爲界面渲染提供了諸多便利的方法,但修改模板中的 HTML 文件後都須要手動刷新 Chrome 瀏覽器以便觀察變化。若是能給將界面的渲染從服務端分離出來,服務端只須要提供數據或相應的 API,界面由其餘框架負責處理,那麼將給程序開發帶來極大的便利,能夠考慮採用 Vue+Flask 的模式構建應用。前端

Vue 的使用很是靈活,既能夠將其應用在現有網站的部分頁面中(可兼容已經完成的網站項目),又能夠將其做爲一個單獨的完整前端項目進行開發。因爲我所構建的網站較小,並且使用 Flask 模板開發界面並不方便,最終我選擇了將前端界面做爲一個獨立於服務端的項目進行開發,後端的數據或驗證以 api 的形式開放給前端調用。vue

先後端分離的好處是,界面上的複雜的東西能夠輕鬆的使用 Vue 框架處理,由 webpackdev server 監聽文件事件,界面改動後自動刷新瀏覽器,同時能夠利用 Vue Devtoools 能夠很方便的查看界面中的相應變量。另外 Vue 的文檔比較全面(含有官方中文文檔),而且入門門檻較低容易上手。python

環境

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

  • Python 3.6.7
  • 服務端依賴項:flask 等,具體見文件
  • Vue 2.5.17
  • Vue 3.2.0
  • 前端其餘依賴項見文件

若是環境和你的有所不一樣,參照相應的官方文檔進行操做。webpack

Flask 後端

儘管前端分離成的一個單獨的項目,可是在生產環境中仍是須要 Flask 提供路由訪問生成好的 html 界面文件。不過訪問頁面的路由能夠作的比較簡單。
與其餘的 Flask 應用沒有區別,首先實例化一個 Flask 應用:ios

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 構建出來的),這樣的話就能夠避免藍圖和應用的模板相互影響。git

另外一個要注意的地方是,必須在定義 home 藍圖的全部路由後再調用 app.register_blueprint(home), 不然將會出現找不到相應路由的錯誤提示。github

咱們這裏將會構建單頁應用,因此對於 home 的路由訪問所有渲染到 home.html 頁面上。web

咱們在項目根目錄下面新建一個 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': ''
        }
    }
}

以上劃掉的內容爲第一次編寫時的錯誤說法,保留以供參考。和正確的作法相比,錯誤的作法中 historyApiFallBack 有兩處用錯了:

  1. 在 devServer 中聲明瞭兩次,可能致使了重寫路由失敗
  2. 重寫的路由中,「to」 的內容不是以根目錄結尾的,能夠看到每一個路由都比正確的作法少了兩個斜槓。

如今咱們在開發服務器中訪問 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
相關文章
相關標籤/搜索