基于项目搭建个人笔记本放在树莓派上吧!
项目代码已上传至GITEE
前后端分离
技术栈:
后端 :
1.flask
2.sqlalchemy
3.sqlite
前端 :
- VUE
- AXIOS
- ELEMENTUI
介绍
使用el-table模拟文件Tree系统,使用懒加载来增强体验,编辑器选用为mavon-editor
,预览使用markdown-it
,在后端使用正则表达式解析markdown toc目录增强浏览体验,编辑期间前端定时器会简单的定时缓存数据到后端
演示地址 因为css和js没有走cdn会很慢
总体分为两部分: flask搭建的后端接口以及VUE的前端界面
前端代码后台基于GITHUB大佬的vue-element-admin,在这里使用了大佬的简单模板并进行修改,模板代码源于vue-admin-template。
项目准备
基于vue-cli
进行构建,电脑需安装npm
环境
在vue-admin-template(文中以VAT来简称)中clone代码, 根据markdown中文文档步骤构建环境并启动
准备一个最小的flask应用,若未使用过flask请于链接了解,文档中会有完整代码
1.开始吧!和Flask交互起来
以npm run dev
模式启动VAT框架,框架启动后可以发现输入任意密码既可以登录进后台中,是因为后台框架使用的是./mock文件中的数据进行后端模拟;
进入后发现框架已经做好了侧边栏,面包屑,以及基于AXIOS的token验证机制,只需要简单的修改既可以与后端进行交互起来
简单的调整
1.1 需要更改后端接口地址
更改文件 ./.env.development
# just a flag
ENV = 'development'
# base api
# VUE_APP_BASE_API = '/dev-api' # 更改为后端接口地址
VUE_APP_BASE_API = 'http://localhost:5000' # FLASK端口5000
删除模板中的mock数据的引用
更改文件./src/main.js 对如下部分进行注释 并自己选择是否使用中文版本的 element-ui
根据注释说明修改
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online ! ! !
*/
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}
// set ElementUI lang to EN
Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明
// Vue.use(ElementUI)
重启前端服务,直到登录时显示network Error
1.2 阅览VAT框架中与后端交互的request模块
解析 @utils/requests.js
上半部分是对axios请求做了一个基本的配置,请求路径为 env.development中的 VUE_APP_BASE_API,超时时间为5s
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
中间部分为配置axios的请求头,对请求(request)进行预处理,携带token进行验证
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
后面部分为axios的返回数据(response)拦截器
如下代码可知 VAT框架模板预留了几个 response status code 英文注释比较清晰明了
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
1.3 根据需求进行request请求代码的更改
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// vuex 携带token验证
Object.assign(config.headers, { Authorization: 'Bearer ' + getToken() })
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
response => {
const res = response.data
// flask all return 200
if (res.code !== 200) {
Message({
message: 'requests error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
// flask因为使用了一些模块(flask_httpauth) token验证失败只返回401
if (error.response) {
switch (error.response.status) {
case 401:
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
break
case 400:
Message({
message: error.response.data.data.message,
type: 'error',
duration: 3 * 1000
})
break
case 422:
Message({
message: error.response.data.data.message,
type: 'error',
duration: 3 * 1000
})
break
}
}
return Promise.reject(error)
}
)
export default service
1.4 搭建一个最小的Flask应用进行与VAT框架的交互
在python环境中pip install flask
,编写如下脚本代码启动一个后端服务
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello Flask!'
if __name__ == '__main__':
app.run()
运行 在浏览器中输入 127.0.0.1:5000 浏览器中打印出 ‘Hello Flask!’ 服务启动成功!
1.5 登录接口的解析
登录接口需要post数据 获取token存在浏览器本地中,与后端的每个接口都需要携带token进行验证
- @\src\utils\auth.js: 浏览器Cookie对token的处理
- @\src\store\getters.js: 本地储存用户信息的全局字典
- @\src\store\modules\user.js: 与用户登录的请求和处理基本都在这里
根据user.js的代码可在flask设计出登录接口返回的数据格式
import { login, logout, getInfo } from '@/api/user'
login
: 无需携带token
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
返回数据
{
"token":"Bearer x",
}
info
: 携带token
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
return reject('Verification failed, please Login again.')
}
const { name, avatar } = data
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
返回数据
{
"name":"admin",
"avatar":".png"
}
logout
: 清除token并退出
无数据返回
1.6 登录接口的实现
接口位置 @\src\api\user.js
, 修改为如下
import request from '@/utils/request'
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
export function getInfo(token) {
return request({
url: '/auth/info',
method: 'get'
// params: { token } # 获取到token后会自己携带token进行请求
})
}
export function logout() {
return request({
url: '/auth/logout',
method: 'post'
})
}
登录组件位置 @\src\views\login\index.vue
, 可以看到post form如下
loginForm: {
username: 'admin',
password: '111111'
},
修改flask的接口来兼容VAT吧! 复制如下代码到最小的flask应用中重新启动测试
def returnVueDataModel(message, code, **kwargs):
kwargs['message'] = message
kwargs['code'] = code
return {"data": kwargs, 'code': 200}
@app.route('/auth/login', methods=['POST'])
def login():
params = request.get_json()
if 'username' not in params or 'password' not in params:
return jsonify(returnVueDataModel('miss arguments', 400)), 400
if params['username'] == 'admin' and params['password'] == '111111':
return jsonify(returnVueDataModel('login success', 200, token='x'))
return jsonify(returnVueDataModel('no existing user', 400)), 400
@app.route('/auth/info', methods=['GET'])
def info():
token = request.headers.get('Authorization')
if 'Bearer' not in token:
return my_json(returnVueDataModel('please login!', 400)), 400
return jsonify(returnVueDataModel('token verify pass', 200, name='Admin', avatar=""))
1.7 粗暴的解决跨域问题
Access to XMLHttpRequest at 'http://localhost:5000/auth/login' from origin 'http://localhost:9528' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
:5000/auth/login:1 Failed to load resource: net::ERR_FAILED
:9528/#/login?redirect=%2Fdashboard:1 Access to XMLHttpRequest at 'http://localhost:5000/auth/login' from origin 'http://localhost:9528' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
服务启动并登录后 会出现如上问题,需要配置flask的跨域请求 使用 flask_cors
pip install flask_cors
from flask import Flask, request, jsonify
app = Flask(__name__)
from flask_cors import * # 导入模块
CORS(app, supports_credentials=True, origins=['http://localhost:9528'])
def returnVueDataModel(message, code, **kwargs):
kwargs['message'] = message
kwargs['code'] = code
return {"data": kwargs, 'code': 200}
@app.route('/', methods=['GET'])
def hello_world():
return 'Hello Flask!'
@app.route('/auth/login', methods=['POST'])
def login():
params = request.get_json()
if 'username' not in params or 'password' not in params:
return jsonify(returnVueDataModel('miss arguments', 400)), 400
if params['username'] == 'admin' and params['password'] == '111111':
return jsonify(returnVueDataModel('login success', 200, token='x'))
return jsonify(returnVueDataModel('no existing user', 400)), 400
@app.route('/auth/info', methods=['GET'])
def info():
token = request.headers.get('Authorization')
if 'Bearer' not in token:
return my_json(returnVueDataModel('please login!', 400)), 400
return jsonify(returnVueDataModel('token verify pass', 200, name='Admin', avatar=""))
if __name__ == '__main__':
app.run()
Success
短短十多分钟 完成了一个前后端分离的小项目,那么根据这个框架,可以搭建起来任意一个喜欢的后台管理系统,本次我们搭建一个小型的Blog,当然这个Blog因为也是闲着无聊写的,所以可能会有bug哟,比如markdown的Toc目录因为我使用的正则解析的,可能你们写法跟我不一样就无法解析出正常的目录啦
2. 后端的搭建
2.1 项目的启动
clone到本地,在pycharm或喜欢的编辑器中打开,pycharm会自动创建虚拟环境以及pip 环境包
进入虚拟环境,输入如下命令,搭建sqlalchemy数据库环境
flask db init
flask db migrate
flask db upgrade
搭建完成后,在虚拟环境下输入>>flask shell 进入flask shell环境fake数据(noteapp\faker.py),如下为flask shell中包含的对象
def register_shell_context(app):
"""Register shell context objects."""
def shell_context():
"""Shell context objects."""
return {"db": db, "User": models.User, "Fold": models.Fold, "Blog": models.Blog,
"fake": faker.faker, "wafer": models.Wafer}
app.shell_context_processor(shell_context)
输入命令如下
>>> fake
<class 'noteapp.faker.faker'>
>>> fake().do()
--- 等待完成 若有报错缺少faker模块请pip安装
>>> User.query.all()
[<User('David Richardson')>, <User('Leonard Roberts')>, <User('Samuel Scott')>, <User('Eduardo Jones')>, <User('Shirley Johns')>]
--- 生成的用户列表 密码均为123456
>>> eixt()
回到虚拟环境 输入flask run
启动服务
2.2 项目目录解析
目录结构
├── README.md 项目介绍
├── autoapp.py 程序入口
├── requirements.txt 项目依赖
├── migrations flask db init 生成的数据库管理 若非必要请在git ignore
├── tests 一些单元测试及一些个人测试文件
├── noteapp 源码目录
│ ├── models 持久化数据表模型
│ ├── upload 文件上传目录
│ ├── util 公共函数方法
│ ├── views 主要蓝图和业务代码
│ ├── app.py 程序初始化入口
│ ├── commands.py 创建虚拟环境下flask自定义命令
│ └── database.py CURD
│ └── dev.db 持久化sqlite
│ └── extensions.py 引入的库
│ └── faker.py 虚拟数据
│ └── settings.py 程序配置
└──
3. 前端的搭建
3.1 项目的启动
clone到本地,在vscode或喜欢的编辑器中打开,根据markdown文档搭建项目,npm run dev
运行
启动成功后浏览器会跳到文章开头的界面中,操作中会需要登录,请使用前面flask shell查到的用户密码登录
Tree中使用elementUi的table懒加载和根据懒加载更新某行数据的方式来模拟文件夹系统