使用 Vue + Flask 搭建单页应用
單頁(yè)應(yīng)用,只加載一個(gè)主頁(yè)面,然后通過(guò) AJAX 無(wú)刷新加載其它頁(yè)面片段。表面上看,就只有一個(gè) HTML 文件,所謂單頁(yè)。開(kāi)發(fā)上,做到了前后端分離,前端專注于渲染模板,而后端只要提供 API 就行,不用自己去套模板了。效果上,頁(yè)面和共用的 JS、CSS 文件都只加載一次,能減輕服務(wù)器壓力和節(jié)省一定的網(wǎng)絡(luò)帶寬。另外,由于不需要每次都加載頁(yè)面以及共用的靜態(tài)文件,響應(yīng)速度也有一定提高,用戶體驗(yàn)比較好。當(dāng)然,也有一些缺點(diǎn),比如 SEO 優(yōu)化不大方便,不過(guò)也有相應(yīng)的解決方案。總的來(lái)說(shuō),使用單頁(yè)應(yīng)用的好處還是遠(yuǎn)多于壞處,這也是越來(lái)越多的人使用單頁(yè)應(yīng)用的原因。
構(gòu)建單頁(yè)應(yīng)用的方式有很多,這里我們選擇 Flask + Vue 實(shí)現(xiàn)。本文以實(shí)現(xiàn)一個(gè) CRUD 的 Demo 為主線,在其中穿插必要的技術(shù)點(diǎn)進(jìn)行講述。里面可能涉及了一些你沒(méi)接觸或者不熟悉的概念,不過(guò)不要緊,我會(huì)給出相應(yīng)的參考文章幫助你理解。當(dāng)然,大牛可忽略這些 :)。看完這篇文章后,相信你也能搭建自己的單頁(yè)應(yīng)用了。
1 前端
這里我們會(huì)用到 Vue 框架。如果你之前沒(méi)有接觸過(guò),推薦去看下官方文檔的「基礎(chǔ)」一節(jié)。也可以先直接向下看,Demo 用的都是一些基礎(chǔ)的東西,大致看下應(yīng)該就能理解。即使暫時(shí)不理解,照著例子實(shí)踐一遍后,去看下文檔收獲也應(yīng)該更多。
為了更便捷的創(chuàng)建基于 Vue 的項(xiàng)目,我們可以使用 Vue Cli 腳手架。通過(guò)腳手架創(chuàng)建項(xiàng)目的時(shí)候,它會(huì)輔助我們做一些配置,省去我們手動(dòng)配置的時(shí)間。剛接觸的伙伴前期會(huì)用它創(chuàng)建項(xiàng)目就行了,至于更深的一些東西后期再去了解。
安裝腳手架
$ npm install -g @vue/cli 復(fù)制代碼這里我們安裝的是最新的 3 版本。
基于 Vue 的 UI 組件庫(kù)很多,比如 iView、Element、Vuetify 等。國(guó)內(nèi)使用 iView、Element 的特別多,而使用 Vuetify 的人相對(duì)要少很多,不知道是大家看不慣它的 Material Design 風(fēng)格還是它的中文文檔稀缺的緣故。不過(guò)我個(gè)人挺喜歡 Vuetify 的風(fēng)格的,所以我會(huì)使用這個(gè)組件庫(kù)搭建前端頁(yè)面。
如果你沒(méi)使用過(guò)這個(gè)組件庫(kù),照著本文一步步實(shí)踐下去,也能對(duì) Vuetify 的用法有個(gè)大致的了解。如果這個(gè)過(guò)程中,感覺(jué)碰到的疑問(wèn)太多,可以看下 YouTube 上的這個(gè)視頻教程。
https://dwz.cn/lxMHF4bY
也不要到處去找類似的資源了,就是這個(gè)系列的視頻看完再加上官方文檔,掌握常用的點(diǎn)基本沒(méi)問(wèn)題。
不過(guò),還是建議先照著本文實(shí)現(xiàn)一下 Demo,再去學(xué)習(xí),我覺(jué)得這樣效果更好。
新建目錄 spa-demo,然后切換到該目錄下新建前端項(xiàng)目 client
$ vue create client 復(fù)制代碼創(chuàng)建項(xiàng)目時(shí)會(huì)讓你手動(dòng)選擇一些配置,這里貼下我當(dāng)時(shí)的設(shè)置
? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Linter ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Basic ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json ? Save this as a preset for future projects? (y/N) N 復(fù)制代碼回車安裝完成后,我們切換到 client 目錄下,執(zhí)行命令
$ npm run serve 復(fù)制代碼上述命令執(zhí)行完成后會(huì)有類似這樣的輸出
...App running at: - Local: http://localhost:8080/ - Network: http://172.20.10.3:8080/... 復(fù)制代碼在瀏覽器中訪問(wèn)
http://localhost:8080/
如果看到包含下面文字的頁(yè)面
Welcome to Your Vue.js App
說(shuō)明項(xiàng)目安裝成功。
安裝 Vuetify
$ vue add vuetify 復(fù)制代碼同樣會(huì)提示你選擇一些配置,這里我選擇的 Default
? Choose a preset: Default (recommended) 復(fù)制代碼回車安裝完成后,重新開(kāi)下服務(wù)器
$ npm run serve 復(fù)制代碼執(zhí)行完畢后,我們?cè)跒g覽器中訪問(wèn)
http://localhost:8080/
會(huì)看到頁(yè)面內(nèi)容又些改變,有這么一行文字
Welcome to Vuetify
這里說(shuō)明 Vuetify 安裝成功。
看下此時(shí)的目錄結(jié)構(gòu)
spa-demo └── client├── README.md├── babel.config.js├── package-lock.json├── package.json├── node_module│?? └── ...├── public│?? ├── favicon.ico│?? └── index.html└── src├── App.vue├── assets│?? ├── logo.png│?? └── logo.svg├── components│?? └── HelloWorld.vue├── main.js├── plugins│?? └── vuetify.js├── router.js└── views├── About.vue└── Home.vue 復(fù)制代碼簡(jiǎn)化 spa-demo/client/src/App.vue,將其修改為
<template><v-app><v-content><router-view></router-view></v-content></v-app> </template><script>export default {name: 'App',data () {return {//}}} </script> 復(fù)制代碼修改 spa-demo/client/src/views/Home.vue,在頁(yè)面放入一個(gè) Data table
<template><div class="home"><v-container class="my-5"><!-- 對(duì)話框 --><!-- 表格 --><v-data-table:headers="headers":items="books"hide-actionsclass="elevation-1"><template slot="items" slot-scope="props"><td>{{ props.item.name }}</td><td>{{ props.item.category }}</td><td class="layout px-0"><v-icon small class="ml-4" @click="editItem(props.item)">edit</v-icon><v-icon small @click="deleteItem(props.item)">delete</v-icon></td></template><template slot="no-data"><v-alert :value="true" color="info" outline>無(wú)數(shù)據(jù)</v-alert></template></v-data-table></v-container></div> </template><script>export default {data: () => ({headers: [{ text: '書(shū)名', value: 'name', sortable: false, align: 'left'},{ text: '分類', value: 'category', sortable: false },{ text: '操作', value: 'name', sortable: false }],books: [],}),created () {this.books = [{ name: '生死疲勞', category: '文學(xué)' },{ name: '國(guó)家寶藏', category: '人文社科' },{ name: '人類簡(jiǎn)史', category: '科技' },]},} </script> 復(fù)制代碼我們使用了數(shù)據(jù) headers 和 books 控制表的頭部和數(shù)據(jù),并在創(chuàng)建的時(shí)候,給 books 填充了一些臨時(shí)數(shù)據(jù)。
這個(gè)頁(yè)面中涉及到了 Data table 的使用,相關(guān)代碼不用記,在 Vuetify 文檔中搜索 Data table 有很多例子,看了幾個(gè)之后你就知道怎么使用了。對(duì)于新手來(lái)說(shuō),不好理解的可能就是那個(gè) slot-scope(作用域插槽 ),這個(gè)看下 Vue 官方文檔這些內(nèi)容
- 「基礎(chǔ)」一節(jié)的「組件基礎(chǔ)」
- 「深入了解組件」一節(jié)的「組件注冊(cè)」、「Prop」、「自定義事件」、「插槽」
靜下心來(lái)讀讀就明白了,不難,這里我不再贅述。
同樣,這里你也可以先照葫蘆畫(huà)瓢,可以先暫時(shí)忽略掉一些不好理解的地方,待實(shí)踐一遍之后再去搞清楚。
打開(kāi)
http://localhost:8080/
看到的頁(yè)面是這樣的
就是一個(gè)圖書(shū)列表。
現(xiàn)在我們要做個(gè)可以彈出的對(duì)話框,用于新增書(shū)籍。我們?cè)?<!-- 對(duì)話框 --> 位置新增如下代碼
<v-toolbar flat class="white"><v-toolbar-title>圖書(shū)列表</v-toolbar-title><v-spacer></v-spacer><v-dialog v-model="dialog" max-width="600px"><v-btn slot="activator" class="primary" dark>新增</v-btn><v-card><v-card-title><span class="headline">{{ formTitle }}</span></v-card-title><v-card-text><v-alert :value="Boolean(errMsg)" color="error" icon="warning" outline>{{ errMsg }}</v-alert><v-container grid-list-md><v-layout><v-flex xs12 sm6 md4><v-text-field label="書(shū)名" v-model="editedItem.name"></v-text-field></v-flex><v-flex xs12 sm6 md4><v-text-field label="分類" v-model="editedItem.category"></v-text-field></v-flex></v-layout></v-container></v-card-text><v-card-actions><v-spacer></v-spacer><v-btn color="blue darken-1" flat @click="close">取消</v-btn><v-btn color="blue darken-1" flat @click="save">保存</v-btn></v-card-actions></v-card></v-dialog> </v-toolbar> 復(fù)制代碼對(duì)應(yīng)的,要在 <script></script> 之間添加一些 JS
export default {data: () => ({dialog: false, // 是否展示對(duì)話框errMsg: '', // 是否有錯(cuò)誤信息editedIndex: -1, // 當(dāng)前在對(duì)話框中編輯的圖書(shū)在列表中的序號(hào)editedItem: { // 當(dāng)前在對(duì)話框中編輯的圖書(shū)內(nèi)容id: 0,name: '',category: ''},defaultItem: { // 默認(rèn)的圖書(shū)內(nèi)容,用于初始化新增對(duì)話框內(nèi)容id: 0,name: '',category: ''}}),computed: {formTitle () {return this.editedIndex === -1 ? '新增' : '編輯'}},watch: {dialog (val) {if (!val) {this.close()this.clearErrMsg()}}},methods: {clearErrMsg () {this.errMsg = ''},close () {this.dialog = falsesetTimeout(() => {this.editedItem = Object.assign({}, this.defaultItem)this.editedIndex = -1}, 300)}} } 復(fù)制代碼為了讓文章簡(jiǎn)潔一些,貼代碼的時(shí)候我將之前已有的片段進(jìn)行了省略,你寫(xiě)的時(shí)候可以將上面的代碼根據(jù)位置添加到合適的地方。
我們使用了 Toolbar、Dialog 在表格上面添加對(duì)話框相關(guān)的東西,同樣,不必記代碼,不知道怎么寫(xiě)的時(shí)候查閱下文檔就行。
數(shù)據(jù) dialog 表示當(dāng)前對(duì)話框是否展示,errMsg 控制錯(cuò)誤信息的展示,監(jiān)聽(tīng) dialog 當(dāng)它變化為 false 的時(shí)候關(guān)閉對(duì)話框并清空 errMsg。計(jì)算屬性 formTitle 用于控制對(duì)話框的標(biāo)題。然后添加了兩個(gè)表單元素用于填寫(xiě)書(shū)籍的名稱以及分類。
當(dāng)我們點(diǎn)擊新增后,頁(yè)面是這樣的
其實(shí),到這里,我們的前端頁(yè)面差不多就 OK 了,后面便是增刪改的實(shí)現(xiàn)。這個(gè)我們先在前端單方面的實(shí)現(xiàn)下,后面再和后端進(jìn)行整合。這樣,會(huì)讓前端的 Demo 更完整一些。
實(shí)現(xiàn)保存方法,在 methods 新增 save
save() {if (this.editedIndex > -1) { // 編輯Object.assign(this.books[this.editedIndex], this.editedItem)} else { // 新增this.books.push(this.editedItem)}this.close() } 復(fù)制代碼編輯的時(shí)候,要展示彈框,我們需要添加 editItem 方法
editItem (item) {this.editedIndex = this.books.indexOf(item)this.editedItem = Object.assign({}, item)this.dialog = true } 復(fù)制代碼保存方法和新增時(shí)的一致。
實(shí)現(xiàn)刪除方法 deleteItem
deleteItem (item) {const index = this.books.indexOf(item)confirm('確認(rèn)刪除?') && this.books.splice(index, 1) } 復(fù)制代碼至此,前端項(xiàng)目告一段落。
2 后端
后端,我們只需要提供增刪改查的接口供前端使用就行。RESTful API 是目前比較成熟的一套互聯(lián)網(wǎng)應(yīng)用程序設(shè)計(jì)理論,我也會(huì)基于此實(shí)現(xiàn)圖書(shū)的相關(guān)操作接口。
考慮到有對(duì) RESTful API 不大熟悉的伙伴,我列了幾個(gè)我之前學(xué)習(xí)的文章,供大家參考。
- 《理解RESTful架構(gòu)》
- https://dwz.cn/eXu0p6pv
- 《RESTful API 設(shè)計(jì)指南》
- https://dwz.cn/8v4B0twY
- 《RESTful API 最佳實(shí)踐》
- https://dwz.cn/2aSnI8fF
- 知乎問(wèn)題《怎樣用通俗的語(yǔ)言解釋REST,以及RESTful?》
- https://dwz.cn/bVxrSsf4
看完上面的相關(guān)資料,你對(duì)這種設(shè)計(jì)理論應(yīng)該就有一定掌握了。
同樣,你暫時(shí)可不必對(duì) RESTful API 了解得很全面,暫時(shí)像下面這樣理解它就行
就是用 URL 定位資源,用 HTTP 描述操作。
這個(gè)是在刷上面知乎問(wèn)題看到的一個(gè)回答,作者是 @Ivony。寫(xiě)得很簡(jiǎn)潔,但確實(shí)有道理。
等到自己實(shí)踐一次后,再回頭看看理論的一些東西,印象更深。
首先列下我們需要實(shí)現(xiàn)的接口
| 1 | GET | http://domain/api/v1/books | 獲取所有圖書(shū) |
| 2 | GET | http://domain/api/v1/books/123 | 獲取主鍵為 123 的圖書(shū) |
| 3 | POST | http://domain/api/v1/books | 新增圖書(shū) |
| 4 | PUT | http://domain/api/v1/books/123 | 更新主鍵為 123 的圖書(shū) |
| 5 | DELETE | http://domain/api/v1/books/123 | 刪除主鍵為 123 的圖書(shū) |
我們可以直接使用 Flask 實(shí)現(xiàn)上面的接口,不過(guò)當(dāng)資源多的時(shí)候,我們寫(xiě)代碼時(shí)會(huì)寫(xiě)很多重復(fù)的片段,違反了 DRY(Don't Repeat Yourself) 原則,后面維護(hù)起來(lái)比較麻煩,所以我們借助 Flask-RESTful 擴(kuò)展實(shí)現(xiàn)。
另外,本節(jié)的重心是放在接口的實(shí)現(xiàn)上,也為了行文更簡(jiǎn)潔,我們將數(shù)據(jù)直接存在字典里,就不涉及數(shù)據(jù)庫(kù)相關(guān)的操作了。
在 spa-demo 目錄下新建 server 目錄,并切換到該目錄下,初始化 Python 環(huán)境
$ pipenv --python 3.6.0 復(fù)制代碼Pipenv 是當(dāng)前官方推薦的虛擬環(huán)境和包管理工具,我之前寫(xiě)過(guò)一篇文章《Pipenv 快速上手》介紹過(guò),沒(méi)接觸過(guò)的可以去看下。
安裝 Flask
$ pipenv install flask 復(fù)制代碼安裝 Flask-RESTful
$ pipenv install flask-restful 復(fù)制代碼新建 spa-demo/server/app.py
# coding=utf-8from flask import Flask, request from flask_restful import Api, Resource, reqparse, abortapp = Flask(__name__) api = Api(app)books = [{'id': 1, 'name': 'book1', 'category': 'cat1'},{'id': 2, 'name': 'book2', 'category': 'cat2'},{'id': 3, 'name': 'book3', 'category': 'cat3'}]# 公共方法區(qū)class BookApi(Resource):def get(self, book_id):passdef put(self, book_id):passdef delete(self, book_id):passclass BookListApi(Resource):def get(self):return booksdef post(self):passapi.add_resource(BookApi, '/api/v1/books/<int:book_id>', endpoint='book') api.add_resource(BookListApi, '/api/v1/books', endpoint='books')if __name__ == '__main__':app.run(debug=True) 復(fù)制代碼上面就是一個(gè)標(biāo)準(zhǔn)的整合了 Flask-RESTful 的代碼結(jié)構(gòu),在 Flask-RESTful 的官方文檔中可以看到相似的例子。對(duì)于每一種資源,我們都可以用類似的結(jié)構(gòu)實(shí)現(xiàn)接口。BookApi 類中的 get、put、delete 方法對(duì)應(yīng)接口 2、4、5,BookListApi 類中的 get、post 方法對(duì)應(yīng)接口 1、3。之后便是注冊(cè)路由。看到這,有的伙伴可能會(huì)有疑問(wèn),為什么同一個(gè)資源需要定義兩個(gè)類呢?其實(shí)就是方便給一個(gè)資源注冊(cè)帶主鍵和不帶主鍵的路由。
此時(shí),項(xiàng)目結(jié)構(gòu)為
spa-demo ├── client │?? └── ... └── server├── Pipfile├── Pipfile.lock└── app.py 復(fù)制代碼切換到 spa-demo/server 目錄,運(yùn)行 app.py
$ pipenv run python app.py 復(fù)制代碼然后測(cè)試獲取所有圖書(shū)接口是否可用。由于是 API 測(cè)試,不建議直接使用瀏覽器,畢竟有時(shí)構(gòu)造參數(shù)和看 HTTP 信息不大方便,推薦使用 Postman,當(dāng)然簡(jiǎn)單的測(cè)試的話可以直接使用命令 curl。
請(qǐng)求接口 1,獲取所有圖書(shū)信息
$ curl -i http://127.0.0.1:5000/api/v1/books 復(fù)制代碼得到結(jié)果
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 249 Server: Werkzeug/0.14.1 Python/3.6.0 Date: Thu, 13 Dec 2018 15:21:56 GMT[{"id": 1,"name": "book1","category": "cat1"},{"id": 2,"name": "book2","category": "cat2"},{"id": 3,"name": "book3","category": "cat3"} ] 復(fù)制代碼成功獲取所有圖書(shū),說(shuō)明接口 1 已經(jīng) OK。
然后實(shí)現(xiàn)接口 2,獲取指定 ID 的圖書(shū)。由于根據(jù) ID 獲取圖書(shū)以及圖書(shū)不存在時(shí)拋 404 的操作后面會(huì)頻繁使用到,所以這里提兩個(gè)方法到「公共方法區(qū)」。
def get_by_id(book_id):book = [v for v in books if v['id'] == book_id]return book[0] if book else Nonedef get_or_abort(book_id):book = get_by_id(book_id)if not book:abort(404, message=f'Book {book_id} not found')return book 復(fù)制代碼然后實(shí)現(xiàn) BookApi 中 get 方法
def get(self, book_id):book = get_or_abort(book_id)return book 復(fù)制代碼取 ID 為 1 的圖書(shū)測(cè)試下
$ curl -i http://127.0.0.1:5000/api/v1/books/1 復(fù)制代碼結(jié)果
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 61 Server: Werkzeug/0.14.1 Python/3.6.0 Date: Thu, 13 Dec 2018 15:31:48 GMT{"id": 1,"name": "book1","category": "cat1" } 復(fù)制代碼取 ID 為 5 的圖書(shū)測(cè)試下
$ curl -i http://127.0.0.1:5000/api/v1/books/5 復(fù)制代碼結(jié)果
HTTP/1.0 404 NOT FOUND Content-Type: application/json Content-Length: 149 Server: Werkzeug/0.14.1 Python/3.6.0 Date: Thu, 13 Dec 2018 15:32:47 GMT{"message": "Book 5 not found. You have requested this URI [/api/v1/books/5] but did you mean /api/v1/books/<int:book_id> or /api/v1/books ?" } 復(fù)制代碼ID 為 1 時(shí),成功獲取到圖書(shū)信息;ID 為 5 時(shí),由于圖書(shū)不存在,所以會(huì)返回 404 的響應(yīng)。測(cè)試結(jié)果與預(yù)期一致,說(shuō)明這個(gè)接口也 OK 了。
實(shí)現(xiàn)接口 3,新增圖書(shū)。新增圖書(shū)的時(shí)候,我們應(yīng)該校驗(yàn)參數(shù)是否符合要求。Flask-RESTFul 給我們提供了比較優(yōu)雅的實(shí)現(xiàn),不需要我們使用多個(gè) if 判斷的硬編碼的形式去檢測(cè)參數(shù)是否有效。
由于圖書(shū)名稱和分類都是不能為空的,我們需要自定義規(guī)則,我們可以在「公共方法區(qū)」新增一個(gè)方法
def not_empty_str(s):s = str(s)if not s:raise ValueError("Must not be empty string")return s 復(fù)制代碼重寫(xiě) BookListApi 的初始化方法
def __init__(self):self.reqparse = reqparse.RequestParser()self.reqparse.add_argument('name', type=not_empty_str, required=True, location='json')self.reqparse.add_argument('category', type=not_empty_str, required=True, location='json')super(BookListApi, self).__init__() 復(fù)制代碼然后實(shí)現(xiàn) post 方法
def post(self):args = self.reqparse.parse_args()book = {'id': books[-1]['id'] + 1 if books else 1,'name': args['name'],'category': args['category'],}books.append(book)return book, 201 復(fù)制代碼方法中,首先檢測(cè)參數(shù)是否有效,然后取最后一本書(shū)的 ID 加上 1 作為新書(shū)的 ID 保存,最后返回添加的圖書(shū)信息和狀態(tài)碼 201(表示已創(chuàng)建)。
測(cè)試下參數(shù)校驗(yàn)是否 OK
$ curl -i \-H "Content-Type: application/json" \-X POST \-d '{"name":"","category":""}' \http://127.0.0.1:5000/api/v1/books 復(fù)制代碼結(jié)果
HTTP/1.0 400 BAD REQUEST Content-Type: application/json Content-Length: 70 Server: Werkzeug/0.14.1 Python/3.6.0 Date: Thu, 13 Dec 2018 15:46:18 GMT{"message": {"name": "Must not be empty string"} } 復(fù)制代碼返回 400 的錯(cuò)誤,說(shuō)明參數(shù)校驗(yàn)有效。
看下新增接口是否可用
$ curl -i \-H "Content-Type: application/json" \-X POST \-d '{"name":"t_name","category":"t_cat"}' \http://127.0.0.1:5000/api/v1/books 復(fù)制代碼結(jié)果
HTTP/1.0 201 CREATED Content-Type: application/json Content-Length: 63 Server: Werkzeug/0.14.1 Python/3.6.0 Date: Thu, 13 Dec 2018 15:53:54 GMT{"id": 4,"name": "t_name","category": "t_cat" } 復(fù)制代碼說(shuō)明創(chuàng)建成功。我們通過(guò)獲取指定 ID 的圖書(shū)接口確認(rèn)下
$ curl -i http://127.0.0.1:5000/api/v1/books/4 復(fù)制代碼結(jié)果
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 63 Server: Werkzeug/0.14.1 Python/3.6.0 Date: Thu, 13 Dec 2018 15:54:18 GMT{"id": 4,"name": "t_name","category": "t_cat" } 復(fù)制代碼獲取成功,說(shuō)明確實(shí)創(chuàng)建成功,說(shuō)明接口 3 也好了。
接口 4、5 的實(shí)現(xiàn)與上面類似,這里貼下代碼,就不詳細(xì)說(shuō)明了。
和 BookListApi 類似,首先重寫(xiě) BookApi 的初始化方法
def __init__(self):self.reqparse = reqparse.RequestParser()self.reqparse.add_argument('name', type=not_empty_str, required=True, location='json')self.reqparse.add_argument('category', type=not_empty_str, required=True, location='json')super(BookApi, self).__init__() 復(fù)制代碼然后實(shí)現(xiàn) put 和 delete 方法
def put(self, book_id):book = get_or_abort(book_id)args = self.reqparse.parse_args()for k, v in args.items():book[k] = vreturn book, 201def delete(self, book_id):book = get_or_abort(book_id)del bookreturn '', 204 復(fù)制代碼至此,后端項(xiàng)目基本完畢。
當(dāng)然,這是不完整的,比如這里面都沒(méi)有實(shí)現(xiàn)對(duì) API 的認(rèn)證,這個(gè)可以通過(guò) Flask-HTTPAuth 或者其它方式實(shí)現(xiàn)。限于篇幅,這里就不展開(kāi)說(shuō)明了,有興趣的可以看下這個(gè)這個(gè)擴(kuò)展的文檔或者自己研究實(shí)現(xiàn)下。
3 整合
單獨(dú)的前端或后端都有了雛形,就差整合這一步了。
前端需要請(qǐng)求數(shù)據(jù),這里我們使用 axios,切換到 spa-demo/client 目錄下進(jìn)行安裝
$ npm install axios --save 復(fù)制代碼修改 spa-demo/client/src/views/Home.vue,在 script 標(biāo)簽之間引入 axios,并初始化 API 地址
import axios from 'axios'const booksApi = 'http://localhost:5000/api/v1/books'export default {... } 復(fù)制代碼修改鉤子 created 的邏輯,從后端獲取數(shù)據(jù)
created () {axios.get(booksApi).then(response => {this.books = response.data}).catch(error => {console.log(error)}) } 復(fù)制代碼運(yùn)行前端項(xiàng)目后,查看首頁(yè),會(huì)發(fā)現(xiàn)沒(méi)有數(shù)據(jù)。查看開(kāi)發(fā)者工具,我們會(huì)發(fā)現(xiàn)這么一個(gè)錯(cuò)誤
Access to XMLHttpRequest at 'http://localhost:5000/api/v1/books' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 復(fù)制代碼就是說(shuō)當(dāng)前項(xiàng)目不支持 CORS(Cross-Origin Resource Sharing,即跨域資源訪問(wèn))。這個(gè)我們可以在前端添加代理的形式實(shí)現(xiàn),也可以在后端通過(guò) Flask-CORS 實(shí)現(xiàn)。這里,我使用的后者。
切換到 spa-demo/server 目錄,安裝 Flask-CORS
$ pipenv install flask-cors 復(fù)制代碼修改 spa-demo/server/app.py,在頭部引入 CORS
from flask_cors import CORS 復(fù)制代碼在代碼
app = Flask(__name__) 復(fù)制代碼和
api = Api(app) 復(fù)制代碼之間添加一行
CORS(app, resources={r"/api/*": {"origins": "*"}}) 復(fù)制代碼然后重新運(yùn)行 app.py,刷新首頁(yè),我們會(huì)看到列表有數(shù)據(jù)了,說(shuō)明 CORS 的問(wèn)題成功解決。
在 spa-demo/client/src/views/Home.vue 中,修改 save 方法,同時(shí)新增輔助方法 setErrMsg
setErrMsg (errResponse) {let errResMsg = errResponse.data.messageif (typeof errResMsg === 'string') {this.errMsg = errResMsg} else {let errMsgs = []let kfor (k in errResMsg) {errMsgs.push('' + k + ' ' + errResMsg[k])}this.errMsg = errMsgs.join(',')} }, save() {if (this.editedIndex > -1) { // 編輯axios.put(booksApi + '/' + this.editedItem.id, this.editedItem).then(response => {Object.assign(this.books[this.editedIndex], response.data)this.close()}).catch(error => {this.setErrMsg(error.response)console.log(error)})} else { // 新增axios.post(booksApi, this.editedItem).then(response => {this.books.push(response.data)this.close()}).catch(error => {this.setErrMsg(error.response)console.log(error)})} } 復(fù)制代碼此時(shí),圖書(shū)新增、保存搞定。
修改 deleteItem 方法
deleteItem (item) {const index = this.books.indexOf(item)confirm('確認(rèn)刪除?') && axios.delete(booksApi + '/' + this.books[0].id).then(response => {this.books.splice(index, 1)}).catch(error => {this.setErrMsg(error.response)console.log(error)}) } 復(fù)制代碼此時(shí),刪除方法也搞定了。
至此,整合完畢,基于 Vue + Flask 的前后端分離的一個(gè) CRUD Demo 就完成了。
看完本文,你可以按著步驟自己實(shí)現(xiàn)下。剛接觸的伙伴在看的過(guò)程中在某些地方可能有疑惑,我也在我能想到的地方提供了一些資料,你可以試著看下。如果沒(méi)能提供全,你需要自己百度/谷歌下解決。不過(guò),我還是建議不要妄求每個(gè)點(diǎn)都了解的特別清楚,先明白關(guān)鍵點(diǎn),試著實(shí)現(xiàn)一下,回頭去看相關(guān)資料的時(shí)候,也更有感觸一些。
完整代碼可到 GitHub 查看
https://github.com/kevinbai-cn/spa-demo
4 參考
- 《Full-stack single page application with Vue.js and Flask》
- https://bit.ly/2C9kSiG
- 《Developing a Single Page App with Flask and Vue.js》
- https://bit.ly/2ElaXrB
- 《Vuetify Documents》
- https://bit.ly/2QupMzx
- 《Designing a RESTful API with Python and Flask》
- https://bit.ly/2vqq3Y1
- 《Designing a RESTful API using Flask-RESTful》
- https://bit.ly/2nGDNtL
本文首發(fā)于公眾號(hào)「小小后端」。
與50位技術(shù)專家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的使用 Vue + Flask 搭建单页应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CodeForces 780 E Und
- 下一篇: methods vue 使用过滤器_Vu