Создание одностраничного приложения с Vue.js и Flask. Часть 1
В этой статье мы настроим базовое CRUD-приложении, используя Vue и Flask. Сначала создадим новое Vue-приложение с помощью Vue CLI, а потом приступим к выполнению CRUD-операций посредством RESTful API на бэкенде под управлением Flask и Python.
Ожидаемый результат:
Что нам потребуется: • Vue v2.5.2; • Vue CLI v2.9.3; • Python v3.6.5; • npm v6.1.0; • Node v10.3.0; • Flask v1.0.2.
Настраиваем 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-запросов, следовательно, надо включить общий доступ к ресурсам (CORS).
Запускаем наше приложение:
(env)$ python app.py
Чтобы выполнить проверку, вводим в строку браузера http://localhost:5000/ping. Ожидаемый результат — «Pong!».
Чтобы завершить работу сервера, нажимаем Ctrl + C. После этого можно перейти к фронтенду и настройкам Vue.
Настраиваем Vue
Чтобы создать индивидуальный темплейт проекта, мы будем задействовать мощный интерфейс Vue CLI. Устанавливаем его глобально:
$ npm install -g [email protected]
Теперь в каталоге flask-vue-crud выполняем команду, которая нужна для инициализации нового проекта Vue с конфигурацией webpack и под именем client :
$ vue init webpack client
Что касается webpack, то это инструмент для сборки и пакетный модуль, применяемый для создания, минимизации и объединения JS-файлов и прочих клиентских ресурсов.
При создании нового Vue-проекта вам потребуется ответить на несколько вопросов: 1. Установить vue-router? — Да. 2. Использовать для линтинга кода ESLint? — Да. 3. Выберите пресет ESLint — Airbnb. 4. Настраивать юнит-тесты? — Нет. 5. Настраивать тесты e2e с Nightwatch? — Нет. 6. Запускать установку npm после создания проекта? — Да, применять NPM.
Vue-сборка в нашем случае — Runtime + Compiler.
Vue CLI v3.7.0 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Linter ? Use history mode for router? Yes ? Pick a linter / formatter config: Airbnb ? Pick additional lint features: Lint on save ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json ? Save this as a preset for future projects? (y/N) No
Если посмотреть на сгенерированную структуру проекта, то может показаться, что она очень большая. Однако по факту вам придётся иметь дело лишь с папками и файлами в каталоге /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>
Кроме того, у нас есть элемент
Давайте посмотрим, на каталоги и файлы внутри 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, разбитым на 3 различных подраздела: • template: для компонентного HTML; • script: тут компонентная логика реализована посредством JS; • style: для CSS-стилей.
Запускаем dev-сервер:
$ 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:
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. Установим её:
$ npm install [email protected] –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!».
Настраиваем Bootstrap
Чтобы быстро настраивать стиль нашего приложения, добавляем Bootstrap:
$ npm install [email protected] --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 подключён корректно, для чего используем Container и Button в компоненте Ping:
<template> <div class="container"> <button type="button" class="btn btn-primary">{{ msg }}</button> </div> </template>
Запускаем dev-сервер:
$ npm run dev
Ожидаемый результат:
Теперь добавляем в новый файл Books.vue компонент Books:
<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', });
Как обычно, тестируем: 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>
Ожидаемый результат:
Итак, тепреь можно создавать функциональность CRUD-приложения.
Что предстоит создавать?
Наша задача — разработка бэкенда RESTful API, работающего на Python и Flask. API должен следовать принципам RESTful-разработки, применяя основные HTTP-команды: POST, PUT, GET, 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>
После того, как компонент инициализирован, вызываем метод
Для просмотра книг в темплейте используется директива v-for, создающая на каждой итерации новую строку таблицы. При этом значение индекса применяется в виде ключа (key). Потом применяется директива v-if, обеспечивающая отображение Yes либо No (читал ли пользователь книгу).
Bootstrap Vue
Далее используем компонент Modal, необходимый для добавления новых книг. Пригодится библиотека Bootstrap Vue, предоставляющая нам набор Vue-компонентов, стилизованных посредством CSS и HTML на основе Bootstrap.
Почему мы выбрали Bootstrap Vue? Потому что Modal Bootstrap использует jQuery, а нам желательно избегать применения в одном проекте jQuery и Vue, ведь последний задействует для обновления DOM-структуры Virtual Dom. В результате, если вы используете для манипуляций с DOM jQuery, Vue об этом не узнает.
Выполняем установку:
$ npm install [email protected] –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/>', });
Это ещё далеко не всё, продолжение смотрите в следующей части нашей статьи.
Источник — «Developing a Single Page App with Flask and Vue.js».