[譯] 用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索