使用 Vue + Flask 搭建單頁應用

小小後端發表於2019-05-13

單頁應用,只載入一個主頁面,然後通過 AJAX 無重新整理載入其它頁面片段。表面上看,就只有一個 HTML 檔案,所謂單頁。開發上,做到了前後端分離,前端專注於渲染模板,而後端只要提供 API 就行,不用自己去套模板了。效果上,頁面和共用的 JS、CSS 檔案都只載入一次,能減輕伺服器壓力和節省一定的網路頻寬。另外,由於不需要每次都載入頁面以及共用的靜態檔案,響應速度也有一定提高,使用者體驗比較好。當然,也有一些缺點,比如 SEO 優化不大方便,不過也有相應的解決方案。總的來說,使用單頁應用的好處還是遠多於壞處,這也是越來越多的人使用單頁應用的原因。

構建單頁應用的方式有很多,這裡我們選擇 Flask + Vue 實現。本文以實現一個 CRUD 的 Demo 為主線,在其中穿插必要的技術點進行講述。裡面可能涉及了一些你沒接觸或者不熟悉的概念,不過不要緊,我會給出相應的參考文章幫助你理解。當然,大牛可忽略這些 :)。看完這篇文章後,相信你也能搭建自己的單頁應用了。

1 前端

這裡我們會用到 Vue 框架。如果你之前沒有接觸過,推薦去看下官方文件的「基礎」一節。也可以先直接向下看,Demo 用的都是一些基礎的東西,大致看下應該就能理解。即使暫時不理解,照著例子實踐一遍後,去看下文件收穫也應該更多。

為了更便捷的建立基於 Vue 的專案,我們可以使用 Vue Cli 腳手架。通過腳手架建立專案的時候,它會輔助我們做一些配置,省去我們手動配置的時間。剛接觸的夥伴前期會用它建立專案就行了,至於更深的一些東西后期再去了解。

安裝腳手架

$ npm install -g @vue/cli
複製程式碼

這裡我們安裝的是最新的 3 版本。

基於 Vue 的 UI 元件庫很多,比如 iView、Element、Vuetify 等。國內使用 iView、Element 的特別多,而使用 Vuetify 的人相對要少很多,不知道是大家看不慣它的 Material Design 風格還是它的中文文件稀缺的緣故。不過我個人挺喜歡 Vuetify 的風格的,所以我會使用這個元件庫搭建前端頁面。

如果你沒使用過這個元件庫,照著本文一步步實踐下去,也能對 Vuetify 的用法有個大致的瞭解。如果這個過程中,感覺碰到的疑問太多,可以看下 YouTube 上的這個視訊教程。

https://dwz.cn/lxMHF4bY

也不要到處去找類似的資源了,就是這個系列的視訊看完再加上官方文件,掌握常用的點基本沒問題。

不過,還是建議先照著本文實現一下 Demo,再去學習,我覺得這樣效果更好。

新建目錄 spa-demo,然後切換到該目錄下新建前端專案 client

$ vue create client
複製程式碼

建立專案時會讓你手動選擇一些配置,這裡貼下我當時的設定

? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)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) N
複製程式碼

回車安裝完成後,我們切換到 client 目錄下,執行命令

$ npm run serve
複製程式碼

上述命令執行完成後會有類似這樣的輸出

...

App running at:
- Local:   http://localhost:8080/
- Network: http://172.20.10.3:8080/

...
複製程式碼

在瀏覽器中訪問

http://localhost:8080/

如果看到包含下面文字的頁面

Welcome to Your Vue.js App

說明專案安裝成功。

安裝 Vuetify

$ vue add vuetify
複製程式碼

同樣會提示你選擇一些配置,這裡我選擇的 Default

? Choose a preset: Default (recommended)
複製程式碼

回車安裝完成後,重新開下伺服器

$ npm run serve
複製程式碼

執行完畢後,我們在瀏覽器中訪問

http://localhost:8080/

會看到頁面內容又些改變,有這麼一行文字

Welcome to Vuetify

這裡說明 Vuetify 安裝成功。

看下此時的目錄結構

spa-demo
└── client
    ├── README.md
    ├── babel.config.js
    ├── package-lock.json
    ├── package.json
    ├── node_module
    │   └── ...
    ├── public
    │   ├── favicon.ico
    │   └── index.html
    └── src
        ├── App.vue
        ├── assets
        │   ├── logo.png
        │   └── logo.svg
        ├── components
        │   └── HelloWorld.vue
        ├── main.js
        ├── plugins
        │   └── vuetify.js
        ├── router.js
        └── views
            ├── About.vue
            └── Home.vue
複製程式碼

簡化 spa-demo/client/src/App.vue,將其修改為

<template>
  <v-app>
    <v-content>
      <router-view></router-view>
    </v-content>
  </v-app>
</template>

<script>
  export default {
    name: 'App',
    data () {
      return {
        //
      }
    }
  }
</script>
複製程式碼

修改 spa-demo/client/src/views/Home.vue,在頁面放入一個 Data table

<template>
  <div class="home">

    <v-container class="my-5">

      <!-- 對話方塊 -->

      <!-- 表格 -->
      <v-data-table
        :headers="headers"
        :items="books"
        hide-actions
        class="elevation-1"
      >
        <template slot="items" slot-scope="props">
          <td>{{ props.item.name }}</td>
          <td>{{ props.item.category }}</td>
          <td class="layout px-0">
            <v-icon small class="ml-4" @click="editItem(props.item)">
              edit
            </v-icon>
            <v-icon small @click="deleteItem(props.item)">
              delete
            </v-icon>
          </td>
        </template>
        <template slot="no-data">
          <v-alert :value="true" color="info" outline>
            無資料
          </v-alert>
        </template>
      </v-data-table>
    </v-container>

  </div>
</template>

<script>
  export default {
    data: () => ({
      headers: [
        { text: '書名', value: 'name', sortable: false, align: 'left'},
        { text: '分類', value: 'category', sortable: false },
        { text: '操作', value: 'name', sortable: false }
      ],
      books: [],
    }),
    created () {
      this.books = [
        { name: '生死疲勞', category: '文學' },
        { name: '國家寶藏', category: '人文社科' },
        { name: '人類簡史', category: '科技' },
      ]
    },
  }
</script>
複製程式碼

我們使用了資料 headers 和 books 控制表的頭部和資料,並在建立的時候,給 books 填充了一些臨時資料。

這個頁面中涉及到了 Data table 的使用,相關程式碼不用記,在 Vuetify 文件中搜尋 Data table 有很多例子,看了幾個之後你就知道怎麼使用了。對於新手來說,不好理解的可能就是那個 slot-scope(作用域插槽 ),這個看下 Vue 官方文件這些內容

  • 「基礎」一節的「元件基礎」
  • 「深入瞭解元件」一節的「元件註冊」、「Prop」、「自定義事件」、「插槽」

靜下心來讀讀就明白了,不難,這裡我不再贅述。

同樣,這裡你也可以先照葫蘆畫瓢,可以先暫時忽略掉一些不好理解的地方,待實踐一遍之後再去搞清楚。

開啟

http://localhost:8080/

看到的頁面是這樣的

使用 Vue + Flask 搭建單頁應用

就是一個圖書列表。

現在我們要做個可以彈出的對話方塊,用於新增書籍。我們在 <!-- 對話方塊 --> 位置新增如下程式碼

<v-toolbar flat class="white">
  <v-toolbar-title>圖書列表</v-toolbar-title>
  <v-spacer></v-spacer>
  <v-dialog v-model="dialog" max-width="600px">
    <v-btn slot="activator" class="primary" dark>新增</v-btn>
    <v-card>
      <v-card-title>
        <span class="headline">{{ formTitle }}</span>
      </v-card-title>
      <v-card-text>
        <v-alert :value="Boolean(errMsg)" color="error" icon="warning" outline>
          {{ errMsg }}
        </v-alert>
        <v-container grid-list-md>
          <v-layout>
            <v-flex xs12 sm6 md4>
              <v-text-field label="書名" v-model="editedItem.name"></v-text-field>
            </v-flex>
            <v-flex xs12 sm6 md4>
              <v-text-field label="分類" v-model="editedItem.category"></v-text-field>
            </v-flex>
          </v-layout>
        </v-container>
      </v-card-text>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn color="blue darken-1" flat @click="close">取消</v-btn>
        <v-btn color="blue darken-1" flat @click="save">儲存</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</v-toolbar>
複製程式碼

對應的,要在 <script></script> 之間新增一些 JS

export default {
  data: () => ({
    dialog: false, // 是否展示對話方塊
    errMsg: '', // 是否有錯誤資訊
    editedIndex: -1, // 當前在對話方塊中編輯的圖書在列表中的序號
    editedItem: { // 當前在對話方塊中編輯的圖書內容
      id: 0,
      name: '',
      category: ''
    },
    defaultItem: { // 預設的圖書內容,用於初始化新增對話方塊內容
      id: 0,
      name: '',
      category: ''
    }
  }),
  computed: {
    formTitle () {
      return this.editedIndex === -1 ? '新增' : '編輯'
    }
  },
  watch: {
    dialog (val) {
      if (!val) {
        this.close()
        this.clearErrMsg()
      }
    }
  },
  methods: {
    clearErrMsg () {
      this.errMsg = ''
    },
    close () {
      this.dialog = false
      setTimeout(() => {
        this.editedItem = Object.assign({}, this.defaultItem)
        this.editedIndex = -1
      }, 300)
    }
  }
}
複製程式碼

為了讓文章簡潔一些,貼程式碼的時候我將之前已有的片段進行了省略,你寫的時候可以將上面的程式碼根據位置新增到合適的地方。

我們使用了 Toolbar、Dialog 在表格上面新增對話方塊相關的東西,同樣,不必記程式碼,不知道怎麼寫的時候查閱下文件就行。

資料 dialog 表示當前對話方塊是否展示,errMsg 控制錯誤資訊的展示,監聽 dialog 當它變化為 false 的時候關閉對話方塊並清空 errMsg。計算屬性 formTitle 用於控制對話方塊的標題。然後新增了兩個表單元素用於填寫書籍的名稱以及分類。

當我們點選新增後,頁面是這樣的

使用 Vue + Flask 搭建單頁應用

其實,到這裡,我們的前端頁面差不多就 OK 了,後面便是增刪改的實現。這個我們先在前端單方面的實現下,後面再和後端進行整合。這樣,會讓前端的 Demo 更完整一些。

實現儲存方法,在 methods 新增 save

save() {
  if (this.editedIndex > -1) { // 編輯
    Object.assign(this.books[this.editedIndex], this.editedItem)
  } else { // 新增
    this.books.push(this.editedItem)
  }
  this.close()
}
複製程式碼

編輯的時候,要展示彈框,我們需要新增 editItem 方法

editItem (item) {
  this.editedIndex = this.books.indexOf(item)
  this.editedItem = Object.assign({}, item)
  this.dialog = true
}
複製程式碼

儲存方法和新增時的一致。

實現刪除方法 deleteItem

deleteItem (item) {
  const index = this.books.indexOf(item)
  confirm('確認刪除?') && this.books.splice(index, 1)
}
複製程式碼

至此,前端專案告一段落。

2 後端

後端,我們只需要提供增刪改查的介面供前端使用就行。RESTful API 是目前比較成熟的一套網際網路應用程式設計理論,我也會基於此實現圖書的相關操作介面。

考慮到有對 RESTful API 不大熟悉的夥伴,我列了幾個我之前學習的文章,供大家參考。

  • 《理解RESTful架構》
    • https://dwz.cn/eXu0p6pv
  • 《RESTful API 設計指南》
    • https://dwz.cn/8v4B0twY
  • 《RESTful API 最佳實踐》
    • https://dwz.cn/2aSnI8fF
  • 知乎問題《怎樣用通俗的語言解釋REST,以及RESTful?》
    • https://dwz.cn/bVxrSsf4

看完上面的相關資料,你對這種設計理論應該就有一定掌握了。

同樣,你暫時可不必對 RESTful API 瞭解得很全面,暫時像下面這樣理解它就行

就是用 URL 定位資源,用 HTTP 描述操作。

這個是在刷上面知乎問題看到的一個回答,作者是 @Ivony。寫得很簡潔,但確實有道理。

等到自己實踐一次後,再回頭看看理論的一些東西,印象更深。

首先列下我們需要實現的介面

序號 方法 URL 描述
1 GET http://domain/api/v1/books 獲取所有圖書
2 GET http://domain/api/v1/books/123 獲取主鍵為 123 的圖書
3 POST http://domain/api/v1/books 新增圖書
4 PUT http://domain/api/v1/books/123 更新主鍵為 123 的圖書
5 DELETE http://domain/api/v1/books/123 刪除主鍵為 123 的圖書

我們可以直接使用 Flask 實現上面的介面,不過當資源多的時候,我們寫程式碼時會寫很多重複的片段,違反了 DRY(Don't Repeat Yourself) 原則,後面維護起來比較麻煩,所以我們藉助 Flask-RESTful 擴充套件實現。

另外,本節的重心是放在介面的實現上,也為了行文更簡潔,我們將資料直接存在字典裡,就不涉及資料庫相關的操作了。

在 spa-demo 目錄下新建 server 目錄,並切換到該目錄下,初始化 Python 環境

$ pipenv --python 3.6.0
複製程式碼

Pipenv 是當前官方推薦的虛擬環境和包管理工具,我之前寫過一篇文章《Pipenv 快速上手》介紹過,沒接觸過的可以去看下。

安裝 Flask

$ pipenv install flask
複製程式碼

安裝 Flask-RESTful

$ pipenv install flask-restful
複製程式碼

新建 spa-demo/server/app.py

# coding=utf-8

from flask import Flask, request
from flask_restful import Api, Resource, reqparse, abort


app = Flask(__name__)
api = Api(app)


books = [{'id': 1, 'name': 'book1', 'category': 'cat1'},
         {'id': 2, 'name': 'book2', 'category': 'cat2'},
         {'id': 3, 'name': 'book3', 'category': 'cat3'}]


# 公共方法區


class BookApi(Resource):
    def get(self, book_id):
        pass

    def put(self, book_id):
        pass

    def delete(self, book_id):
        pass


class BookListApi(Resource):
    def get(self):
        return books

    def post(self):
        pass


api.add_resource(BookApi, '/api/v1/books/<int:book_id>', endpoint='book')
api.add_resource(BookListApi, '/api/v1/books', endpoint='books')

if __name__ == '__main__':
    app.run(debug=True)
複製程式碼

上面就是一個標準的整合了 Flask-RESTful 的程式碼結構,在 Flask-RESTful 的官方文件中可以看到相似的例子。對於每一種資源,我們都可以用類似的結構實現介面。BookApi 類中的 get、put、delete 方法對應介面 2、4、5,BookListApi 類中的 get、post 方法對應介面 1、3。之後便是註冊路由。看到這,有的夥伴可能會有疑問,為什麼同一個資源需要定義兩個類呢?其實就是方便給一個資源註冊帶主鍵和不帶主鍵的路由。

此時,專案結構為

spa-demo
├── client
│   └── ...
└── server
    ├── Pipfile
    ├── Pipfile.lock
    └── app.py
複製程式碼

切換到 spa-demo/server 目錄,執行 app.py

$ pipenv run python app.py
複製程式碼

然後測試獲取所有圖書介面是否可用。由於是 API 測試,不建議直接使用瀏覽器,畢竟有時構造引數和看 HTTP 資訊不大方便,推薦使用 Postman,當然簡單的測試的話可以直接使用命令 curl。

請求介面 1,獲取所有圖書資訊

$ curl -i http://127.0.0.1:5000/api/v1/books
複製程式碼

得到結果

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 249
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:21:56 GMT

[
    {
        "id": 1,
        "name": "book1",
        "category": "cat1"
    },
    {
        "id": 2,
        "name": "book2",
        "category": "cat2"
    },
    {
        "id": 3,
        "name": "book3",
        "category": "cat3"
    }
]
複製程式碼

成功獲取所有圖書,說明介面 1 已經 OK。

然後實現介面 2,獲取指定 ID 的圖書。由於根據 ID 獲取圖書以及圖書不存在時拋 404 的操作後面會頻繁使用到,所以這裡提兩個方法到「公共方法區」。

def get_by_id(book_id):
    book = [v for v in books if v['id'] == book_id]
    return book[0] if book else None


def get_or_abort(book_id):
    book = get_by_id(book_id)
    if not book:
        abort(404, message=f'Book {book_id} not found')
    return book
複製程式碼

然後實現 BookApi 中 get 方法

def get(self, book_id):
    book = get_or_abort(book_id)
    return book
複製程式碼

取 ID 為 1 的圖書測試下

$ curl -i http://127.0.0.1:5000/api/v1/books/1
複製程式碼

結果

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 61
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:31:48 GMT

{
    "id": 1,
    "name": "book1",
    "category": "cat1"
}
複製程式碼

取 ID 為 5 的圖書測試下

$ curl -i http://127.0.0.1:5000/api/v1/books/5
複製程式碼

結果

HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 149
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:32:47 GMT

{
    "message": "Book 5 not found. You have requested this URI [/api/v1/books/5] but did you mean /api/v1/books/<int:book_id> or /api/v1/books ?"
}
複製程式碼

ID 為 1 時,成功獲取到圖書資訊;ID 為 5 時,由於圖書不存在,所以會返回 404 的響應。測試結果與預期一致,說明這個介面也 OK 了。

實現介面 3,新增圖書。新增圖書的時候,我們應該校驗引數是否符合要求。Flask-RESTFul 給我們提供了比較優雅的實現,不需要我們使用多個 if 判斷的硬編碼的形式去檢測引數是否有效。

由於圖書名稱和分類都是不能為空的,我們需要自定義規則,我們可以在「公共方法區」新增一個方法

def not_empty_str(s):
    s = str(s)
    if not s:
        raise ValueError("Must not be empty string")
    return s
複製程式碼

重寫 BookListApi 的初始化方法

def __init__(self):
    self.reqparse = reqparse.RequestParser()
    self.reqparse.add_argument('name', type=not_empty_str, required=True, location='json')
    self.reqparse.add_argument('category', type=not_empty_str, required=True, location='json')
    super(BookListApi, self).__init__()
複製程式碼

然後實現 post 方法

def post(self):
    args = self.reqparse.parse_args()
    book = {
        'id': books[-1]['id'] + 1 if books else 1,
        'name': args['name'],
        'category': args['category'],
    }
    books.append(book)
    return book, 201
複製程式碼

方法中,首先檢測引數是否有效,然後取最後一本書的 ID 加上 1 作為新書的 ID 儲存,最後返回新增的圖書資訊和狀態碼 201(表示已建立)。

測試下引數校驗是否 OK

$ curl -i \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{"name":"","category":""}' \
    http://127.0.0.1:5000/api/v1/books
複製程式碼

結果

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json
Content-Length: 70
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:46:18 GMT

{
    "message": {
        "name": "Must not be empty string"
    }
}
複製程式碼

返回 400 的錯誤,說明引數校驗有效。

看下新增介面是否可用

$ curl -i \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{"name":"t_name","category":"t_cat"}' \
    http://127.0.0.1:5000/api/v1/books
複製程式碼

結果

HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 63
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:53:54 GMT

{
    "id": 4,
    "name": "t_name",
    "category": "t_cat"
}
複製程式碼

說明建立成功。我們通過獲取指定 ID 的圖書介面確認下

$ curl -i http://127.0.0.1:5000/api/v1/books/4
複製程式碼

結果

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 63
Server: Werkzeug/0.14.1 Python/3.6.0
Date: Thu, 13 Dec 2018 15:54:18 GMT

{
    "id": 4,
    "name": "t_name",
    "category": "t_cat"
}
複製程式碼

獲取成功,說明確實建立成功,說明介面 3 也好了。

介面 4、5 的實現與上面類似,這裡貼下程式碼,就不詳細說明了。

和 BookListApi 類似,首先重寫 BookApi 的初始化方法

def __init__(self):
    self.reqparse = reqparse.RequestParser()
    self.reqparse.add_argument('name', type=not_empty_str, required=True, location='json')
    self.reqparse.add_argument('category', type=not_empty_str, required=True, location='json')
    super(BookApi, self).__init__()
複製程式碼

然後實現 put 和 delete 方法

def put(self, book_id):
    book = get_or_abort(book_id)
    args = self.reqparse.parse_args()
    for k, v in args.items():
        book[k] = v
    return book, 201

def delete(self, book_id):
    book = get_or_abort(book_id)
    del book
    return '', 204
複製程式碼

至此,後端專案基本完畢。

當然,這是不完整的,比如這裡面都沒有實現對 API 的認證,這個可以通過 Flask-HTTPAuth 或者其它方式實現。限於篇幅,這裡就不展開說明了,有興趣的可以看下這個這個擴充套件的文件或者自己研究實現下。

3 整合

單獨的前端或後端都有了雛形,就差整合這一步了。

前端需要請求資料,這裡我們使用 axios,切換到 spa-demo/client 目錄下進行安裝

$ npm install axios --save
複製程式碼

修改 spa-demo/client/src/views/Home.vue,在 script 標籤之間引入 axios,並初始化 API 地址

import axios from 'axios'

const booksApi = 'http://localhost:5000/api/v1/books'

export default {
  ...
}
複製程式碼

修改鉤子 created 的邏輯,從後端獲取資料

created () {
  axios.get(booksApi)
    .then(response => {
      this.books = response.data
    })
    .catch(error => {
      console.log(error)
    })
}
複製程式碼

執行前端專案後,檢視首頁,會發現沒有資料。檢視開發者工具,我們會發現這麼一個錯誤

Access to XMLHttpRequest at 'http://localhost:5000/api/v1/books' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
複製程式碼

就是說當前專案不支援 CORS(Cross-Origin Resource Sharing,即跨域資源訪問)。這個我們可以在前端新增代理的形式實現,也可以在後端通過 Flask-CORS 實現。這裡,我使用的後者。

切換到 spa-demo/server 目錄,安裝 Flask-CORS

$ pipenv install flask-cors
複製程式碼

修改 spa-demo/server/app.py,在頭部引入 CORS

from flask_cors import CORS
複製程式碼

在程式碼

app = Flask(__name__)
複製程式碼

api = Api(app)
複製程式碼

之間新增一行

CORS(app, resources={r"/api/*": {"origins": "*"}})
複製程式碼

然後重新執行 app.py,重新整理首頁,我們會看到列表有資料了,說明 CORS 的問題成功解決。

spa-demo/client/src/views/Home.vue 中,修改 save 方法,同時新增輔助方法 setErrMsg

setErrMsg (errResponse) {
  let errResMsg = errResponse.data.message
  if (typeof errResMsg === 'string') {
    this.errMsg = errResMsg
  } else {
    let errMsgs = []
    let k
    for (k in errResMsg) {
      errMsgs.push('' + k + ' ' + errResMsg[k])
    }
    this.errMsg = errMsgs.join(',')
  }
},
save() {
  if (this.editedIndex > -1) { // 編輯
    axios.put(booksApi + '/' + this.editedItem.id, this.editedItem)
    .then(response => {
      Object.assign(this.books[this.editedIndex], response.data)
      this.close()
    }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
  } else { // 新增
    axios.post(booksApi, this.editedItem)
      .then(response => {
        this.books.push(response.data)
        this.close()
      }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
  }
}
複製程式碼

此時,圖書新增、儲存搞定。

修改 deleteItem 方法

deleteItem (item) {
  const index = this.books.indexOf(item)
  confirm('確認刪除?') && axios.delete(booksApi + '/' + this.books[0].id)
    .then(response => {
      this.books.splice(index, 1)
    }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
}
複製程式碼

此時,刪除方法也搞定了。

至此,整合完畢,基於 Vue + Flask 的前後端分離的一個 CRUD Demo 就完成了。

看完本文,你可以按著步驟自己實現下。剛接觸的夥伴在看的過程中在某些地方可能有疑惑,我也在我能想到的地方提供了一些資料,你可以試著看下。如果沒能提供全,你需要自己百度/谷歌下解決。不過,我還是建議不要妄求每個點都瞭解的特別清楚,先明白關鍵點,試著實現一下,回頭去看相關資料的時候,也更有感觸一些。

完整程式碼可到 GitHub 檢視

https://github.com/kevinbai-cn/spa-demo

4 參考

  • 《Full-stack single page application with Vue.js and Flask》
    • https://bit.ly/2C9kSiG
  • 《Developing a Single Page App with Flask and Vue.js》
    • https://bit.ly/2ElaXrB
  • 《Vuetify Documents》
    • https://bit.ly/2QupMzx
  • 《Designing a RESTful API with Python and Flask》
    • https://bit.ly/2vqq3Y1
  • 《Designing a RESTful API using Flask-RESTful》
    • https://bit.ly/2nGDNtL

本文首發於公眾號「小小後端」。

相關文章