原文链接:codeburst.io
:-: ![](https://img.kancloud.cn/bf/44/bf440e8cd02cb45f71630b86d4e5f058_1080x608.png)
在本教程中,我将向大家展示如何使用前端的 Vue.js 单页面应用和后端的 Flask 进行交互。
如果你只是想使用 Vue.js 库和 Flask 模板基本上是没什么问题的。但...好吧,其实还是有一个比较显而易见的问题:跟 Vue.js 一样,Jinji(模板引擎)也是使用双大括号来渲染页面,但已经有一个很好的解决方案 在这里 了。
我想要一个跟上面方案有点不同的例子。如果我要一个用 Vue.js(使用单页面组件,在`vue-router` 开启 HTML5 history 模式,还有使用其他一些非常棒的特性)框架的单页面和 Flask 做后台服务的应用?应该能按下面的要求工作:
* Flask运行的服务可以访问 `index.html` 首页和 Vue.js 应用
* 在前端开发环境,使用 Webpack 和它提供的很多非常棒的功能
* 可以从前端的单页面应用访问 Flask 的 API 接口
* 以 Node.js 服务运行的前端开发环境同样也可以访问 API 接口
这看起来很有趣,不是吗?那就让我们开始吧。
> 你可以在github上查看所有的源代码:
>
> https://github.com/oleg-agapov/flask-vue-spa
### 客户端
我用 vue-cli 命令行工具搭建起 Vue.js 的基础框架。如果你还没有安装,可以运行:
~~~
$ npm install -g vue-cli
~~~
客户端和后端代码将会放到不同的文件夹下,初始化前端部分执行如下操作:
~~~
$ mkdir flaskvue
$ cd flaskvue
$ vue init webpack frontend
~~~
以下是我通过安装向导的项目设置:
* Vue build — Runtime only (Vue 构建的版本 - 运行时)
* Install vue-router? — Yes (安装 vue-router?- 是)
* Use ESLint to lint your code? — Yes (使用 ESLint 校验你的代码?- 是)
* Pick an ESLint preset — Standard (选择 ESList 的预置版本 - 标准)
* 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` 你应该看到相应的页面。
:-: ![](https://img.kancloud.cn/d6/bc/d6bc91414ecadb7532b1a0dfe59a0d42_1080x674.png)
在我们构建生成项目静态资源前还需要修改它们的输出路径。在 `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` 去构建项目了
:-: ![](https://img.kancloud.cn/10/1f/101f5f299c3b3ae1377188784665d5f6_1080x699.png)
### 后端
Flask 后端,我将使用 3.6 版本的 python。在根目录 `/flaskvue` 文件夹下为后端代码和初始化虚拟环境创建新的子目录:
~~~
$ mkdir backend
$ cd backend
$ virtualenv -p python3 venv
~~~
开启虚拟环境执行(mac系统):
~~~
$ source venv/bin/activate
~~~
在 Windows 上开启请看这里 docs。
在虚拟环境中安装 Flask 如下:
~~~
(venv) pip install Flask
~~~
现在让我们开始写 Flask 服务器端代码。在根目录下创建 `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 入门教程 “Hello world” 上的代码稍有不同。最主要的不同点在于我们详细指明了前端的静态和模板文件夹输出到 `/dist` 文件夹。然后在根目录下运行 Flask 服务。
~~~
(venv) FLASK_APP=run.py FLASK_DEBUG=1 flask run
~~~
这将会在 `localhost:5000` 开启一个后台服务。`FLASK_APP` 指向服务启动文件,`FLASK_DEBUG=1` 将会以调试模式运行。如果没有错误,你将会看到熟悉的首页,这样,服务器就成功运行 Vue 应用了。
与此同时如果你试图访问 `/about` 页面将会出现一个错误。Flask 会抛出一个找不到请求地址的错误。实际上是因为在 `vue-router` 用了 HTML5 的 history 模式, 所以我们需要配置我们的后台服务去重定向所有的路由都跳转到 `index.html` 上。这在 Flask 上可以很简单做到。做如下修改:
~~~
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
return render_template("index.html")
~~~
现在地址 `localhost:5000/about` 将会重定向到 `index.html` 和 `vue-router` 将会在它自己内部处理。
### 添加 404 页面
因为在我们的后台服务里设置捕捉所有路由是非常困难的,所以我们用 Flask 捕捉 404 错误会重定向 所有 请求到 `index.html`(连同不存在的页面)。在 Vue.js 应用里处理未定义的路由。当然,所有的工作均可在我们的路由文件设置。
在 `frontend/src/router/index.js` 增加一行:
~~~
const routerOptions = [
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' },
{ path: '*', component: 'NotFound' }
]
~~~
通配符 `'*'` 在 `vue-router` 里的含义是以上路由定义之外的情况。现在我们需要在 `/components` 文件夹新建 `NotFound.vue` 文件。我简单地创建它:
~~~
// NotFound.vue
<template>
<div>
<p>404 - Not Found</p>
</div>
</template>
~~~
现在 通过 `npm run dev` 重新启动前台服务然后随意输入网址像 `localhost:8080/gljhewrgoh`。你应该看到 “Not Found” 两个单词。
### 添加后端 API 接口
我的 Vue.js/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")
~~~
我首先从 Flask 资源库导入 `random` 库和 `jsonify` 函数。然后我增加一个返回 JSON 数据格式的新路由 `/api/random`, 如下:
~~~
{
"randomNumber": 36
}
~~~
你可以通过地址:`localhost:5000/api/random` 来测试这个路由。
到这里,服务端的工作已经完成了。该到客户端上场了。我将修改 `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) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
},
getRandom () {
this.randomNumber = this.getRandomInt(1, 100)
}
},
created () {
this.getRandom()
}
}
</script>
~~~
在这一步,我将在客户端模拟随机数的生成。所以,组件的工作过程如下:
* 初始变量 `randomNumber` 等于 `0`
* 在 `methods` 部分,我们用 `getRandomInt(min, max)` 函数从指定区间返回一个数字, `getRandom` 函数将调用上一个函数生成一个值赋给 `randomNumber`
* 之后在组件被创建时调用 `getRandom` 方法给 `randomNumber` 赋个初始数值
* 在按钮点击事件里,我们将触发 `getRandom` 方法去得到一个数值
现在,在首页上你将看到由前端生成的随机数。让我们继续来连接后端。
我将用 axios 库来连接后端。它将允许我们创建能返回 `Promise` 对象的 HTTP 请求。我们先安装它:
~~~
(venv) cd frontend
(venv) npm install --save axios
~~~
再次打开 `Home.uve`,修改 `<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`](http://localhost:5000/api/random`)
axios.get(path)
.then(response => {
this.randomNumber = response.data.randomNumber
})
.catch(error => {
console.log(error)
})
}
}
~~~
在文件顶部,我们先导入 axios 库。然后用 axios 去异步调用新方法 `getRandonFromBackend` 接收返回的结果。最后, `getRandom` 方法调用 `getRandomFromBackend` 去获取随机值。
保存文件,打开浏览器,再次运行前端开发服务器环境,刷新 `localhost:8080` 然后... 你应该看到控制台报了没有随机值的错误。但不用担心,一切正常运行中。我们得到 cors 的错误,它的意思是我们的 Flask 后台 API 默认不对其他的域名和端口(我们的例子运行的是 Vue.js 应用)开放。当你用 `npm run build` 生成包然后打开 `localhost:5000`(Flask 服务)你会看到应用正常运行不再报错了。但如果每次在客户端改了一点东西都要重新构建包,显然不是很方便。
Flask 的 CORS 插件允许我们为访问 API 创建规则。插件叫 flask-cors,我们先来安装它:
~~~
(venv) pip install -U flask-cors
~~~
你可以通过阅读文档选择更好的方法来在你的服务器上开启 CORS。我这里将会用资源指定的方法应用 `{"origins": "*"}` 去允许所有 `/api/*` 下的路由(所以任何人都可以访问 `/api` 接口)。修改 `run.py`:
~~~
from flask_cors import CORS
app = Flask(__name__,
static_folder = "./dist/static",
template_folder = "./dist")
cors = CORS(app, resources={"/api/*": {"origins": "*"}})
~~~
改好之后,你就可以从前端的开发环境调用 Flask API 接口了。
太神奇了 ✨!
现在你拥有了一个用你喜爱的技术完成的全栈应用。
:-: ![](https://img.kancloud.cn/44/a6/44a662daa0cb05845f461220ba283411_1080x1173.png)
![](https://img.kancloud.cn/e4/62/e462aaa53e718f9a53bdc0f2cdc559ad_1080x1173.png)
#### 后记
最后我想说说如何改进这个方案。
首先,在你代码里所有使用到的环境变量。主要是关于使用 `FLASK_DEBUG` 变量。我们在 CORS 设置中使用到它。例如,如果服务运行在开发环境设置 `FLASK_DEBUG=1` 你可以允许任何的请求源。如果不是,禁用 CORS 或者只允许可信源请求。
另外一个改进是避免在客户端硬编码 API 路由。也许你需要思考为 API 接口创建映射表。所以当你改变 API 路由,你所需要做的只是更新映射表。前端的调用接口将不需要改变。
还有个小建议 - 我通常同时开启至少3个终端窗口:一个运行 Flask,二个运行 Vue.js(第一个运行 Node.js 服务,第二个用来做项目构建打包)。
> 源代码:https://github.com/oleg-agapov/flask-vue-spa