看了看,上篇开发手记是去年8月份写的,到现在差2个月整一年了。停更这么长时间,第一个原因是中间帮朋友忙一个活,那个技术架构是用springboot的,虽然前端也用layUI,但和Flask-python完全不搭界,所以,有半年时间就忙那事了。第二个原因,从今年三月份虽然又开始继续做Flask+Layui的框架,不过对layUI的编程已经从造猫画虎的模仿阶段变成了理解其内在机制可以天马行空实现功能的阶段了。
现在再看前面写的这些手记,如果用两个字来形容,那就是“生涩”(其实我很想说垃圾的,但这样说去年的自己,确实不太好)。
讲真,从layui.use()到layui.define()再到layui.config(),这一层层学上来,把这三个都理解透了,也敢用了,才敢真正说自己掌握了layui的脉络,也才能理解layui的强大。虽然layUI现在确实已经不流行了(不过还是在更新中),之所以选择这个,还是因为这是一个传统程序员最喜欢的工具箱,而不是VUE那种加入诸多工具的框架。
你可以随时在原生JS、JQuery和layUI的编程之间无缝切换,遇到困难,觉得layUI里有什么好用就马上拿过来用,没好用的,转头上网找个小工具程序加进来,也可以。而不是“一入框架深似海,从此JS成路人”那样,被粘上后就只能在框架打滚了。当然,VUE和layUI本来就是两个层次的东西,两个是可以结合的,这也是下一步准备尝试的。
而且,就算某一天layUI不更新完全过气了,有在layUI上的编程经验,转身去学element-UI也没啥难度,其实这些工具箱的思路都是一样的,就是最大程度的把一些编程中经常用到的组件模块化工具化。在这个AI的时代,遇到问题可以通过各种途径去查答案,限制程序员能力的从来都只是想象力,而不是技术水平了。
好吧,废话不多说,继续上次的手记,这次介绍一下头像上传的功能实现,同样也是前端用layUI的上传组件,后台用flask-python的接收文件功能。加一点特色的地方,就是上传的头像目录没有放在static下面,而是在项目根目录下新开了一个srvdata目录,在上传文件时当然不是问题,但是html静态文件的<img>标签中内嵌头像文件名时就出问题了,好在这些也都解决了。
整个程序分成了三个部分,第一是前端页面+JS程序,第二个是服务端接收文件的路由服务程序,第三个是如何在静态html中实现图像文件不在static目录下。
首先是第一部分前端页面程序,这个包括html的界面展示和JS的程序实现。注意,现在的程序里,将完全取消掉jinja2模板的编程实现,所有后端和前端的数据交互均通过ajax/post完成。这主要是在年初接触了restful API编程概念,前后端分离,前端进行流控,后端无状态只提供资源,觉得完全是我想要的,所以,基本把以前的程序都翻了一遍,除了页面流转用到render_template()外,其它flask提供的前后端连接函数基本都弃了。
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><title>设置头像</title><link rel="stylesheet" href="/static/layui/css/layui.css" media="all">
</head>
<body>
<div style="padding:20px;"><form class="layui-form" id="avatarform" action="" enctype='multipart/form-data' method="post" lay-filter="avatarform" ><div class="layui-form-item"><label class="layui-form-label">上传头像</label><div class="layui-input-block"><img id="userAvatar" src="/static/images/avatar/avatar_def.png" alt="默认头像" width="100" height="100"><div class="layui-row layui-inline" style="margin-left:40px;width:420px;"><div class="layui-row layui-inline" style="width:38%"><button type="button" class="layui-btn" id="ID-upload-btn"><i class="layui-icon layui-icon-username"></i>更换头像</button><button type="button" class="layui-btn" id="ID-upload-action" style="margin-top:10px"><i class="layui-icon layui-icon-upload"></i>开始上传</button><div class="layui-word-aux">图片限制2MB以下</div></div><div class="layui-row layui-inline" style="width:40%"><div class="layui-upload-list layui-inline" style="text-align:center;width:120px"><img class="layui-upload-img" id="ID-upload-img" style="width:100%; height: 92px;"><div id="ID-upload-text"></div><div class="layui-progress layui-progress-big" lay-showPercent="yes" lay-filter="filter-upload"><div class="layui-progress-bar" lay-percent=""></div></div></div></div></div></div></form>
</div>
<script src="/static/layui/layui.js"></script>
<script>layui.use(['layer'], function () {var $ = layui.jquery,layer = layui.layer,element = layui.element,upload = layui.upload;let loginInfo = JSON.parse(sessionStorage.getItem('loginInfo'));console.log('loginInfo:',loginInfo);user_avatar = loginInfo.user_avatar;if (user_avatar && user_avatar!='None') {$('#userAvatar').attr('src',user_avatar);}var uploadInst = upload.render({elem: '#ID-upload-btn',url: '/avatar', auto : false,bindAction: '#ID-upload-action',size : 2000,acceptMime: 'image/*',choose: function(obj){// 预读本地文件示例,不支持ie8obj.preview(function(index, file, result){$('#ID-upload-img').attr('src', result); // 图片链接(base64)});},before: function(obj){element.progress('filter-upload', '0%'); // 进度条复位layer.msg('上传中', {icon: 16, time: 0});},done: function(res){// 若上传失败if(res.success == 0){return layer.msg('上传失败');}// 上传成功的操作$('#ID-upload-text').html(''); // 置空上传失败的状态let src_file = res.avatar + '?time=' + new Date().getTime();$('#userAvatar').attr('src',src_file);let p_userava = parent.layui.$('#userAvatar');$(p_userava).attr('src',src_file);},error: function(){// 演示失败状态,并实现重传var demoText = $('#ID-upload-text');demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-xs demo-reload">重试</a>');demoText.find('.demo-reload').on('click', function(){uploadInst.upload();});},// 进度条progress: function(n, elem, e){element.progress('filter-upload', n + '%'); // 可配合 layui 进度条元素使用if(n == 100){layer.msg('上传完毕', {icon: 1});}}});});
</script>
</body>
</html>
html部分就不仔细介绍了,基本是从layUI教程中扒下来的示例,只是做了一些界面设计,如下图这样。流程上就是先点击“更换头像”选择本地图像文件,之后在右框中会显示缩微图像,如果满意,再点击“开始上传”,之后,会显示进度条,传完后,就OK。
JS程序也不复杂,主体就是调用layUI的upload文件上传控件,相关的内容说明文档中都有。本程序为了详细测试一下upload组件,采用了两阶段提交模式,即先选择文件之后再上传,所以用到choose参数,正常的图像上传建议还是选择上传自动连续比较好。
JS开头部分是设置头像文件路径,先从sessionStorage中取出loginInfo,这个对象存储了用户相关的注册信息,包括用户ID、姓名以及头像文件路径,是在系统主框架部分从服务端取下来放到客户端session中的,然后系统中所有的页面程序均可取出来使用。sessionStorage中只能存字符串,所以,要存储loginInfo,得先用JSON.stringify()将其变为字符串再存,相应的,取出时则用JSON.parse()解析回对象即可。
下面是第二部分,服务端的python程序。主体就是接收图像文件,将其更名为“avatar_用户ID.png”的文件,并存入到srvdata/uploads/avatar这个目录下。在存储成功后,要去更新用户表中将对相应的avatar文件路径字段,并修改系统中一些环境变量,在调试时可以去掉那些不用的东西。
from flask import Blueprint,send_from_directory,render_template
from flask import make_response,Response,request,session,g,jsonify
from io import BytesIO
import json
from PIL import ImageADMIN_USER_AVATAR = "HEBOANHEAV"
ADMIN_USER_ID = 'HEBOANHEHE'
SRVDATA_UPLOAD = 'srvdata/uploads'#头像服务
@app.route('/avatar',methods=['GET','POST'])
@login_required
def avatar():if request.method == 'GET':return render_template('admin/avatar.html.j2')else:avt = request.files.get('file')sour_file_name = avt.filenameextname = sour_file_name.split('.')[1]uid = session[ADMIN_USER_ID] new_file_name = 'avatar_' + str(uid) + '.' + 'png'save_path = SRVDATA_UPLOAD + '/avatar/' + new_file_name#avt.save(save_path)img = Image.open(avt)#img = img.resize(128,128)img.save(save_path,'PNG')avatar_file = '/' + save_pathif avatar_userupdate(uid,avatar_file) :rs_data = {'success':1,'msg':'更新头像成功','avatar':avatar_file,'code':0}else :rs_data = {'success':0,'msg':'更新头像失败','avatar': '','code':201}return json.dumps(rs_data)def avatar_userupdate(id,sava_path):irow = db.session.query(Users).filter_by(id=id).first()if irow :irow.avatar = sava_pathdb.session.commit()session[ADMIN_USER_AVATAR] = sava_pathg.admin_avatar = sava_pathreturn True
上面两段程序交互后,即可把头像文件上传,如果图像文件上传到static目录下,那程序到这儿就结束了。但是,熟悉JAVA/WEB编程的人都知道,static目录只能存静态文件,象上传下载的文件,应该开辟新目录,省得对程序打包安装时出麻烦。不过,当在flask编程时这么想时,那麻烦就出来了,静态html页面中<img>src="文件名“</img>中这个文件名必须在static目录下,放在别的目录,系统提示404。
好在虽然flask没有啥地方能配置增加资源目录,但还是有变通办法解决的,就是写下面一段程序来解决。
from flask import Flask,send_from_directory@app.route('/srvdata/<path:filename>')
def server_get_file(filename):logging.debug('srvdata...... %s' % filename)#return 'srvdata ..' + filenamereturn send_from_directory('srvdata/', filename)
写一个路由程序,将文件名带入,。这段程序十分短小,但却真是能解决大问题,开始还没看明白,后来是越看越觉得思路巧妙。flask服务,页面上任何的路径都会先被理解为路由,找不到路由服务的话,才会被当成文件路径来处理,这段程序就是使用了这个规则。
在html页面上定义的图像文件是一个全路径名称,比如”/srvdata/uploads/avatar/avatar-4.png",那么我们就定义一个与主目录名完全一样的路由程序,路由命名为“主路由+路径参数”,这样所有在此目录下的文件名都会被定向到到这个路由服务中,之后要做的,就是如何把文件下传了,send_from_directory()就是干这个活的。 上传完了的效果是这样的。
同时,别忘了把系统主框架上的用户头像更新一下。JS程序中这两句就是做这个用的。
let p_userava = parent.layui.$('#userAvatar');
$(p_userava).attr('src',src_file);