文件上传
一、上传到自己的服务器:
使用 AdminLTE 2 网站中的 form 表单的代码:https://adminlte.io/themes/AdminLTE/pages/forms/general.html (使用其原先css样式及HTML代码,代码内容经过精简)
前端代码:
首先:一个带有输入框跟上传按钮的HTML文件(样式图如下:)
1 <div class="form-group"> 2 <label for="thumbnail-form">缩略图</label> 3 <div class="input-group"> 4 <input type="text" class="form-control" id="thumbnail-form" name="thumbnail"> 5 <span class="input-group-btn"> 6 <input type="file" class="btn btn-default" id="thumbnail-btn" value="上传文件"> 7 </span> 8 </div> 9 </div>
使用 input标签 , type=file 时显示的样式;
将上图修改为下图的样式,使其更美观;
1 <div class="form-group"> 2 <label for="thumbnail-form">缩略图</label> 3 <div class="input-group"> 4 <input type="text" class="form-control" id="thumbnail-form" name="thumbnail"> 5 <span class="input-group-btn"> 6 <label class="btn btn-default btn-file"> 7 上传图片<input hidden type="file" class="btn btn-default" id="thumbnail-btn"> 8 </label> 9 </span> 10 </div> 11 </div>
id = "thumbnail-form":输入框的ID;
id = "thumbnail-btn":作为隐藏起来的 file标签 文件的一个按钮ID;
使用 label标签,其中 for 中的值等于 input 的ID:相当于点击“缩略图”时会默认点击下方输入框;点击“上传图片”会默认点击“type=file”的标签;二者之间存在关联;
使用 hidden 将 file标签 隐藏起来,使“上传图片”的字体将其覆盖;
使用js文件监听其上传时的点击事件;
1 function News() { 2 3 } 4 5 News.prototype.run = function () { 6 var self = this; 7 self.ListenUploadFileEvent(); 8 }; 9 10 // 监听文件上传的事件; 11 News.prototype.ListenUploadFileEvent = function () { 12 var uploadBtn = $('#thumbnail-btn'); 13 uploadBtn.change(function () { 14 var file = uploadBtn[0].files[0]; 15 var formData = new FormData(); 16 formData.append('file', file); 17 xfzajax.post({ 18 'url': '/cms/upload_file/', 19 'data': formData, 20 'processData': false, 21 'contentType': false, 22 'success': function (result) { 23 if (result['code'] === 200){ 24 var url = result['data']['url']; 25 var thumbnailInput = $('#thumbnail-form'); 26 thumbnailInput.val(url); 27 } 28 } 29 }) 30 }); 31 }; 32 33 $(function () { 34 var news = new News(); 35 news.run(); 36 });
change:用来监听进入上传文件选定文件后点击打开按钮的事件,此处不能使用 click 点击事件;
因为 uploadBtn 得到的是一个集合,不是一个单一的对象。所以 var file = uploadBtn[0].files[0]; 中的 uploadBtn[0] 用来获取其中的唯一一个按钮,与其存储的所有文件中的第一个文件。
formData:用来存储文件;
formData.append('file', file); :引号中的字段名需与后台通过 ‘get’ 获取文件填写的字段名保持一致;
然后通过ajax请求发送给后端服务器,跳转的 url 及其数据 formData,后两个 false 为告知上层 jquery-3.3.1.min.js 文件不对该文件的数据再次进行处理或添加其它内容,以及成功或者失败的返回内容(失败内容定义在ajax文件中);
在后端中得到的url完整路径,调用 ‘restful’ 中的 ‘result’ 函数,再将获取的 ‘url’ 的值给到前端输入框中显示。
ajax代码
1 //static:xfzajax.js 2 function getCookie(name) { 3 var cookieValue = null; 4 if (document.cookie && document.cookie !== '') { 5 var cookies = document.cookie.split(';'); 6 for (var i = 0; i < cookies.length; i++) { 7 var cookie = jQuery.trim(cookies[i]); 8 // Does this cookie string begin with the name we want? 9 if (cookie.substring(0, name.length + 1) === (name + '=')) { 10 cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 11 break; 12 } 13 } 14 } 15 return cookieValue; 16 } 17 18 var xfzajax = { 19 'get': function (args) { 20 args['method'] = 'get'; 21 this.ajax(args); 22 }, 23 'post': function (args) { 24 args['method'] = 'post'; 25 this._ajaxSetup(); 26 this.ajax(args); 27 }, 28 'ajax': function (args) { 29 var success = args['success']; 30 args['success'] = function (result) { 31 if (result['code'] === 200) { 32 if (success) { 33 success(result) 34 } 35 } else { 36 var messageObject = result['message']; 37 if (typeof messageObject == 'string' || messageObject.constructor === String) { 38 window.messageBox.showError(messageObject); 39 } else { 40 // {"password":['密码最大长度不能超过20为!','xxx'],"telephone":['xx','x']} 41 for (var key in messageObject) { 42 var messages = messageObject[key]; 43 var message = messages[0]; 44 window.messageBox.showError(message); 45 } 46 } 47 if(success){ 48 success(result) 49 } 50 } 51 }; 52 args['fail'] = function (error) { 53 console.log(error); 54 window.showError('服务器内部错误!') 55 }; 56 $.ajax(args); 57 }, 58 '_ajaxSetup': function () { 59 $.ajaxSetup({ 60 beforeSend: function (xhr, settings) { 61 if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type) && !this.crossDomain) { 62 xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); 63 } 64 } 65 }); 66 } 67 };
后端代码:
settings.py文件的设置:
1 # 在末尾添加内容; 2 3 STATIC_URL = '/static/' 4 STATICFILES_DIRS = [ 5 os.path.join(BASE_DIR,'front','dist'), 6 ] 7 8 MEDIA_URL = '/media/' 9 MEDIA_ROOT = os.path.join(BASE_DIR,'media')
使用MEDIA_ROOT与MEDIA_URL,用来指定文件上传后存储的位置,在主目录中新建‘media’文件用来存储内容;
文件的映射路径,与前端 ajax 内容中跳转的 url 一致。
1 # 总的url链接; 2 3 from django.urls import path,include 4 from django.conf.urls.static import static 5 from django.conf import settings 6 7 urlpatterns = [ 8 path('cms/',include('apps.cms.urls')), 9 ] + static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) 10 11 12 # cms的url: 13 14 from django.urls import path 15 from . import views 16 17 app_name = 'cms' 18 19 urlpatterns = [ 20 path('upload_file/',views.upload_file,name='upload_file'), 21 ]
视图函数
1 from django.views.decorators.http import require_POST, 2 from utils import restful 3 from django.conf import settings 4 import os 5 6 7 # 文件上传; 8 @require_POST 9 def upload_file(request): 10 file = request.FILES.get('file') 11 name = file.name 12 with open(os.path.join(settings.MEDIA_ROOT,name),'wb') as fp: 13 for chunk in file.chunks(): 14 fp.write(chunk) 15 url = request.build_absolute_uri(settings.MEDIA_URL + name) 16 return restful.result(data={'url':url})
使用 “get” 获取的 “file” 与前端提到 “file” 一致;
使用 build_absolute_uri 可以获取完整的url地址;
restful.py文件(简化每个文件代码运行结果的代码量)
1 from django.http import JsonResponse 2 3 class HttpCode(object): 4 ok = 200 # 正常运行; 5 paramserror = 400 # 参数错误; 6 unauth = 401 # 没授权; 7 methoderror = 405 # 请求方法错误; 8 servererror = 500 # 服务器内部错误; 9 10 def result(code=HttpCode.ok,message='',data=None,kwargs=None): 11 json_dict = {'code':code,'message':message,'data':data} 12 if kwargs and isinstance(kwargs,dict) and kwargs.keys(): 13 json_dict.update(kwargs) 14 return JsonResponse(json_dict) 15 16 def ok(): 17 return result() 18 19 def params_error(message='',data=None): 20 return result(code=HttpCode.paramserror,message=message,data=data) 21 22 def unauth(message='',data=None): 23 return result(code=HttpCode.unauth,message=message,data=data) 24 25 def method_error(message='',data=None): 26 return result(code=HttpCode.methoderror,message=message,data=data) 27 28 def server_error(message='',data=''): 29 return result(code=HttpCode.servererror,message=message,data=data)
二、上传到七牛云
1、登录注册七牛云:https://www.qiniu.com//
2、登录后进入右上方管理控制台后,再点击右上角的个人图标进入密钥管理,记住 AK 与 SK ,后续用到;
3、点击左侧 对象存储,新建存储空间 bucket 。
后端:
1、下载 python SDK:pip install qiniu;
2、创建一个获取 token 的 url;
1 # 上传七牛云; 2 @require_GET 3 def qntokon(request): 4 access_key = 'GjdN0XJxtrLbINSxx4ZjXXitk7Aux5h046x9viB8' 5 secret_key = '8ULlOQezsKZIRFl_Bie09GZIJyonH50aJH6RONhu' 6 bucket = 'qmspace' 7 # 创建授权信息q; 8 q = qiniu.Auth(access_key,secret_key) 9 # 生成上传凭证,传给前端; 10 tokon = q.upload_token(bucket) 11 return restful.result(data={'tokon':tokon})
前端:
基于七牛 API 开发的前端 JavaScript SDK:https://github.com/qiniu/js-sdk(详细的官方文档讲解)
在HTML代码的缩略图代码下添加进度条代码,默认隐藏;
1 <!--html代码--> 2 <script src="https://unpkg.com/qiniu-js@2.4.0/dist/qiniu.min.js 3 "></script> 4 --snip-- 5 <div class="form-group"> 6 <label for="thumbnail-form">缩略图</label> 7 --snip-- 8 </div> 9 </div> 10 <!--上传进度条--> 11 <div class="form-group" id="progress-group" style="display: none;"> 12 <div class="progress"> 13 <div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="20" 14 aria-valuemin="0" aria-valuemax="100" style="width: 0"> 15 0% 16 </div> 17 </div> 18 </div>
通过 js 代码实现与七牛云的对接及文件的上传,以及进度条的显示情况;
1 // 上传到七牛云; 2 News.prototype.ListenQiniuuploadFileEvent = function(){ 3 var self = this; 4 var uploadBtn = $('#thumbnail-btn'); 5 uploadBtn.change(function () { 6 var file = this.files[0]; 7 xfzajax.get({ 8 'url': '/cms/qntokon/', 9 'success': function (result) { 10 if (result['code'] ===200 ){ 11 12 // token: 上传验证信息,前端通过接口请求后端获得; 13 var tokon = result['data']['tokon']; 14 15 // key: 文件资源名(当前时间+文件格式); 16 var key = (new Date()).getTime() + '.' + file.name.split('.')[-1]; 17 18 var putExtra = { 19 fname: key, 20 params: {}, 21 mimeType: ['image/png','image/jpeg','image/gif','video/x-ms-wmv','video/mp4','video/x-flv'] 22 }; 23 24 var config = { 25 useCdnDomain: true, 26 retryCount: 6, 27 region: qiniu.region.z2 28 }; 29 30 // 文件上传; 31 var observable = qiniu.upload(file,key,tokon,putExtra,config); 32 observable.subscribe({ 33 'next': self.handFileUploadProcess, 34 'error': self.handFileUploadError, 35 'complete': self.handFileUploadComplete, 36 }) 37 } 38 } 39 }) 40 }) 41 }; 42 43 News.prototype.handFileUploadProcess = function(response){ 44 var total = response.total; 45 var percent = total.percent; 46 var percentText = percent.toFixed() + '%'; 47 var progressGroup = News.progressGroup; 48 progressGroup.show(); 49 var progressBar = $('.progress-bar'); 50 progressBar.css({"width":percentText}); 51 progressBar.text(percentText); 52 }; 53 54 News.prototype.handFileUploadError = function(error){ 55 window.messageBox.showError(error.message); 56 var progressGroup = News.progressGroup; 57 progressGroup.hide(); 58 console.log(error.message); 59 }; 60 61 News.prototype.handFileUploadComplete = function(response){ 62 console.log(response); 63 var progressGroup = News.progressGroup; 64 progressGroup.hide(); 65 var domain = 'http://ps96zui1h.bkt.clouddn.com/'; 66 var filename = response.key; 67 var url = domain + filename; 68 var thumbnailInput = $("input[name='thumbnail']"); 69 thumbnailInput.val(url) 70 }; 71 72 $(function () { 73 var news = new News(); 74 news.run(); 75 News.progressGroup = $('#progress-group'); 76 });
(官方文档很详细,不再做个人注解)
上传成功: