这篇文章会一步一步的教会你如何用 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 文件和其他客户端资源。
它会请求你对这个项目进行一些配置。按下回车键去选择前三个为默认设置,然后使用以下的设置去完成后续的配置:
- 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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。