Написать пост

Пишем одностраничное приложение с Flask и Vue.js

Аватар Klara Oswald

Это пошаговое руководство по настройке CRUD-приложения с помощью Vue и Flask. Вы узнаете как создать новое приложения с Vue CLI.

Обложка поста Пишем одностраничное приложение с Flask и Vue.js

Эта статья — пошаговое руководство по настройке базового 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:

  1. Vue-сборка:  Runtime + Compiler.
  2. Установить vue-router? —  Да.
  3. Использовать ESLint для линтинга кода? —  Да.
  4. Выберите пресет ESLint —  Airbnb.
  5. Настроить юнит-тесты? —  Нет.
  6. Настроить тесты e2e с Nightwatch? —  Нет.
  7. Запустить установку 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 в браузере. Вы должны увидеть следующее:

Пишем одностраничное приложение с Flask и Vue.js 2

Добавьте новый компонент с именем 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
		

Должно отобразиться:

Пишем одностраничное приложение с Flask и Vue.js 3

Добавим компонент 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',
});
		

Протестируем:

  1. http://localhost:8080.
  2. 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>
		

Отображаться должно следующее:

Пишем одностраничное приложение с Flask и Vue.js 4

Теперь можно приступить к созданию функциональности 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 — читал пользователь книгу или нет.

Пишем одностраничное приложение с Flask и Vue.js 6

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>
		

Рассмотрим, что происходит в этом фрагменте кода.

  1. addBookForm() привязывается к входным данным формы через v-model. Это называется двусторонней привязкой. Узнать об этом подробнее вы можете здесь.
  2. onSubmit() запускается, когда пользователь успешно отправляет форму. При отправке предотвращается обычное поведение браузера (evt.preventDefault()), закрывается modal (this.$Refs.addBookModal.hide()), запускается метод addBook() и очищается форма (initForm()).
  3. addBook() отправляет POST-запрос в /books для добавления новой книги.
  4. Остальные изменения вы можете посмотреть самостоятельно в документации 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>
		

Обновите браузер. Должно быть отображено следующее:

Пишем одностраничное приложение с Flask и Vue.js 8

Теперь добавим фактический компонент 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>
		
Пишем одностраничное приложение с Flask и Vue.js 9

Больше информации о 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
		

Клиент

Будем идти пошагово:

  1. Добавление modal и формы.
  2. Обработка нажатия кнопки Update.
  3. Подключение AJAX-запроса.
  4. Оповещение пользователя (Alert).
  5. Обработка нажатия кнопки 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.

Следите за новыми постами
Следите за новыми постами по любимым темам
69К открытий69К показов