Эта статья — пошаговое руководство по настройке базового CRUD-приложения с помощью Vue и Flask. Начнём с создания нового приложения Vue, используя Vue CLI, а затем перейдём к выполнению основных операций CRUD с помощью RESTful API на бэкенде под управлением Python и Flask.
Результат:
Основные зависимости:
- 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 и как он соотносится с другими UI-библиотеками и фронтенд-фреймворками вроде Angular и React.
И научитесь:
- выстраивать Vue-проект, используя Vue CLI;
- создавать и рендерить компоненты Vue в браузере;
- создавать одностраничные приложения с компонентами Vue;
- подключать Vue-приложение к бэкенду Flask;
- разрабатывать RESTful API с помощью Flask;
- стилизовать компоненты Vue с помощью Bootstrap;
- использовать Vue Router для создания маршрутов и рендеринга компонентов.
Что такое Flask?
Flask — это простой, но мощный микро-фреймворк для Python, идеально подходящий для создания RESTful API. Как Sinatra (Ruby) и Express (Node), он минималистичен и гибок, поэтому вы можете начинать с простых проектов и при необходимости создавать более сложные приложения.
Если вы первый раз работаете с Flask, вам стоит изучить следующие ресурсы:
Что такое Vue?
Vue — это JavaScript-фреймворк с открытым исходным кодом. Используется для создания пользовательских интерфейсов. Он содержит некоторые из лучших концепций React и Angular, но по сравнению с ними он более доступен, поэтому новички могут быстро приступать к работе. Также он не уступает этим фреймворкам в мощности и предоставляет все необходимые функции для создания современных фронтенд-приложений.
Чтобы узнать больше о Vue, а также о плюсах и минусах его использования по сравнению с Angular и React, можете посмотреть следующие статьи:
Если вы первый раз работаете с Vue, вам следует прочитать введение из официального руководства Vue.
Настройка Flask
Начнём с создания новой директории проекта:
$ mkdir flask-vue-crud
$ cd flask-vue-crud
В директории flask-vue-crud
создайте новый каталог с именем 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 нужен для отправки cross-origin-запросов (запросы, исходящие из другого протокола, IP-адреса, имени домена или порта), поэтому необходимо включить общий доступ к ресурсам (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, вам будет полезно почитать официальное руководство по нему.
В каталоге flask-vue-crud
выполните следующую команду для инициализации нового проекта Vue под именем client
с конфигом webpack:
$ vue init webpack client
webpack — это пакетный модуль и инструмент для сборки, используемый для создания, минимизации и объединения файлов JavaScript и других клиентских ресурсов.
Шаги создания нового проекта Vue:
- Vue-сборка: Runtime + Compiler.
- Установить vue-router? — Да.
- Использовать ESLint для линтинга кода? — Да.
- Выберите пресет ESLint — Airbnb.
- Настроить юнит-тесты? — Нет.
- Настроить тесты e2e с Nightwatch? — Нет.
- Запустить установку npm после создания проекта? — Да, использовать NPM.
Должно получиться следующее:
? Имя проекта client
? Описание проекта Vue.js
? Автор Michael Herman michael@mherman.org
? Автономная сборка Vue
? Установить vue-router? — Да
? Использовать ESLint для линтинга кода? — Да
? Выберите пресет ESLint — Airbnb
? Настроить юнит-тесты? — Нет
? Настроить тесты e2e с Nightwatch? — Нет
? Запустить установку npm после создания проекта? (рекомендуется) — 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>
<!-- встроенные файлы будут введены автоматически -->
</body>
</html>
Также обратите внимание на элемент <div>
с идентификатором app
. Это контейнер, который Vue будет использовать для присоединения сгенерированных HTML и CSS при создании пользовательского интерфейса.
Файлы и каталоги внутри папки src
:
├── App.vue
├── assets
│ └──── logo.png
├── components
│ └──── HelloWorld.vue
├── main.js
├── router
└─── index.js
main.js
— точка входа в приложение, которая загружает и инициализирует Vue вместе с корневым компонентом.
App.vue
— корневой компонент, из которого будут рендериться все остальные компоненты (отправная точка).
assets
— место хранения статических ассетов вроде изображений и шрифтов.
components
— место хранения UI-компонентов.
router
— место определения URL-адресов и сопоставление их с компонентами.
Взгляните на файл client/src/components/HelloWorld.vue
. Это компонент Single File, который разбит на три разных подраздела:
- template: для компонентного HTML;
- script: здесь компонентная логика реализована через JavaScript;
- style: для стилей CSS.
Запустите dev-сервер:
$ cd client
$ npm run dev
Перейдите по адресу http://localhost:8080 в браузере. Вы должны увидеть следующее:
Добавьте новый компонент с именем Ping.vue
в папку client/src/components
:
<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>
<div id="app">
<router-view/>
</div>
</template>
Теперь в браузере должно отобразиться «Hello!
».
Чтобы соединить клиентское Vue-приложение с бэкендом на Flask, можно использовать библиотеку axios для отправки AJAX-запросов.
Начнём с установки:
$ npm install axios@0.18.0 --save
Обновим раздел script
компонента в Ping.vue
следующим образом:
<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-выключение следующей строки
console.error(error);
});
},
},
created() {
this.getMessage();
},
};
</script>
Запустите приложение Flask в новом окне. В браузере по адресу http://localhost:8080 должно отобразиться «pong!
». По сути, после ответа от серверной части устанавливаем в msg
значение, полученное из data
вернувшегося объекта.
Настройка Bootstrap
Добавим 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-отключение no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
});
Обновим раздел style
в client/src/App.vue
:
<style>
#app {
margin-top: 60px
}
</style>
Убедитесь, что Bootstrap подключён корректно, используя Button и Container в компоненте Ping
:
<template>
<div class="container">
<button type="button" class="btn btn-primary">{{ msg }}</button>
</div>
</template>
Запустим dev-сервер:
$ npm run dev
Должно отобразиться:
Добавим компонент Books
в новый файл Books.vue
:
<template>
<div class="container">
<p>books</p>
</div>
</template>
Обновим router:
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',
});
Протестируем:
- http://localhost:8080.
- http://localhost:8080/#/ping.
Для избавления от хеша в 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, для единственного ресурса — книги. API должен следовать принципам разработки RESTful, используя основные HTTP-команды: GET, POST, PUT и DELETE.
GET-маршрут
Сервер
Добавим список книг в 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
})
Теперь запустим приложение и проверим маршрут по адресу http://localhost:5000/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 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-отключение следующей строки
console.error(error);
});
},
},
created() {
this.getBooks();
},
};
</script>
После инициализации компонента вызываем метод getBooks()
через хук жизненного цикла (lifecycle hook) created, который выбирает книги из только что настроенного маршрута на бэкенде.
Больше информации про Lifecycle Hook находится здесь.
В темплейте просматривается список книг с помощью директивы v-for, которая создаёт новую строку таблицы на каждой итерации. Значение индекса используется в качестве ключа (key). Затем используется директива v-if для отображения Yes или No — читал пользователь книгу или нет.
Bootstrap Vue
В следующем разделе используем компонент Modal
для добавления новых книг. Для этого добавим библиотеку Bootstrap Vue, которая предоставляет набор Vue-компонентов, стилизованных с помощью HTML и CSS на основе Bootstrap.
Выбор Bootstrap Vue обоснован тем, что компонент Modal Bootstrap использует jQuery. Следует избегать совместного использования jQuery и Vue в одном проекте, поскольку последний использует Virtual Dom для обновления DOM-структуры. Другими словами, если вы используете jQuery для манипуляций с DOM, Vue об этом не узнает. По крайней мере, если вам необходимо использовать jQuery, не используйте его вместе с Vue на одних и тех же элементах DOM.
Установим:
$ npm install bootstrap-vue@2.0.0-rc.11 --save
Подключим библиотеку Bootstrap Vue в файле client/src/main.js
:
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-отключение 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)
Обновим импорты:
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.
Клиент
Внесём следующий modal
для добавления новой книги. Начнём с 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-отключение следующей строки
console.error(error);
});
},
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
// eslint-отключение следующей строки
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, // сокращённое свойство
};
this.addBook(payload);
this.initForm();
},
onReset(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
this.initForm();
},
},
created() {
this.getBooks();
},
};
</script>
Рассмотрим, что происходит в этом фрагменте кода.
addBookForm()
привязывается к входным данным формы черезv-model
. Это называется двусторонней привязкой. Узнать об этом подробнее вы можете здесь.onSubmit()
запускается, когда пользователь успешно отправляет форму. При отправке предотвращается обычное поведение браузера (evt.preventDefault()
), закрывается modal (this.$Refs.addBookModal.hide()
), запускается методaddBook()
и очищается форма (initForm()
).addBook()
отправляет POST-запрос в/books
для добавления новой книги.- Остальные изменения вы можете посмотреть самостоятельно в документации Vue по мере необходимости.
Теперь обновим кнопку «Add Book» в темплейте, чтобы при её нажатии отображался modal:
<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>{{ 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>
<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-отключение следующей строки
console.error(error);
});
},
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
})
.catch((error) => {
// eslint-отключение следующей строки
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, // сокращённое свойство
};
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>
Затем импортируем его в разделе script
компонента Books
и зарегистрируем:
<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>
Обновите браузер. Должно быть отображено следующее:
Теперь добавим фактический компонент b-alert в шаблон:
<template>
<div>
<b-alert variant="success" show>{{ message }}</b-alert>
<br>
</div>
</template>
<script>
export default {
props: ['message'],
};
</script>
Обратите внимание на параметр props в разделе script
. Можно передавать сообщение из родительского компонента (Books
) следующим образом:
<alert message="hi"></alert>
Больше информации о props находится здесь.
Чтобы сделать Alert динамическим и передать пользовательское сообщение, можно использовать выражение привязки (binding expression) в Books.vue
:
<alert :message="message"></alert>
Добавьте message в параметр data
в Books.vue
:
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
message: '',
};
},
Обновим сообщение в addBook
:
addBook(payload) {
const path = 'http://localhost:5000/books';
axios.post(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book added!';
})
.catch((error) => {
// eslint-отключение следующей строки
console.log(error);
this.getBooks();
});
},
Добавим v-if
, чтобы alert отображался, только если 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-отключение следующей строки
console.log(error);
this.getBooks();
});
},
Теперь можно проверить работу.
PUT-маршрут
Сервер
Для обновлений необходимо использовать уникальный идентификатор, поскольку нельзя полагаться на то, что заголовок книги будет уникальным. Можно использовать uuid
из стандартной библиотеки Python.
Обновим BOOKS
в server/app.py
:
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
для учёта уникального идентификатора при добавлении новой книги:
@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/', 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
Клиент
Будем идти пошагово:
- Добавление modal и формы.
- Обработка нажатия кнопки Update.
- Подключение AJAX-запроса.
- Оповещение пользователя (Alert).
- Обработка нажатия кнопки Cancel.
1. Добавление modal и формы
Сначала добавим новый modal к темплейту, сразу после первого modal:
<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>
Добавим стейт формы в часть data
раздела script
:
editForm: {
id: '',
title: '',
author: '',
read: [],
},
2. Обработка нажатия кнопки Update
Обновим кнопку «Update» в таблице:
<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-отключение следующей строки
console.error(error);
this.getBooks();
});
},
4. Оповещение пользователя (Alert)
Обновим 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-отключение следующей строки
console.error(error);
this.getBooks();
});
},
5. Обработка нажатия кнопки Cancel
Добавим метод:
onResetUpdate(evt) {
evt.preventDefault();
this.$refs.editBookModal.hide();
this.initForm();
this.getBooks();
},
Обновим initForm()
:
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.editForm.id = '';
this.editForm.title = '';
this.editForm.author = '';
this.editForm.read = [];
},
Обязательно протестируйте приложение. Убедитесь, что modal отображается при нажатии кнопки и что введённые значения заполнены правильно.
DELETE-маршрут
Сервер
Обновим обработчик маршрута:
@app.route('/books/', 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)
Клиент
Обновим кнопку «Delete» следующим образом:
<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-отключение следующей строки
console.error(error);
this.getBooks();
});
},
onDeleteBook(book) {
this.removeBook(book.id);
},
Когда пользователь нажимает кнопку удаления, вызывается метод onDeleteBook()
, который запускает другой метод removeBook()
. Этот метод отправляет DELETE-запрос на сервер. Когда приходит ответ, отображается Alert и запускается getBooks()
.
Заключение
В этой статье были рассмотрены основы настройки CRUD-приложения с помощью Vue и Flask. Исходный код из тега v1 вы можете найти в репозитории flask-vue-crud.
Перевод статьи «Developing a Single Page App with Flask and Vue.js»