本文主要参考 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,姓没变,欢迎检阅
相关链接: