Создание одностраничного приложения с помощью Vue + Flask

Flask

Одностраничные приложения загружают только одну главную страницу, а затем загружают другие фрагменты страницы через AJAX без обновления. На первый взгляд есть только один файл HTML, так называемая отдельная страница. С точки зрения разработки, интерфейс и сервер разделены, интерфейс ориентирован на шаблоны рендеринга, а серверу нужно только предоставить API, поэтому нет необходимости устанавливать шаблоны самостоятельно. По сути, страница и общие файлы JS и CSS загружаются только один раз, что может снизить нагрузку на сервер и сэкономить определенную пропускную способность сети. Кроме того, поскольку нет необходимости каждый раз загружать страницы и совместно используемые статические файлы, скорость отклика также в определенной степени повышается, а взаимодействие с пользователем улучшается. Есть, конечно, и некоторые минусы, например SEO-оптимизация неудобна, но соответствующие решения есть. В целом преимущества использования одностраничных приложений намного перевешивают недостатки, поэтому все больше и больше людей используют одностраничные приложения.

Есть много способов создать одностраничное приложение, здесь мы выбираем реализацию Flask + Vue. В этой статье в качестве основной линии используется демонстрация CRUD Demo, а для ее описания добавлены необходимые технические моменты. Это может включать некоторые концепции, с которыми вы не знакомы или не знакомы, но это не имеет значения, я дам вам соответствующие справочные статьи, чтобы помочь вам понять. Конечно, Даниэль может их игнорировать :). Прочитав эту статью, я считаю, что вы также можете создать свое собственное одностраничное приложение.

1 передняя часть

Здесь мы будем использовать фреймворк Vue.Если вы еще не прикасались к нему, рекомендуется прочитать раздел «Основы» официальной документации. Вы также можете сначала посмотреть прямо вниз.Демо использует некоторые основные вещи, и вы должны быть в состоянии понять это, взглянув на него примерно. Даже если вы пока не понимаете этого, попрактиковавшись на примере, вы должны получить больше информации из документации.

Для более удобного создания проекта на основе Vue мы можем использовать скаффолдинг Vue Cli. При создании проекта с помощью строительных лесов это поможет нам выполнить некоторую настройку, сэкономив нам время на ручную настройку. Новые партнеры будут использовать его для создания проектов на ранней стадии, а о некоторых более глубоких вещах они узнают позже.

Установить леса

$ npm install -g @vue/cli

Здесь мы устанавливаем последнюю версию 3.

Существует множество библиотек компонентов пользовательского интерфейса, основанных на Vue, таких как iView, Element, Vuetify и т. д. Есть много людей, которые используют iView и Element в Китае, но относительно мало людей используют Vuetify, Я не знаю, потому ли это, что людям не нравится его стиль Material Design, или из-за нехватки его китайских документов. Но лично мне нравится стиль Vuetify, поэтому я буду использовать эту библиотеку компонентов для создания интерфейсных страниц.

Если вы еще не использовали эту библиотеку компонентов, вы также можете получить общее представление об использовании Vuetify, следуя пошаговым инструкциям в этой статье. Если вы чувствуете, что в этом процессе слишком много вопросов, вы можете посмотреть этот видеоурок на YouTube.

https://dwz.cn/lxMHF4bY

Не ищите везде одинаковые ресурсы, прочитав серию видео и добавив официальные документы, освоить общеупотребительные пункты в принципе не составит труда.

Тем не менее, все же рекомендуется сначала реализовать Демо по этой статье, а потом изучить его, я думаю, это будет эффективнее.

Создайте новый каталог spa-demo, а затем переключитесь в этот каталог, чтобы создать новый интерфейсный клиент проекта.

$ vue create client

При создании проекта вам будет предложено вручную выбрать некоторые конфигурации, вот мои настройки на тот момент

? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)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) N

После завершения установки мы переключаемся в каталог клиента и выполните команду

$ npm run serve

После выполнения вышеуказанной команды будет вывод, подобный этому

...

App running at:
- Local:   http://localhost:8080/
- Network: http://172.20.10.3:8080/

...

Доступ в браузере

http://localhost:8080/

Если вы видите страницу с текстом ниже

Welcome to Your Vue.js App

Установка проекта прошла успешно.

Установить Vuetify

$ vue add vuetify

Вам также будет предложено выбрать некоторые конфигурации, здесь я выбираю Default

? Choose a preset: Default (recommended)

После завершения установки перезагрузите сервер

$ npm run serve

После выполнения заходим в браузер

http://localhost:8080/

Вы увидите некоторые изменения в содержании страницы, там такая строчка текста

Welcome to Vuetify

Это показывает, что Vuetify успешно установлен.

Посмотрите на структуру каталогов в это время

spa-demo
└── client
    ├── README.md
    ├── babel.config.js
    ├── package-lock.json
    ├── package.json
    ├── node_module
    │   └── ...
    ├── public
    │   ├── favicon.ico
    │   └── index.html
    └── src
        ├── App.vue
        ├── assets
        │   ├── logo.png
        │   └── logo.svg
        ├── components
        │   └── HelloWorld.vue
        ├── main.js
        ├── plugins
        │   └── vuetify.js
        ├── router.js
        └── views
            ├── About.vue
            └── Home.vue

упрощатьspa-demo/client/src/App.vue, измените его на

<template>
  <v-app>
    <v-content>
      <router-view></router-view>
    </v-content>
  </v-app>
</template>

<script>
  export default {
    name: 'App',
    data () {
      return {
        //
      }
    }
  }
</script>

Исправлятьspa-demo/client/src/views/Home.vue, поместите таблицу данных на страницу

<template>
  <div class="home">

    <v-container class="my-5">

      <!-- 对话框 -->

      <!-- 表格 -->
      <v-data-table
        :headers="headers"
        :items="books"
        hide-actions
        class="elevation-1"
      >
        <template slot="items" slot-scope="props">
          <td>{{ props.item.name }}</td>
          <td>{{ props.item.category }}</td>
          <td class="layout px-0">
            <v-icon small class="ml-4" @click="editItem(props.item)">
              edit
            </v-icon>
            <v-icon small @click="deleteItem(props.item)">
              delete
            </v-icon>
          </td>
        </template>
        <template slot="no-data">
          <v-alert :value="true" color="info" outline>
            无数据
          </v-alert>
        </template>
      </v-data-table>
    </v-container>

  </div>
</template>

<script>
  export default {
    data: () => ({
      headers: [
        { text: '书名', value: 'name', sortable: false, align: 'left'},
        { text: '分类', value: 'category', sortable: false },
        { text: '操作', value: 'name', sortable: false }
      ],
      books: [],
    }),
    created () {
      this.books = [
        { name: '生死疲劳', category: '文学' },
        { name: '国家宝藏', category: '人文社科' },
        { name: '人类简史', category: '科技' },
      ]
    },
  }
</script>

Мы использовали заголовки данных и книги для управления заголовком и данными таблицы и заполнили книги некоторыми временными данными при ее создании.

На этой странице используется таблица данных. Вам не нужно запоминать соответствующий код. В документации Vuetify есть много примеров поиска таблицы данных. Прочитав несколько, вы узнаете, как ее использовать. Новичкам может быть трудно понять слот-область (scope slot), см. официальную документацию Vue для этого контента.

  • "Основы компонентов" в разделе "Основы"
  • «Регистрация компонента», «Реквизит», «Пользовательские события», «Слоты» в разделе «Погружение в компоненты».

Просто остановитесь и прочитайте, и вы поймете, это не сложно, я не буду повторяться здесь.

Точно так же и здесь можно проследить за тыквой и сначала нарисовать совок, можно временно проигнорировать какие-то непонятные места, а потом попробовать разобраться после практики.

Открытым

http://localhost:8080/

Страница, которую вы видите, похожа на эту

Просто список книг.

Теперь нам нужно сделать всплывающий диалог для добавления книг. мы в<!-- 对话框 -->Добавьте следующий код в местоположение

<v-toolbar flat class="white">
  <v-toolbar-title>图书列表</v-toolbar-title>
  <v-spacer></v-spacer>
  <v-dialog v-model="dialog" max-width="600px">
    <v-btn slot="activator" class="primary" dark>新增</v-btn>
    <v-card>
      <v-card-title>
        <span class="headline">{{ formTitle }}</span>
      </v-card-title>
      <v-card-text>
        <v-alert :value="Boolean(errMsg)" color="error" icon="warning" outline>
          {{ errMsg }}
        </v-alert>
        <v-container grid-list-md>
          <v-layout>
            <v-flex xs12 sm6 md4>
              <v-text-field label="书名" v-model="editedItem.name"></v-text-field>
            </v-flex>
            <v-flex xs12 sm6 md4>
              <v-text-field label="分类" v-model="editedItem.category"></v-text-field>
            </v-flex>
          </v-layout>
        </v-container>
      </v-card-text>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn color="blue darken-1" flat @click="close">取消</v-btn>
        <v-btn color="blue darken-1" flat @click="save">保存</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</v-toolbar>

соответственно, в<script></script>добавить немного JS между ними

export default {
  data: () => ({
    dialog: false, // 是否展示对话框
    errMsg: '', // 是否有错误信息
    editedIndex: -1, // 当前在对话框中编辑的图书在列表中的序号
    editedItem: { // 当前在对话框中编辑的图书内容
      id: 0,
      name: '',
      category: ''
    },
    defaultItem: { // 默认的图书内容,用于初始化新增对话框内容
      id: 0,
      name: '',
      category: ''
    }
  }),
  computed: {
    formTitle () {
      return this.editedIndex === -1 ? '新增' : '编辑'
    }
  },
  watch: {
    dialog (val) {
      if (!val) {
        this.close()
        this.clearErrMsg()
      }
    }
  },
  methods: {
    clearErrMsg () {
      this.errMsg = ''
    },
    close () {
      this.dialog = false
      setTimeout(() => {
        this.editedItem = Object.assign({}, this.defaultItem)
        this.editedIndex = -1
      }, 300)
    }
  }
}

Чтобы сделать статью более лаконичной, я опустил предыдущие фрагменты, когда размещал код.При написании вы можете добавить приведенный выше код в соответствующее место в зависимости от местоположения.

Мы использовали панель инструментов и диалоговое окно, чтобы добавить в таблицу элементы, связанные с диалогами. Точно так же нам не нужно запоминать код. Если вы не знаете, как его написать, просто обратитесь к документации.

Диалоговое окно данных указывает, отображается ли текущее диалоговое окно, errMsg управляет отображением сообщения об ошибке, а диалоговое окно прослушивателя закрывает диалоговое окно и очищает errMsg, когда оно изменяется на false. Вычисляемое свойство formTitle используется для управления заголовком диалогового окна. Затем добавляются два элемента формы для заполнения названия книги и категории.

Когда мы нажимаем «Добавить», страница выглядит так

На самом деле, на данный момент наша страница интерфейса почти в порядке, после чего следует реализация дополнений, удалений и изменений. Сначала мы реализуем это в одностороннем порядке во внешнем интерфейсе, а затем интегрируем с бэкэндом. Таким образом, демонстрационная версия интерфейса будет более полной.

Реализовать метод сохранения, добавить методы сохранения

save() {
  if (this.editedIndex > -1) { // 编辑
    Object.assign(this.books[this.editedIndex], this.editedItem)
  } else { // 新增
    this.books.push(this.editedItem)
  }
  this.close()
}

При редактировании для отображения попапа нам нужно добавить метод editItem

editItem (item) {
  this.editedIndex = this.books.indexOf(item)
  this.editedItem = Object.assign({}, item)
  this.dialog = true
}

Способ сохранения такой же, как и при его добавлении.

Реализовать метод удаления deleteItem

deleteItem (item) {
  const index = this.books.indexOf(item)
  confirm('确认删除?') && this.books.splice(index, 1)
}

На этом фронтенд-проект подошёл к концу.

2 задний конец

На серверной части нам нужно только предоставить интерфейс для добавления, удаления, изменения и проверки использования внешнего интерфейса. RESTful API — это относительно зрелый набор теорий проектирования интернет-приложений, и на его основе я также реализую соответствующий рабочий интерфейс книги.

Учитывая, что есть партнеры, которые не знакомы с RESTful API, я перечислил несколько статей, которые я изучил ранее, для справки.

  • «Понимание архитектуры RESTful»
    • https://dwz.cn/eXu0p6pv
  • Руководство по дизайну RESTful API
    • https://dwz.cn/8v4B0twY
  • Лучшие практики RESTful API
    • https://dwz.cn/2aSnI8fF
  • Зная вопрос «Как объяснить REST простым языком, а RESTful? 》
    • https://dwz.cn/bVxrSsf4

Прочитав соответствующую информацию выше, вы должны иметь определенное представление об этой теории дизайна.

Кроме того, вам пока не нужно досконально разбираться в RESTful API, просто поймите его, как показано ниже.

Это использование URL для поиска ресурсов и использование HTTP для описания операций.

Это ответ, который я видел на вышеупомянутый вопрос Zhihu, автор @Ivony. Написано лаконично, но со смыслом.

После того, как я попрактиковался один раз, я оглянусь на некоторые теоретические вещи, и я буду более впечатлен.

Сначала перечислите интерфейсы, которые нам нужно реализовать.

серийный номер метод URL описывать
1 GET http://domain/api/v1/books Получить все книги
2 GET http://domain/api/v1/books/123 Получить книги с первичным ключом 123
3 POST http://domain/api/v1/books Добавить книги
4 PUT http://domain/api/v1/books/123 Книга обновлений с первичным ключом 123
5 DELETE http://domain/api/v1/books/123 удалить книгу с первичным ключом 123

Мы можем напрямую использовать Flask для реализации вышеописанного интерфейса, но когда ресурсов много, мы будем писать много повторяющихся фрагментов при написании кода, что нарушает принцип DRY (Don’t Repeat Yourself), и поддерживать его более проблематично. позже, поэтому мы используем реализацию расширения Flask -RESTful.

Кроме того, в этом разделе основное внимание уделяется реализации интерфейса, и для простоты мы храним данные непосредственно в словаре и не задействуем операции, связанные с базой данных.

Создайте новый каталог сервера в каталоге spa-demo и переключитесь в этот каталог, чтобы инициализировать среду Python.

$ pipenv --python 3.6.0

В настоящее время Pipenv является официально рекомендуемой виртуальной средой и инструментом управления пакетами.Я уже писал статью «Быстрый старт Pipenv», и вы можете прочитать ее, если не прикасались к ней.

Установить колбу

$ pipenv install flask

Установить Flask-RESTful

$ pipenv install flask-restful

новыйspa-demo/server/app.py

# coding=utf-8

from flask import Flask, request
from flask_restful import Api, Resource, reqparse, abort


app = Flask(__name__)
api = Api(app)


books = [{'id': 1, 'name': 'book1', 'category': 'cat1'},
         {'id': 2, 'name': 'book2', 'category': 'cat2'},
         {'id': 3, 'name': 'book3', 'category': 'cat3'}]


# 公共方法区


class BookApi(Resource):
    def get(self, book_id):
        pass

    def put(self, book_id):
        pass

    def delete(self, book_id):
        pass


class BookListApi(Resource):
    def get(self):
        return books

    def post(self):
        pass


api.add_resource(BookApi, '/api/v1/books/<int:book_id>', endpoint='book')
api.add_resource(BookListApi, '/api/v1/books', endpoint='books')

if __name__ == '__main__':
    app.run(debug=True)

Выше приведена стандартная структура кода, интегрирующая Flask-RESTful, похожие примеры можно увидеть в официальной документации Flask-RESTful. Для каждого ресурса мы можем реализовать интерфейс с аналогичной структурой. Методы get, put и delete в классе BookApi соответствуют интерфейсам 2, 4 и 5, а методы get и post в классе BookListApi соответствуют интерфейсам 1 и 3. Затем идет путь регистрации. Увидев это, у некоторых партнеров могут возникнуть вопросы, зачем нужно определять два класса для одного и того же ресурса? На самом деле удобно прописывать маршрут с первичным ключом для ресурса и без него.

На данный момент структура проекта

spa-demo
├── client
│   └── ...
└── server
    ├── Pipfile
    ├── Pipfile.lock
    └── app.py

переключить наspa-demo/serverкаталог, запуститьapp.py

$ pipenv run python app.py

Затем проверьте, доступен ли интерфейс получения всех книг. Поскольку это тест API, не рекомендуется использовать браузер напрямую, ведь иногда неудобно конструировать параметры и просматривать информацию HTTP. Рекомендуется использовать Postman. Конечно, для простых тестов можно использовать команду curl напрямую.

Интерфейс запроса 1 для получения всей информации о книге

$ curl -i http://127.0.0.1:5000/api/v1/books

получил ответ

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 249
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:21:56 GMT

[
    {
        "id": 1,
        "name": "book1",
        "category": "cat1"
    },
    {
        "id": 2,
        "name": "book2",
        "category": "cat2"
    },
    {
        "id": 3,
        "name": "book3",
        "category": "cat3"
    }
]

Все книги успешно получены, что указывает на то, что интерфейс 1 в порядке.

Затем реализуйте интерфейс 2, чтобы получить книгу с указанным идентификатором. Поскольку операция получения книг на основе идентификатора и выдача 404, когда книга не существует, будет часто использоваться, здесь в «области общедоступных методов» упомянуты два метода.

def get_by_id(book_id):
    book = [v for v in books if v['id'] == book_id]
    return book[0] if book else None


def get_or_abort(book_id):
    book = get_by_id(book_id)
    if not book:
        abort(404, message=f'Book {book_id} not found')
    return book

Затем реализуйте метод get в BookApi.

def get(self, book_id):
    book = get_or_abort(book_id)
    return book

Возьмите книгу с ID 1 для проверки

$ curl -i http://127.0.0.1:5000/api/v1/books/1

результат

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 61
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:31:48 GMT

{
    "id": 1,
    "name": "book1",
    "category": "cat1"
}

Возьмите книгу с ID 5 и протестируйте ее.

$ curl -i http://127.0.0.1:5000/api/v1/books/5

результат

HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 149
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:32:47 GMT

{
    "message": "Book 5 not found. You have requested this URI [/api/v1/books/5] but did you mean /api/v1/books/<int:book_id> or /api/v1/books ?"
}

Если идентификатор равен 1, информация о книге получена успешно; если идентификатор равен 5, будет возвращен ответ 404, поскольку книга не существует. Результаты теста соответствуют ожиданиям, указывая на то, что интерфейс в порядке.

Реализуйте интерфейс 3 и добавьте новые книги. При добавлении новых книг мы должны проверять, соответствуют ли параметры требованиям. Flask-RESTFul предоставляет нам более элегантную реализацию, которая не требует от нас использования жестко закодированной формы множественных суждений if для проверки правильности параметров.

Поскольку название книги и категория не могут быть пустыми, нам нужно настроить правила, мы можем добавить метод в «область общедоступных методов».

def not_empty_str(s):
    s = str(s)
    if not s:
        raise ValueError("Must not be empty string")
    return s

Переопределить метод инициализации BookListApi

def __init__(self):
    self.reqparse = reqparse.RequestParser()
    self.reqparse.add_argument('name', type=not_empty_str, required=True, location='json')
    self.reqparse.add_argument('category', type=not_empty_str, required=True, location='json')
    super(BookListApi, self).__init__()

Затем реализуйте метод post

def post(self):
    args = self.reqparse.parse_args()
    book = {
        'id': books[-1]['id'] + 1 if books else 1,
        'name': args['name'],
        'category': args['category'],
    }
    books.append(book)
    return book, 201

В методе сначала проверьте правильность параметров, затем возьмите идентификатор последней книги плюс 1 и сохраните его как идентификатор новой книги, и, наконец, верните добавленную информацию о книге и код состояния 201 (указывающий, что она была созданный).

Проверить правильность проверки параметра

$ curl -i \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{"name":"","category":""}' \
    http://127.0.0.1:5000/api/v1/books

результат

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json
Content-Length: 70
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:46:18 GMT

{
    "message": {
        "name": "Must not be empty string"
    }
}

Возвращается ошибка 400, указывающая, что проверка параметра действительна.

Проверьте, доступен ли новый интерфейс

$ curl -i \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{"name":"t_name","category":"t_cat"}' \
    http://127.0.0.1:5000/api/v1/books

результат

HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 63
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:53:54 GMT

{
    "id": 4,
    "name": "t_name",
    "category": "t_cat"
}

Указывает, что создание прошло успешно. Подтверждаем получением книжного интерфейса указанного ID

$ curl -i http://127.0.0.1:5000/api/v1/books/4

результат

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 63
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:54:18 GMT

{
    "id": 4,
    "name": "t_name",
    "category": "t_cat"
}

Если приобретение прошло успешно, значит, создание действительно удалось, а значит, и с интерфейсом 3 все в порядке.

Реализация интерфейсов 4 и 5 аналогична вышеописанной, а код выложен здесь, и подробно его объяснять не буду.

Как и в случае с BookListApi, сначала перепишите метод инициализации BookApi.

def __init__(self):
    self.reqparse = reqparse.RequestParser()
    self.reqparse.add_argument('name', type=not_empty_str, required=True, location='json')
    self.reqparse.add_argument('category', type=not_empty_str, required=True, location='json')
    super(BookApi, self).__init__()

Затем реализуйте методы put и delete

def put(self, book_id):
    book = get_or_abort(book_id)
    args = self.reqparse.parse_args()
    for k, v in args.items():
        book[k] = v
    return book, 201

def delete(self, book_id):
    book = get_or_abort(book_id)
    del book
    return '', 204

На этом бэкэнд-проект в основном завершен.

Конечно, это неполный вариант, например, нет аутентификации для API, это можно сделать с помощью Flask-HTTPAuth или других методов. Из-за нехватки места я не буду расширять описание здесь, если вам интересно, вы можете прочитать документацию этого расширения или исследовать и реализовать его самостоятельно.

3 Интеграция

У отдельного фронтенда или бэкенда есть прототип, а до интеграции всего один шаг.

Фронтенду нужно запросить данные, здесь мы используем axios, переключаемся наspa-demo/clientУстановить в каталог

$ npm install axios --save

Исправлятьspa-demo/client/src/views/Home.vue,существуетscriptВвести аксиомы между тегами и инициализировать адрес API

import axios from 'axios'

const booksApi = 'http://localhost:5000/api/v1/books'

export default {
  ...
}

Измените логику хука, созданного для получения данных из бэкенда.

created () {
  axios.get(booksApi)
    .then(response => {
      this.books = response.data
    })
    .catch(error => {
      console.log(error)
    })
}

После запуска внешнего проекта проверьте домашнюю страницу и обнаружите, что данных нет. Заглянув в инструменты разработчика, мы найдем такую ​​ошибку

Access to XMLHttpRequest at 'http://localhost:5000/api/v1/books' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

То есть текущий проект не поддерживает CORS (Cross-Origin Resource Sharing, то есть доступ к ресурсам между источниками). Мы можем реализовать это в виде добавления прокси на интерфейсе или через Flask-CORS на сервере. Здесь я использую последний.

переключить наspa-demo/serverкаталог, установите Flask-CORS

$ pipenv install flask-cors

Исправлятьspa-demo/server/app.py, ввести CORS в голову

from flask_cors import CORS

в коде

app = Flask(__name__)

и

api = Api(app)

добавить линию между

CORS(app, resources={r"/api/*": {"origins": "*"}})

Затем повторно запустите app.py, обновите домашнюю страницу, мы увидим, что в списке есть данные, указывающие на то, что проблема CORS была успешно решена.

существуетspa-demo/client/src/views/Home.vue, измените метод сохранения и добавьте вспомогательный метод setErrMsg

setErrMsg (errResponse) {
  let errResMsg = errResponse.data.message
  if (typeof errResMsg === 'string') {
    this.errMsg = errResMsg
  } else {
    let errMsgs = []
    let k
    for (k in errResMsg) {
      errMsgs.push('' + k + ' ' + errResMsg[k])
    }
    this.errMsg = errMsgs.join(',')
  }
},
save() {
  if (this.editedIndex > -1) { // 编辑
    axios.put(booksApi + '/' + this.editedItem.id, this.editedItem)
    .then(response => {
      Object.assign(this.books[this.editedIndex], response.data)
      this.close()
    }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
  } else { // 新增
    axios.post(booksApi, this.editedItem)
      .then(response => {
        this.books.push(response.data)
        this.close()
      }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
  }
}

На этом этапе книга добавлена ​​и сохранена.

Изменить метод deleteItem

deleteItem (item) {
  const index = this.books.indexOf(item)
  confirm('确认删除?') && axios.delete(booksApi + '/' + this.books[0].id)
    .then(response => {
      this.books.splice(index, 1)
    }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
}

На этом этапе также выполняется метод удаления.

На этом интеграция завершена, и завершена демонстрация CRUD, основанная на разделении интерфейса и сервера Vue + Flask.

Прочитав эту статью, вы можете выполнить шаги, чтобы реализовать это самостоятельно. У новых партнеров могут возникнуть сомнения в некоторых местах в процессе чтения, и я также предоставил некоторую информацию, о которой я могу подумать, вы можете попробовать ее. Если вы не можете предоставить их все, вам нужно решить это самостоятельно в Baidu/Google.Тем не менее, я все же рекомендую не пытаться понять каждый пункт очень четко, сначала понять ключевые моменты, постараться их осознать, и когда вы оглянетесь на соответствующую информацию, вы почувствуете себя более эмоционально.

Полный код можно посмотреть на GitHub

https://github.com/kevinbai-cn/spa-demo

4 Ссылка

  • «Полнофункциональное одностраничное приложение с Vue.js и Flask»
    • https://bit.ly/2C9kSiG
  • «Разработка одностраничного приложения с помощью Flask и Vue.js»
    • https://bit.ly/2ElaXrB
  • Проверка документов
    • https://bit.ly/2QupMzx
  • «Разработка RESTful API с помощью Python и Flask»
    • https://bit.ly/2vqq3Y1
  • «Разработка RESTful API с использованием Flask-RESTful»
    • https://bit.ly/2nGDNtL

Эта статья была впервые опубликована на паблике «Little Backend».