想做些简单的 Web 工具,首先想到的是 Flask + Vue.js, 当然可以完全用 Flask 自己的页面模板 Jinja2, 但一个网站项目不能享受到像 Vue.js, React 类似框架的灵活性真是太可惜了。于是 Flask 只专注于 API, 页面逻辑全用 Vue.js 的组合就成了我的首选,Flask 方面还能进一步选择 FlaskRESTful 框架。还需做得更漂亮的话,CSS 框架可选择 Bootstrap 或与 Vue 紧密集成的 BootstrapVue, 这是后话。
本文主要参考 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 |
flask-vue-app └── backend └── app.py |
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, 接下回答几个问题
最后 flask-vue-app 的目录结构为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
flask-vue-app ├── backend │ └── app.py └── frontend ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src ├── App.vue ├── assets │ └── logo.png ├── components │ └── HelloWorld.vue ├── main.js ├── router │ └── index.js └── views ├── About.vue └── Home.vue |
启动 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
绿色为目录, 文件目录树层次是
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 |
flask-vue-app ├── backend │ └── app.py ├── frontend │ ├── README.md │ ├── babel.config.js │ ├── dist │ │ ├── css │ │ │ └── app.ee9fa358.css │ │ ├── favicon.ico │ │ ├── img │ │ │ └── logo.82b9c7a5.png │ │ ├── index.html │ │ └── js │ │ ├── about.838e43ea.js │ │ ├── about.838e43ea.js.map │ │ ├── app.38688cdc.js │ │ ├── app.38688cdc.js.map │ │ ├── chunk-vendors.6ddee4a6.js │ │ └── chunk-vendors.6ddee4a6.js.map │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── HelloWorld.vue │ │ └── Ping.vue │ ├── main.js │ ├── router │ │ └── index.js │ └── views │ ├── About.vue │ └── Home.vue └── requirements.txt |
dist 是由 npm run build 生成的
这时候只要在 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,姓没变,欢迎检阅
相关链接:
- Flask和Vue.js构建全栈单页面web应用【通过Flask开发RESTful API】
- Developing a Single Page App with Flask and Vue.js
- Vue SPA and Flask together
- Best practices to deploy a Flask and Vue app?
本文链接 https://yanbin.blog/flask-vue-js-integration-dev-deploy/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] Flask 和 Vue.js 开发及整合部署实例 的 创建 Vue 项目一节,此文简单些,就只选择 default (babel, […]
[…] Flask 和 Vue.js 开发及整合部署实例,来体验一下它们与 Bootstrap/BootstrapVue 的集成。漂亮的网站少不得一个好的 […]
应该继续再来一篇:《使用Flask+Vue.js实现简单的CRUD功能》
慢慢来,下面想继续实践 Bootstrap 和 BootstrapVue.