原文鏈接:codeburst.io
:-: 
在本教程中,我將向大家展示如何使用前端的 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`?你應該看到相應的頁面。
:-: 
在我們構建生成項目靜態資源前還需要修改它們的輸出路徑。在?`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 后端,我將使用 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 接口了。
太神奇了 ?!
現在你擁有了一個用你喜愛的技術完成的全棧應用。
:-: 

#### 后記
最后我想說說如何改進這個方案。
首先,在你代碼里所有使用到的環境變量。主要是關于使用?`FLASK_DEBUG`?變量。我們在 CORS 設置中使用到它。例如,如果服務運行在開發環境設置?`FLASK_DEBUG=1`?你可以允許任何的請求源。如果不是,禁用 CORS 或者只允許可信源請求。
另外一個改進是避免在客戶端硬編碼 API 路由。也許你需要思考為 API 接口創建映射表。所以當你改變 API 路由,你所需要做的只是更新映射表。前端的調用接口將不需要改變。
還有個小建議 - 我通常同時開啟至少3個終端窗口:一個運行 Flask,二個運行 Vue.js(第一個運行 Node.js 服務,第二個用來做項目構建打包)。
> 源代碼:https://github.com/oleg-agapov/flask-vue-spa