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
相关文章
相关标签/搜索