Flask-Vue-Crud - 02. 添加组件及CRUD操作

添加 Books 组件

新建组件src/components/Books.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="container">
<p>books</p>
</div>
</template>

<script>
/* eslint-disable */
import axios from 'axios';

export default {
name: 'Books',
data() {
return {
};
},
methods: {
},
created() {
},
};
</script>

更新路由如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Vue from 'vue';
import Router from 'vue-router';
import HelloWorld from '@/components/HelloWorld';
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,
},
{
path: '/books',
name: 'Books',
component: Books,
},
],
mode: 'history',
});

修改 Books 组件的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>书籍</h1>
<hr><br><br>
<button type="button" class="btn btn-success btn-sm">添加</button>
<br><br>
<table class="table table-hover">
<thead>
<tr>
<th>书名</th>
<th>作者</th>
<th>读过?</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>失控</td>
<td>凯文凯利</td>
<td>是</td>
<td>
<button type="button" class="btn btn-warning btn-sm">更新</button>
<button type="button" class="btn btn-danger btn-sm">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>

接下来,就可以集中处理CRUD的功能逻辑了。

GET 操作

服务端

首先,在后台构造书籍数据,修改app.py文件,添加书籍列表,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOKS = [
{
'title': '失控',
'author': '凯文凯利',
'read': True
},
{
'title': '黑客与画家',
'author': '保罗格雷厄姆',
'read': False
},
{
'title': '未来简史',
'author': '尤瓦尔赫拉利',
'read': True
}
]

添加路由处理,代码如下:

1
2
3
4
5
6
@app.route('/books', methods=['GET'])
def all_books():
return jsonify({
'status': 'success',
'books': BOOKS
})

运行 Flask 应用,手动测试路由:http://localhost:5000/books

客户端

更新组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
...
<tbody>
<tr v-for="(book, index) in books" :key="index">
<td>{{ book.title }}</td>
<td>{{ book.author}}</td>
<td>
<span v-if="book.read">是</span>
<span v-else>否</span>
</td>
<td>
<button type="button" class="btn btn-warning btn-sm">更新</button>
<button type="button" class="btn btn-danger btn-sm">删除</button>
</td>
</tr>
</tbody>
...
<script>
/* eslint-disable */
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>

POST 操作

在点击添加按钮时,我们希望弹出模态对话框,然后键入书籍信息,bootstrap 的 modal 组件使用 jQuery,而 Vue 使用虚Dom,所以,尽量避免 jQuery 和 Vue 同时使用。为此,可使用 Bootstrap Vue 这个库来进行样式处理。

Bootstrap Vue

执行如下命令,安装 bootstrap-vue :
upload successful

在 app 的入口文件client/src/main.js中导入bootstrap-vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, jsonify, request

@app.route('/books', methods=['GET', 'POST'])
def all_books():
res_obj = {'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')
}
)
res_obj['message'] = '添加成功!'
else:
res_obj['books'] = BOOKS

return jsonify(res_obj)

if __name__ == '__main__':
app.run()

在终端中测试如下:
upload successful

客户端

增加添加书籍的模态呈现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<b-modal ref="addBookModal"
id="book-modal"
title="添加一本新书"
hide-footer>
<b-form @submit="onSubmit" @reset="onReset">
<b-form-group id="form-title-group"
label="书名:"
label-for="form-title-input">
<b-form-input id="form-title-input"
type="text"
v-model="addBookForm.title"
required
placeholder="请输入书名">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-group"
label="作者:"
label-for="form-author-input">
<b-form-input id="form-author-input"
type="text"
v-model="addBookForm.author"
required
placeholder="请输入作者">
</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">读过?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">提交</b-button>
<b-button type="reset" variant="danger">取消</b-button>
</b-form>
</b-modal>

以上代码插入在最后一个封闭的 div 之前。

接着,更新 script 部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<script>
/* eslint-disable */
import axios from 'axios';

export default {
name: 'Books',
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) => {
console.error(error);
}
);
},
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload).then(
(res) => {
this.getBooks();
}
).catch(
(error) => {
console.error(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>

最后,在模板中更新添加按钮,使点击按钮时出现模态对话框:

1
<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>添加</button>

Alert 组件

接着,添加 Alert 组件用来向用户展示添加新书后的消息。

测试版

首先,添加新的组件client/src/components

1
2
3
<template>
<p>It works!</p>
</template>

接着,在 Books 组件中导入并注册该组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
import axios from 'axios';
import Alert from './Alert';

...

export default {
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
};
},
components: {
alert: Alert,
},

...

};
</script>

现在就可以在模板中使用该组件了:

1
2
3
4
5
6
7
8
9
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>书籍</h1>
<hr><br><br>
<alert></alert>
<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>添加</button>
...

刷新浏览器,能看到该组件生效了:
upload successful

真实版

接下来,在Alert组件中添加真正的b-alert组件:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<b-alert variant="success" show>{{ message }}</b-alert>
<br>
</div>
</template>

<script>
export default {
props: ['message'],
};
</script>

试着在父组件Books中,向 message属性传值:

1
<alert message="hi"></alert>

浏览器结果如下所示:
upload successful

为了真正实现动态消息,在Books组件中,进行数据绑定:

1
<alert :message="message"></alert>

然后,在data选项中,添加message

1
2
3
4
5
6
7
8
9
10
11
12
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
message: '',
showMsg: false,
};
},

接着,在addBook中,更新message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload).then(
(res) => {
this.getBooks();
this.message = '添加成功!';
this.showMsg = true;
}
).catch(
(error) => {
console.error(error);
this.getBooks();
}
);
},

PUT 操作

服务端

考虑到更新书的内容,书必须有一个唯一的标识符。可以使用 python 的 uuid库。

更新 app.py 中的 BOOKS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import uuid

BOOKS = [
{
'id': uuid.uuid4().hex,
'title': '失控',
'author': '凯文凯利',
'read': True
},
{
'id': uuid.uuid4().hex,
'title': '黑客与画家',
'author': '保罗格雷厄姆',
'read': False
},
{
'id': uuid.uuid4().hex,
'title': '未来简史',
'author': '尤瓦尔赫拉利',
'read': True
}
]

重构all_books方法,为每一个新添加的书添加id字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route('/books', methods=['GET', 'POST'])
def all_books():
res_obj = {'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')
}
)
res_obj['message'] = '添加成功!'
else:
res_obj['books'] = BOOKS

return jsonify(res_obj)

添加新的路由处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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'] = '更新成功!'
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

客户端

步骤

  1. 添加 modal 和 form
  2. 处理更新按钮的点击事件
  3. 进行 AJAX 请求
  4. 提醒用户
  5. 处理取消按钮点击事件

添加 modal 和 form

增加新的更新书籍的模态对话框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<b-modal ref="editBookModal"
id="book-update-modal"
title="更新"
hide-footer>
<b-form @submit="onSubmitUpdate" @reset="onResetUpdate">
<b-form-group id="form-title-edit-group"
label="书名:"
label-for="form-title-edit-input">
<b-form-input id="form-title-edit-input"
type="text"
v-model="editForm.title"
required
placeholder="请输入书名">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-edit-group"
label="作者:"
label-for="form-author-edit-input">
<b-form-input id="form-author-edit-input"
type="text"
v-model="editForm.author"
required
placeholder="请输入作者">
</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">读过?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">更新</b-button>
<b-button type="reset" variant="danger">取消</b-button>
</b-form>
</b-modal>

data选项中添加 editForm

1
2
3
4
5
6
editForm: {
id: '',
title: '',
author: '',
read: [],
},

处理更新按钮的点击事件

在模板中增加更新按钮的点击事件:

1
2
3
4
5
6
<button
type="button"
class="btn btn-warning btn-sm"
v-b-modal.book-update-modal
@click="editBook(book)">更新
</button>

增加新的方法,用来更新editForm

1
2
3
editBook(book) {
this.editForm = book;
},

接着,增加提交按钮的处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
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);
},

进行 AJAX 请求

1
2
3
4
5
6
7
8
9
10
11
12
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,增加如下代码:

1
2
this.message = '已更新!';
this.showMsg = true;

处理取消按钮点击事件

添加处理方法:

1
2
3
4
5
6
onResetUpdate(evt) {
evt.preventDefault();
this.$refs.editBookModal.hide();
this.initForm();
this.getBooks();
},

更新initForm

1
2
3
4
5
6
7
8
9
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.editForm.id = '';
this.editForm.title = '';
this.editForm.author = '';
this.editForm.read = [];
},

DELETE 操作

服务端

更新路由处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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'] = '更新成功!'
if request.method == 'DELETE':
remove_book(book_id)
response_object['message'] = '已移除!'
return jsonify(response_object)

客户端

更新模板中的删除按钮:

1
2
3
4
5
<button
type="button"
class="btn btn-danger btn-sm"
@click="onDeleteBook(book)">删除
</button>

添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
removeBook(bookID) {
const path = `http://localhost:5000/books/${bookID}`;
axios.delete(path)
.then(() => {
this.getBooks();
this.message = '已移除!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
onDeleteBook(book) {
this.removeBook(book.id);
},