这是一篇中文翻译文章。
原文作者:Michael Herman
上次更新 April 22nd, 2023
以下是如何使用Vue和Flask设置基本CRUD应用程序的分步演练。我们将从搭建一个新的Vue应用程序开始,然后通过由Python和Flask支持的后端RESTful API执行基本的CRUD操作。
最终应用程序:
主要依赖项:
- Vue v3.2.47
- Node v20.0.0
- npm v9.6.4
- Flask v2.2.3
- Python v3.11.3
目标
在本教程结束时,您将能够:
- 解释什么是Flask
- 解释什么是Vue,以及它与其他UI库和前端框架(如React和Angular)的比较
- 用Vite搭建Vue项目
- 在浏览器中创建和渲染Vue组件
- 使用Vue组件创建单页应用程序(SPA)
- 将Vue应用程序连接到Flask后端
- 使用Flask开发REST风格的API
- 使用Bootstrap对Vue组件进行样式化
- 使用Vue Router创建路由和渲染组件
Flask和Vue
让我们快速查看每个框架。
什么是Flask?
Flask是一个简单但功能强大的Python微型Web框架,非常适合构建RESTful API。像Sinatra(Ruby)和Express(Node)一样,它是最小的和灵活的,所以你可以从小开始,根据需要构建一个更复杂的应用程序。
第一次和Flask在一起查看以下两个资源:
Vue是什么?
Vue是一个用于构建用户界面的开源JavaScript框架。它采用了React和Angular的一些最佳实践。也就是说,与React和Angular相比,它更加平易近人,因此初学者可以快速上手。它也同样强大,因此它提供了创建现代前端应用程序所需的所有功能。
有关Vue的更多信息,沿着使用它与React和Angular相比的优点和缺点,请查看资源:
第一次使用Vue?花点时间阅读官方Vue指南中的介绍 。
Flask 设置
开始,创建一个新的项目目录:
$ mkdir flask-vue-crud
$ cd flask-vue-crud
在“flask-vue-crud”中,创建一个名为“server”的新目录。然后,在“server”目录中创建并激活一个虚拟环境:
$ python3.11 -m venv env
$ source env/bin/activate
(env)$
请随意将virtualenv和Pip换成 Poetry 或 Pipenv。有关更多信息,请查看现代Python环境。
安装Flask和它的扩展Flask-CORS :
(env)$ pip install Flask==2.2.3 Flask-Cors==3.0.10
将app.py文件添加到新创建的“server”目录中:
from flask import Flask, jsonify
from flask_cors import CORS
# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)
# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})
# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
return jsonify('pong!')
if __name__ == '__main__':
app.run()
为什么需要Flask-CORS?为了进行跨域请求,如果请求来自不同的协议、IP地址、域名或端口,则需要启用跨域资源共享 (CORS)。Flask-CORS为我们处理这个。
值得注意的是,上述设置允许来自任何域、协议或端口的所有路由上的跨域请求。在生产环境中,您应该只允许来自前端应用程序所在域的跨域请求。请参阅Flask-CORS文档以了解更多信息。
运行应用程序:
(env)$ flask run --port=5001 --debug
要进行测试,请将浏览器指向http://localhost:5001/ping 您应该看到:
"pong!"
回到终端,按Ctrl+C杀死服务器,然后导航回项目根目录。有了这些,让我们把注意力转向前端并设置Vue。
Vue设置
我们将通过Vite Vite使用功能强大的 create-vue工具 来生成一个定制的项目样板。
在“flask-vue-crud”中,运行以下命令来初始化一个新的Vue项目:
$ npm create vue@3.6.1
第一次使用NPM?查看关于npm的官方指南 。
这将要求您回答有关该项目的几个问题:
Vue.js - The Progressive JavaScript Framework
✔ Project name: › client
✔ Add TypeScript? › No
✔ Add JSX Support? › No
✔ Add Vue Router for Single Page Application development? › Yes
✔ Add Pinia for state management? › No
✔ Add Vitest for Unit Testing? › No
✔ Add ESLint for code quality? › No
快速查看生成的项目结构。这看起来可能很多,但我们只处理“src”文件夹中的文件和文件夹,沿着index.html文件。
index.html文件是Vue应用程序的起点:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
注意 id 为 app 的<div> 元素。这是一个占位符,Vue将使用它来附加生成的HTML和CSS,以生成UI。
请注意“src”文件夹中的文件夹:
client/src
├── App.vue
├── assets
│ ├── base.css
│ ├── logo.svg
│ └── main.css
├── components
│ ├── HelloWorld.vue
│ ├── TheWelcome.vue
│ ├── WelcomeItem.vue
│ └── icons
│ ├── IconCommunity.vue
│ ├── IconDocumentation.vue
│ ├── IconEcosystem.vue
│ ├── IconSupport.vue
│ └── IconTooling.vue
├── main.js
├── router
│ └── index.js
└── views
├── AboutView.vue
└── HomeView.vue
细分:
| Name 名称 | Purpose 目的 |
|---|---|
| main.js | 入口点,它将沿着根组件一起加载和调用Vue |
| App.vue | 根组件,从其开始所有其他组件将被渲染 |
| "components" | 组件目录,存储UI组件的位置 |
| router/index.js | 路由文件,其中定义了URL并将其映射到组件 |
| "views" | 视图目录,其中存储了绑定到路由器的UI组件 |
| "assets" | 静态资源 存储静态资源(如图像和字体)的位置 |
查看client/src/components/HelloWorld.vue文件。这是一个单一文件组件 ,分为三个不同的部分:
- template:用于特定于组件的HTML
- script:组件逻辑通过JavaScript实现
- style: CSS样式
接下来,安装依赖项,然后启动开发服务器:
$ cd client
$ npm install
$ npm run dev
在您选择的浏览器中导航到http://localhost:5173 您应该会看到下列内容:
为了简化操作,删除“client/src/views”和“client/src/components/icons”文件夹以及client/src/components/TheWelcome.vue和client/src/components/WelcomeItem.vue组件。 然后,在“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'映射到 Ping 组件,如下所示:
import { createRouter, createWebHistory } from 'vue-router'
import Ping from '../components/Ping.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/ping',
name: 'ping',
component: Ping
},
]
})
export default router
最后,在client/src/App.vue中,删除导航沿着样式:
<template>
<RouterView />
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>
您现在应该在浏览器 http://localhost:5173/ping 中看到 Hello! 。
要连接客户端Vue应用程序和后端Flask应用程序,我们可以使用 Axios库发送HTTP请求。
首先安装它:
$ npm install axios@1.3.6 --save
在Ping.vue中更新组件的 script 部分,如下所示:
<script>
import axios from 'axios';
export default {
name: 'Ping',
data() {
return {
msg: '',
};
},
methods: {
getMessage() {
const path = 'http://localhost:5001/ping';
axios.get(path)
.then((res) => {
this.msg = res.data;
})
.catch((error) => {
console.error(error);
});
},
},
created() {
this.getMessage();
},
};
</script>
在新的终端窗口中启动Flask应用程序。在不同的终端窗口中运行Vue应用程序,您现在应该在浏览器中看到 pong! 。本质上,当从后端返回响应时,我们将 msg 设置为响应对象中的 data 值。
Bootstrap设置
接下来,让我们将Bootstrap(一个流行的CSS框架)添加到应用程序中,以便我们可以快速添加一些样式。
安装方式:
$ npm install bootstrap@5.2.3 --save
将Bootstrap样式导入到client/src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
import './assets/main.css'
const app = createApp(App)
app.use(router)
app.mount('#app')
确保Bootstrap正确连接,通过使用 Ping 组件中的按钮Button 和容器Container :
<template>
<div class="container">
<button type="button" class="btn btn-primary">{{ msg }}</button>
</div>
</template>
Run the dev server: 运行开发服务器:
$ npm run serve
您应该看到:
接下来,在名为components\Books.vue的新文件中添加一个名为 Books 的新组件:
<template>
<div class="container">
<p>books</p>
</div>
</template>
更新路由器router\index.js:
import { createRouter, createWebHistory } from 'vue-router'
import Books from '../components/Books.vue'
import Ping from '../components/Ping.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'ping',
component: Ping
},
]
})
export default router
更新client/src/App.vue中的 style 部分:
<style>
#app {
margin-top: 60px
}
</style>
删除client/src/assets/base.css和client/src/assets/main.css文件。 确保删除client/src/main.js中的 import './assets/main.css' 行:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
const app = createApp(App)
app.use(router)
app.mount('#app')
测试:
最后,让我们将一个快速的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>
<div class="btn-group" role="group">
<button type="button" class="btn btn-warning btn-sm">Update</button>
<button type="button" class="btn btn-danger btn-sm">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
您现在应该看到:
现在我们可以开始构建CRUD应用程序的功能了。
我们在建造什么?
我们的目标是设计一个后端RESTful API,由Python和Flask提供支持,用于单一资源-书籍。API本身应该遵循RESTful设计原则,使用基本的HTTP动词:GET、POST、PUT和POST。
我们还将使用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:5001/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>
<div class="btn-group" role="group">
<button type="button" class="btn btn-warning btn-sm">Update</button>
<button type="button" class="btn btn-danger btn-sm">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
books: [],
};
},
methods: {
getBooks() {
const path = 'http://localhost:5001/books';
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
console.error(error);
});
},
},
created() {
this.getBooks();
},
};
</script>
组件初始化后,通过created的生命周期钩子调用 getBooks() 方法,该方法从我们刚刚设置的后端端点获取书籍。
回顾一下生命周期钩子,了解更多关于组件生命周期和可用方法的信息。
在模板中,我们通过 v-for指令遍历图书列表,在每次迭代中创建一个新的表行。索引值用作键key。最后,使用v-if来呈现 Yes 或 No ,指示用户是否读过这本书。
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)
更新导入:
from flask import Flask, jsonify, request
在Flask服务器运行时,您可以在新的终端选项卡中测试POST路由:
curl -X POST http://localhost:5001/books -d \
'{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
-H 'Content-Type: application/json'
您应该看到:
{
"message": "Book added!",
"status": "success"
}
您还应该在来自 http://localhost:5001/books 端点的响应中看到这本新书。
如果标题已经存在呢?如果一本书有不止一个作者呢?通过处理这些案例来检查您的理解。另外,如果缺少
title、author和/或read,您将如何处理无效的有效负载?
客户端
在客户端,让我们添加一个用于向 Books 组件添加新书的模式,从HTML开始:
<!-- add new book modal -->
<div
ref="addBookModal"
class="modal fade"
:class="{ show: activeAddBookModal, 'd-block': activeAddBookModal }"
tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add a new book</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleAddBookModal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="addBookTitle" class="form-label">Title:</label>
<input
type="text"
class="form-control"
id="addBookTitle"
v-model="addBookForm.title"
placeholder="Enter title">
</div>
<div class="mb-3">
<label for="addBookAuthor" class="form-label">Author:</label>
<input
type="text"
class="form-control"
id="addBookAuthor"
v-model="addBookForm.author"
placeholder="Enter author">
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="addBookRead"
v-model="addBookForm.read">
<label class="form-check-label" for="addBookRead">Read?</label>
</div>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-primary btn-sm"
@click="handleAddSubmit">
Submit
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleAddReset">
Reset
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div v-if="activeAddBookModal" class="modal-backdrop fade show"></div>
在结束 div 标签之前添加此内容。快速查看代码。 v-model 是一个用于将输入值绑定bind 回状态的指令。您很快就会看到这一点。
更新 script 部分:
<script>
import axios from 'axios';
export default {
data() {
return {
activeAddBookModal: false,
addBookForm: {
title: '',
author: '',
read: [],
},
books: [],
};
},
methods: {
addBook(payload) {
const path = 'http://localhost:5001/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
console.log(error);
this.getBooks();
});
},
getBooks() {
const path = 'http://localhost:5001/books';
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
console.error(error);
});
},
handleAddReset() {
this.initForm();
},
handleAddSubmit() {
this.toggleAddBookModal();
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();
},
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
},
toggleAddBookModal() {
const body = document.querySelector('body');
this.activeAddBookModal = !this.activeAddBookModal;
if (this.activeAddBookModal) {
body.classList.add('modal-open');
} else {
body.classList.remove('modal-open');
}
},
},
created() {
this.getBooks();
},
};
</script>
这是怎么回事
addBookForm再次通过v-model绑定bound 到表单输入。换句话说,当一个被更新时,另一个也会被更新。这被称为双向绑定。花点时间在这里阅读它。想想这件事的后果。你认为这会使状态管理更容易还是更困难?React和Angular如何处理这个问题?在我看来,双向绑定(沿着可变性)使Vue比React更容易接近。- 当用户单击提交按钮时,将触发
handleAddSubmit。在这里,我们关闭模态(this.toggleAddBookModal();),激发addBook方法,并清除表单(initForm())。 addBook向/books发送POST请求以添加新书。
您可以自己查看其余的更改,必要时参考Vue文档(vuejs.org/guide/intro…)。
你能想到客户端或服务器上的任何潜在错误吗?自己处理这些以改善用户体验。
最后,更新模板中的“Add Book”按钮,以便在单击按钮时显示模态:
<button
type="button"
class="btn btn-success btn-sm"
@click="toggleAddBookModal">
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"
@click="toggleAddBookModal">
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>
<div class="btn-group" role="group">
<button type="button" class="btn btn-warning btn-sm">Update</button>
<button type="button" class="btn btn-danger btn-sm">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- add new book modal -->
<div
ref="addBookModal"
class="modal fade"
:class="{ show: activeAddBookModal, 'd-block': activeAddBookModal }"
tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add a new book</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleAddBookModal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="addBookTitle" class="form-label">Title:</label>
<input
type="text"
class="form-control"
id="addBookTitle"
v-model="addBookForm.title"
placeholder="Enter title">
</div>
<div class="mb-3">
<label for="addBookAuthor" class="form-label">Author:</label>
<input
type="text"
class="form-control"
id="addBookAuthor"
v-model="addBookForm.author"
placeholder="Enter author">
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="addBookRead"
v-model="addBookForm.read">
<label class="form-check-label" for="addBookRead">Read?</label>
</div>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-primary btn-sm"
@click="handleAddSubmit">
Submit
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleAddReset">
Reset
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div v-if="activeAddBookModal" class="modal-backdrop fade show"></div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
activeAddBookModal: false,
addBookForm: {
title: '',
author: '',
read: [],
},
books: [],
};
},
methods: {
addBook(payload) {
const path = 'http://localhost:5001/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
console.log(error);
this.getBooks();
});
},
getBooks() {
const path = 'http://localhost:5001/books';
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
console.error(error);
});
},
handleAddReset() {
this.initForm();
},
handleAddSubmit() {
this.toggleAddBookModal();
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();
},
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
},
toggleAddBookModal() {
const body = document.querySelector('body');
this.activeAddBookModal = !this.activeAddBookModal;
if (this.activeAddBookModal) {
body.classList.add('modal-open');
} else {
body.classList.remove('modal-open');
}
},
},
created() {
this.getBooks();
},
};
</script>
试试看!尝试添加一本书:
Alert Component 警报组件
接下来,让我们添加一个 Alert 组件,以便在添加新书后向最终用户显示一条消息。我们将为此创建一个新组件,因为您可能会在许多组件中使用该功能。
在“client/src/components”中添加一个名为Alert.vue的新文件:
<template>
<p>It works!</p>
</template>
然后,将其导入到 Books 组件的 script 部分并注册组件:
<script>
import axios from 'axios';
import Alert from './Alert.vue';
export default {
data() {
return {
activeAddBookModal: false,
addBookForm: {
title: '',
author: '',
read: [],
},
books: [],
};
},
components: {
alert: Alert,
},
...
};
</script>
现在,我们可以在 template 部分引用新组件:
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Books</h1>
<hr><br><br>
<alert></alert>
<button
type="button"
class="btn btn-success btn-sm"
@click="toggleAddBookModal">
Add Book
</button>
<br><br>
...
</div>
</div>
...
</div>
</template>
刷新浏览器。您现在应该看到:
查看官方Vue文档中的Using a Component,了解有关在其他组件中使用组件的更多信息。
接下来,让我们将实际的Bootstrap Alert 组件添加到client/src/components/Alert.vue:
<template>
<div>
<div class="alert alert-success" role="alert">{{ message }}</div>
<br/>
</div>
</template>
<script>
export default {
props: ['message'],
};
</script>
注意 script 部分中的props 选项。我们可以从父组件( Books )向下传递一条消息,如下所示:
<alert message="hi"></alert>
试试这个:
查看文档docs了解更多关于组件信息传递的知识。
要使其成为动态的,以便传递自定义消息,请在Books.vue中使用 绑定表达式:
<alert :message="message"></alert>
将 message 添加到 data 选项中,在Books.vue中也是如此:
data() {
return {
activeAddBookModal: false,
addBookForm: {
title: '',
author: '',
read: [],
},
books: [],
message: '',
};
},
然后,在 addBook 中更新消息:
addBook(payload) {
const path = 'http://localhost:5001/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book added!';
})
.catch((error) => {
console.log(error);
this.getBooks();
});
},
最后,添加一个 v-if ,这样只有在 showMessage 为true时才会显示警报:
<alert :message=message v-if="showMessage"></alert>
将 showMessage 添加到 data :
data() {
return {
activeAddBookModal: false,
addBookForm: {
title: '',
author: '',
read: [],
},
books: [],
message: '',
showMessage: false,
};
},
再次更新 addBook ,将 showMessage 设置为 true :
addBook(payload) {
const path = 'http://localhost:5001/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book added!';
this.showMessage = true;
})
.catch((error) => {
console.log(error);
this.getBooks();
});
},
试试看!
Challenges: 挑战:
- Think about where
showMessageshould be set tofalse. Update your code. 考虑一下showMessage应该设置为false的位置。更新您的代码。- 尝试使用警报组件显示错误。
- 将警报重构为可忽略dismissible.
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:
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)
添加helper:
def remove_book(book_id):
for book in BOOKS:
if book['id'] == book_id:
BOOKS.remove(book)
return True
return False
花点时间想想你会如何处理一个不存在的
id的情况。如果载荷不正确怎么办?重构帮助器中的for循环,使其更像Python。
客户端
步骤:
- 添加模态和表单
- 处理"更新"按钮的点击事件
- 连接HTTP请求
- 提醒用户
- 处理"取消"按钮的点击事件
(1)添加模态和表单
首先,在模板中添加一个新的modal,就在第一个modal的下面:
<!-- edit book modal -->
<div
ref="editBookModal"
class="modal fade"
:class="{ show: activeEditBookModal, 'd-block': activeEditBookModal }"
tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleEditBookModal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="editBookTitle" class="form-label">Title:</label>
<input
type="text"
class="form-control"
id="editBookTitle"
v-model="editBookForm.title"
placeholder="Enter title">
</div>
<div class="mb-3">
<label for="editBookAuthor" class="form-label">Author:</label>
<input
type="text"
class="form-control"
id="editBookAuthor"
v-model="editBookForm.author"
placeholder="Enter author">
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="editBookRead"
v-model="editBookForm.read">
<label class="form-check-label" for="editBookRead">Read?</label>
</div>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-primary btn-sm"
@click="handleEditSubmit">
Submit
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleEditCancel">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div v-if="activeEditBookModal" class="modal-backdrop fade show"></div>
将表单状态添加到 script 部分的 data 部分:
activeEditBookModal: false,
editBookForm: {
id: '',
title: '',
author: '',
read: [],
},
挑战:不要使用新的模式,尝试使用相同的模式来处理POST和PUT请求。
(2) 处理"更新"按钮的点击事件
更新表中的“更新”按钮:
<button
type="button"
class="btn btn-warning btn-sm"
@click="toggleEditBookModal(book)">
Update
</button>
添加一个新的方法toggleEditBookModal来显示/隐藏模式:
toggleEditBookModal(book) {
if (book) {
this.editBookForm = book;
}
const body = document.querySelector('body');
this.activeEditBookModal = !this.activeEditBookModal;
if (this.activeEditBookModal) {
body.classList.add('modal-open');
} else{
body.classList.remove('modal-open');
}
},
您是否注意到我们还更新了 editBookForm 的值?
然后,添加另一个新方法handleEditSubmit来处理表单提交:
handleEditSubmit() {
this.toggleEditBookModal(null);
let read = false;
if (this.editBookForm.read) read = true;
const payload = {
title: this.editBookForm.title,
author: this.editBookForm.author,
read,
};
this.updateBook(payload, this.editBookForm.id);
},
(3) 连接HTTP请求
updateBook(payload, bookID) {
const path = `http://localhost:5001/books/${bookID}`;
axios.put(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
console.error(error);
this.getBooks();
});
},
(4)提醒用户
更新 updateBook:
updateBook(payload, bookID) {
const path = `http://localhost:5001/books/${bookID}`;
axios.put(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book updated!';
this.showMessage = true;
})
.catch((error) => {
console.error(error);
this.getBooks();
});
},
(5) 处理取消按钮单击
添加方法handleEditCancel:
handleEditCancel() {
this.toggleEditBookModal(null);
this.initForm();
this.getBooks(); // why?
},
更新 initForm:
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.editBookForm.id = '';
this.editBookForm.title = '';
this.editBookForm.author = '';
this.editBookForm.read = [];
},
确保在继续之前检查代码。一旦完成,测试应用程序。确保单击按钮时显示模态,并正确填充输入值。
Challenges: 挑战:
- 您可以通过将进行HTTP调用的方法移动到utils或services文件来清理组件。
- 此外,尝试将包含类似逻辑的方法进行合并(如
handleAddSubmit和handleEditSubmit)。
删除路由
服务器
更新路由处理程序:
@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="handleDeleteBook(book)">
Delete
</button>
添加处理按钮的方法点击然后删除本书:
handleDeleteBook(book) {
this.removeBook(book.id);
},
removeBook(bookID) {
const path = `http://localhost:5001/books/${bookID}`;
axios.delete(path)
.then(() => {
this.getBooks();
this.message = 'Book removed!';
this.showMessage = true;
})
.catch((error) => {
console.error(error);
this.getBooks();
});
},
现在,当用户单击delete按钮时, handleDeleteBook 方法被触发,这反过来又触发了 removeBook 方法。此方法将HTTP请求发送到后端。当响应返回时,将显示警报消息并运行 getBooks 。
挑战:
- 删除按钮点击时,添加一个确认警报。
- 当表中没有书时,显示“没有书!请添加一个”的消息。
结论
本教程涵盖了使用Vue和Flask设置CRUD应用程序的基础知识。
通过回顾本教程开始时的目标并通过每个挑战来检查您的理解。
你可以在 [flask-vue-crud] 中找到源代码。谢谢你的阅读。
Michael Herman 迈克尔赫尔曼
Michael是一名软件工程师和教育工作者,在丹佛/博尔德地区生活和工作。他是真实的Python的联合创始人/作者。除了开发,他还喜欢建立金融模型,技术写作,内容营销和教学。