Single-page applications that load only one main page and then load other page fragments without a refresh through AJAX. On the surface, there is only one HTML file, called a single page. Development, to achieve the separation of the front and back end, front-end focus on rendering templates, and the back end as long as the API on the line, do not have to set their own templates. In effect, pages and shared JS and CSS files are loaded only once, which can reduce server pressure and save some network bandwidth. In addition, because there is no need to load pages and common static files every time, the response speed is also improved, and the user experience is better. Of course, there are some disadvantages, such as SEO optimization is not convenient, but there are corresponding solutions. Overall, the benefits of using a one-page app far outweigh the disadvantages, which is why more and more people are using one-page apps.

There are many ways to build a single page application, and here we choose the Flask + Vue implementation. This article takes the implementation of a CRUD Demo as the main line, interspersed with the necessary technical points. It may cover some concepts that you are not familiar with or familiar with, but don’t worry, I will give you a reference article to help you understand. Of course, Daniel can ignore these :). After reading this article, you should be able to build your own one-page app.

1 the front-end

Here we will use the Vue framework. If you haven’t seen this before, I recommend you check out the “Basics” section of the official documentation. Or you can go straight down, because the Demo is pretty basic, so it should make sense. Even if you don’t understand it at the moment, you should learn more when you read the document after you practice it.

To make it easier to create Vue based projects, we can use the Vue Cli scaffolding. When creating a project through scaffolding, it will help us do some configuration, saving us the time of manual configuration. New partners will use it to create projects in the early stage, as for some deeper things to understand later.

Erection of scaffolding

$ npm install -g @vue/cli
Copy the code

Here we have installed the latest version 3.

There are many UE-BASED UI component libraries, such as iView, Element, Vuetify, etc. IView and Element are widely used in China, while Vuetify is used by relatively few people. I don’t know whether it is because people are not used to its Material Design style or its Chinese documents are scarce. But I personally like the Vuetify style, so I use this component library to build the front end.

If you haven’t used Vuetify, follow this article step by step to get a sense of what Vuetify can do. If you get too many questions along the way, check out this video on YouTube.

https://dwz.cn/lxMHF4bY

Don’t go looking for similar resources, but after watching this series of videos plus the official documentation, it’s basically no problem to master common points.

However, it is still recommended to implement the Demo according to this article first, and then to learn, I think this effect is better.

Create a directory spa-demo and switch to this directory to create a front-end project client

$ vue create client
Copy the code

When you create a project, you will be asked to manually select some configurations. Here are my Settings for that time

? 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
Copy the code

After the installation is complete, switch to the client directory and run the command

$ npm run serve
Copy the code

After the preceding command is executed, output similar to this is displayed

. App Running at: - Local: http://localhost:8080/ - Network: http://172.20.10.3:8080/...Copy the code

Access it in a browser

http://localhost:8080/

If you see a page that contains the following text

Welcome to Your Vue.js App

The project is successfully installed.

Install Vuetify

$ vue add vuetify
Copy the code

You will also be prompted to select some configuration, in this case I chose Default

? Choose a preset: Default (recommended)
Copy the code

Press Enter to restart the server

$ npm run serve
Copy the code

After executing, we access it in the browser

http://localhost:8080/

You’ll see that the content of the page has changed a little bit

Welcome to Vuetify

Vuetify has been successfully installed.

Take a look at the directory structure

Spa - demo └ ─ ─ client ├ ─ ─ the README. Md ├ ─ ─ Babel. Config. Js ├ ─ ─ package - lock. Json ├ ─ ─ package. The json ├ ─ ─ node_module │ └ ─ ─... ├ ─ ─ public │ ├ ─ ─ the favicon. Ico │ └ ─ ─ index. The HTML └ ─ ─ the SRC ├ ─ ─ App. Vue ├ ─ ─ assets │ ├ ─ ─ logo. The PNG │ └ ─ ─ logo. The SVG ├ ─ ─ │ ├─ ├─ how do you do it? │ ├─ how do you do it Home.vueCopy the code

Simplify spA-demo /client/ SRC/app.vue and change it to

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

<script>
  export default {
    name: 'App'.data () {
      return {
        //
      }
    }
  }
</script>
Copy the code

Modify the spa – demo/client/SRC/views/Home. Vue, the page in a Data table

<template>
  <div class="home">

    <v-container class="my-5"> <! -- Dialog box --> <! --> < 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 > No data </v-alert> </template> </v-data-table> </v-container> </div> </template> <script>export default {
    data: () => ({
      headers: [
        { text: 'title', value: 'name', sortable: false, align: 'left'},
        { text: 'classification', value: 'category', sortable: false },
        { text: 'operation', value: 'name', sortable: false }
      ],
      books: [],
    }),
    created () {
      this.books = [
        { name: 'Life and Death are Wearing me out', category: 'literature' },
        { name: 'National Treasure', category: 'Humanities and Social Sciences' },
        { name: 'A Brief History of Mankind', category: 'technology' },
      ]
    },
  }
</script>
Copy the code

We used the data Headers and books to control the head and data of the table and, at the time of creation, populated the books with some temporary data.

This page covers the use of Data tables, so don’t forget the code. There are many examples of searching for a Data table in the Vuetify documentation, and after looking ata few examples you will know how to use them. One of the things that may be confusing to newcomers is the slot-scope slot. Check out the official Vue documentation

  • Component Basics in the Basics section
  • “Component Registration,” “Prop,” “Custom Events,” “Slots” in the “Look into Components” section

Calm down to read to understand, it is not difficult, here I will not repeat.

Again, you can do the same thing here. You can ignore some of the things that are hard to understand for the time being, and then try to figure it out again.

Open the

http://localhost:8080/

The page looks something like this

It’s a list of books.

Now we are going to make a pop-up dialog box for adding books. We in the
Location add the following code

<v-toolbar flat class="white">< v-tool-title > Book list </ v-tool-title >< V-spacer >< v-dialog V-model ="dialog" max-width="600px">
    <v-btn slot="activator" class="primary"Dark > add </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="Title" v-model="editedItem.name"></v-text-field>
            </v-flex>
            <v-flex xs12 sm6 md4>
              <v-text-field label="Classification" 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"- > save < / v BTN > < / v - card - actions > - < / v card > < / v - dialog > < / v - the toolbar >Copy the code

Instead, add some JS between

export default {
  data: () => ({
    dialog: false// Whether to display the dialog box errMsg:' 'EditedItem: {editedItem: {id: 0, name: {editedItem: {id: 0, name: {' ',
      category: ' '}, defaultItem: {// The default book content, used to initialize the new dialog box content id: 0, name:' ',
      category: ' '
    }
  }),
  computed: {
    formTitle () {
      return this.editedIndex === -1 ? 'new' : 'edit'
    }
  },
  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)
    }
  }
}
Copy the code

In order to keep the article concise, I have omitted the existing snippets when Posting the code, so you can add the above code to the appropriate place as you write.

We used toolbars, Dialogs to add dialog-related stuff to the table, again, without remembering the code, just refer to the documentation if you don’t know how to write it.

The data dialog indicates whether the current dialog is displayed, and the errMsg controls the display of error messages, listening for the dialog to close when it changes to false and empties the errMsg. The calculate property formTitle is used to control the title of the dialog box. I then added two form elements to fill in the book’s name and category.

When we click Add, the page looks like this

In fact, here, our front page is almost OK, behind is the implementation of the increase, deletion and change. This we first under the front-end unilateral implementation, and then the integration of the backend. This will make the front-end Demo more complete.

Realize the save method, add save in methods

save() {
  if (this.editedIndex > -1) { // 编辑
    Object.assign(this.books[this.editedIndex], this.editedItem)
  } else{// Add this.books.push(this.editedItem)} this.close()}Copy the code

To display pop-ups while editing, we need to add the editItem method

editItem (item) {
  this.editedIndex = this.books.indexOf(item)
  this.editedItem = Object.assign({}, item)
  this.dialog = true
}
Copy the code

The saving method is the same as the new one.

Implement the delete method deleteItem

deleteItem (item) {
  const index = this.books.indexOf(item)
  confirm('Confirm deletion? ') && this.books.splice(index, 1)
}
Copy the code

At this point, the front-end project comes to an end.

2 the back-end

Back end, we only need to provide add, delete, change and check interface for the front end to use. RESTful API is a relatively mature set of Internet application program design theory at present, AND I will also implement the relevant operation interface of books based on this.

In consideration of those who are not familiar with RESTful apis, I have listed a few articles THAT I have studied before for your reference.

  • Understanding RESTful Architecture
    • https://dwz.cn/eXu0p6pv
  • RESTful API Design Guide
    • https://dwz.cn/8v4B0twY
  • RESTful API Best Practices
    • https://dwz.cn/2aSnI8fF
  • Zhihu question “How to Explain REST and REST in Popular Language?”
    • https://dwz.cn/bVxrSsf4

After reading the relevant information above, you should have a certain grasp of this design theory.

Again, you don’t have to have a complete understanding of RESTful apis just for now

It uses URLS to locate resources and HTTP to describe operations.

This is an answer to a question from zhihu on the brush. The author is @ivony. It’s neat, but it does make sense.

Wait until oneself practice after, turn head to see some things of theory again, impression is deeper.

Let’s start by listing the interfaces we need to implement

The serial number methods URL describe
1 GET http://domain/api/v1/books Get all books
2 GET http://domain/api/v1/books/123 Gets the book with primary key 123
3 POST http://domain/api/v1/books The new book
4 PUT http://domain/api/v1/books/123 Update the book whose primary key is 123
5 DELETE http://domain/api/v1/books/123 Delete the book whose primary key is 123

We can use Flask to implement the above interface directly, but when there are many resources, we will write a lot of repeated fragments in the code, which violates the DRY(Don’t Repeat Yourself) principle, so it is difficult to maintain later, so we use flask-restful extension implementation.

In addition, the focus of this section is on the implementation of the interface, and for the sake of brevity, we will store the data directly in the dictionary, not database related operations.

Create a server directory in the SPa-demo directory, switch to this directory, and initialize the Python environment

$pipenv - python 3.6.0Copy the code

Pipenv is the current official recommended virtual environment and package management tool, I wrote a previous article “Pipenv quick to Get started” introduced, you can go to see.

Install the Flask

$ pipenv install flask
Copy the code

Install the Flask – RESTful

$ pipenv install flask-restful
Copy the code

The new 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'}]


# Public method area


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)
Copy the code

Flask-restful: Flask-restful: Flask-restful: Flask-restful: Flask-restful: Flask-restful: Flask-restful: Flask-restful: Flask-restful For each resource, we can implement interfaces with a similar structure. The get, PUT, and Delete methods in the BookApi class correspond to interfaces 2, 4, and 5, and the GET and POST methods in the BookListApi class correspond to interfaces 1, and 3. The next step is to register the route. Seeing this, some partners may wonder why two classes need to be defined for the same resource. It is convenient to register routes with and without primary keys for a resource.

At this point, the project structure is

Spa - demo ├ ─ ─ client │ └ ─ ─... ├── ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─Copy the code

Switch to the spa-demo/server directory and run app.py

$ pipenv run python app.py
Copy the code

Then test whether the access to all books interface is available. Using a browser is not recommended because it is an API test. After all, it is not convenient to construct parameters and view HTTP information. Using Postman is recommended.

Request interface 1 to obtain all book information

$curl -i http://127.0.0.1:5000/api/v1/booksCopy the code

results

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"}]Copy the code

If all books are successfully obtained, interface 1 is OK.

Then interface 2 is implemented to get the book with the specified ID. Since getting a book by ID and throwing 404 if the book does not exist are frequently used, two methods are referred to the “public method area”.

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
Copy the code

Then implement the GET method in the BookApi

def get(self, book_id):
    book = get_or_abort(book_id)
    return book
Copy the code

Select the book whose ID is 1 and test it

$curl -i http://127.0.0.1:5000/api/v1/books/1Copy the code

The results of

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"
}
Copy the code

Test the book with ID 5

$curl -i http://127.0.0.1:5000/api/v1/books/5Copy the code

The results of

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 ?"
}
Copy the code

If the ID is 1, the book information is successfully obtained. When ID is 5, a 404 response is returned because the book does not exist. The test results are as expected, indicating that the interface is also OK.

Interface 3, add books. When adding books, we should check whether the parameters meet the requirements. Flask-restful provides us with an elegant implementation, which does not require us to use the hard-coded form of multiple IF judgments to detect whether the parameters are valid.

Since the book name and category cannot be empty, we need to customize the rule. We can add a method in the “public method area”

def not_empty_str(s):
    s = str(s)
    if not s:
        raise ValueError("Must not be empty string")
    return s
Copy the code

Overrides the BookListApi initialization method

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__()
Copy the code

Then implement the POST method

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
Copy the code

Method, first check whether the parameter is valid, then take the ID of the last book plus 1 as the ID of the new book, and finally return the added book information and status code 201 (indicating that it has been created).

Check whether the parameter verification is OK

$ curl -i \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{"name":"","category":""}' \
    http://127.0.0.1:5000/api/v1/books
Copy the code

The results of

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"}}Copy the code

If an error of 400 is returned, the verification is valid.

Check whether the new interface is available

$ 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
Copy the code

The results of

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"
}
Copy the code

The vm is created successfully. Let’s verify by getting the book interface with the specified ID

$curl -i http://127.0.0.1:5000/api/v1/books/4Copy the code

The results of

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"
}
Copy the code

If it succeeds, the interface is successfully created, and interface 3 is fine.

The implementation of interfaces 4 and 5 is similar to the above, but the code is posted here and not explained in detail.

Like BookListApi, we first override the initialization method of 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__()
Copy the code

Then implement the PUT and DELETE methods

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,Copy the code

At this point, the back-end project is almost complete.

Of course, this is not complete, for example there is no authentication of the API, this can be done by flask-httpauth or other methods. Limited by space, I won’t expand the description here. If you are interested, you can take a look at the documentation of this extension or research and implement it yourself.

3 integration

A single front end or back end has a prototype, just short of integration.

The front-end needs to request data, so here we use axios and switch to the SPA-Demo /client directory to install

$ npm install axios --save
Copy the code

Modify the spa – demo/client/SRC/views/Home. Vue, introducing axios between script tags, and initialize the API address

import axios from 'axios'

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

export default {
  ...
}
Copy the code

Modify hook created logic to fetch data from the back end

created () {
  axios.get(booksApi)
    .then(response => {
      this.books = response.data
    })
    .catch(error => {
      console.log(error)
    })
}
Copy the code

After running the front-end project, you can view the home page and find no data. Looking at the developer tools, we will find this 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.
Copy the code

The current project does not support CORS (Cross-origin Resource Sharing). This can be done by adding proxies at the front end or by flask-CORS at the back end. Here, I’m using the latter.

Switch to the SPa-demo /server directory and install flask-cors

$ pipenv install flask-cors
Copy the code

Modify spa-demo/server/app.py to introduce CORS in the header

from flask_cors import CORS
Copy the code

In the code

app = Flask(__name__)
Copy the code

and

api = Api(app)
Copy the code

Add a line between

CORS(app, resources={r"/api/*": {"origins": "*"}})
Copy the code

Then re-run app.py and refresh the home page. We should see that the list has data, indicating that the CORS problem has been successfully resolved.

In the spa – demo/client/SRC/views/Home. Vue, modify the save method, at the same time new setErrMsg auxiliary method

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) {// Edit 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)
    })
  }
}
Copy the code

At this point, books are added and saved.

Modify the deleteItem method

deleteItem (item) {
  const index = this.books.indexOf(item)
  confirm('Confirm deletion? ') && axios.delete(booksApi + '/' + this.books[0].id)
    .then(response => {
      this.books.splice(index, 1)
    }).catch(error => {
      this.setErrMsg(error.response)
      console.log(error)
    })
}
Copy the code

At this point, the delete method is also done.

At this point, integration is complete, and a CRUD Demo based on Vue + Flask’s front and back end separation is complete.

After reading this article, you can follow the steps to implement it yourself. If you’re new to this, you might be confused in some places, but I’ve also provided some information where I can think of, so you can have a look. If not, you need to search baidu/Google to solve the problem. However, I still suggest not trying to understand each point is particularly clear, first understand the key points, try to achieve, when looking back at the relevant materials, also more feeling.

The full code can be viewed at GitHub

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

4 reference

  • 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
  • The 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

This article was first published on the public account “Little Back end”.