前言
本来在着手写关于使用Flask构建RESTful网络服务的文章,正好看到一篇相关文章,提供了一个Flask+vue构建全栈单页面应用的例子,觉得不错,就在此分享给大家。
用Flask+Vue.js打造全栈单页面应用
在本教程中,我将向您展示如何将Vue.js单页面应用程序与Flask后端整合起来。
基本上,如果你只想在Flask模板中使用Vue.js库,那么问题不大。唯一值得注意是Jinja(模板引擎)使用双花括号来渲染变量,而Vue插值也使用双花括号.
本文要说的是另一种情况:我需要一个使用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
<template>
<div>
<p>Home page</p>
</div>
</template>
// About.vue
<template>
<div>
<p>About</p>
</div>
</template>
现在要构建前端路由,前端应用要知道怎么处理我们我们在浏览器地址栏中填写地址。在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
要在Windows中激活。
在虚拟环境下安装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('/<path:path>')
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
<template>
<div>
<p>404 - Not Found</p>
</div>
</template>
现在再次使用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('/<path:path>')
def catch_all(path):
return render_template("index.html")
首先导入 random 库和 jsonify 函数。然后创建一个 /api/random 对应的视图返回一个JSON 响应,如:
{
"randomNumber": 36
}
现在访问 localhost:5000/api/random可以得到正确的Json响应。
服务器端工作已经完成了,是时候在客户端展示了。我们来改写Home.vue。
<template>
<div>
<p>Home page</p>
<p>Random number from backend: {{ randomNumber }}</p>
<button @click="getRandom">New random number</button>
</div>
</template>
<script>
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()
}
}
</script>
在目前,我只是在客户端模拟随机数生成过程。所以,这个组件是这样工作的:
- 初始化变量随机数为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相关文档。
在run.py:
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的工作。在生产环境中提供静态文件,我们可以这样做:
import requests
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
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服务器。