前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。
核心思想是前端html页面通过ajax调用后端的restuful api接口并使用json数据进行交互
1、可以实现真正的前后端解耦,前端服务器使用nginx。前端/WEB服务器放的是css,js,图片等等一系列静态资源(甚至你还可以css,js,图片等资源放到特定的文件服务器,例如阿里云的oss,并使用cdn加速),前端服务器负责控制页面引用&跳转&路由,前端页面异步调用后端的接口,后端/应用服务器使用tomcat(把tomcat想象成一个数据提供者),加快整体响应速度。(这里需要使用一些前端工程化的框架比如nodejs,react,router,react,redux,webpack)
2、发现bug,可以快速定位是谁的问题,不会出现互相踢皮球的现象。页面逻辑,跳转错误,浏览器兼容性问题,脚本错误,页面样式等问题,全部由前端工程师来负责。接口数据出错,数据没有提交成功,应答超时等问题,全部由后端工程师来解决。双方互不干扰,前端与后端是相亲相爱的一家人。
3、在大并发情况下,我可以同时水平扩展前后端服务器,比如淘宝的一个首页就需要2000+台前端服务器做集群来抗住日均多少亿+的日均pv。(去参加阿里的技术峰会,听他们说他们的web容器都是自己写的,就算他单实例抗10万http并发,2000台是2亿http并发,并且他们还可以根据预知洪峰来无限拓展,很恐怖,就一个首页。。。)
4、减少后端服务器的并发/负载压力。除了接口以外的其他所有http请求全部转移到前端nginx上,接口的请求调用tomcat,参考nginx反向代理tomcat。且除了第一次页面请求外,浏览器会大量调用本地缓存。
5、即使后端服务暂时超时或者宕机了,前端页面也会正常访问,只不过数据刷不出来而已。
6、也许你也需要有微信相关的轻应用,那样你的接口完全可以共用,如果也有app相关的服务,那么只要通过一些代码重构,也可以大量复用接口,提升效率。(多端应用)
7、页面显示的东西再多也不怕,因为是异步加载。
8、nginx支持页面热部署,不用重启服务器,前端升级更无缝。
9、增加代码的维护性&易读性(前后端耦在一起的代码读起来相当费劲)。
10、提升开发效率,因为可以前后端并行开发,而不是像以前的强依赖。
11、在nginx中部署证书,外网使用https访问,并且只开放443和80端口,其他端口一律关闭(防止黑客端口扫描),内网使用http,性能和安全都有保障。
12、前端大量的组件代码得以复用,组件化,提升开发效率,抽出来!
需求: 随着开发进度不断向前,你会发现你的数据库模型需要更改,而当这种情况发生时需要更新数据库。
解决: Flask-SQLAlchemy只有当数据库表不存在了才从模型创建它们,所以更新表的唯一途径就是销毁旧的表,当然这将导致所有数据库中的数据丢失。
有个更好的解决方案就是使用数据库迁移框架。和源码版本控制工具跟踪更改源码文件一样,数据库迁移框架跟踪更改数据库模型,然后将增量变化应用到数据库中。
SQLAlchemy的主要开发人员写了一个Alembic迁移框架,但我们不直接使用Alembic,Flask应用可以使用Flask-Migrate扩展,一个集成了Flask-Script来提供所有操作命令的轻量级Alembic包。
功能: 管理升级迁移数据库
Flask-Migrate 插件提供了和 Django 自带的 migrate 类似的功能。
多数情况下 Flask-Migrate 是会和命令行工具插件 Flask-Script 和数据库插件 flask_sqlalchemy 一起使用的
即 Alembic(Database migration 数据迁移跟踪记录)提供的数据库升级和降级的功能。它所能实现的效果有如 Git 管理项目代码一般。
安装:
pip install flask-migrate
配置flask- script的命令
manager.add_command('db',MigrateCommand)
指令使用
python manage.py db init
python manage.py db migrate
python manage.py db upgrade
python manage.py db --help
manage.py db migrate --message '更新了XX'
init.py
import logging
from logging.handlers import RotatingFileHandler
# 设置日志的记录等级
logging.basicConfig(level=logging.DEBUG) # 调试debug级
# 创建日志记录器,指明日志保存的路径、每个日志文件的最大大小、保存的日志文件个数上限
file_log_handler = RotatingFileHandler("logs/log", maxBytes=1024*1024*100, backupCount=10)
# 创建日志记录的格式 日志等级 输入日志信息的文件名 行数 日志信息
formatter = logging.Formatter('%(levelname)s %(filename)s:%(lineno)d %(message)s')
# 为刚创建的日志记录器设置日志记录格式
file_log_handler.setFormatter(formatter)
# 为全局的日志工具对象(flask app使用的)添加日志记录器
logging.getLogger().addHandler(file_log_handler)
开发模式默DBUG=TRUE ,会将
logging.basicConfig(level=logging.DEBUG) 忽略
common.py
from werkzeug.routing import BaseConverter
class ReConverter(BaseConverter):
""" 自定义re转换器 """
# url 传递的正则表达式
def __init__(self, url_map, regex):
super().__init__(url_map)
# 保存正则表达式
self.regex = regex
from flask import Blueprint, current_app, make_response
from ihome.utils.common import csrf_wrap
html = Blueprint('web_html', __name__)
@html.route("/" )
@csrf_wrap
def get_html(html_file_name):
""" 提供静态文件访问 """
if not html_file_name:
html_file_name = 'index.html'
if html_file_name != 'favicon.ico':
html_file_name = 'html/' + html_file_name
# 前后端分离后,给rsp添加csrf的cookie
# rsp = make_response(current_app.send_static_file(html_file_name))
# rsp.set_cookie('csrf_token', csrf.generate_csrf())
# return rsp
return current_app.send_static_file(html_file_name)
flask的应用对象中添加
将自定义转换器类,添加到默认的转换列表中,注册蓝图
app.url_map.converters['re'] = ReConverter
from ihome.web_html import html
app.register_blueprint(html)
common.py
# 前后端分离后,给rsp添加csrf的cookie
rsp = make_response(current_app.send_static_file(html_file_name))
rsp.set_cookie('csrf_token', csrf.generate_csrf())
return rsp
# return current_app.send_static_file(html_file_name)
@api.route('/image_code/' )
def get_image_code(image_code_id):
"""
获取图片验证码, 并将验证值保存到redis中
:param image_code_id: 浏览器端带入的图片验证码编号
:return 正常: 返回图片验证码, 异常:返回json
"""
# 1. 业务处理逻辑
# 2. 生成图片验证码
# 名字, 真是文本, 图片二进制数据
name, text, image_data = captcha.generate_captcha()
# 3. 如果使用哈希表,只能同一设置过期时间,不合适
# 将图片验证吗保存到redis中, 字符串
try:
redis_store.setex(
f'image_code_{image_code_id}',
constants.IMAGE_CODE_REDIS_EXPIRES,
text
)
except Exception as e:
current_app.logger.error(e)
# 图片保存失败, 返回错误json
return jsonify(errno=RET.DBERR, errmsg='保存图片验证码失败!')
# 4. 返回
rsp = make_response(image_data)
rsp.headers['Content-Type'] = 'image/jpg'
return rsp
http://127.0.0.1:5000/api/v1.0/image_code/123
register.html
<div class="form-group form-group-lg">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-image fa-lg fa-fw">i>div>
<input type="text" class="form-control" name="imagecode" id="imagecode" placeholder="图片验证码" required>
<div class="input-group-addon image-code" onclick="generateImageCode();"><img src="">div>
div>
div>
register.js
var imageCodeId = "";
// 生成uuid
function generateUUID() {
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
/**
* 生成图片验证码
*/
function generateImageCode() {
imageCodeId = generateUUID()
$('.image-code img').attr('src', '/api/v1.0/image_code/' + imageCodeId)
}
$(document).ready(function () {
generateImageCode();
@api.route('/sms_code/' )
def get_sms_code(mobile_num):
"""
验证图片验证码, 再发送手机短信验证码
:param mobile_num 手机号码
:return sms_code 手机验证码
"""
# 1. 验证图片验证码
image_code = request.args.get('image_code')
image_code_id = request.args.get('image_code_id')
# 检验参数完整性
if not all([image_code_id, image_code]):
return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
# 检验参数正确性
try:
real_image_code = redis_store.get(f'image_code_{image_code_id}')
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='redis数据库异常')
if not real_image_code:
return jsonify(errno=RET.NODATA, errmsg='图片验证码失效')
# 删除redis中图片验证码,防止用户多次尝试, 生产环境开启
try:
redis_store.delete(f'image_code_{image_code_id}')
except Exception as e:
current_app.logger.error(e)
if real_image_code.lower() != image_code.lower():
return jsonify(errno=RET.DATAERR, errmsg='图片验证失败')
# 验证手机是否在一分钟内已发送验证码,限制一分钟内只能发送一次
try:
send_flag = redis_store.get(f'send_sms_code_{mobile_num}')
except Exception as e:
current_app.logger.error(e)
else:
if send_flag:
return jsonify(errno=RET.REQERR, errmsg='请求过于频繁,请60秒后重试')
# 2. 验证手机是否以注册
try:
user = User.query.filter_by(mobile=mobile_num).first()
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库异常')
else:
if user is not None:
return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册,请直接登录')
# 3. 验证通过,保存到redis中
sms_code = '%06d' % randint(0, 999999)
try:
redis_store.setex(
f'sms_code_{mobile_num}',
constants.SMS_CODE_REDIS_EXPIRES,
sms_code
)
# 保存手机发送短信验证码的记录,限制60秒内重复发送
redis_store.setex(
f'send_sms_code_{mobile_num}',
constants.SEND_SMS_CODE_INTERVAL,
1
)
except Exception as e:
return jsonify(errno=RET.DBERR, errmsg='redis数据库异常')
# 4. 发送手机验证码
# try:
# status = ccp.send_template_sms(
# mobile_num,
# [sms_code, str(constants.SMS_CODE_REDIS_EXPIRES//60)],
# 1
# )
# except Exception as e:
# current_app.logger.error(e)
# return jsonify(errno=RET.THIRDERR, errmsg='短信发送异常')
# celery 发布异步任务
send_sms.delay(mobile_num, sms_code, str(
constants.SMS_CODE_REDIS_EXPIRES//60), 1)
return jsonify(errno=RET.OK, errmsg='短信发送成功')
/**
* 获取短信验证码方法
*/
function sendSMSCode() {
$(".phonecode-a").removeAttr("onclick");
var mobile = $("#mobile").val();
if (!mobile) {
$("#mobile-err span").html("请填写正确的手机号!");
$("#mobile-err").show();
$(".phonecode-a").attr("onclick", "sendSMSCode();");
return;
}
var imageCode = $("#imagecode").val();
if (!imageCode) {
$("#image-code-err span").html("请填写验证码!");
$("#image-code-err").show();
$(".phonecode-a").attr("onclick", "sendSMSCode();");
return;
}
$.get("/api/v1.0/sms_code/" + mobile, {
image_code: imageCode,
image_code_id: imageCodeId
},
function (data) {
if (0 != data.errno) {
$("#image-code-err span").html(data.errmsg);
$("#image-code-err").show();
if (2 == data.errno || 3 == data.errno) {
generateImageCode();
}
$(".phonecode-a").attr("onclick", "sendSMSCode();");
} else {
var $time = $(".phonecode-a");
var duration = 60;
var intervalid = setInterval(function () {
$time.html(duration + "秒");
if (duration === 1) {
clearInterval(intervalid);
$time.html('获取验证码');
$(".phonecode-a").attr("onclick", "sendSMSCode();");
}
duration = duration - 1;
}, 1000, 60);
}
}, 'json');
}
@api.route('/users', methods=['POST'])
def register():
"""
用户注册
:return:
"""
# 1. 检查参数缺失
req_dict = request.get_json()
print(req_dict)
mobile = req_dict.get('mobile')
password = req_dict.get('password')
password2 = req_dict.get('password2')
sms_code = req_dict.get('sms_code')
if not all([mobile, password, password2, sms_code]):
return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
# 2. 业务处理
# 检查手机号
if not re.match(r'1[345789]\d{9}', mobile):
return jsonify(errno=RET.PARAMERR, errmsg='手机号码格式错误')
# 检查密码是否一致
if password != password2:
return jsonify(errno=RET.PWDERR, errmsg='两次密码不一致')
# 检查短信验证码是否正确
try:
real_sms_code = int(redis_store.get(f'sms_code_{mobile}'))
except Exception as e:
return jsonify(errno=RET.DBERR, errmsg='读取真实手机验证码错误')
if real_sms_code is None:
return jsonify(errno=RET.NODATA, errmsg='短信验证码失效')
# 删除短信验证码,防止重复利用
try:
redis_store.delete(f'sms_code_{mobile}')
except Exception as e:
current_app.logger.error(e)
# print(real_sms_code, sms_code)
if real_sms_code == sms_code:
return jsonify(errno=RET.DATAERR, errmsg='短信验证码错误')
# 3. 检查手机唯一性
# try:
# user = User.query.filter_by(mobile=mobile).first()
# except Exception as e:
# current_app.logger.error(e)
# return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')
# 4. 保存用户信息
user = User(name=mobile, mobile=mobile)
user.password = password
try:
db.session.add(user)
db.session.commit()
# 由于手机号码具有唯一性, 一旦重复插入就会报错, 利用此性质, 不需要进行查询
# 会抛出 IntegrityError 异常,减少数据库操作
except IntegrityError as e:
db.session.rollback()
# 手机号重复
current_app.logger.error(e)
return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库异常')
# 保存登录状态到session中
session['name'] = mobile
session['mobile'] = mobile
session['user_id'] = user.id
return jsonify(errno=RET.OK, errmsg='注册成功')
@api.route('/users', methods=['POST'])
def register():
"""
用户注册
:return:
"""
# 1. 检查参数缺失
req_dict = request.get_json()
print(req_dict)
mobile = req_dict.get('mobile')
password = req_dict.get('password')
password2 = req_dict.get('password2')
sms_code = req_dict.get('sms_code')
if not all([mobile, password, password2, sms_code]):
return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
# 2. 业务处理
# 检查手机号
if not re.match(r'1[345789]\d{9}', mobile):
return jsonify(errno=RET.PARAMERR, errmsg='手机号码格式错误')
# 检查密码是否一致
if password != password2:
return jsonify(errno=RET.PWDERR, errmsg='两次密码不一致')
# 检查短信验证码是否正确
try:
real_sms_code = int(redis_store.get(f'sms_code_{mobile}'))
except Exception as e:
return jsonify(errno=RET.DBERR, errmsg='读取真实手机验证码错误')
if real_sms_code is None:
return jsonify(errno=RET.NODATA, errmsg='短信验证码失效')
# 删除短信验证码,防止重复利用
try:
redis_store.delete(f'sms_code_{mobile}')
except Exception as e:
current_app.logger.error(e)
# print(real_sms_code, sms_code)
if real_sms_code == sms_code:
return jsonify(errno=RET.DATAERR, errmsg='短信验证码错误')
# 3. 检查手机唯一性
# try:
# user = User.query.filter_by(mobile=mobile).first()
# except Exception as e:
# current_app.logger.error(e)
# return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')
# 4. 保存用户信息
user = User(name=mobile, mobile=mobile)
user.password = password
try:
db.session.add(user)
db.session.commit()
# 由于手机号码具有唯一性, 一旦重复插入就会报错, 利用此性质, 不需要进行查询
# 会抛出 IntegrityError 异常,减少数据库操作
except IntegrityError as e:
db.session.rollback()
# 手机号重复
current_app.logger.error(e)
return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库异常')
# 保存登录状态到session中
session['name'] = mobile
session['mobile'] = mobile
session['user_id'] = user.id
return jsonify(errno=RET.OK, errmsg='注册成功')
$(document).ready(function () {
generateImageCode();
$("#mobile").focus(function () {
$("#mobile-err").hide();
});
$("#imagecode").focus(function () {
$("#image-code-err").hide();
});
$("#phonecode").focus(function () {
$("#phone-code-err").hide();
});
$("#password").focus(function () {
$("#password-err").hide();
$("#password2-err").hide();
});
$("#password2").focus(function () {
$("#password2-err").hide();
});
// 为表单的提交添加自定义函数 (提交事件)
$(".form-register").submit(function (e) {
// 阻止浏览器默认的表单提交事件
e.preventDefault();
mobile = $("#mobile").val();
phoneCode = $("#phonecode").val();
passwd = $("#password").val();
passwd2 = $("#password2").val();
if (!mobile) {
$("#mobile-err span").html("请填写正确的手机号!");
$("#mobile-err").show();
return;
}
if (!phoneCode) {
$("#phone-code-err span").html("请填写短信验证码!");
$("#phone-code-err").show();
return;
}
if (!passwd) {
$("#password-err span").html("请填写密码!");
$("#password-err").show();
return;
}
if (passwd != passwd2) {
$("#password2-err span").html("两次密码不一致!");
$("#password2-err").show();
return;
}
var req_data = {
"mobile": mobile,
"password": passwd,
"password2": passwd2,
"sms_code": phoneCode
};
var req_json = JSON.stringify(req_data);
$.ajax({
type: "post",
url: "/api/v1.0/users",
data: req_json,
contentType: "application/json",
dataType: "json",
headers: {
"X-CSRFToken": getCookie("csrf_token")
},
success: function (rsp) {
if (rsp.errno == 0) {
location.href = "/index.html"
} else {
alert(rsp.errmsg)
}
}
});
});
})
// js读取cookie的方法
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var req_json = JSON.stringify(req_data);
$.ajax({
type: "post",
url: "/api/v1.0/users",
data: req_json,
contentType: "application/json",
dataType: "json",
headers: {
"X-CSRFToken": getCookie("csrf_token")
}, // 请求头
success: function (rsp) {
if (rsp.errno == 0) {
location.href = "/index.html"
} else {
alert(rsp.errmsg)
}
}
});