Flask和Vue.js構建全棧單頁面web應用【經過Flask開發RESTful API】

前言css

看了一些國外的關於介紹flask和vue的先後端分離的文章,但沒看到比較通俗易懂,代碼完善的,昨天看到一篇很新的文章,並且內容很是棒,因此想翻譯過來,供你們一塊兒學習。html

原文來自Developing a CRUD App with Flask and Vue.js前端

正文:vue

下面會逐步演示經過Flask和Vue如何完成一個基本的CRUD應用程序。咱們將從搭建框架開始,使用Vue CLI構建一個新的Vue應用程序,而後經過Python和Flask開發的RESTful API執行基本的CRUD操做。node

主要依賴的庫包括:python

  • 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

本文章的目標

在文章結束時,你將可以知道:react

  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(節點)同樣,它很是小並且很靈活,因此你能夠先開始一個小型的應用,並在它的基礎上根據需求創建更加複雜的應用程序。jquery

若是是第一次使用Flask,能夠參考如下兩個學習資源:ios

  1. Flaskr TDD
  2. Node開發者的Flask

什麼是Vue?

VUE是一個開源JavaScript框架,用於構建用戶界面。它採用了React和Angular方面的一些最佳作法。也就是說,與React和Angular相比,它更平易近人,因此初學者能夠快速地開始和運用Vue。它一樣很強大,提供了建立最新前端應用程序所須要的全部功能。git

有關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

感謝您的閱讀。

相關文章
相關標籤/搜索