[译] 用 Flask 和 Vue.js 开发一个单页面应用

这篇文章会一步一步的教会你如何用 VUE 和 Flask 建立一个基础的 CRUD 应用。咱们将从使用 Vue CLI 建立一个新的 Vue 应用开始,接着咱们会使用 Python 和 Flask 提供的后端接口 RESTful API 执行基础的 CRUD 操做。css

最终效果:html

final app

主要依赖:前端

  • Vue v2.5.2
  • Vue CLI v2.9.3
  • Node v10.3.0
  • npm v6.1.0
  • Flask v1.0.2
  • Python v3.6.5

目录

目的

在本教程结束的时候,你可以...vue

  1. 解释什么是 Flask
  2. 解释什么是 Vue 而且它和其余 UI 库以及 Angular、React 等前端框架相比又如何
  3. 使用 Vue CLI 搭建一个 Vue 项目
  4. 在浏览器中建立并渲染 Vue 组件
  5. 使用 Vue 组件建立一个单页面应用(SPA)
  6. 将一个 Vue 应用与后端的 Flask 链接
  7. 使用 Flask 开发一个 RESTful API
  8. 在 Vue 组件中使用 Bootstrap 样式
  9. 使用 Vue Router 去建立路由和渲染组件

什么是 Flask?

Flask 是一个用 Python 编写的简单,可是及其强大的轻量级 Web 框架,很是适合用来构建 RESTful API。就像 Sinatra(Ruby)和 Express(Node)同样,它也十分简便,因此你能够从小处开始,根据需求构建一个十分复杂的应用。node

第一次使用 Flask?看看这下面两个教程吧:python

  1. Flaskr TDD
  2. Flask for Node Developers

什么是 Vue?

Vue 是一个用于构建用户界面的开源 JavaScript 框架。它综合了一些 React 和 Angular 的优势。也就是说,与 React 和 Angular 相比,它更加友好,因此初学者额可以很快的学习并掌握。它也一样强大,所以它可以提供全部你须要用来建立一个前端应用所须要的功能。react

有关 Vue 的更多信息,以及使用它与 Angular 和 React 的利弊,请查看如下文章:jquery

  1. Vue: Comparison with Other Frameworks
  2. Angular vs. React vs. Vue: A 2017 comparison

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

安装 Flask

首先建立一个新项目文件夹:webpack

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

在 “flask-vue-crud” 文件夹中,建立一个新文件夹并取名为 “server”。而后,在 “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?为了进行跨域请求 — e.g.,来自不一样协议,IP 地址,域名或端口的请求 — 你须要容许 跨域资源共享(CORS)。而这正是 Flask-CORS 能为咱们提供的。

值得注意的是上述安装容许跨域请求在所有路由不管任何域,协议或者端口均可用。在生产环境中,你应该容许跨域请求成功在前端应用托管的域上。参考 Flask-CORS 文档 得到更多信息。

运行应用:

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

开始测试,将你的浏览器指向到 http://localhost:5000/ping。你将会看到:

"pong!"
复制代码

返回终端,按下 Ctrl+C 来终止服务端而后退回到项目根目录。接下来,让咱们把注意力转到前端进行 Vue 的安装。

安装 Vue

咱们将会使用强力的 Vue CLI 来生成一个自定义项目模板。

全局安装:

$ npm install -g vue-cli@2.9.3
复制代码

第一次使用 npm?浏览一下 什么是 npm? 官方指南吧

而后,在 “flask-vue-crud” 中,运行如下命令初始化一个叫作 client 的新 Vue 项目并包含 webpack 配置:

$ vue init webpack client
复制代码

webpack 是一个模块打包构建工具,用于构建,压缩以及打包 JavaScript 文件和其余客户端资源。

它会请求你对这个项目进行一些配置。按下回车键去选择前三个为默认设置,而后使用如下的设置去完成后续的配置:

  1. Vue build: Runtime + Compiler
  2. Install vue-router?: Yes
  3. Use ESLint to lint your code?: Yes
  4. Pick an ESLint preset: Airbnb
  5. Set up unit tests: No
  6. Setup e2e tests with Nightwatch: No
  7. Should we run npm install for you after the project has been created: Yes, use NPM

你会看到一些配置请求好比:

? Project name client
? Project description A Vue.js project
? Author Michael Herman michael@mherman.org
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Airbnb
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm
复制代码

快速浏览一下生成的项目架构。看起来好像特别多,可是咱们会用到那些在 “src” 中的文件和 index.html 文件。

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

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>client</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
复制代码

注意那个 idapp<div> 元素。那是一个占位符,Vue 将会用来链接生成的 HTML 和 CSS 构建 UI。

注意那些在 “src” 文件夹中的文件夹:

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

分解:

名字 做用
main.js app 接入点,将会和根组件一块儿加载并初始化 Vue
App.vue 根组件 —— 起点,全部其余组件都将今后处开始渲染
“assets” 储存图像和字体等静态资源
“components” 储存 UI 组件
“router” 定义 URL 地址并映射到组件

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

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

运行开发服务端:

$ cd client
$ npm run dev
复制代码

在你的浏览器中导航到 http://localhost:8080。你将会看到:

default vue app

添加一个新组件在 “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 组件:

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 里删除掉图片:

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

你如今应该能在浏览器中看见一个 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 你会看到 pong!。基本上,当咱们从后端获得回复的时候,咱们会将 msg 设置为响应对象的 data 的值。

安装 Bootstrap

接下来,让咱们引入一个热门 CSS 框架 Bootstrap 到应用中以方便咱们快速添加一些样式。

安装:

$ npm install bootstrap@4.1.1 --save
复制代码

忽略 jquerypopper.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-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>',
});
复制代码

更新 client/src/App.vue 中的 style

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

经过使用 ButtonContainer 确保 Bootstrap 在 Ping 组件中正确链接:

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

运行开发服务端:

$ npm run dev
复制代码

你应该会看到:

vue with bootstrap

而后,添加一个叫作 Books 的新组件到新文件 Books.vue 中:

<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 中的哈希值吗?更改 modehistory 以使用浏览器的 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>
复制代码

你如今应该会看到:

books component

如今咱们能够开始构建咱们的 CRUD 应用的功能。

咱们的目的是什么?

咱们的目标是设计一个后端 RESTful API,由 Python 和 Flask 驱动,对应一个单一资源 — books。这个 API 应当遵照 RESTful 设计原则,使用基本的 HTTP 动词:GET、POST、PUT 和 DELETE。

咱们还会使用 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:5000/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>
                <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-disable-next-line
          console.error(error);
        });
    },
  },
  created() {
    this.getBooks();
  },
};
</script>
复制代码

当组件初始化完成后,经过 created 生命周期钩子调用 getBooks() 方法,它从咱们刚刚设置的后端接口获取书籍。

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

在模板中,咱们经过 v-for 指令遍历书籍列表,每次遍历建立一个新表格行。索引值用做 key。最后,使用 v-ifYesNo,来表现用户已读或未读这本书。

books component

Bootstrap Vue

在下一节中,咱们将会使用一个模态去添加新书。为此,咱们在本节会加入 Bootstrap Vue 库到项目中,它提供了一组基于 Bootstrap 的 HTML 和 CSS 设计的 Vue 组件。

为何选择 Bootstrap Vue?Bootstrap 的 模态 组件使用 jQuery,但你应该避免把它和 Vue 在同一项目中一块儿使用,由于 Vue 使用 虚拟 DOM 来更新 DOM。换句话来讲,若是你用 jQuery 来操做 DOM,Vue 不会有任何反应。至少,若是你必定要使用 jQuery,不要在同一个 DOM 元素上同时使用 jQuery 和 Vue。

安装:

$ npm install bootstrap-vue@2.0.0-rc.11 --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';
import router from './router';

Vue.config.productionTip = false;

Vue.use(BootstrapVue);

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>',
});
复制代码

POST 路由

服务端

更新现有路由以处理添加新书的 POST 请求:

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

更新 imports:

from flask import Flask, jsonify, request
复制代码

运行 Flask 服务端后,你能够在新的终端里测试 POST 路由:

$ curl -X POST http://localhost:5000/books -d \
  '{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
  -H 'Content-Type: application/json'
复制代码

你应该会看到:

{
  "message": "Book added!",
  "status": "success"
}
复制代码

你应该会在 http://localhost:5000/books 的末尾看到新书。

若是书名已经存在了呢?若是一个书名对应了几个做者呢?经过处理这些小问题能够加深你的理解,另外,如何处理 书名做者,以及 阅览状态 都缺失的无效负载状况。

客户端

在客户端上,让咱们添加那个模态以添加一本新书,从 HTML 开始:

<b-modal ref="addBookModal"
         id="book-modal"
         title="Add a new book"
         hide-footer>
  <b-form @submit="onSubmit" @reset="onReset" class="w-100">
  <b-form-group id="form-title-group"
                label="Title:"
                label-for="form-title-input">
      <b-form-input id="form-title-input"
                    type="text"
                    v-model="addBookForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-group"
                  label="Author:"
                  label-for="form-author-input">
        <b-form-input id="form-author-input"
                      type="text"
                      v-model="addBookForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-group">
      <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button type="submit" variant="primary">Submit</b-button>
    <b-button type="reset" variant="danger">Reset</b-button>
  </b-form>
</b-modal>
复制代码

div 标签中添加这段代码。而后简单阅览一下。v-model 是一个用于 表单输入绑定 的指令。你立刻就会看到。

hide-footer 具体干了什么?在 Bootstrap Vue 的 文档 中了解更多

更新 script 部分:

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-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-model。当数据更新时,另外一个也会跟着更新。这被称之为双向绑定。花点时间从 这里 了解一下吧。想一想这个带来的结果。你认为这会使状态管理更简单仍是更复杂?React 和 Angular 又会如何作到这点?在我看来,双向数据绑定(可变性)使得 Vue 和 React 相比更加友好,可是从长远看扩展性不足。

  2. onSubmit 会在用户提交表单成功时被触发。在提交时,咱们会阻止浏览器的正常行为(evt.preventDefault()),关闭模态框(this.$refs.addBookModal.hide()),触发 addBook 方法,而后清空表单(initForm())。

  3. addBook 发送一个 POST 请求到 /books 去添加一本新书。

  4. 根据本身的须要查看其余更改,并根据须要参考 Vue 的 文档

你能想到客户端或者服务端还有什么潜在的问题吗?思考这些问题去试着增强用户体验吧。

最后,更新 template 中的 “Add Book” 按钮,这样一来咱们点击按钮就会显示出模态框:

<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></td>
              <td></td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <button type="button" class="btn btn-warning btn-sm">Update</button>
                <button type="button" class="btn btn-danger btn-sm">Delete</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    <b-modal ref="addBookModal"
             id="book-modal"
             title="Add a new book"
             hide-footer>
      <b-form @submit="onSubmit" @reset="onReset" class="w-100">
      <b-form-group id="form-title-group"
                    label="Title:"
                    label-for="form-title-input">
          <b-form-input id="form-title-input"
                        type="text"
                        v-model="addBookForm.title"
                        required
                        placeholder="Enter title">
          </b-form-input>
        </b-form-group>
        <b-form-group id="form-author-group"
                      label="Author:"
                      label-for="form-author-input">
            <b-form-input id="form-author-input"
                          type="text"
                          v-model="addBookForm.author"
                          required
                          placeholder="Enter author">
            </b-form-input>
          </b-form-group>
        <b-form-group id="form-read-group">
          <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
            <b-form-checkbox value="true">Read?</b-form-checkbox>
          </b-form-checkbox-group>
        </b-form-group>
        <b-button type="submit" variant="primary">Submit</b-button>
        <b-button type="reset" variant="danger">Reset</b-button>
      </b-form>
    </b-modal>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-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>
复制代码

赶忙测试一下!试着添加一本书:

add new book

alert 组件

接下来,让咱们添加一个 Alert 组件,当添加一本新书后,它会显示一个信息给当前用户。咱们将为此建立一个新组件,由于你之后可能会在不少组件中常常用到这个功能。

添加一个新文件 Alert.vue 到 “client/src/components” 中:

<template>
  <p>It works!</p>
</template>
复制代码

而后,在 Books 组件的 script 中引入它并注册这个组件:

<script>
import axios from 'axios';
import Alert from './Alert';

...

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  components: {
    alert: Alert,
  },

  ...

};
</script>
复制代码

如今,咱们能够在 template 中引用这个新组件:

<template>
  <b-container>
    <b-row>
      <b-col col sm="10">
        <h1>Books</h1>
        <hr><br><br>
        <alert></alert>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>

        ...

      </b-col>
    </b-row>
  </b-container>
</template>
复制代码

刷新浏览器,你会看到:

bootstrap alert

从 Vue 官方文档的 组件化应用构建 中得到更多有关组件化应用构建的信息。

接下来,让咱们加入 b-alert 组件到 template 中:

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

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

记住 script 中的 props 选项。咱们能够从父组件(Books)传递信息,就像这样:

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

试试这个:

bootstrap alert

文档 中获取更多 props 相关信息。

为了方便咱们动态传递自定义消息,咱们须要在 Books.vue 中使用 bind 绑定数据。

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

message 添加到 Books.vue 中的 data 中:

data() {
  return {
    books: [],
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    message: '',
  };
},
复制代码

接下来,在 addBook 中,更新 message 内容。

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

添加 showMessagedata 中:

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

赶快测试一下吧!

add new book

挑战:

  1. 想一想什么状况下 showMessage 应该被设定为 false。更新你的代码。
  2. 试着用 Alert 组件去显示错误信息。
  3. 修改 Alert 为 可取消 的样式。

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

添加辅助方法:

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 中,就在第一个模态下面:

<b-modal ref="editBookModal"
         id="book-update-modal"
         title="Update"
         hide-footer>
  <b-form @submit="onSubmitUpdate" @reset="onResetUpdate" class="w-100">
  <b-form-group id="form-title-edit-group"
                label="Title:"
                label-for="form-title-edit-input">
      <b-form-input id="form-title-edit-input"
                    type="text"
                    v-model="editForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-edit-group"
                  label="Author:"
                  label-for="form-author-edit-input">
        <b-form-input id="form-author-edit-input"
                      type="text"
                      v-model="editForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-edit-group">
      <b-form-checkbox-group v-model="editForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button type="submit" variant="primary">Update</b-button>
    <b-button type="reset" variant="danger">Cancel</b-button>
  </b-form>
</b-modal>
复制代码

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

editForm: {
  id: '',
  title: '',
  author: '',
  read: [],
},
复制代码

挑战:不使用新的模态,使用一个模态框处理 POST 和 PUT 请求。

(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 = [];
},
复制代码

在继续下一步以前先检查一下代码。检查结束后,测试一下应用。确保按钮按下后显示模态框,并正确显示输入值。

update book

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 方法会被调用。这个方法会发送删除请求到后端。当返回响应后,通知消息会显示出来而后 getBooks 会被调用。

挑战:

  1. 在删除按钮点击时加入一个确认提示。
  2. 当没有书的时候,显示一个“没有书籍,请添加”消息。

delete book

总结

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

从头回顾这篇文章以及其中的挑战来加深你的理解。

你能够在 flask-vue-crud 仓库 中的 v1 标签里找到源码。感谢你的阅读。

想知道更多? 看看这篇文章的续做 Accepting Payments with Stripe, Vue.js, and Flask

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索