如何使用Docker设置和构建带有Flask后端的Vue应用程序
Vue.js是一个用于构建Web用户界面的JavaScript前端框架。Vue通常被用来构建基于单页的、运行在客户端的应用程序。
然而,Vue也可以用来构建一个全栈式的网络应用程序,其他后端技术如Node.js和Flask也会发挥作用。这是通过向服务器发出HTTP请求并将其填充到基于Vue的界面中来实现的。
Vue是伟大的,由于它有很好的概述功能。这包括使用虚拟DOM的能力,容易与其他技术(如Node.js和Python)集成,以及高运行时间性能。
另一方面,Flask是一个用Python编写的基于Web应用的微型框架,用于操作基于服务器的数据。Flask被设计成能够快速和容易地入门,并能够扩展到复杂的应用程序。
当构建一个全栈应用程序时,Vue和Flask可以作为一个单一的应用程序使用和运行。这使你有能力操纵网页的外观,并使用Flask处理基于服务器的数据。
当一起运行这些不同的技术时,如果总是设置本地环境来运行全栈应用,可能会变得很麻烦。
因此,Docker扮演了一个非常重要的角色。它允许你建立这样一个应用程序,并通过容器虚拟运行它们。这意味着任何本地环境都不会影响你的应用程序的运行。
Docker会将Vue和Flask旋转起来,将它们容器化并作为一个整体运行。你所要做的就是设置简单的指令,解释应用程序运行所需的不同依赖性。这包括Python的版本和你想运行Flask的库。
在本指南中,我们将使用Vue和Flask构建一个应用程序,并使用Docker运行它。我们将使用SQLite作为应用程序的数据库。
前提条件
要跟上这篇文章,必须具备以下条件。
- 在你的电脑上安装了[Python]。
- 有一些关于Flask的工作知识。
- 在你的电脑上安装[Node.js]。
- 对Vue.js有一定的了解。
- 在你的电脑上安装了[Docker]。
使用Flask建立服务器端环境
对于服务器端,我们将使用Flask和SQLite(一个轻量级的SQL数据库)建立一个REST API。
为了实现这一目标,我们将遵循以下步骤。
首先创建一个项目文件夹,用来创建Flask REST API,命名为:flask-todos-rest-api 。
为了设置我们的Flask环境,我们将使用[pipenv]。
要检查你是否安装了pipenv ,运行以下命令。
python -m pipenv --version
如果你没有安装,运行以下命令。
pip install pipenv
通过运行以下命令来初始化环境。
python -m pipenv shell
安装软件包
我们将使用以下软件包。
- [Flask]:为应用程序提供架构设置的框架。
- [Flask-sqlalchemy]:提供有用的默认值和额外的帮助器,使其更容易完成数据库任务。
- [Flask-marshmallow]:构建API时有用的Flask的薄集成层。
- [Flask-cors]:用于处理跨来源的资源访问。
要安装上述所有软件包,请运行此命令。
python -m pipenv install flask flask-sqlalchemy flask-marshmallow marshmallow-sqlalchemy flask-cors
使用Flask设置服务器端应用程序
要设置服务器端应用程序,在你的flask-todos-rest-api 文件夹中创建一个app.py 文件。
在这个文件中,通过添加以下几行代码设置一个基本的Flask应用程序。
from flask import Flask
## Init app
app = Flask(__name__)
# Start the app
if __name__ == '__main__':
app.run(debug=True)
通过上面的命令,我们正在导入Flask模块,初始化它,并启动它。
设置数据库
要设置SQLite数据库,首先要导入以下包。
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
import os
然后设置应用程序的基础目录。
basedir = os.path.abspath(os.path.dirname(__file__))
添加数据库应用配置。
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir,'db.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
由于我们使用的是Flask-sqlalchemy和Flask-marshmallow,我们现在可以设置SQLAlchemy 来初始化数据库Marshmallow 来初始化marshmallow,如下图所示。
db = SQLAlchemy(app)
ma = Marshmallow(app)
现在数据库的配置已经设置好了,我们可以开始设置todo模型了。这将构成一个存储在SQLite数据库中的样本todo列表。
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
description = db.Column(db.String(400))
def __init__(self,title,description):
# Add the data to the instance
self.title = title
self.description = description
从上面来看,我们定义了一个Todo将有一个ID、标题和描述。
由于我们使用的是SQLite数据库,我们需要设置一个模式来存储我们的todo。当查询todos数据时,该模式将被调用。
class TodoSchema(ma.Schema):
class Meta:
fields = ('id','title','description')
上面,我们定义了每个todo,我们将对id、title和description感兴趣。
为了初始化上述模式,我们必须对单个todo和多个todos采取不同的方法。
为此,我们将添加以下内容。
todo_schema = TodoSchema()
todos_schema = TodoSchema(many=True)
第一个是针对单个todo,另一个是针对多个todos。
设置SQLite数据库和表
从你的代码编辑器中打开终端,运行以下命令,启动一个交互式的python环境。
python -m pipenv run python
- 从shell中运行下面的命令。
from app import db # import db
db.create_all() # create database and tables
- 关闭交互式shell。
exit()
设置路由
要设置路由,首先要导入软件包。
from flask import Flask,request, jsonify
from flask_cors import CORS,cross_origin
request 将用于获取有效载荷(发送的数据),而 将用于返回JSON数据。 和 用于设置访问策略。jsonify CORS cross_origin
然后添加CORS配置,以处理跨来源的人进来消费这个API。
CORS(app,resources={r"/api": {"origins": "*"}})
app.config['CORS_HEADERS'] = 'Content-Type'
从上面来看,我们接受所有进入/api 端点的来源,我们将从该端点公开API。
现在让我们添加所有必要的路由来处理CRUD操作。
创建一个todo
下面的路由创建一个todo。
@app.route('/api/todo', methods=['POST'])
@cross_origin(origin='*',headers=['content-type'])
def add_todo():
# get the data
title = request.json['title']
description = request.json['description']
# Create an instance
new_todo = Todo(title, description)
# Save the todo in the db
db.session.add(new_todo)
db.session.commit()
# return the created todo
return todo_schema.jsonify(new_todo)
从上面的路由中,我们接受所有来源,从有效载荷中接收todo的标题和描述,将其保存到数据库中,并返回保存的todo。
获取所有todos
下面的路由获取所有todos。
# Get all todos
@app.route('/api/todo', methods=['GET'])
@cross_origin(origin='*',headers=['Content-Type'])
def get_todos():
# get the todos from db
all_todos = Todo.query.all()
# get the todos as per the schema
result = todos_schema.dump(all_todos)
# return the todos
return jsonify(result)
从上面来看,我们接受所有的起源,获取所有保存的todos,并返回它们。
获取一个单一的路由
下面的路由获取一个单一的路由。
# Get a single todo
@app.route('/api/todo/<id>', methods=['GET'])
@cross_origin(origin='*',headers=['Content-Type'])
def get_todo(id):
# get a single todo
todo = Todo.query.get(id)
# return the todo as per the schema
return todo_schema.jsonify(todo)
从上面来看,我们从URL中接受todo的id,接受所有的起源,获取那个特定的todo,并返回它。
更新一个todo路由
下面的路由更新一个todo。
# update a todo
@app.route('/api/todo/<id>', methods=['PUT'])
@cross_origin(origin='*',headers=['Content-Type'])
def update_todo(id):
# get the todo first
todo = Todo.query.get(id)
# get the data
title = request.json['title']
description = request.json['description']
# set the data
todo.title = title
todo.description = description
# commit to the database
db.session.commit()
# return the new todo as per the schema
return todo_schema.jsonify(todo)
在上面,我们接受要更新的todo的ID,接受所有的来源,获得特定的todo和数据,设置新的数据,保存到数据库,并返回保存的数据库。
删除一个todo路线
下面的路由可以删除一个todo。
# Delete a todo
@app.route('/api/todo/<id>', methods=['DELETE'])
@cross_origin(origin='*',headers=['Content-Type'])
def delete_todo(id):
# get the todo to be deleted
todo = Todo.query.get(id)
# delete from the database
db.session.delete(todo)
# commit on the database
db.session.commit()
# return thr deleted todo as per the schema
return todo_schema.jsonify(todo)
上面的路由接受要删除的todo的id,接受所有的起源,得到todo,从数据库中删除它,并返回被删除的todo。
设置完路由后,通过运行以下命令启动你的应用程序。
python -m pipenv run python app.py
一切都应该正常,开发服务器也应该启动了。如果你遇到了错误,请重温一下步骤。
你的控制台输出应该类似于。
使用Vue设置客户端
要设置客户端,首先要使用Vue CLI创建一个骨架应用程序。
要检查你是否安装了CLI,请使用以下命令。
vue --version
如果你没有安装CLI,用下面的命令安装它。
npm install -g @vue/cli
使用下面的命令创建骨架应用程序。
vue create todos-flask-app
对于后面的问题,请随意使用默认值或自己的选择。
我们还将添加一些额外的包来处理服务器端的路由。
这些包是。
- Axios:用于处理客户端/服务器端的请求。
- Vue-router:用于处理导航。
npm install axios vue-router
设置Vue前台应用程序
安装完软件包后,我们需要在src/main.js 中对其进行配置,如下所示。
import axios from 'axios'
import VueRouter from 'vue-router'
Vue.config.productionTip = false
Vue.prototype.$http = axios;
Vue.use(VueRouter);
在src/App.vue ,编辑<template> ,如下所示。
<div id="app">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
<title>Todos</title>
</head>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/"> Todos app </a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mx-auto">
<li class="nav-item" :class="home_class">
<a class="nav-link" href="/"> Home </a>
</li>
<li class="nav-item" :class="add_todo_class">
<a class="nav-link" href="/add-todo"> Add todo </a>
</li>
</ul>
</div>
</nav>
<router-view> </router-view>
</div>
我们在外部链接bootstrap的CSS来处理我们的造型,添加一个简单的导航栏,并在导航不同页面时添加动态内容区。
编辑JavaScript,如下所示。
<script>
export default {
data() {
return {
home_class: this.$route.path === "/" ? "active" : "",
add_todo_class: this.$route.path === "/add-todo" ? "active" : "",
};
},
};
</script>
在上面的片段中,我们正在设置导航栏的动态类。
编辑样式如下。
<style>
#app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
上面我们正在为应用程序组件添加自定义样式。
Todos列表卡
在src/components 文件夹中,创建一个Todos.vue 文件。
在该文件中,添加以下HTML。
<template>
<div class="todos">
<div class="container">
<div class="row">
<div class="col-sm-6 offset-sm-3">
<!-- Showing the added todos -->
<div v-if="todos.length == 0">
<div class="card mt-2 mb-2">
<div class="card-body">
<h4 class="card-title">You do not have any saved todo</h4>
<div class="d-flex justify-content-between">
<a class="btn btn-info text-white" href="/add-todo">Add todo</a>
</div>
</div>
</div>
</div>
<div v-else-if="todos.length > 0" v-for="todo in todos" v-bind:key="todo.id">
<div class="card mt-2 mb-2">
<div class="card-body">
<h4 class="card-title">{{todo.title}}</h4>
<p class="card-text">{{todo.description}}</p>
<div class="d-flex justify-content-between">
<button class="btn btn-info text-white" @click="editTodo(todo.id)">
Edit
</button>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
在这里,我们正在检查我们是否有todos;如果没有,我们会显示一个信息。否则,我们将循环浏览它们,输出每一个。
添加下面的JavaScript。
<script>
export default {
// component data
data() {
return {
todos: [],
};
},
methods: {
// fetching todos
async getData() {
try {
const response = await this.$http.get(
"http://localhost:5000/api/todo"
);
this.todos = response.data;
} catch (error) {
console.log(error);
}
},
// editing a todo
async editTodo(todoId) {
// Push to the edit todo page
this.$router.push({
path: `/edit-todo/${todoId}`,
});
return;
},
// deleting a todo
async deleteTodo(todoId) {
// confirm with the user
let confirmation = confirm("Do you want to delete this todo?");
if (confirmation) {
try {
await this.$http.delete(`http://localhost:5000/api/todo/${todoId}`);
// refresh the todos
this.getData();
} catch (error) {
console.log(error);
}
}
},
},
// Fetch the todos on load
created() {
this.getData();
},
};
</script>
这里我们输出组件加载时获取的todos,编辑和删除todo的功能。
添加以下样式。
<style scoped>h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
.card-body {
text-align: left;
}
.todos {
margin-top: 10px;
}
</style>
上面的代码片断是添加到我们的todo组件的简单样式。
添加一个todo表单
创建一个AddTodo.vue 文件并添加以下组件。
HTML
<template>
<div class="container">
<div class="row">
<div class="col-sm-6 offset-sm-3">
<form id="todo-form" method="post" @submit.prevent="checkForm" novalidate="true">
<div v-if="todo.error" class="form-group mt-1">
<div class="alert alert-danger">{{todo.error}}</div>
</div>
<div v-if="todo.message" class="form-group mt-1">
<div class="alert alert-success">{{todo.message}}</div>
</div>
<div class="form-group mt-3" style="text-align: left">
<label for="title">Title</label>
<input v-model="todo.title" type="text" class="form-control" id="title" placeholder="Enter todo's title" />
<small id="titleHelp" class="form-text text-muted">E.g taking a walk.</small>
</div>
<div class="form-group mt-3" style="text-align: left">
<label for="description">Description</label>
<textarea v-model="todo.description" class="form-control" name="description" id="description" placeholder="Todo's description"></textarea>
<small id="descriptionHelp" class="form-text text-muted">E.g A long walk around the estate.</small>
</div>
<div class="form-group mt-3">
<button type="submit" class="btn btn-primary btn-lg btn-block">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</template>
这里我们展示的是一个由JavaScript填充字段的表单。
JavaScript
<script>
export default {
data() {
return {
todo: {
title: "",
description: "",
error: null,
message: null,
},
};
},
methods: {
checkForm: async function(e) {
if (this.todo.title && this.todo.description) {
try {
// send data to the server
await this.$http.post("http://localhost:5000/api/todo", {
title: this.todo.title,
description: this.todo.description,
});
//reset the fields
this.todo.title = "";
this.todo.description = "";
// set the message
this.todo.message = "Todo added successfully";
return;
} catch (error) {
this.todo.error = error;
return;
}
}
this.todo.error = null;
if (!this.todo.title) {
this.todo.error = "Title is required";
return;
}
if (!this.todo.description) {
this.todo.error = "Description is required";
return;
}
e.preventDefault();
},
},
};
</script>
从上面的脚本中,我们将从组件中导出数据,以及一个处理表单提交时的验证和数据提交的方法。
添加一个编辑待办事项表单
创建一个EditTodo.vue 文件并添加以下内容。
<template>
<div class="container">
<div class="row">
<div class="col-sm-6 offset-sm-3">
<form id="todo-form" method="post" @submit.prevent="checkForm" novalidate="true">
<div v-if="todo.error" class="form-group mt-1">
<div class="alert alert-danger">{{todo.error}}</div>
</div>
<div v-if="todo.message" class="form-group mt-1">
<div class="alert alert-success">{{todo.message}}</div>
</div>
<div class="form-group mt-3" style="text-align: left">
<label for="title">Title</label>
<input v-model="todo.title" type="text" class="form-control" id="title" placeholder="Enter todo's title" />
<small id="titleHelp" class="form-text text-muted">E.g taking a walk.</small>
</div>
<div class="form-group mt-3" style="text-align: left">
<label for="description">Description</label>
<textarea v-model="todo.description" class="form-control" name="description" id="description" placeholder="Todo's description"></textarea>
<small id="descriptionHelp" class="form-text text-muted">E.g A long walk around the estate.</small>
</div>
<div class="form-group mt-3">
<button type="submit" class="btn btn-primary btn-lg btn-block">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</template>
与add todo 表单类似,我们将输出一个edit todo 表单,该表单预先填充了要编辑的特定todo的JavaScript的数据。
JavaScript
<script>
export default {
data() {
return {
todo: {
loading: false,
title: "",
description: "",
error: null,
message: null,
id: this.$route.params.id,
},
};
},
methods: {
getTodo: async function() {
// the current todo id
let todoId = this.todo.id;
// start loading
this.todo.loading = true;
// get the todo
try {
let response = await this.$http.get(
`http://localhost:5000/api/todo/${todoId}`
);
this.todo.title = response.data.title;
this.todo.description = response.data.description;
this.todo.loading = false;
return;
} catch (error) {
this.todo.error = error;
return;
}
},
checkForm: async function(e) {
// Custom validation
if (this.todo.title && this.todo.description) {
try {
// send data to the server
await this.$http.put(
`http://localhost:5000/api/todo/${this.todo.id}`, {
title: this.todo.title,
description: this.todo.description,
}
);
//reset the fields
this.todo.title = "";
this.todo.description = "";
// set the message
this.todo.message = "Todo edited successfully";
return;
} catch (error) {
this.todo.error = error;
return;
}
}
this.todo.error = null;
if (!this.todo.title) {
this.todo.error = "Title is required";
return;
}
if (!this.todo.description) {
this.todo.error = "Description is required";
return;
}
e.preventDefault();
},
},
created() {
// Called on load
this.getTodo();
},
};
</script>
上面我们正在导出todo数据,在页面加载时获得todo,处理自定义验证,以及提交编辑过的todo的数据。
在设置好这些组件后,我们需要处理进入各个页面的路由。
为了做到这一点,我们将在src/main.js 文件中添加以下内容。
导入AddTodo,EditTodo 和Todos 组件。
import AddTodo from "./components/AddTodo"
import EditTodo from "./components/EditTodo"
import Todos from "./components/Todos"
然后创建各种VueRouter 实例来处理上面的组件。
// create a vuerouter instance
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [{
path: '/',
component: Todos,
name: 'home'
},
{
path: '/add-todo',
component: AddTodo,
name: 'add-todo'
},
{
path: '/edit-todo/:id',
component: EditTodo,
name: 'edit-todo'
},
]
});
// pass the router to the app config
new Vue({
router: router,
render: h => h(App),
}).$mount('#app');
从上面来看,我们正在创建一个VueRouter 实例,传入mode,base, 和routes 。对于路由,我们为每个路由传递path,component, 和name 。
创建实例后,我们将其传递给Vue对象。这样,我们就可以启动开发服务器并测试我们实现的功能了。
要做到这一点,运行以下命令。
npm run serve
上述命令将启动开发服务器,端口为8080 。你可以从http://localhost:8080 访问你的应用程序。
你的应用程序应该类似于以下内容。
Todos页面
添加Tododo页面
对应用程序进行停靠
要对我们建立的应用程序进行dockerize,我们将遵循以下步骤。
Dockerize Flask API
要对API进行dockerize,需要在API文件夹中创建一个Dockerfile,以及一个.dockerignore 文件。Dockerfile将承载创建镜像时的指令,而.dockerignore 文件将承载复制到镜像时要忽略的文件。
为了让Flask应用程序在Docker中工作,我们需要确保我们使用的所有包都是可用的,并能被容器化的REST API访问。
为了使这些包能够被Docker访问,我们将把它们导入到一个requirements.txt 文件。然后,Docker将运行这个文件,并在运行API的容器中安装这些包。
在你的flask-todos-rest-api 目录中,运行这个命令。
pip freeze > requirements.txt
这将创建一个requirements.txt ,并导入我们所使用的所有包。
在Docker文件中添加以下内容。
# Base python package
FROM python:3.8-slim-buster
# Working directory
WORKDIR /app
# Copy the dependencies
COPY requirements.txt
# Install the dependencies
RUN pip3 install -r requirements.txt
# Copy the files
COPY . .
# Executable commands
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]
这里我们从外部导入Python包,定义工作目录,复制依赖关系,安装依赖关系,复制文件,并设置执行命令。
在.dockerignore 文件中添加以下内容。
__pycache__/
.gitignore
Pipfile
Pipfile.lock
README.MD
从上面来看,我们正在添加所有不应该包含在docker镜像中的文件。
Dockerize Vue应用程序
为了对Vue应用程序进行dockerize,我们还将在项目文件夹中创建一个Dockerfile。与之前的实例类似,它将承载创建docker镜像时的说明。
在Dockerfile中,添加以下内容。
#Base image
FROM node:lts-alpine
#Install serve package
RUN npm i -g serve
# Set the working directory
WORKDIR /app
# Copy the package.json and package-lock.json
COPY package*.json ./
# install project dependencies
RUN npm install
# Copy the project files
COPY . .
# Build the project
RUN npm run build
# Expose a port
EXPOSE 5000
# Executables
CMD [ "serve", "-s", "dist" ]
从上面来看,我们要导入节点镜像,设置工作目录,复制package.json ,和package-lock.json ,安装项目的依赖性,复制项目文件,构建项目,暴露一个端口,并设置可执行文件。
设置一个整体的docker-compose文件
在为每个文件夹即api-folder ,和client-folder ,设置了一个Docker文件后,我们将在这两个文件夹外设置一个docker-compose.yml 文件。
首先,在API和客户端文件夹外创建一个docker-compose.yml 文件。
在docker-compose.yml 文件中,添加以下内容。
version: '3.8'
services:
flask-todos-api:
build: ./flask-todos-rest-api
ports:
- 5000:5000
vue-todos-app:
build: ./todos-flask-app
ports:
- 8080:5000
这里我们定义了docker-compose 的版本,并设置了两个服务。对于每个服务,我们定义构建(存放Docker文件的文件夹)和端口(项目要运行的地方)。为了使服务不在平行端口上发生冲突,client-side 将在端口8080 上运行。
构建Docker镜像
为了构建Docker镜像,从docker-compose.yml 文件的位置,运行以下命令。
docker-compose up -d --build
上述命令将构建Docker镜像。
启动Docker容器
要从与上一步相同的位置启动Docker容器,请运行以下命令。
docker-compose up
上述命令将启动两个服务。两个服务启动后,继续进入http://localhost:8080,与应用程序进行交互。
与应用程序交互后,你可以按CRTL + C ,停止容器。你还可以与朋友分享docker镜像,以展示你所建立的东西。
总结
在这篇文章中,我们已经创建了一个Vue.js应用,该应用消耗了一个可靠的Flask API。