- 原文地址: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 操作。
最終效果:
主要依賴:
- Vue v2.5.2
- Vue CLI v2.9.3
- Node v10.3.0
- npm v6.1.0
- Flask v1.0.2
- Python v3.6.5
目錄
- 目的
- 什麼是 Flask?
- 什麼是 Vue?
- 安裝 Flask
- 安裝 Vue
- 安裝 Bootstrap
- 我們的目的是什麼?
- 獲取路由
- Bootstrap Vue
- POST 路由
- Alert 元件
- PUT 路由
- DELETE 路由
- 總結
目的
在本教程結束的時候,你能夠...
- 解釋什麼是 Flask
- 解釋什麼是 Vue 並且它和其他 UI 庫以及 Angular、React 等前端框架相比又如何
- 使用 Vue CLI 搭建一個 Vue 專案
- 在瀏覽器中建立並渲染 Vue 元件
- 使用 Vue 元件建立一個單頁面應用(SPA)
- 將一個 Vue 應用與後端的 Flask 連線
- 使用 Flask 開發一個 RESTful API
- 在 Vue 元件中使用 Bootstrap 樣式
- 使用 Vue Router 去建立路由和渲染元件
什麼是 Flask?
Flask 是一個用 Python 編寫的簡單,但是及其強大的輕量級 Web 框架,非常適合用來構建 RESTful API。就像 Sinatra(Ruby)和 Express(Node)一樣,它也十分簡便,所以你可以從小處開始,根據需求構建一個十分複雜的應用。
第一次使用 Flask?看看這下面兩個教程吧:
什麼是 Vue?
Vue 是一個用於構建使用者介面的開源 JavaScript 框架。它綜合了一些 React 和 Angular 的優點。也就是說,與 React 和 Angular 相比,它更加友好,所以初學者額能夠很快的學習並掌握。它也同樣強大,因此它能夠提供所有你需要用來建立一個前端應用所需要的功能。
有關 Vue 的更多資訊,以及使用它與 Angular 和 React 的利弊,請檢視以下文章:
第一次使用 Vue?不妨花點時間閱讀官方指南中的 介紹。
安裝 Flask
首先建立一個新專案資料夾:
$ 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
我們將會使用強力的 Vue CLI 來生成一個自定義專案模板。
全域性安裝:
$ npm install -g vue-cli@2.9.3
複製程式碼
第一次使用 npm?瀏覽一下 什麼是 npm? 官方指南吧
然後,在 “flask-vue-crud” 中,執行以下命令初始化一個叫做 client
的新 Vue 專案幷包含 webpack 配置:
$ vue init webpack client
複製程式碼
webpack 是一個模組打包構建工具,用於構建,壓縮以及打包 JavaScript 檔案和其他客戶端資源。
它會請求你對這個專案進行一些配置。按下Enter鍵去選擇前三個為預設設定,然後使用以下的設定去完成後續的配置:
- Vue build:
Runtime + Compiler
- 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:
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 檔案。這是一個 單檔案元件,它分為三個不同的部分:
- template:特定元件的 HTML
- script:通過 JavaScript 實現元件邏輯
- style:CSS 樣式
執行開發服務端:
$ 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
的值。
安裝 Bootstrap
接下來,讓我們引入一個熱門 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 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 路由
服務端
更新現有路由以處理新增新書的 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 元件,當新增一本新書後,它會顯示一個資訊給當前使用者。我們將為此建立一個新元件,因為你以後可能會在很多元件中經常用到這個功能。
新增一個新檔案 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 為 可取消 的樣式。
PUT 路由
服務端
對於更新,我們需要使用唯一識別符號,因為我們不能依靠標題作為唯一。我們可以使用 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。
客戶端
步驟:
- 新增模態和表單
- 處理更新按鈕點選事件
- 傳送 AJAX 請求
- 通知使用者
- 處理取消按鈕點選事件
(1)新增模態和表單
首先,加入一個新的模態到 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 請求。
(2)處理更新按鈕點選事件
更新表格中的“更新”按鈕:
<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);
},
複製程式碼
(3)傳送 AJAX 請求
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();
});
},
複製程式碼
(4)通知使用者
更新 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();
});
},
複製程式碼
(5)處理取消按鈕點選事件
新增方法:
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 = [];
},
複製程式碼
在繼續下一步之前先檢查一下程式碼。檢查結束後,測試一下應用。確保按鈕按下後顯示模態框,並正確顯示輸入值。
DELETE 路由
服務端
更新路由操作:
@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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。