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”.