- 原文地址:Developing a Single Page App with Flask and Vue.js
- 原文做者:Michael Herman
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Mcskiller
這篇文章會一步一步的教會你如何用 VUE 和 Flask 建立一個基礎的 CRUD 應用。咱們將從使用 Vue CLI 建立一個新的 Vue 應用開始,接着咱們會使用 Python 和 Flask 提供的後端接口 RESTful API 執行基礎的 CRUD 操做。css
最終效果:html
主要依賴:前端
在本教程結束的時候,你可以...vue
Flask 是一個用 Python 編寫的簡單,可是及其強大的輕量級 Web 框架,很是適合用來構建 RESTful API。就像 Sinatra(Ruby)和 Express(Node)同樣,它也十分簡便,因此你能夠從小處開始,根據需求構建一個十分複雜的應用。node
第一次使用 Flask?看看這下面兩個教程吧:python
Vue 是一個用於構建用戶界面的開源 JavaScript 框架。它綜合了一些 React 和 Angular 的優勢。也就是說,與 React 和 Angular 相比,它更加友好,因此初學者額可以很快的學習並掌握。它也一樣強大,所以它可以提供全部你須要用來建立一個前端應用所須要的功能。react
有關 Vue 的更多信息,以及使用它與 Angular 和 React 的利弊,請查看如下文章:jquery
第一次使用 Vue?不妨花點時間閱讀官方指南中的 介紹。android
首先建立一個新項目文件夾:webpack
$ mkdir flask-vue-crud
$ cd flask-vue-crud
複製代碼
在 「flask-vue-crud」 文件夾中,建立一個新文件夾並取名爲 「server」。而後,在 「server」 文件夾中建立並運行一個虛擬環境:
$ python3.6 -m venv env
$ source env/bin/activate
複製代碼
以上命令因環境而異。
安裝 Flask 和 Flask-CORS 擴展:
(env)$ pip install Flask==1.0.2 Flask-Cors==3.0.4
複製代碼
在新建立的文件夾中添加一個 app.py 文件
from flask import Flask, jsonify
from flask_cors import CORS
# configuration
DEBUG = True
# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)
# enable CORS
CORS(app)
# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
return jsonify('pong!')
if __name__ == '__main__':
app.run()
複製代碼
爲何咱們須要 Flask-CORS?爲了進行跨域請求 — e.g.,來自不一樣協議,IP 地址,域名或端口的請求 — 你須要容許 跨域資源共享(CORS)。而這正是 Flask-CORS 能爲咱們提供的。
值得注意的是上述安裝容許跨域請求在所有路由不管任何域,協議或者端口均可用。在生產環境中,你應該只容許跨域請求成功在前端應用託管的域上。參考 Flask-CORS 文檔 得到更多信息。
運行應用:
(env)$ python app.py
複製代碼
開始測試,將你的瀏覽器指向到 http://localhost:5000/ping。你將會看到:
"pong!"
複製代碼
返回終端,按下 Ctrl+C 來終止服務端而後退回到項目根目錄。接下來,讓咱們把注意力轉到前端進行 Vue 的安裝。
咱們將會使用強力的 Vue CLI 來生成一個自定義項目模板。
全局安裝:
$ npm install -g vue-cli@2.9.3
複製代碼
第一次使用 npm?瀏覽一下 什麼是 npm? 官方指南吧
而後,在 「flask-vue-crud」 中,運行如下命令初始化一個叫作 client
的新 Vue 項目幷包含 webpack 配置:
$ vue init webpack client
複製代碼
webpack 是一個模塊打包構建工具,用於構建,壓縮以及打包 JavaScript 文件和其餘客戶端資源。
它會請求你對這個項目進行一些配置。按下回車鍵去選擇前三個爲默認設置,而後使用如下的設置去完成後續的配置:
Runtime + Compiler
Yes
Yes
Airbnb
No
No
Yes, use NPM
你會看到一些配置請求好比:
? Project name client
? Project description A Vue.js project
? Author Michael Herman michael@mherman.org
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Airbnb
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm
複製代碼
快速瀏覽一下生成的項目架構。看起來好像特別多,可是咱們只會用到那些在 「src」 中的文件和 index.html 文件。
index.html 文件是咱們 Vue 應用的起點。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>client</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
複製代碼
注意那個 id
是 app
的 <div>
元素。那是一個佔位符,Vue 將會用來鏈接生成的 HTML 和 CSS 構建 UI。
注意那些在 「src」 文件夾中的文件夾:
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── main.js
└── router
└── index.js
複製代碼
分解:
名字 | 做用 |
---|---|
main.js | app 接入點,將會和根組件一塊兒加載並初始化 Vue |
App.vue | 根組件 —— 起點,全部其餘組件都將今後處開始渲染 |
「assets」 | 儲存圖像和字體等靜態資源 |
「components」 | 儲存 UI 組件 |
「router」 | 定義 URL 地址並映射到組件 |
查看 client/src/components/HelloWorld.vue 文件。這是一個 單文件組件,它分爲三個不一樣的部分:
運行開發服務端:
$ cd client
$ npm run dev
複製代碼
在你的瀏覽器中導航到 http://localhost:8080。你將會看到:
添加一個新組件在 「client/src/components」 文件夾中,並取名爲 Ping.vue:
<template>
<div>
<p>{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'Ping',
data() {
return {
msg: 'Hello!',
};
},
};
</script>
複製代碼
更新 client/src/router/index.js 使 ‘/’ 映射到 Ping
組件:
import Vue from 'vue';
import Router from 'vue-router';
import Ping from '@/components/Ping';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'Ping',
component: Ping,
},
],
});
複製代碼
最後,在 client/src/App.vue 中,從 template 裏刪除掉圖片:
<template>
<div id="app">
<router-view/>
</div>
</template>
複製代碼
你如今應該能在瀏覽器中看見一個 Hello!
。
爲了更好地使客戶端 Vue 應用和後端 Flask 應用鏈接,咱們可使用 axios 庫來發送 AJAX 請求。
那麼咱們開始安裝它:
$ npm install axios@0.18.0 --save
複製代碼
而後在 Ping.vue 中更新組件的 script
部分,就像這樣:
<script>
import axios from 'axios';
export default {
name: 'Ping',
data() {
return {
msg: '',
};
},
methods: {
getMessage() {
const path = 'http://localhost:5000/ping';
axios.get(path)
.then((res) => {
this.msg = res.data;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
},
created() {
this.getMessage();
},
};
</script>
複製代碼
在新的終端窗口啓動 Flask 應用。在瀏覽器中打開 http://localhost:8080 你會看到 pong!
。基本上,當咱們從後端獲得回覆的時候,咱們會將 msg
設置爲響應對象的 data
的值。
接下來,讓咱們引入一個熱門 CSS 框架 Bootstrap 到應用中以方便咱們快速添加一些樣式。
安裝:
$ npm install bootstrap@4.1.1 --save
複製代碼
忽略
jquery
和popper.js
的警告。不要把它們添加到你的項目中。稍後會告訴你爲何。
插入 Bootstrap 樣式到 client/src/main.js 中:
import 'bootstrap/dist/css/bootstrap.css';
import Vue from 'vue';
import App from './App';
import router from './router';
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
});
複製代碼
更新 client/src/App.vue 中的 style
:
<style>
#app {
margin-top: 60px
}
</style>
複製代碼
經過使用 Button 和 Container 確保 Bootstrap 在 Ping
組件中正確鏈接:
<template>
<div class="container">
<button type="button" class="btn btn-primary">{{ msg }}</button>
</div>
</template>
複製代碼
運行開發服務端:
$ npm run dev
複製代碼
你應該會看到:
而後,添加一個叫作 Books
的新組件到新文件 Books.vue 中:
<template>
<div class="container">
<p>books</p>
</div>
</template>
複製代碼
更新路由:
import Vue from 'vue';
import Router from 'vue-router';
import Ping from '@/components/Ping';
import Books from '@/components/Books';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'Ping',
component: Ping,
},
],
mode: 'hash',
});
複製代碼
測試:
想要擺脫掉 URL 中的哈希值嗎?更改
mode
到history
以使用瀏覽器的 history API 來導航:
export default new Router({
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'Ping',
component: Ping,
},
],
mode: 'history',
});
複製代碼
查看文檔以得到更多路由 信息。
最後,讓咱們添加一個高效的 Bootstrap 風格表格到 Books
組件中:
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Books</h1>
<hr><br><br>
<button type="button" class="btn btn-success btn-sm">Add Book</button>
<br><br>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Read?</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>foo</td>
<td>bar</td>
<td>foobar</td>
<td>
<button type="button" class="btn btn-warning btn-sm">Update</button>
<button type="button" class="btn btn-danger btn-sm">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
複製代碼
你如今應該會看到:
如今咱們能夠開始構建咱們的 CRUD 應用的功能。
咱們的目標是設計一個後端 RESTful API,由 Python 和 Flask 驅動,對應一個單一資源 — books。這個 API 應當遵照 RESTful 設計原則,使用基本的 HTTP 動詞:GET、POST、PUT 和 DELETE。
咱們還會使用 Vue 搭建一個前端應用來使用這個後端 API:
本教程只設計簡單步驟。處理錯誤是讀者(就是你!)的額外練習。經過你的理解解決先後端出現的問題吧。
添加一個書單到 server/app.py 中:
BOOKS = [
{
'title': 'On the Road',
'author': 'Jack Kerouac',
'read': True
},
{
'title': 'Harry Potter and the Philosopher\'s Stone', 'author': 'J. K. Rowling', 'read': False }, { 'title': 'Green Eggs and Ham', 'author': 'Dr. Seuss', 'read': True } ] 複製代碼
添加路由接口:
@app.route('/books', methods=['GET'])
def all_books():
return jsonify({
'status': 'success',
'books': BOOKS
})
複製代碼
運行 Flask 應用,若是它並無運行,嘗試在 http://localhost:5000/books 手動測試路由。
想更有挑戰性?寫一個自動化測試吧。查看 這個 資源能夠了解更多關於測試 Flask 應用的信息。
更新組件:
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Books</h1>
<hr><br><br>
<button type="button" class="btn btn-success btn-sm">Add Book</button>
<br><br>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Read?</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(book, index) in books" :key="index">
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>
<span v-if="book.read">Yes</span>
<span v-else>No</span>
</td>
<td>
<button type="button" class="btn btn-warning btn-sm">Update</button>
<button type="button" class="btn btn-danger btn-sm">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
books: [],
};
},
methods: {
getBooks() {
const path = 'http://localhost:5000/books';
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
},
created() {
this.getBooks();
},
};
</script>
複製代碼
當組件初始化完成後,經過 created 生命週期鉤子調用 getBooks()
方法,它從咱們剛剛設置的後端接口獲取書籍。
查閱 實例生命週期鉤子 瞭解更多有關組件生命週期和可用方法的信息。
在模板中,咱們經過 v-for 指令遍歷書籍列表,每次遍歷建立一個新表格行。索引值用做 key。最後,使用 v-if 的 Yes
或 No
,來表現用戶已讀或未讀這本書。
在下一節中,咱們將會使用一個模態去添加新書。爲此,咱們在本節會加入 Bootstrap Vue 庫到項目中,它提供了一組基於 Bootstrap 的 HTML 和 CSS 設計的 Vue 組件。
爲何選擇 Bootstrap Vue?Bootstrap 的 模態 組件使用 jQuery,但你應該避免把它和 Vue 在同一項目中一塊兒使用,由於 Vue 使用 虛擬 DOM 來更新 DOM。換句話來講,若是你用 jQuery 來操做 DOM,Vue 不會有任何反應。至少,若是你必定要使用 jQuery,不要在同一個 DOM 元素上同時使用 jQuery 和 Vue。
安裝:
$ npm install bootstrap-vue@2.0.0-rc.11 --save
複製代碼
在 client/src/main.js 中啓用 Bootstrap Vue 庫:
import 'bootstrap/dist/css/bootstrap.css';
import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import App from './App';
import router from './router';
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
});
複製代碼
更新現有路由以處理添加新書的 POST 請求:
@app.route('/books', methods=['GET', 'POST'])
def all_books():
response_object = {'status': 'success'}
if request.method == 'POST':
post_data = request.get_json()
BOOKS.append({
'title': post_data.get('title'),
'author': post_data.get('author'),
'read': post_data.get('read')
})
response_object['message'] = 'Book added!'
else:
response_object['books'] = BOOKS
return jsonify(response_object)
複製代碼
更新 imports:
from flask import Flask, jsonify, request
複製代碼
運行 Flask 服務端後,你能夠在新的終端裏測試 POST 路由:
$ curl -X POST http://localhost:5000/books -d \
'{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
-H 'Content-Type: application/json'
複製代碼
你應該會看到:
{
"message": "Book added!",
"status": "success"
}
複製代碼
你應該會在 http://localhost:5000/books 的末尾看到新書。
若是書名已經存在了呢?若是一個書名對應了幾個做者呢?經過處理這些小問題能夠加深你的理解,另外,如何處理
書名
,做者
,以及閱覽狀態
都缺失的無效負載狀況。
在客戶端上,讓咱們添加那個模態以添加一本新書,從 HTML 開始:
<b-modal ref="addBookModal"
id="book-modal"
title="Add a new book"
hide-footer>
<b-form @submit="onSubmit" @reset="onReset" class="w-100">
<b-form-group id="form-title-group"
label="Title:"
label-for="form-title-input">
<b-form-input id="form-title-input"
type="text"
v-model="addBookForm.title"
required
placeholder="Enter title">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-group"
label="Author:"
label-for="form-author-input">
<b-form-input id="form-author-input"
type="text"
v-model="addBookForm.author"
required
placeholder="Enter author">
</b-form-input>
</b-form-group>
<b-form-group id="form-read-group">
<b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
<b-form-checkbox value="true">Read?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
<b-button type="reset" variant="danger">Reset</b-button>
</b-form>
</b-modal>
複製代碼
在 div
標籤中添加這段代碼。而後簡單閱覽一下。v-model
是一個用於 表單輸入綁定 的指令。你立刻就會看到。
hide-footer
具體幹了什麼?在 Bootstrap Vue 的 文檔 中瞭解更多
更新 script
部分:
<script>
import axios from 'axios';
export default {
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
};
},
methods: {
getBooks() {
const path = 'http://localhost:5000/books';
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
this.getBooks();
});
},
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
},
onSubmit(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
let read = false;
if (this.addBookForm.read[0]) read = true;
const payload = {
title: this.addBookForm.title,
author: this.addBookForm.author,
read, // property shorthand
};
this.addBook(payload);
this.initForm();
},
onReset(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
this.initForm();
},
},
created() {
this.getBooks();
},
};
</script>
複製代碼
實現了什麼?
addBookForm
的值被 表單輸入綁定 到,沒錯,v-model
。當數據更新時,另外一個也會跟着更新。這被稱之爲雙向綁定。花點時間從 這裏 瞭解一下吧。想一想這個帶來的結果。你認爲這會使狀態管理更簡單仍是更復雜?React 和 Angular 又會如何作到這點?在我看來,雙向數據綁定(可變性)使得 Vue 和 React 相比更加友好,可是從長遠看擴展性不足。
onSubmit
會在用戶提交表單成功時被觸發。在提交時,咱們會阻止瀏覽器的正常行爲(evt.preventDefault()
),關閉模態框(this.$refs.addBookModal.hide()
),觸發 addBook
方法,而後清空表單(initForm()
)。
addBook
發送一個 POST 請求到 /books
去添加一本新書。
根據本身的須要查看其餘更改,並根據須要參考 Vue 的 文檔。
你能想到客戶端或者服務端還有什麼潛在的問題嗎?思考這些問題去試着增強用戶體驗吧。
最後,更新 template 中的 「Add Book」 按鈕,這樣一來咱們點擊按鈕就會顯示出模態框:
<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
複製代碼
那麼組件應該是這樣子的:
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Books</h1>
<hr><br><br>
<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
<br><br>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Read?</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(book, index) in books" :key="index">
<td></td>
<td></td>
<td>
<span v-if="book.read">Yes</span>
<span v-else>No</span>
</td>
<td>
<button type="button" class="btn btn-warning btn-sm">Update</button>
<button type="button" class="btn btn-danger btn-sm">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<b-modal ref="addBookModal"
id="book-modal"
title="Add a new book"
hide-footer>
<b-form @submit="onSubmit" @reset="onReset" class="w-100">
<b-form-group id="form-title-group"
label="Title:"
label-for="form-title-input">
<b-form-input id="form-title-input"
type="text"
v-model="addBookForm.title"
required
placeholder="Enter title">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-group"
label="Author:"
label-for="form-author-input">
<b-form-input id="form-author-input"
type="text"
v-model="addBookForm.author"
required
placeholder="Enter author">
</b-form-input>
</b-form-group>
<b-form-group id="form-read-group">
<b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
<b-form-checkbox value="true">Read?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
<b-button type="reset" variant="danger">Reset</b-button>
</b-form>
</b-modal>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
};
},
methods: {
getBooks() {
const path = 'http://localhost:5000/books';
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
this.getBooks();
});
},
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
},
onSubmit(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
let read = false;
if (this.addBookForm.read[0]) read = true;
const payload = {
title: this.addBookForm.title,
author: this.addBookForm.author,
read, // property shorthand
};
this.addBook(payload);
this.initForm();
},
onReset(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
this.initForm();
},
},
created() {
this.getBooks();
},
};
</script>
複製代碼
趕忙測試一下!試着添加一本書:
接下來,讓咱們添加一個 Alert 組件,當添加一本新書後,它會顯示一個信息給當前用戶。咱們將爲此建立一個新組件,由於你之後可能會在不少組件中常常用到這個功能。
添加一個新文件 Alert.vue 到 「client/src/components」 中:
<template>
<p>It works!</p>
</template>
複製代碼
而後,在 Books
組件的 script
中引入它並註冊這個組件:
<script>
import axios from 'axios';
import Alert from './Alert';
...
export default {
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
};
},
components: {
alert: Alert,
},
...
};
</script>
複製代碼
如今,咱們能夠在 template
中引用這個新組件:
<template>
<b-container>
<b-row>
<b-col col sm="10">
<h1>Books</h1>
<hr><br><br>
<alert></alert>
<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
...
</b-col>
</b-row>
</b-container>
</template>
複製代碼
刷新瀏覽器,你會看到:
從 Vue 官方文檔的 組件化應用構建 中得到更多有關組件化應用構建的信息。
接下來,讓咱們加入 b-alert 組件到 template 中:
<template>
<div>
<b-alert variant="success" show>{{ message }}</b-alert>
<br>
</div>
</template>
<script>
export default {
props: ['message'],
};
</script>
複製代碼
記住 script
中的 props 選項。咱們能夠從父組件(Books
)傳遞信息,就像這樣:
<alert message="hi"></alert>
複製代碼
試試這個:
從 文檔 中獲取更多 props 相關信息。
爲了方便咱們動態傳遞自定義消息,咱們須要在 Books.vue 中使用 bind 綁定數據。
<alert :message="message"></alert>
複製代碼
將 message
添加到 Books.vue 中的 data
中:
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
message: '',
};
},
複製代碼
接下來,在 addBook
中,更新 message 內容。
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book added!';
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
this.getBooks();
});
},
複製代碼
最後,添加一個 v-if
,以保證只有 showMessage
值爲 true 的時候警告纔會顯示。
<alert :message=message v-if="showMessage"></alert>
複製代碼
添加 showMessage
到 data
中:
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
message: '',
showMessage: false,
};
},
複製代碼
再次更新 addBook
,設定 showMessage
的值爲 true
:
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book added!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
this.getBooks();
});
},
複製代碼
趕快測試一下吧!
挑戰:
- 想一想什麼狀況下
showMessage
應該被設定爲false
。更新你的代碼。- 試着用 Alert 組件去顯示錯誤信息。
- 修改 Alert 爲 可取消 的樣式。
對於更新,咱們須要使用惟一標識符,由於咱們不能依靠標題做爲惟一。咱們可使用 Python 基本庫 提供的 uuid
做爲惟一。
在 server/app.py 中更新 BOOKS
:
BOOKS = [
{
'id': uuid.uuid4().hex,
'title': 'On the Road',
'author': 'Jack Kerouac',
'read': True
},
{
'id': uuid.uuid4().hex,
'title': 'Harry Potter and the Philosopher\'s Stone', 'author': 'J. K. Rowling', 'read': False }, { 'id': uuid.uuid4().hex, 'title': 'Green Eggs and Ham', 'author': 'Dr. Seuss', 'read': True } ] 複製代碼
不要忘了引入:
import uuid
複製代碼
咱們須要重構 all_books
來保證每一本添加的書都有它的惟一 ID:
@app.route('/books', methods=['GET', 'POST'])
def all_books():
response_object = {'status': 'success'}
if request.method == 'POST':
post_data = request.get_json()
BOOKS.append({
'id': uuid.uuid4().hex,
'title': post_data.get('title'),
'author': post_data.get('author'),
'read': post_data.get('read')
})
response_object['message'] = 'Book added!'
else:
response_object['books'] = BOOKS
return jsonify(response_object)
複製代碼
添加一個新的路由:
@app.route('/books/<book_id>', methods=['PUT'])
def single_book(book_id):
response_object = {'status': 'success'}
if request.method == 'PUT':
post_data = request.get_json()
remove_book(book_id)
BOOKS.append({
'id': uuid.uuid4().hex,
'title': post_data.get('title'),
'author': post_data.get('author'),
'read': post_data.get('read')
})
response_object['message'] = 'Book updated!'
return jsonify(response_object)
複製代碼
添加輔助方法:
def remove_book(book_id):
for book in BOOKS:
if book['id'] == book_id:
BOOKS.remove(book)
return True
return False
複製代碼
想一想看若是你沒有
id
標識符你會怎麼辦?若是有效載荷不正確怎麼辦?重構輔助方法中的 for 循環,讓他更加 pythonic。
步驟:
首先,加入一個新的模態到 template 中,就在第一個模態下面:
<b-modal ref="editBookModal"
id="book-update-modal"
title="Update"
hide-footer>
<b-form @submit="onSubmitUpdate" @reset="onResetUpdate" class="w-100">
<b-form-group id="form-title-edit-group"
label="Title:"
label-for="form-title-edit-input">
<b-form-input id="form-title-edit-input"
type="text"
v-model="editForm.title"
required
placeholder="Enter title">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-edit-group"
label="Author:"
label-for="form-author-edit-input">
<b-form-input id="form-author-edit-input"
type="text"
v-model="editForm.author"
required
placeholder="Enter author">
</b-form-input>
</b-form-group>
<b-form-group id="form-read-edit-group">
<b-form-checkbox-group v-model="editForm.read" id="form-checks">
<b-form-checkbox value="true">Read?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">Update</b-button>
<b-button type="reset" variant="danger">Cancel</b-button>
</b-form>
</b-modal>
複製代碼
添加表單狀態到 script
中的 data
部分:
editForm: {
id: '',
title: '',
author: '',
read: [],
},
複製代碼
挑戰:不使用新的模態,使用一個模態框處理 POST 和 PUT 請求。
更新表格中的「更新」按鈕:
<button
type="button"
class="btn btn-warning btn-sm"
v-b-modal.book-update-modal
@click="editBook(book)">
Update
</button>
複製代碼
添加一個新方法去更新 editForm
中的值:
editBook(book) {
this.editForm = book;
},
複製代碼
而後,添加一個方法去處理表單提交:
onSubmitUpdate(evt) {
evt.preventDefault();
this.$refs.editBookModal.hide();
let read = false;
if (this.editForm.read[0]) read = true;
const payload = {
title: this.editForm.title,
author: this.editForm.author,
read,
};
this.updateBook(payload, this.editForm.id);
},
複製代碼
updateBook(payload, bookID) {
const path = `http://localhost:5000/books/${bookID}`;
axios.put(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
複製代碼
更新 updateBook
:
updateBook(payload, bookID) {
const path = `http://localhost:5000/books/${bookID}`;
axios.put(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book updated!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
複製代碼
添加方法:
onResetUpdate(evt) {
evt.preventDefault();
this.$refs.editBookModal.hide();
this.initForm();
this.getBooks(); // why?
},
複製代碼
更新 initForm
:
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.editForm.id = '';
this.editForm.title = '';
this.editForm.author = '';
this.editForm.read = [];
},
複製代碼
在繼續下一步以前先檢查一下代碼。檢查結束後,測試一下應用。確保按鈕按下後顯示模態框,並正確顯示輸入值。
更新路由操做:
@app.route('/books/<book_id>', methods=['PUT', 'DELETE'])
def single_book(book_id):
response_object = {'status': 'success'}
if request.method == 'PUT':
post_data = request.get_json()
remove_book(book_id)
BOOKS.append({
'id': uuid.uuid4().hex,
'title': post_data.get('title'),
'author': post_data.get('author'),
'read': post_data.get('read')
})
response_object['message'] = 'Book updated!'
if request.method == 'DELETE':
remove_book(book_id)
response_object['message'] = 'Book removed!'
return jsonify(response_object)
複製代碼
更新「刪除」按鈕:
<button
type="button"
class="btn btn-danger btn-sm"
@click="onDeleteBook(book)">
Delete
</button>
複製代碼
添加方法來處理按鈕點擊而後刪除書籍:
removeBook(bookID) {
const path = `http://localhost:5000/books/${bookID}`;
axios.delete(path)
.then(() => {
this.getBooks();
this.message = 'Book removed!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
onDeleteBook(book) {
this.removeBook(book.id);
},
複製代碼
如今,當用戶點擊刪除按鈕時,將會觸發 onDeleteBook
方法。同時,removeBook
方法會被調用。這個方法會發送刪除請求到後端。當返回響應後,通知消息會顯示出來而後 getBooks
會被調用。
挑戰:
- 在刪除按鈕點擊時加入一個確認提示。
- 當沒有書的時候,顯示一個「沒有書籍,請添加」消息。
這篇文章介紹了使用 Vue 和 Flask 設置 CRUD 應用程序的基礎知識。
從頭回顧這篇文章以及其中的挑戰來加深你的理解。
你能夠在 flask-vue-crud 倉庫 中的 v1 標籤裏找到源碼。感謝你的閱讀。
想知道更多? 看看這篇文章的續做 Accepting Payments with Stripe, Vue.js, and Flask。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。