前言
本来在着手写关于使用Flask构建RESTful网络服务的文章,正好看到一篇相关文章,提供了一个Flask+vue构建全栈单页面应用的例子,觉得不错,就在此分享给大家。
作者Oleg Agapov ,似乎是俄语使用者,不过这篇文章是用英文写的,文风细致友好,可以一读。考虑到一些朋友不方便查看外网,特此翻译并转载到知乎专栏。以下是全文及几条网友问答翻译。
注意:阅读本文需要一点点的Flask知识和一点点的Vue知识(只要你看过任何一点点新建一个Flask应用且看过任何一点点新建一个Vue应用的文章,就满足了本条要求)
另外:Python构建RESTful网络服务[Django篇]没有完结,仍在连载中,见专栏更新。
用Flask+Vue.js打造全栈单页面应用
在本教程中,我将向您展示如何将Vue.js单页面应用程序与Flask后端整合起来。
基本上,如果你只想在Flask模板中使用Vue.js库,那么问题不大。唯一值得注意是Jinja(模板引擎)使用双花括号来渲染变量,而Vue插值也使用双花括号,解决这问题可以见这里https://github.com/yymm/flask-vuejs。
而本文要说的是另一种情况:我需要一个使用Vue.js构建的单页面应用程序(使用单页面组件、history模式的vue-router和其他优秀特性),并通过Flask服务器提供服务。简而言之,应该是这样工作的:Flask伺服一个index.html,这是我的Vue.js应用入口文件
使用Webpack进行前端工程化开发
可以从SPA访问Flask的API端点
使用Node.js运行前端项目时,也可以访问Flask的API端点
这种情况该怎么办呢?听起来是不是很有趣,说干就干吧!
客户端
我使用vue-cli生成基本的Vue.js应用程序。如果你还没有安装Vue.js,只要运行:
$ npm install -g vue-cli
客户端和后端代码将被分别放到不同的文件夹。初始化前端部分运行如下:
$ mkdir flaskvue
$ cd flaskvue
$ vue init webpack frontend
通过安装向导,按如下设置是:Vue build — Runtime only #仅在运行时构建
Install vue-router? — Yes # 使用vue-router提供的前端路由
Use ESLint to lint your code? — Yes # 使用ESLint检查代码
Pick an ESLint preset — Standard # 设置ESLint为标准模式
Setup unit tests with Karma + Mocha? — No # 不使用Karma + Mocha进行单元测试
Setup e2e tests with Nightwatch? — No # 不使用Nightwatch进行端对端测试
下一步:
$ cd frontend
$ npm install
# after installation
$ npm run dev
启动Vue.js应用开发服务器。让我们先来添加一些页面。
添加 Home.vue 和 About.vue 到 frontend/src/components 文件夹。简单填写一下,就像这样:
// Home.vue
Home page
// About.vue
About
现在要构建前端路由,前端应用要知道怎么处理我们我们在浏览器地址栏中填写地址。在frontend/src/router/index.js文件中填写如下内容来渲染我们的新组件:
import Vue from 'vue'
import Router from 'vue-router'
const routerOptions = [
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' }
]
const routes = routerOptions.map(route => {
return {
...route,
component: () => import(`@/components/${route.component}.vue`)
}
})
Vue.use(Router)
export default new Router({
routes,
mode: 'history'
})
使用浏览器访问 localhost:8080 和 localhost:8080/about 可以看到相应的页面。
我们现在几乎已经可以构建包含一系列静态文件的前端应用了,不过在此之前,我们还要重设前端应用文件的路径。在frontend/config/index.js 进行下一步设置,把如下代码
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
改成
index: path.resolve(__dirname, '../../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../../dist'),
现在包含了项目所需的html/css/js文件的 /dist 文件夹与 /frontend处于同级了。现在使用
$ npm run build
来构建项目。
后端
Flask 服务使用的Python版本为3.6。在 /flaskvue 文件夹中,为后端代码创建新的子文件夹,并在其中初始化虚拟环境:
$ mkdir backend
$ cd backend
$ virtualenv -p python3 venv
启用虚拟环境运行(在macOs上):
$ source venv/bin/activate
在虚拟环境下安装Flask
(venv) pip install Flask
现在让我们为Flask server编写代码。在根目录中创建run.py文件:
(venv) cd ..
(venv) touch run.py
添加如下代码到这个文件:
from flask import Flask, render_template
app = Flask(
__name__,
static_folder = "./dist/static",
template_folder = "./dist"
)
@app.route('/')
def index():
return render_template("index.html")
我们的代码与Flask starter的“Hello world”代码略有不同。主要的不同之处在于,我们指定了静态文件和模板文件目录为/dist文件夹,就是前文构建的前端应用所在文件夹。在Flask应用所在文件夹运行Flask服务器:
(venv)
$ FLASK_APP=run.py
$ FLASK_DEBUG=1
$ flask run
这将在localhost:5000上启动一个web服务器。FLASK_APP指向服务器启动文件,FLASK_DEBUG=1将在调试模式下运行。如果一切正常可以看到我们熟悉的界面,就是之前构建的前面项目一样的页面。
但是,如果试图进入/about页面,页面就会报错。Flask抛出一个错误,说请求的URL没有找到。这是因为,我们在前端路由中使用了HTML5 history模式,当我们访问Flask的/about页面时,后端路由并不知道要去哪里找我们想要的页面。我们需要配置web服务器将所有路由重定向到index.html。在Flask里很容易做到。将现有路由修改为:
@app.route('/', defaults={'path': ''})
@app.route('/')
def catch_all(path):
return render_template("index.html")
现在URL localhost:5000/about将被重定向到index.html,vue-router将自己处理它。
添加 404 页面
现在,无论对Flask服务器发送什么样的请求,它都不会返回404页面了,因为即使找不到资源,它也只会把url交给前端应用,而不会返回404状态。所以404状态的响应,要由Vue.js完成。
在 frontend/src/router/index.js 加上如下代码行:
const routerOptions = [
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' },
{ path: '*', component: 'NotFound' }
]
这里的 path '*' 是 vue-router 的通配符,表示除以上url以外的路由,都交给NotFound。现在在 /components 创建 NotFound.vue 文件,就简单写一下 好了。
// NotFound.vue
404 - Not Found
现在再次使用npm运行dev前端服务器,并尝试输入一些无意义的地址,比如localhost:8080/gljhewrgoh。应该看到我们的“Not Found”信息。添加API端点
使用Flask伺服所有服务
这是本教程的最后一步,我们在服务器端创建API并在客户端使用它。我将创建一个简单的端点,它将返回一个从1到100的随机数。
打开run.py并添加:
from flask import Flask, render_template, jsonify
from random import *
app = Flask(__name__,
static_folder = "./dist/static",
template_folder = "./dist")
@app.route('/api/random')
def random_number():
response = {
'randomNumber': randint(1, 100)
}
return jsonify(response)
@app.route('/', defaults={'path': ''})
@app.route('/')
def catch_all(path):
return render_template("index.html")
首先导入 random 库和 jsonify 函数。然后创建一个 /api/random 对应的视图返回一个JSON 响应,如:
{
"randomNumber": 36
}
现在访问 localhost:5000/api/random可以得到正确的Json响应。
服务器端工作已经完成了,是时候在客户端展示了。我们来改写Home.vue。
Home page
Random number from backend: {{ randomNumber }}
New random number
export default {
data () {
return {
randomNumber: 0
}
},
methods: {
getRandomInt (min, max) {
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
},
getRandom () {
this.randomNumber = this.getRandomInt(1, 100)
}
},
created () {
this.getRandom()
}
}
在目前,我只是在客户端模拟随机数生成过程。所以,这个组件是这样工作的:初始化变量随机数为0
在method部分,我们有getRandomInt(min, max)函数,它将返回一个指定范围内的数字,getRandom函数将调度之前的函数并将其值赋给randomNumber
创建组件方法后,将调用getRandom初始化randomNumber
在按钮点击事件上,我们将发送getRandom方法来获取新号码
现在在首页你应该看到我们的随机数。但它是前端自己产生的,不是后端传过来的,现在我们来改写代码,从后端获取数据。
为此,我将使用axios库。它允许我们发出HTTP请求并返回带有JSON answer的JavaScript Promise。安装:
(venv) cd frontend
(venv) npm install --save axios
打开Home.vue,修改script,
import axios from 'axios'
methods: {
getRandom () {
// this.randomNumber = this.getRandomInt(1, 100) this.randomNumber = this.getRandomFromBackend()
},
getRandomFromBackend () {
const path = 'http://localhost:5000/api/random';
axios.get(path)
.then(response => {
this.randomNumber = response.data.randomNumber
})
.catch(error => {
console.log(error)
})
}
}
首先我们要导入axios库。然后有一个新的方法getRandomFromBackend,它将使用axios异步地从API获取结果。最后,getRandom方法现在应该使用getRandomFromBackend函数来获得一个随机值。
保存文件,运行前端开发服务器(localhost:8080),访问首页,你应该会在控制台看到一个错误,没有随机值。但别担心,只是发生了CORS错误(跨站请求被拒绝),这意味着我们的Flask服务器API默认情况下是对其他web服务器关闭的(在我们的例子中,“其他web服务器”是指运行Vue应用程序的Node.js服务器)。我们可以使用npm的build命令构建一个应用,并打开localhost:5000 (Flask服务),就可以看到我们的成果。但是,每次对客户端应用程序进行一些更改时,都要重新构建,并不十分方便。
我们使用Flask的CORS 插件,它将允许我们为API访问创建一个规则。插件是flask-cors,安装:
(venv) pip install -U flask-cors
我将使用特定于资源的方法,并将{" origin ": " * "}应用于所有/api/*路由(这样每个人都可以使用我的/api端点)。更多配置见CORS相关文档。
from flask_cors import CORS
app = Flask(
__name__,
static_folder = "./dist/static",
template_folder = "./dist"
)
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
更改后,可以直接从前端开发服务器调用Flask api。
实际上,如果通过Flask提供静态文件,那么就不需要CORS扩展。感谢Carson Gee(https://github.com/carsongee)的工作。在生产环境中提供静态文件,我们可以这样做:
import requests
@app.route('/', defaults={'path': ''})
@app.route('/')
def catch_all(path):
if app.debug:
return requests.get('http://localhost:8080/{}'.format(path)).text
return render_template("index.html")
简洁而优雅!魔法✨!
现在,你已经使用你喜欢的技术构建了一个全栈应用程序了。
后记:
最后,我想谈谈如何改进这个解决方案。
首先,只有在希望为外部服务器提供对API端点的访问权限时,才使用CORS扩展。否则,只需使用代理前端开发服务器的技巧。
另一个改进是避免在客户端使用硬编码的API路由。也许你需要考虑一些带有API端点的字典。当你改变你的API路由时,你只需要刷新一个字典。前端仍然有一个有效的端点。
通常在开发过程中,我们将至少有两个终端窗口:一个用于Flask,另一个用于Vue.js。在生产中,我们将避免为Vue运行单独的Node.js服务器。
译者摘选的网友问答
问:为什么不直接用SPA连到RESTful API,而用Flask代理呢?
答:因为我想在生产环境中,只用启用一个服务,而不是两个服务。用Flask代理,我就不用启动一个Node.js服务了。
问:你用什么IDE开发?
答:我用好用的VS Code,它对Python和Vue都有很好的支持。
问:我在处理登录逻辑时,使用Flask重定向到了登录视图,但是出现了错误?
答:后端只提供数据,你应当在Vue端设置这一重定向。
问:在开发环境,使用两个服务,是因为可以使用前面服务的热重载是吗?
答:基本是的。
问:我把项目部署到heroku上,需要对这个设置执行什么特殊的操作吗?我部署的似乎没能正常工作。我添加了一个Procfile并安装了gunicorn,复制了这个包到根文件夹,并添加了一个heroku/nodejs构建包 ……
答:我没有在heroku上部署过,好像是vue的路由问题。你看看是不是没有重定向到vue的index.html文件上。(译者注,看原提问者的描述,应该是使用Nginx之类的服务器时,没有正确伺服静态文件。)
本文为Flask+Vue整合开发提供了一个很好的模板,虽然只是一个很小很小的应用,但是却可以在这种模型思路下,搭建完整的前后端分离项目。文章作者写JS似乎不喜欢写分号,代码也写得极为简单(甚至不完整,完整代码见于其Github),但不妨碍正想使用Python结合前端框架Vue编写Web项目的读者汲取一些有用的思路。
Python构建RESTful网络服务[Django篇]仍在连载中,后续会讨论测试与API文档。希望在以的后的文章中,能继续讨论Flask、Tornado等其它Python Web框架的RESTful网络服务开发。
文章列表Python构建RESTful网络服务[Django篇:Vue+Django构建前后端分离项目]: https://zhuanlan.zhihu.com/p/54776124
Python构建RESTful网络服务[Django篇:基于函数视图的API]:https://zhuanlan.zhihu.com/p/55562891
Python构建RESTful网络服务[Django篇:使用PostgreSQL替代SQLite]:https://zhuanlan.zhihu.com/p/55903530
Python构建RESTful网络服务[Django篇:基于类视图的API]:https://zhuanlan.zhihu.com/p/57024322
Python构建RESTful网络服务[Django篇:基于视图集的API]:https://zhuanlan.zhihu.com/p/57791697
Python构建RESTful网络服务[Django篇:用户接入控制,认证与权限]:https://zhuanlan.zhihu.com/p/58426061