阅读 296

Flask和Vue.js构建全栈单页面web应用【通过Flask开发RESTful API】

前言

看了一些国外的关于介绍flask和vue的前后端分离的文章,但没看到比较通俗易懂,代码完善的,昨天看到一篇很新的文章,而且内容非常棒,所以想翻译过来,供大家一起学习。

原文来自Developing a CRUD App with Flask and Vue.js

正文:

下面会逐步演示通过Flask和Vue如何完成一个基本的CRUD应用程序。我们将从搭建框架开始,使用Vue CLI构建一个新的Vue应用程序,然后通过Python和Flask开发的RESTful API执行基本的CRUD操作。

主要依赖的库包括:

  • Vue v2.6.10
  • Vue CLI v3.7.0
  • Node v12.1.0
  • npm v6.9.0
  • Flask v1.0.2
  • Python v3.7.3

本文章的目标

在文章结束时,你将能够知道:

  1. 什么是Flask
  2. 什么是Vue,以及它和其他的UI库或者前端框架(React和Angular)
  3. 使用Vue CLI搭建一个Vue项目
  4. 在浏览器中创建和提交一个Vue组件
  5. 通过Vue组件创建一个单页面应用
  6. 连接Vue应用和Flask后端
  7. 通过Flask开发RESTful API
  8. 使用 Bootstrap给Vue组件添加样式
  9. 使用 Vue Router 创建路由和渲染组件

什么是Flask?

Flask是一个简单但功能强大的Python微Web框架,非常适合构建RESTful API。像Sinatra(Ruby)和Express(节点)一样,它非常小而且很灵活,所以你可以先开始一个小型的应用,并在它的基础上根据需求建立更加复杂的应用程序。

如果是第一次使用Flask,可以参考以下两个学习资源:

  1. Flaskr TDD
  2. Node开发者的Flask

什么是Vue?

VUE是一个开源JavaScript框架,用于构建用户界面。它采用了React和Angular方面的一些最佳做法。也就是说,与React和Angular相比,它更平易近人,所以初学者可以快速地开始和运用Vue。它同样很强大,提供了创建最新前端应用程序所需要的所有功能。

有关Vue的更多信息,以及它与React和Angular的各种优缺点,可以参阅以下文章:

  1. VUE:与其他框架的比较
  2. React vs Angular vs Vue.js:一个完整的比较指南
  3. React vs Angular vs Vue:2017年的比较

第一次用Vue,可以花些时间学习一遍官方的Vue指南

Flask安装

首先新建一个文件夹:

$ mkdir flask-vue-crud
$ cd flask-vue-crud复制代码

接下来,为这个目录创建一个虚拟环境,创建虚拟环境的方式因不同的开发环境可能存在不同。

$ python3.7 -m venv env
$ source env/bin/activate复制代码

安装Flask和和Flask-CORS扩展。

(env)$ pip install Flask==1.0.2 Flask-Cors==3.0.7复制代码

在根目录下新建server文件夹,并在文件夹中创建一个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, 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地址,域名或端口的请求——而Flask-CORS可以帮我们处理这些。

需要注意的是,虽然上面的设置允许所有路由上的跨域请求(来自任何域、协议或端口)。但在生产环境中,您应该只允许来自托管前端应用程序的域的跨域请求。有关此问题的更多信息,请参阅Flask-CORS文档

运行app:

(env)$ python server/app.py复制代码

现在可以用浏览器登录http://localhost:5000/ping来测试了,你会看到一个json格式的

"pong!"复制代码

回到终端中,按Ctrl+C键关闭服务器。现在,我们就可以把注意力转向前端,开始设置Vue。

VUE设置

我们将使用强大的Vue CLI工具来生成一个自定义项目样板。

在全局内安装Vue CLI:

$ npm install -g @vue/cli@3.7.0复制代码
第一次使用npm,可以查阅About npm指南。

安装完成后,用下面的命令来初始化一个名为client的Vue项目:

$ vue create client复制代码

接下来,需要回答一些关于项目的问题。在此项目中,具体的选择如下所示:

Vue CLI v3.7.0
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
❯◉ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
? 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复制代码

等待创建完毕后,项目根目录下多出一个client文件夹。里面有很多的内容,不过我们只需要处理其中‘src’文件夹内的一些内容以及‘public’文件夹内的index.html文件,其他的不需要我们操作。

index.html文件是Vue应用的起点。

src文件夹内的文件结构如下:

├── App.vue
├── assets
│   └── logo.png
├── components
│   └── HelloWorld.vue
├── main.js
├── router.js
└── views
    ├── About.vue
    └── Home.vue复制代码

详解:

  • main.js app入口点,它与根组件一起加载和初始化Vue。
  • app.vue 根组件,它是开始渲染所有其他组件时的起点。
  • 'components' 存储UI组件
  • router.js 定义URL并将URL映射到对应的组件
  • 'views' 存储绑定到路由器的UI组件
  • 'asset' 存储静态资源,如图像和字体

打开/client/src/components/HelloWorld.vue文件。这是一个单文件组件,它包括三个部分:

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

现在开始运行开发服务器:

$ cd client
$ npm run serve复制代码

为了简化项目,我们可以删除views文件夹,然后添加一个名为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.js,将“/ping”映射到ping组件,如下所示:

import Vue from 'vue';
import Router from 'vue-router';
import Ping from './components/Ping.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/ping',
      name: 'Ping',
      component: Ping,
    }
  ],
});复制代码

最后,删除Client/src/App.vue中template部分的导航栏,变为如下所示:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>复制代码

现在,你可以通过浏览器登录http://localhost:8080/ping看到 hello! 了。

要将客户端Vue应用程序与后端Flask应用程序连接起来,我们可以使用axios库发送Ajax请求。

首先安装对应库:

$ npm install axios@0.18.0 --save复制代码

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

<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-disable-next-line
          console.error(error);
        });
    },
  },
  created() {
    this.getMessage();
  },
};
</script>复制代码

在一个新的终端窗口中启动Flask应用程序。你可以看到http://localhost:8080/ping页面不再是hello!而是pong!。实际上,当从后端返回响应时,我们将上面的msg设置为来自服务器响应对象的data的值。

安装Bootstrap

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

安装:

$ npm install bootstrap@4.3.1 --save 复制代码
忽略jquery和popper.js的warnings警告。不要将它们添加到项目中。后面会详细的解释。

将Bootstrap中的样式导入到Client/src/main.js:

import 'bootstrap/dist/css/bootstrap.css';
import Vue from 'vue';
import App from './App.vue';
import router from './router';

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App),
}).$mount('#app');复制代码

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

<style>
#app {
  margin-top: 60px
}
</style>复制代码

通过使用ping组件中的Button和Container,确保Bootstrap能正确连接:

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

运行服务器:

$ npm run serve复制代码

你可以看到:


接下来,新建一个Books.vue的新文件,并在其中添加一个Books组件:

<template>
  <div class="container">
    <p>books</p>
  </div>
</template>复制代码

更新路由文件router.js:

import Vue from 'vue';
import Router from 'vue-router';
import Books from './components/Books.vue';
import Ping from './components/Ping.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'Books',
      component: Books,
    },
    {
      path: '/ping',
      name: 'Ping',
      component: Ping,
    },
  ],
});复制代码

测试:http://localhost:8080

最后,让我们将Bootstrap-styled表单添加到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>复制代码

你现在应该看到:



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

我们要建什么?

我们的目标是为books设计一个后端RESTful API,由Python和Flask实现。API本身应该遵循RESTful设计原则,并且可以使用基本的HTTP功能:GET、POST、PUT和DELETE。

我们还将在后端API的基础上使用Vue搭建完整的前端应用:



本教程只讨论快乐的构建之路,处理错误是一个单独的练习。可以尝试通过您的理解,自己在前端和后端添加适当的错误处理。

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
    })复制代码

运行flask app,并测试路由http://localhost:5000/books.

想要进行更多的挑战吗?可以为这个程序编写一个自动测试。查看这儿有更多关于测试Flask应用的资源信息。

客户端

更新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>
                <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:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.error(error);
        });
    },
  },
  created() {
    this.getBooks();
  },
};
</script>复制代码

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

查看实例的生命周期钩子可以了解更多有关组件生命周期和可用方法的信息。

在模板中,我们通过v-for指令遍历图书列表,在每次迭代中创建一个新的表行。索引值当做key使用。最后,v-if用于呈现“yes”或“no”,指示用户是否已读过书



Bootstrap Vue

在下一节中,我们将使用一个模式添加一本新书。我们将为此添加一个Bootstrap Vue库,它提供了一组使用基于引导的HTML和CSS样式的Vue组件。

为什么要使用Bootstrap Vue库?Bootstrap的modal组件使用的是jQuery,因此,您应该避免在同一个项目中Bootstrap与Vue一起使用,因为Vue使用的是虚拟DOM来更新DOM。换句话说,如果您使用jQuery操作DOM,Vue将无法知道这些操作。如果您一定要使用jQuery,至少不要在同一个DOM元素上同时使用Vue和jQuery。

安装:

$ npm install bootstrap-vue@2.0.0-rc.19 --save复制代码

在Client/src/main.js中启用Bootstrap Vue库:

import 'bootstrap/dist/css/bootstrap.css';
import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import App from './App.vue';
import router from './router';

Vue.use(BootstrapVue);

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App),
}).$mount('#app');复制代码

POST路由

服务器端

更新现在的路由处理程序,让它支持处理POST请求,从而添加新的书籍:

from flask import Flask, jsonify, request

@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)复制代码

当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端点查看响应中的是否成功添加了新书。

如果标题已经存在怎么办?或者,如果一个标题有一个以上的作者呢?你可以自己尝试解决这些问题来检测你的知识理解。还有,如何处理无效的数据体呢,比如在缺少title、author或read的情况下?

客户端

让我们现在在客户端添加POST模式,以便将新书添加到Books组件中,先从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>复制代码

将它添加到最后结束的dev标签之前。可以查看代码。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-disable-next-line
          console.error(error);
        });
    },
    addBook(payload) {
      const path = 'http://localhost:5000/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
          // eslint-disable-next-line
          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, // property shorthand
      };
      this.addBook(payload);
      this.initForm();
    },
    onReset(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      this.initForm();
    },
  },
  created() {
    this.getBooks();
  },
};
</script>复制代码

这段代码做了什么?

  1. addBookForm 通过v-modal绑定到表单输入。当其中一个被更新时,另一个也会被更新,这叫做双向绑定。花点时间思考一下,你认为这会使状态管理变得更容易还是更困难?React和Angular是如何处理这件事的?在我看来,双向绑定(以及可变性)使Vue比Reaction更容易理解,但从长远来看,更不容易扩展。
  2. 当用户成功提交表单时,将触发onSubmit。在提交时,我们阻止正常的浏览器行为(evt.preitDefault()),关闭模态组件(这里是$rens.addBookModal.hid()),触发addBook方法,并清除表单(initForm())。
  3. addBook向/books发送一个POST请求以添加一本新书。
  4. 自己查看其余的更改,必要时可以参考Vue文档
您能发现客户端或服务器上的潜在错误吗?自行处理这些以提高用户体验。

最后,更新模板中的“AddBook”按钮,以便在单击按钮时显示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>
                <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>
    <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-group>
          <b-button type="submit" variant="primary">Submit</b-button>
          <b-button type="reset" variant="danger">Reset</b-button>
        </b-button-group>
      </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-disable-next-line
          console.error(error);
        });
    },
    addBook(payload) {
      const path = 'http://localhost:5000/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
          // eslint-disable-next-line
          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, // property shorthand
      };
      this.addBook(payload);
      this.initForm();
    },
    onReset(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      this.initForm();
    },
  },
  created() {
    this.getBooks();
  },
};
</script>复制代码

试试看!试着增加一本书:


Alert 组件

接下来,让我们添加一个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 {
      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> 复制代码

刷新浏览器。你现在可以看到:

有关在其他组件中使用某一组件的更多信息,可以查看官方vue文档的Composing with Components部分。

接下来,让我们将b-alert组件添加到template中::

<template>
  <div>
    <b-alert variant="success" show>{{ message }}</b-alert>
    <br>
  </div>
</template>

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

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

<alert message="hi"></alert>复制代码

试试效果:

查看docs以获得更多关于props的信息

要使其具有动态,以便传递自定义消息,可以在Books.vue中使用绑定表达式:

<alert :message="message"></alert>复制代码

将message消息添加到Books.vue中的data选项中:

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-disable-next-line
      console.log(error);
      this.getBooks();
    });
},复制代码

最后,添加一个v-if,因此只有在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-disable-next-line
      console.log(error);
      this.getBooks();
    });
},复制代码

再试试效果怎么样!

挑战:

  1. 思考一下,showMessage什么时候应该设置为false,更新你的代码。
  2. 尝试使用Alert组件提示错误。
  3. 将alert重构为可关闭的.


PUT路由

服务器端

对于更新,我们需要使用唯一的标识符,因为我们不能期待所有标题是唯一的。我们可以使用Python标准库中的UUID。

更新server/app.py中的书籍:

import uuid

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
    }
]复制代码

在添加新书时,重构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)复制代码

添加辅助方法:

def remove_book(book_id):
    for book in BOOKS:
        if book['id'] == book_id:
            BOOKS.remove(book)
            return True
    return False复制代码
花点时间思考一下,您将如何处理id不存在时的情况?如果数据体不正确怎么办?此外,还可以尝试着重构辅助方法中的for循环,使其更加Pythonic。

客户端

步骤:

  1. 添加模态和表单
  2. “更新”按钮
  3. 连接Ajax请求
  4. 用户提示
  5. “取消”按钮

(1)增加模态和表单

首先,在template中添加一个新的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-group>
      <b-button type="submit" variant="primary">Update</b-button>
      <b-button type="reset" variant="danger">Cancel</b-button>
    </b-button-group>
  </b-form>
</b-modal> 复制代码

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

editForm: {
  id: '',
  title: '',
  author: '',
  read: [],
},复制代码
挑战:尝试使用相同的modal来处理POST和PUT请求,而不是使用新的modal。

(2)“更新”按钮

更新表格中的“更新”按钮功能:

<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-disable-next-line
      console.error(error);
      this.getBooks();
    });
},复制代码

(4)用户提示

更新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-disable-next-line
      console.error(error);
      this.getBooks();
    });
},复制代码

(5)“取消”按钮

添加方法:

onResetUpdate(evt) {
  evt.preventDefault();
  this.$refs.editBookModal.hide();
  this.initForm();
  this.getBooks(); // why?
},复制代码

更新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/<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="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-disable-next-line
      console.error(error);
      this.getBooks();
    });
},
onDeleteBook(book) {
  this.removeBook(book.id);
},复制代码

现在,当用户单击“删除”按钮时,onDeleteBook方法被触发,接着触发removeBook方法。此方法将DELETE请求发送到后端。当响应返回时,显示提示消息并运行getBooks。

挑战:

  1. 不要单击按钮后直接删除,而是添加一个确认提示。
  2. 当Books中没有书时,显示一条信息,比如“没有书!请加一本”。


结语

这篇文章涵盖了使用Vue和Flask设置CRUD应用程序的基础知识。

可以检查一下自己的学习效果,从这篇文章开始回顾,并完成其中的每一个挑战。

如果想更多了解,可以查看具体的源码,源码地址为flask-vue-crud

感谢您的阅读。


关注下面的标签,发现更多相似文章
评论