Flask 和 Vue.js 开发及整合部署实例

想做些简单的 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, 文件目录结构是

app.py 的内容为

简单说明一下上面的代码

  1. CORS(app, resources={r'/*': {'origins': '*'}}) 允许来自于 Vue 的跨域访问请求
  2. 定义以 /api/* 开头的 Flask 的路由,由 Flask 来处理
  3. / 请求直接发送一个静态文件 /index.html,由于不会用到 Flask 的模板系统,所以也就无需调用  render_template() 方法去渲染。
  4. 后面会将到在 backend 目录中会建立一个到 Vue.js 项目打包后的 dist 目录的符号链接 static, 所以其中有 index.html 等
  5. @app.rout('/<path:fallback>') 里是个关键,凡是 Flask 未定义的路由都会落到这里来。如果访问的是 static(dis) 中的 css, js, img 或 favicon.ico 文件,直接送出内容,其他的请求转到 Vue 的入口 index.html, 最后将由 Vue 中定义的路由来处理
  6. 如果 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 的目录结构为

启动 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 文件,内容为

编辑 src/router/index.js 文件,高亮行为新加的内容

src/App.vue 的 <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 文件,修改为

高亮行为新加的代码, 保存后 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

绿色为目录, 文件目录树层次是

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 的处理过程是

  1. 对 localhost:5000 的请求发往 Flask, Flask 的  @app.route('/<path:fallback>') 进行处理
  2. 不是 css/js/img 和  favicon.iso 的请求,交由 Vue.js 的入口 index.html 处理
  3. Vue.js 在自己的路由表中找到了 /ping/ping_xyz, 进它们进行渲染
  4. 如随意一个 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 行

这时候打开 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,姓没变,欢迎检阅

相关链接:

  1. Flask和Vue.js构建全栈单页面web应用【通过Flask开发RESTful API】
  2. Developing a Single Page App with Flask and Vue.js
  3. Vue SPA and Flask together
  4. Best practices to deploy a Flask and Vue app?

本文链接 https://yanbin.blog/flask-vue-js-integration-dev-deploy/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

4 Comments
Inline Feedbacks
View all comments
trackback

[…] Flask 和 Vue.js 开发及整合部署实例 的 创建 Vue 项目一节,此文简单些,就只选择 default (babel, […]

trackback

[…] Flask 和 Vue.js 开发及整合部署实例,来体验一下它们与 Bootstrap/BootstrapVue 的集成。漂亮的网站少不得一个好的 […]

JackZhou
4 years ago

应该继续再来一篇:《使用Flask+Vue.js实现简单的CRUD功能》