使用Flask 和 Vue.js创建单页应用程序

318 阅读15分钟

这是一篇中文翻译文章。

原文作者:Michael Herman

上次更新 April 22nd, 2023

源代码

以下是如何使用Vue和Flask设置基本CRUD应用程序的分步演练。我们将从搭建一个新的Vue应用程序开始,然后通过由Python和Flask支持的后端RESTful API执行基本的CRUD操作。

最终应用程序:

final app

主要依赖项:

  • Vue v3.2.47
  • Node v20.0.0
  • npm v9.6.4
  • Flask v2.2.3
  • Python v3.11.3

目标

在本教程结束时,您将能够:

  1. 解释什么是Flask
  2. 解释什么是Vue,以及它与其他UI库和前端框架(如React和Angular)的比较
  3. 用Vite搭建Vue项目
  4. 在浏览器中创建和渲染Vue组件
  5. 使用Vue组件创建单页应用程序(SPA)
  6. 将Vue应用程序连接到Flask后端
  7. 使用Flask开发REST风格的API
  8. 使用Bootstrap对Vue组件进行样式化
  9. 使用Vue Router创建路由和渲染组件

Flask和Vue

让我们快速查看每个框架。

什么是Flask?

Flask是一个简单但功能强大的Python微型Web框架,非常适合构建RESTful API。像Sinatra(Ruby)和Express(Node)一样,它是最小的和灵活的,所以你可以从小开始,根据需要构建一个更复杂的应用程序。

第一次和Flask在一起查看以下两个资源:

  1. Flaskr TDD
  2. 用Python和Flask开发Web应用程序

Vue是什么?

Vue是一个用于构建用户界面的开源JavaScript框架。它采用了React和Angular的一些最佳实践。也就是说,与React和Angular相比,它更加平易近人,因此初学者可以快速上手。它也同样强大,因此它提供了创建现代前端应用程序所需的所有功能。

有关Vue的更多信息,沿着使用它与React和Angular相比的优点和缺点,请查看资源:

  1. Vue:与其他框架的比较
  2. 通过构建和部署CRUD应用学习Vue
  3. React vs Angular vs Vue.js

第一次使用Vue?花点时间阅读官方Vue指南中的介绍

Flask 设置

开始,创建一个新的项目目录:

$ mkdir flask-vue-crud
$ cd flask-vue-crud

在“flask-vue-crud”中,创建一个名为“server”的新目录。然后,在“server”目录中创建并激活一个虚拟环境:

$ python3.11 -m venv env
$ source env/bin/activate
(env)$

请随意将virtualenv和Pip换成 PoetryPipenv。有关更多信息,请查看现代Python环境

安装Flask和它的扩展Flask-CORS

(env)$ pip install Flask==2.2.3 Flask-Cors==3.0.10

将app.py文件添加到新创建的“server”目录中:

from flask import Flask, jsonify
from flask_cors import CORS
​
​
# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)
​
# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})
​
​
# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
    return jsonify('pong!')
​
​
if __name__ == '__main__':
    app.run()

为什么需要Flask-CORS?为了进行跨域请求,如果请求来自不同的协议、IP地址、域名或端口,则需要启用跨域资源共享 (CORS)。Flask-CORS为我们处理这个。

值得注意的是,上述设置允许来自任何域、协议或端口的所有路由上的跨域请求。在生产环境中,您应该只允许来自前端应用程序所在域的跨域请求。请参阅Flask-CORS文档以了解更多信息。

运行应用程序:

(env)$ flask run --port=5001 --debug

要进行测试,请将浏览器指向http://localhost:5001/ping 您应该看到:

"pong!"

回到终端,按Ctrl+C杀死服务器,然后导航回项目根目录。有了这些,让我们把注意力转向前端并设置Vue。

Vue设置

我们将通过Vite Vite使用功能强大的 create-vue工具 来生成一个定制的项目样板。

在“flask-vue-crud”中,运行以下命令来初始化一个新的Vue项目:

$ npm create vue@3.6.1

第一次使用NPM?查看关于npm的官方指南

这将要求您回答有关该项目的几个问题:

Vue.js - The Progressive JavaScript Framework

 Project name:  client
 Add TypeScript?  No
 Add JSX Support?  No
 Add Vue Router for Single Page Application development?  Yes
 Add Pinia for state management?  No
 Add Vitest for Unit Testing?  No
 Add ESLint for code quality?  No

快速查看生成的项目结构。这看起来可能很多,但我们只处理“src”文件夹中的文件和文件夹,沿着index.html文件。

index.html文件是Vue应用程序的起点:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

注意 idapp<div> 元素。这是一个占位符,Vue将使用它来附加生成的HTML和CSS,以生成UI。

请注意“src”文件夹中的文件夹:

client/src
├── App.vue
├── assets
│   ├── base.css
│   ├── logo.svg
│   └── main.css
├── components
│   ├── HelloWorld.vue
│   ├── TheWelcome.vue
│   ├── WelcomeItem.vue
│   └── icons
│       ├── IconCommunity.vue
│       ├── IconDocumentation.vue
│       ├── IconEcosystem.vue
│       ├── IconSupport.vue
│       └── IconTooling.vue
├── main.js
├── router
│   └── index.js
└── views
    ├── AboutView.vue
    └── HomeView.vue

细分:

Name 名称Purpose 目的
main.js入口点,它将沿着根组件一起加载和调用Vue
App.vue根组件,从其开始所有其他组件将被渲染
"components"组件目录,存储UI组件的位置
router/index.js路由文件,其中定义了URL并将其映射到组件
"views"视图目录,其中存储了绑定到路由器的UI组件
"assets"静态资源 存储静态资源(如图像和字体)的位置

查看client/src/components/HelloWorld.vue文件。这是一个单一文件组件 ,分为三个不同的部分:

  1. template:用于特定于组件的HTML
  2. script:组件逻辑通过JavaScript实现
  3. style: CSS样式

接下来,安装依赖项,然后启动开发服务器:

$ cd client
$ npm install
$ npm run dev

在您选择的浏览器中导航到http://localhost:5173 您应该会看到下列内容:

default vue app

为了简化操作,删除“client/src/views”和“client/src/components/icons”文件夹以及client/src/components/TheWelcome.vue和client/src/components/WelcomeItem.vue组件。 然后,在“client/src/components”文件夹中添加一个名为Ping.vue的新组件:

<template>
  <div>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: 'Ping',
  data() {
    return {
      msg: 'Hello!',
    };
  },
};
</script>

更新client/src/router/index.js,将'/ping'映射到 Ping 组件,如下所示:

import { createRouter, createWebHistory } from 'vue-router'
import Ping from '../components/Ping.vue'const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/ping',
      name: 'ping',
      component: Ping
    },
  ]
})
​
export default router

最后,在client/src/App.vue中,删除导航沿着样式:

<template>
  <RouterView />
</template><script setup>
import { RouterView } from 'vue-router'
</script>

您现在应该在浏览器 http://localhost:5173/ping 中看到 Hello!

要连接客户端Vue应用程序和后端Flask应用程序,我们可以使用 Axios库发送HTTP请求。

首先安装它:

$ npm install axios@1.3.6 --save

Ping.vue中更新组件的 script 部分,如下所示:

<script>
import axios from 'axios';
​
export default {
  name: 'Ping',
  data() {
    return {
      msg: '',
    };
  },
  methods: {
    getMessage() {
      const path = 'http://localhost:5001/ping';
      axios.get(path)
        .then((res) => {
          this.msg = res.data;
        })
        .catch((error) => {
          console.error(error);
        });
    },
  },
  created() {
    this.getMessage();
  },
};
</script>

在新的终端窗口中启动Flask应用程序。在不同的终端窗口中运行Vue应用程序,您现在应该在浏览器中看到 pong! 。本质上,当从后端返回响应时,我们将 msg 设置为响应对象中的 data 值。

Bootstrap设置

接下来,让我们将Bootstrap(一个流行的CSS框架)添加到应用程序中,以便我们可以快速添加一些样式。

安装方式:

$ npm install bootstrap@5.2.3 --save

将Bootstrap样式导入到client/src/main.js:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'

import './assets/main.css'

const app = createApp(App)

app.use(router)

app.mount('#app')

确保Bootstrap正确连接,通过使用 Ping 组件中的按钮Button 和容器Container

<template>
  <div class="container">
    <button type="button" class="btn btn-primary">{{ msg }}</button>
  </div>
</template>

Run the dev server: 运行开发服务器:

$ npm run serve

您应该看到:

vue with bootstrap

接下来,在名为components\Books.vue的新文件中添加一个名为 Books 的新组件:

<template>
  <div class="container">
    <p>books</p>
  </div>
</template>

更新路由器router\index.js

import { createRouter, createWebHistory } from 'vue-router'
import Books from '../components/Books.vue' 
import Ping from '../components/Ping.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'Books',
      component: Books,
    },
    {
      path: '/ping',
      name: 'ping',
      component: Ping
    },
  ]
})

export default router

更新client/src/App.vue中的 style 部分:

<style>
#app {
  margin-top: 60px
}
</style>

删除client/src/assets/base.cssclient/src/assets/main.css文件。 确保删除client/src/main.js中的 import './assets/main.css' 行:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'

const app = createApp(App)

app.use(router)

app.mount('#app')

测试:

  1. http://localhost:5173
  2. http://localhost:5173/ping

最后,让我们将一个快速的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>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

您现在应该看到:

books component

现在我们可以开始构建CRUD应用程序的功能了。

我们在建造什么?

我们的目标是设计一个后端RESTful API,由Python和Flask提供支持,用于单一资源-书籍。API本身应该遵循RESTful设计原则,使用基本的HTTP动词:GET、POST、PUT和POST。

我们还将使用Vue设置一个前端应用程序,它使用后端API:

final app

本教程只涉及快乐之路。处理错误是一个单独的练习。检查您的理解,并在前端和后端添加适当的错误处理。

获取路径

服务器

将图书列表添加到server/app.py:

BOOKS = [
    {
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'title': 'Harry Potter and the Philosopher's Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]

添加路由处理程序:

@app.route('/books', methods=['GET'])
def all_books():
    return jsonify({
        'status': 'success',
        'books': BOOKS
    })

运行Flask应用程序(如果尚未运行),然后在 http://localhost:5001/books 上手动测试路由。

寻找额外的挑战?为此编写一个自动化测试。查看此资源以了解有关测试Flask应用程序的更多信息。

客户端

更新组件:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm">Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(book, index) in books" :key="index">
              <td>{{ book.title }}</td>
              <td>{{ book.author }}</td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5001/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          console.error(error);
        });
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

组件初始化后,通过created的生命周期钩子调用 getBooks() 方法,该方法从我们刚刚设置的后端端点获取书籍。

回顾一下生命周期钩子,了解更多关于组件生命周期和可用方法的信息。

在模板中,我们通过 v-for指令遍历图书列表,在每次迭代中创建一个新的表行。索引值用作键key。最后,使用v-if来呈现 YesNo ,指示用户是否读过这本书。

books component

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:5001/books -d \
  '{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
  -H 'Content-Type: application/json'

您应该看到:

{
  "message": "Book added!",
  "status": "success"
}

您还应该在来自 http://localhost:5001/books 端点的响应中看到这本新书。

如果标题已经存在呢?如果一本书有不止一个作者呢?通过处理这些案例来检查您的理解。另外,如果缺少 titleauthor 和/或 read ,您将如何处理无效的有效负载?

客户端

在客户端,让我们添加一个用于向 Books 组件添加新书的模式,从HTML开始:

<!-- add new book modal -->
<div
  ref="addBookModal"
  class="modal fade"
  :class="{ show: activeAddBookModal, 'd-block': activeAddBookModal }"
  tabindex="-1"
  role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Add a new book</h5>
        <button
          type="button"
          class="close"
          data-dismiss="modal"
          aria-label="Close"
          @click="toggleAddBookModal">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <form>
          <div class="mb-3">
            <label for="addBookTitle" class="form-label">Title:</label>
            <input
              type="text"
              class="form-control"
              id="addBookTitle"
              v-model="addBookForm.title"
              placeholder="Enter title">
          </div>
          <div class="mb-3">
            <label for="addBookAuthor" class="form-label">Author:</label>
            <input
              type="text"
              class="form-control"
              id="addBookAuthor"
              v-model="addBookForm.author"
              placeholder="Enter author">
          </div>
          <div class="mb-3 form-check">
            <input
              type="checkbox"
              class="form-check-input"
              id="addBookRead"
              v-model="addBookForm.read">
            <label class="form-check-label" for="addBookRead">Read?</label>
          </div>
          <div class="btn-group" role="group">
            <button
              type="button"
              class="btn btn-primary btn-sm"
              @click="handleAddSubmit">
              Submit
            </button>
            <button
              type="button"
              class="btn btn-danger btn-sm"
              @click="handleAddReset">
              Reset
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
<div v-if="activeAddBookModal" class="modal-backdrop fade show"></div>

在结束 div 标签之前添加此内容。快速查看代码。 v-model 是一个用于将输入值绑定bind 回状态的指令。您很快就会看到这一点。

更新 script 部分:

<script>
import axios from 'axios';

export default {
  data() {
    return {
      activeAddBookModal: false,
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
      books: [],
    };
  },
  methods: {
    addBook(payload) {
      const path = 'http://localhost:5001/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {

          console.log(error);
          this.getBooks();
        });
    },
    getBooks() {
      const path = 'http://localhost:5001/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {

          console.error(error);
        });
    },
    handleAddReset() {
      this.initForm();
    },
    handleAddSubmit() {
      this.toggleAddBookModal();
      let read = false;
      if (this.addBookForm.read[0]) {
        read = true;
      }
      const payload = {
        title: this.addBookForm.title,
        author: this.addBookForm.author,
        read, // property shorthand
      };
      this.addBook(payload);
      this.initForm();
    },
    initForm() {
      this.addBookForm.title = '';
      this.addBookForm.author = '';
      this.addBookForm.read = [];
    },
    toggleAddBookModal() {
      const body = document.querySelector('body');
      this.activeAddBookModal = !this.activeAddBookModal;
      if (this.activeAddBookModal) {
        body.classList.add('modal-open');
      } else {
        body.classList.remove('modal-open');
      }
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

这是怎么回事

  1. addBookForm 再次通过 v-model 绑定bound 到表单输入。换句话说,当一个被更新时,另一个也会被更新。这被称为双向绑定。花点时间在这里阅读它。想想这件事的后果。你认为这会使状态管理更容易还是更困难?React和Angular如何处理这个问题?在我看来,双向绑定(沿着可变性)使Vue比React更容易接近。
  2. 当用户单击提交按钮时,将触发 handleAddSubmit 。在这里,我们关闭模态( this.toggleAddBookModal(); ),激发 addBook 方法,并清除表单( initForm() )。
  3. addBook/books 发送POST请求以添加新书。

您可以自己查看其余的更改,必要时参考Vue文档(vuejs.org/guide/intro…)。

你能想到客户端或服务器上的任何潜在错误吗?自己处理这些以改善用户体验。

最后,更新模板中的“Add Book”按钮,以便在单击按钮时显示模态:

<button
  type="button"
  class="btn btn-success btn-sm"
  @click="toggleAddBookModal">
  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"
          @click="toggleAddBookModal">
          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>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
​
    <!-- add new book modal -->
    <div
      ref="addBookModal"
      class="modal fade"
      :class="{ show: activeAddBookModal, 'd-block': activeAddBookModal }"
      tabindex="-1"
      role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">Add a new book</h5>
            <button
              type="button"
              class="close"
              data-dismiss="modal"
              aria-label="Close"
              @click="toggleAddBookModal">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <form>
              <div class="mb-3">
                <label for="addBookTitle" class="form-label">Title:</label>
                <input
                  type="text"
                  class="form-control"
                  id="addBookTitle"
                  v-model="addBookForm.title"
                  placeholder="Enter title">
              </div>
              <div class="mb-3">
                <label for="addBookAuthor" class="form-label">Author:</label>
                <input
                  type="text"
                  class="form-control"
                  id="addBookAuthor"
                  v-model="addBookForm.author"
                  placeholder="Enter author">
              </div>
              <div class="mb-3 form-check">
                <input
                  type="checkbox"
                  class="form-check-input"
                  id="addBookRead"
                  v-model="addBookForm.read">
                <label class="form-check-label" for="addBookRead">Read?</label>
              </div>
              <div class="btn-group" role="group">
                <button
                  type="button"
                  class="btn btn-primary btn-sm"
                  @click="handleAddSubmit">
                  Submit
                </button>
                <button
                  type="button"
                  class="btn btn-danger btn-sm"
                  @click="handleAddReset">
                  Reset
                </button>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div v-if="activeAddBookModal" class="modal-backdrop fade show"></div>
  </div>
</template>
​
<script>
import axios from 'axios';
​
export default {
  data() {
    return {
      activeAddBookModal: false,
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
      books: [],
    };
  },
  methods: {
    addBook(payload) {
      const path = 'http://localhost:5001/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
​
          console.log(error);
          this.getBooks();
        });
    },
    getBooks() {
      const path = 'http://localhost:5001/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
​
          console.error(error);
        });
    },
    handleAddReset() {
      this.initForm();
    },
    handleAddSubmit() {
      this.toggleAddBookModal();
      let read = false;
      if (this.addBookForm.read[0]) {
        read = true;
      }
      const payload = {
        title: this.addBookForm.title,
        author: this.addBookForm.author,
        read, // property shorthand
      };
      this.addBook(payload);
      this.initForm();
    },
    initForm() {
      this.addBookForm.title = '';
      this.addBookForm.author = '';
      this.addBookForm.read = [];
    },
    toggleAddBookModal() {
      const body = document.querySelector('body');
      this.activeAddBookModal = !this.activeAddBookModal;
      if (this.activeAddBookModal) {
        body.classList.add('modal-open');
      } else {
        body.classList.remove('modal-open');
      }
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

试试看!尝试添加一本书:

add new book

Alert Component 警报组件

接下来,让我们添加一个 Alert 组件,以便在添加新书后向最终用户显示一条消息。我们将为此创建一个新组件,因为您可能会在许多组件中使用该功能。

在“client/src/components”中添加一个名为Alert.vue的新文件:

<template>
  <p>It works!</p>
</template>

然后,将其导入到 Books 组件的 script 部分并注册组件:

<script>
import axios from 'axios';
import Alert from './Alert.vue';
​
export default {
  data() {
    return {
      activeAddBookModal: false,
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
      books: [],
    };
  },
  components: {
    alert: Alert,
  },
​
  ...
​
};
</script>

现在,我们可以在 template 部分引用新组件:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <alert></alert>
        <button
          type="button"
          class="btn btn-success btn-sm"
          @click="toggleAddBookModal">
          Add Book
        </button>
        <br><br>
​
        ...
​
      </div>
    </div>
​
    ...
​
  </div>
</template>

刷新浏览器。您现在应该看到:

bootstrap alert

查看官方Vue文档中的Using a Component,了解有关在其他组件中使用组件的更多信息。

接下来,让我们将实际的Bootstrap Alert 组件添加到client/src/components/Alert.vue:

<template>
  <div>
    <div class="alert alert-success" role="alert">{{ message }}</div>
    <br/>
  </div>
</template>

<script>
export default {
  props: ['message'],
};
</script>

注意 script 部分中的props 选项。我们可以从父组件( Books )向下传递一条消息,如下所示:

<alert message="hi"></alert>

试试这个:

bootstrap alert

查看文档docs了解更多关于组件信息传递的知识。

要使其成为动态的,以便传递自定义消息,请在Books.vue中使用 绑定表达式

<alert :message="message"></alert>

message 添加到 data 选项中,在Books.vue中也是如此:

data() {
  return {
    activeAddBookModal: false,
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    books: [],
    message: '',
  };
},

然后,在 addBook 中更新消息:

addBook(payload) {
  const path = 'http://localhost:5001/books';
  axios.post(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book added!';
    })
    .catch((error) => {
      console.log(error);
      this.getBooks();
    });
},

最后,添加一个 v-if ,这样只有在 showMessage 为true时才会显示警报:

<alert :message=message v-if="showMessage"></alert>

showMessage 添加到 data

data() {
  return {
    activeAddBookModal: false,
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    books: [],
    message: '',
    showMessage: false,
  };
},

再次更新 addBook ,将 showMessage 设置为 true

addBook(payload) {
  const path = 'http://localhost:5001/books';
  axios.post(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book added!';
      this.showMessage = true;
    })
    .catch((error) => {
      console.log(error);
      this.getBooks();
    });
},

试试看!

add new book

Challenges: 挑战:

  1. Think about where showMessage should be set to false. Update your code. 考虑一下 showMessage 应该设置为 false 的位置。更新您的代码。
  2. 尝试使用警报组件显示错误。
  3. 将警报重构为可忽略dismissible.

PUT路由

服务器

对于更新,我们需要使用唯一的标识符,因为我们不能依赖于标题是唯一的。我们可以使用Python标准库中的 uuid

server/app.py中更新 BOOKS

BOOKS = [
    {
        'id': uuid.uuid4().hex,
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Harry Potter and the Philosopher's Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]

别忘了import:

import uuid

重构 all_books 以在添加新书时考虑唯一id:

@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)

添加新的路由处理程序:

@app.route('/books/<book_id>', methods=['PUT'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    return jsonify(response_object)

添加helper:

def remove_book(book_id):
    for book in BOOKS:
        if book['id'] == book_id:
            BOOKS.remove(book)
            return True
    return False

花点时间想想你会如何处理一个不存在的 id 的情况。如果载荷不正确怎么办?重构帮助器中的for循环,使其更像Python。

客户端

步骤:

  1. 添加模态和表单
  2. 处理"更新"按钮的点击事件
  3. 连接HTTP请求
  4. 提醒用户
  5. 处理"取消"按钮的点击事件

(1)添加模态和表单

首先,在模板中添加一个新的modal,就在第一个modal的下面:

<!-- edit book modal -->
<div
  ref="editBookModal"
  class="modal fade"
  :class="{ show: activeEditBookModal, 'd-block': activeEditBookModal }"
  tabindex="-1"
  role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Update</h5>
        <button
          type="button"
          class="close"
          data-dismiss="modal"
          aria-label="Close"
          @click="toggleEditBookModal">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <form>
          <div class="mb-3">
            <label for="editBookTitle" class="form-label">Title:</label>
            <input
              type="text"
              class="form-control"
              id="editBookTitle"
              v-model="editBookForm.title"
              placeholder="Enter title">
          </div>
          <div class="mb-3">
            <label for="editBookAuthor" class="form-label">Author:</label>
            <input
              type="text"
              class="form-control"
              id="editBookAuthor"
              v-model="editBookForm.author"
              placeholder="Enter author">
          </div>
          <div class="mb-3 form-check">
            <input
              type="checkbox"
              class="form-check-input"
              id="editBookRead"
              v-model="editBookForm.read">
            <label class="form-check-label" for="editBookRead">Read?</label>
          </div>
          <div class="btn-group" role="group">
            <button
              type="button"
              class="btn btn-primary btn-sm"
              @click="handleEditSubmit">
              Submit
            </button>
            <button
              type="button"
              class="btn btn-danger btn-sm"
              @click="handleEditCancel">
              Cancel
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
<div v-if="activeEditBookModal" class="modal-backdrop fade show"></div>

将表单状态添加到 script 部分的 data 部分:

activeEditBookModal: false,
editBookForm: {
  id: '',
  title: '',
  author: '',
  read: [],
},

挑战:不要使用新的模式,尝试使用相同的模式来处理POST和PUT请求。

(2) 处理"更新"按钮的点击事件

更新表中的“更新”按钮:

<button
  type="button"
  class="btn btn-warning btn-sm"
  @click="toggleEditBookModal(book)">
  Update
</button>

添加一个新的方法toggleEditBookModal来显示/隐藏模式:

toggleEditBookModal(book) {
  if (book) {
    this.editBookForm = book;
  }
  const body = document.querySelector('body');
  this.activeEditBookModal = !this.activeEditBookModal;
  if (this.activeEditBookModal) {
    body.classList.add('modal-open');
  } else{
    body.classList.remove('modal-open');
  }
},

您是否注意到我们还更新了 editBookForm 的值?

然后,添加另一个新方法handleEditSubmit来处理表单提交:

handleEditSubmit() {
  this.toggleEditBookModal(null);
  let read = false;
  if (this.editBookForm.read) read = true;
  const payload = {
    title: this.editBookForm.title,
    author: this.editBookForm.author,
    read,
  };
  this.updateBook(payload, this.editBookForm.id);
},

(3) 连接HTTP请求

updateBook(payload, bookID) {
  const path = `http://localhost:5001/books/${bookID}`;
  axios.put(path, payload)
    .then(() => {
      this.getBooks();
    })
    .catch((error) => {
      console.error(error);
      this.getBooks();
    });
},

(4)提醒用户

更新 updateBook:

updateBook(payload, bookID) {
  const path = `http://localhost:5001/books/${bookID}`;
  axios.put(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book updated!';
      this.showMessage = true;
    })
    .catch((error) => {
      console.error(error);
      this.getBooks();
    });
},

(5) 处理取消按钮单击

添加方法handleEditCancel

handleEditCancel() {
  this.toggleEditBookModal(null);
  this.initForm();
  this.getBooks(); // why?
},

更新 initForm:

initForm() {
  this.addBookForm.title = '';
  this.addBookForm.author = '';
  this.addBookForm.read = [];
  this.editBookForm.id = '';
  this.editBookForm.title = '';
  this.editBookForm.author = '';
  this.editBookForm.read = [];
},

确保在继续之前检查代码。一旦完成,测试应用程序。确保单击按钮时显示模态,并正确填充输入值。

update book

Challenges: 挑战:

  1. 您可以通过将进行HTTP调用的方法移动到utils或services文件来清理组件。
  2. 此外,尝试将包含类似逻辑的方法进行合并(如handleAddSubmithandleEditSubmit)。

删除路由

服务器

更新路由处理程序:

@app.route('/books/<book_id>', methods=['PUT', 'DELETE'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    if request.method == 'DELETE':
        remove_book(book_id)
        response_object['message'] = 'Book removed!'
    return jsonify(response_object)

客户端

像这样更新“删除”按钮:

<button
  type="button"
  class="btn btn-danger btn-sm"
  @click="handleDeleteBook(book)">
  Delete
</button>

添加处理按钮的方法点击然后删除本书:

handleDeleteBook(book) {
  this.removeBook(book.id);
},
removeBook(bookID) {
  const path = `http://localhost:5001/books/${bookID}`;
  axios.delete(path)
    .then(() => {
      this.getBooks();
      this.message = 'Book removed!';
      this.showMessage = true;
    })
    .catch((error) => {
      console.error(error);
      this.getBooks();
    });
},

现在,当用户单击delete按钮时, handleDeleteBook 方法被触发,这反过来又触发了 removeBook 方法。此方法将HTTP请求发送到后端。当响应返回时,将显示警报消息并运行 getBooks

挑战:

  1. 删除按钮点击时,添加一个确认警报。
  2. 当表中没有书时,显示“没有书!请添加一个”的消息。

delete book

结论

本教程涵盖了使用Vue和Flask设置CRUD应用程序的基础知识。

通过回顾本教程开始时的目标并通过每个挑战来检查您的理解。

你可以在 [flask-vue-crud] 中找到源代码。谢谢你的阅读。

Michael Herman 迈克尔赫尔曼

Michael是一名软件工程师和教育工作者,在丹佛/博尔德地区生活和工作。他是真实的Python的联合创始人/作者。除了开发,他还喜欢建立金融模型,技术写作,内容营销和教学。