博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
关于文件上传那些可能不怎么对的姿势
阅读量:5823 次
发布时间:2019-06-18

本文共 12894 字,大约阅读时间需要 42 分钟。

原文地址:

请大家多多指教,鞠躬。

背景

在之前的工作当中,遇到有文件上传的需求基本都是通过引用插件来实现的,效果是完成了,但是其实并没有一个系统的认识,理解比较粗浅。

鲁迅曾经曰过:

好读书,也要求甚解。诸葛村夫不求甚解,所以多智也只能近妖。

最近又遇到了相关的需求,在阅读了Deng mu qin(大神都是这样的,只留下了一串拼音字符,不带走一片云彩~)前辈的upload.js源码后,觉得可能跟业务比较耦合,通用性相对不是那么好,所以决定自己撸一个文件上传的小插件,既当是学习,同时也吸(chao)取(xi)一下前辈的人生经验。第一次写技术文章,其实技术性谈不上多强,主要是提醒自己要不断学习、不断总结,希望以后能成为一方小牛。真心希望能多多讨论,一起进步!

一些热身准备

FileUpload对象

初来乍到,萌新们可能跟我一样对FileUpload对象一无所知,无妨,先看一个最简单的例子:

复制代码

当上面的标签出现在页面中时,一个FileUpload对象就会被创建,然后就会出现一个大家熟悉的银灰色小方块,点击选择文件,出现对应的文件名称和格式。

XMLHttpRequest请求

现代浏览器中(IE10 & IE10+),XMLHttpRequest请求可以传输FormData对象,可以通过XMLHttpRequest对象的upload属性的onprogress事件回调方法获取传输进度,这也是在下的xupload.js的安生立命之本。至于IE9IE8IE7IE6,emmmm...

告辞。:running::running:

注册插件

通过一个经典的自执行匿名函数,再将方法注册到jQuery上,就可以基本实现一个jq插件的初步建立:

// ;冒号防止上下文中有其他方法未写;从而引起不必要的麻烦;(function ($) {    // 创建构造函数Upload    function Upload (config) {        // ...    }    // Upload的原型方法    Upload.prototype = {        // ...    };    // 实例化一个Upload,挂载到jQuery    $.xupload = function (config) {        return new Upload(config)    };})(jQuery);复制代码

代码解析

Upload构造函数

一个构造函数需要做些什么呢?

  1. 通过挂载到this的方式,初始化一些后续需要使用到的变量,此过程可以视后续代码需要不断增量更新
  2. 配置一个defaultConfig默认配置项,在用户直接调用xupload方法时直接使用配置项,当然,当用户传递属于自己的配置项时,需要将用户配置项跟默认配置项进行更新合并,此时可以用到jQuery的函数
  3. 调用初始化函数

代码如下:

function Upload(config) {    var _this = this; // 缓存this    _this.uploading = false; // 设置传输状态初始值    _this.defaultConfig = {        el: null, // {string || jQuery object} 绑定的元素,必填        uploadUrl: null, // {string} 上传路径,必填        uploadParams: {}, // {object} 上传携带参数对象,选填        maxSize: null, // {number} 上传的最大尺寸,选填        autoUpload: false, // {boolean} 是否自动上传,默认否        noGif: false, // {boolean} 是否支持gif上传,默认支持        previewWrap: null, // 图片预览的容器,选填        previewImgClass: 'x-preview-img', // 预览图片的class,previewWrap生效时方可用        start: function () {}, // 开始上传回调        done: function () {}, // 上传完成回调        fail: function () {}, // 上传失败回调        progress: function () {}, // 上传进度回调        checkError: function () {}, // 检测失败回调    };    _this.fileCached = []; // 上传文件缓存数组    _this.$root = null; // 挂载元素    // 防止previewImgClass为null或undefine    if (config.previewImgClass === null || config.previewImgClass === '') {        config.previewImgClass = _this.defaultConfig.previewImgClass; // 置为默认值    }        // 用户传入了配置项且配置项是一个纯粹的对象    if (config && $.isPlainObject(config)) {        // 通过jquery的extend方法进行合并        _this.config = $.extend({}, _this.defaultConfig, config);    } else {        _this.config = _this.defaultConfig; // 继承默认配置项        _this.isDefault = true;    }    _this.init(); // 调用初始化函数}复制代码

构造函数原型的结构

prototype在我看来有点类似于class之于css,你能想象如果css中没有class会发生什么吗?可用性和复用性都成了灾难,这是绝对不行的。

关于prototype的进一步解读,大家可以参考一下。

想象一下,我们把一些常用的工具方法挂载到prototype上,这样调用一个实例,这个实例就自动继承了所有在prototype上的方法,修改一下prototype,所有实例也都自动响应过来,是不是跟css中的class很像呢?

那么让我们来设计一下Upload的原型函数需要哪些基础的方法吧:

  • 首先需要一个init初始化函数,在这里调用必须用到的方法。 仔细想想,一个上传插件,第一步最需要的是不是响应用户选择文件的操作呢?再进一步,页面中是否只有一个上传input?
init: function () {    var _this = this,        config = this.config, // 缓存合并后的config        el = config.el,        isEl = _this._isSelector('el'), // 调用_isSector判断传入的格式是否符合要求        isPreviewWrap = _this._isSelector('previewWrap'); // 同上        // 抛出异常    if (!isEl) {        throw '请输入正确格式的el值'    }        if (!isPreviewWrap) {        throw '请输入正确格式的previewWrap值'    }        _this.$root = $(el); // 将元素赋值,方便后续的调用        _this.$root.each(function () {        $('body').on('change', el, function (e) {            var files = e.target.files;            Array.prototype.push.apply(_this.fileCached, files); // 同之前的深拷贝不同,为了后续的数组操作,我们应该将伪数组转化为真正的数组            _this.handler(e, files); // 调用处理器函数        });    });},_isSelector: function (el) {    var which = this.config[el]; // 拿到config里的属性    return Object.prototype.toString.call(which) === '[object String]' && which !== '' && !/^[0-9]+.?[0-9]*$/.test(which); // 必须是字符串且不能为空字符串且是非负整数}复制代码
  • 其次需要一个处理函数handler,去负责接下来具体的逻辑,比如规则的验证、图片预览等等
handler: function (e, files) {    var _this = this,        config = this.config,        fileCached = this.fileCached,        rules = this.validate(files);            if (rules.result) {        config.autoUpload && _this.triggerUpload();        // 暂时只支持图片预览        if (_this.$root.attr('accept').substr(0, 5) === 'image') { // 预览模式暂时只支持图片,通过判断accept来判断(需改进)            _this.previewBefore(); // 调用上传前函数        }    } else {        _this._checkError(rules.msgQuene); // 验证结果为false则触发_checkError函数    }}复制代码
  • 然后需要一个触发器函数triggerUpload,能够自动或者手动的执行接下来的上传操作,然后再多思考一步,用户会不会只想上传其中某一个文件呢?这是完全有可能的,所以我们得提供多一种思路,这里我们可以使用“函数重载”,当用户不传值时,则默认全部上传,如果传入了指定的index值,则单独上传该文件,之所以带引号,是因为确实只是通过简单的参数去实现的,更高级的函数重载,可以参考。
triggerUpload: function (index) {    var _this = this,        files = this.fileCached,        len = files.length;    var isIndex = (index >= 0); // 判断是否传入参数(排除index为0时的特殊情况)    var isValid = /^\d+$/.test(index) && index < len; // 判断传入的index是否为整数,切数目不能大于文件个数    if (isIndex && isValid) { // 如果传入了index参数且验证通过        if (len > 1) {            _this.upload(files[index]); // 多个文件直接传入指定index文件        } else if (len === 1) {            _this.upload(files[0]); // 否则传入第一个        }    } else if (!isIndex && !isValid) { // 如果传入了没有传入index参数且并没有验证通过        if (len > 1) {            _this.upload(files);        } else if (len === 1) {            _this.upload(files[0]);        }    } else if (isIndex && !isValid) { // 如果传入了index参数且并没有验证通过        throw 'triggerUpload方法传入的索引值为从0开始的整数且不得大于您上传的文件数' // 抛出异常    }}复制代码
  • 接下来就是重头戏upload了,需要这样一个函数去处理上传的POST请求,同时暴露出一些状态函数,比如onloadstart、onerror等等
upload: function (files) {    var _this = this,        uploadParams = this.config.uploadParams, // 有些时候请求需要携带额外的参数        xhr = new XMLHttpRequest(), // 创建一个XMLHttpRequest请求        data = new FormData(), // 创建一个FormData表单对象        fileRequestName = ''; // 文件请求名        // 如果uploadParams有fileRequestName则直接使用,否则为file[]    uploadParams.fileRequestName ?     fileRequestName = uploadParams.fileRequestName :     fileRequestName = 'file[]';    // 多文件上传处理    for (var i = 0, len = files.length; i < len; i++) {        var file = files[i];        // 将fileappend到FormData对象        data.append(fileRequestName, file);    }    // 参数处理    if (uploadParams) {        for (var key in uploadParams) {            // 忽略fileRequestName            if (key !== 'fileRequestName') {                // 将各个参数append到FormData                data.append(key, uploadParams[key]);            }        }    }    // 上传开始    xhr.onloadstart = function (e) {        _this._loadStart(e, xhr); // 调用_loadStart函数    };    // 上传结束    xhr.onload = function (e) {        _this._loaded(e, xhr); // 同上    }    // 上传错误    xhr.onerror = function (e) {        _this._loadFailed(e, xhr); // 同上    };    // 上传进度    xhr.upload.onprogress = function (e) {        _this._loadProgress(e, xhr); // 同上    }      xhr.open('post', _this.config.uploadUrl); // post到uploadUrl    xhr.send(data); // 发送请求}复制代码
  • 接着让我们自己封装一个预览方法previewBefore吧。首先应该明确的是需要一个预览容器,不然图片不知道改放哪;接着图片的样式我们也应该让用户去控制(暂时没有做模版),所以有两个传入的新属性previewWrap、previewImgClass,顾名思义。
previewBefore: function () {    var _this = this,        files = _this.fileCached,        filesNeed = [], // 我们真正需要的file数组,防止往页面里多次append之前存在的dom        filesHad = [], // 已经存在的file数组,方便后续计算        previewWrap = _this.config.previewWrap,        previewImgClass = _this.config.previewImgClass;    var $previewWrap = $(previewWrap);    // 如果已经存在预览位置,即页面中已经存在了预览元素    if ($previewWrap.find('.' + previewImgClass).length > 0) {        $previewWrap.find('.' + previewImgClass).each(function (index, value) {            var $this = $(this);            filesHad.push($this.data('name')); // 把已经存在的file name推入filesHad        });                for (var i = 0; i < files.length; i++) {            if (filesHad.indexOf(files[i].name) < 0) { // 数组的去重                filesNeed.push(files[i]);             }        }    } else {        filesNeed = files; // 首次预览不需要处理    }    for (var i = 0; i < filesNeed.length; i++) {        (function (i) { // 创建一个闭包获取正确的i值            var	reader = new FileReader(); // 新建一个FileReader对象            reader.readAsDataURL(filesNeed[i]); // 获取该file的base64            reader.onload = function () {                var dataUrl = reader.result; // 获取url                var img = $('');                img.appendTo($previewWrap);            };        })(i);    }  }复制代码
  • 有了预览,是不是还差个删除呢,让我们回想triggerUpload方法,此时应该也沿用那种思想,传入指定的index值去删除指定的文件,不传值则默认删除所有。
delBefore: function (index) {    var _this = this,        files = this.fileCached,        len = files.length,        previewWrap = _this.config.previewWrap;        previewImgClass = _this.config.previewImgClass;        var isIndex = (index >= 0); // 判断是否传入参数(排除index为0时的特殊情况)    var isValid = /^\d+$/.test(index) && index < len; // 判断传入的index是否为整数,且数目不能大于文件个数    if (isIndex && isValid) {        files.splice(index, 1); // 删除数组中指定file        $(previewWrap).find('.' + previewImgClass).eq(index).remove();    } else if (!isIndex && !isValid) {        $(previewWrap).find('.' + previewImgClass).each(function () { // 删除所有            $(this).remove();        })    } else if (isIndex && !isValid) {        throw 'delBefore方法传入的索引值为从0开始的整数且不得大于您上传的文件数' // 抛出异常    }}复制代码
  • 同时需要一些私有状态函数来接收xhr的事件回调方法,然后"call"一下暴露在外的config里面的对应的函数,疯狂打call后,就可以在外边接收到xhr的事件回调啦
// 开始上传_loadStart: function (e, xhr) {    this.uploading = true;    this.config.start.call(this, xhr);},// 上传完成_loaded: function (e, xhr) {    // 简单的判断一下请求成功与否    if (xhr.status === 200 || xhr.status === 304) {        this.uploading = false;        var res = JSON.parse(xhr.responseText);        this.config.done.call(this, res);    } else {        this._loadFailed(e, xhr);    }},// 上传失败_loadFailed: function (e, xhr) {    this.uploading = false;                this.config.fail.call(this, xhr);},// 上传进度_loadProgress: function (e, xhr) {    // e.loaded为当前加载值,e.total为文件大小值    if (e.lengthComputable) {        this.config.progress.call(this, e.loaded, e.total);    }},// 验证失败_checkError: function (msgQuene) {    // msgQuene为错误消息队列    this.config.checkError.call(this, msgQuene);},复制代码
  • 当然验证方法validate是必不可少的,但是这里我只是通过rules简单的定义了一些规则,而且感觉这块其实应该给用户去自定义,然后我在代码里面去转义成我的代码能看懂的方法,这里还需要改进,也欢迎大家提宝贵意见
validate: function (files) {    var _this = this,        len = files.length,        msgQuene = [], // 创建一个错误消息队列,因为多文件上传可能有多个错误状态        matchCount = 0; // 创建一个初始值匹配值方便后续计算        if (len > 1) {        for (var i = 0; i < len; i++) {            // 创建一个闭包            (function (index) {                // 参看下面的rules方法                var result = _this.rules(files[index], index);                // 根据rules计算返回的flag进行计数,正确则+1s,否则把错误消息推送到消息队列                result.flag ? matchCount++ : msgQuene.push(result.msg);            })(i);        }    } else {        // 原理同上        var result = _this.rules(files[0]);        result.flag ? matchCount++ : msgQuene.push(result.msg);    }    // 当所有文件都通过validate    if (matchCount === len) {        return {            result: true // 告诉别人通过啦!        };    } else {        return {            result: false, // 告诉别人我觉得不行            msgQuene: msgQuene // 告诉别人哪里不行        };    }}复制代码
  • 具体的规则呢就需要交给具体的人去处理,男女搭配干活不累,说的就是你,rules大妹子
rules: function (item, index) {    var config = this.config,        flag = true,        msg = '';    // 一些暂时想到的验证规则方案,只做参考    // 是否能传gif    if (config.noGif) {        if (item.type === 'image/gif') {            flag = false;            msg = '不支持上传gif格式的图片'        }    }    // 是否设置了大小限制    if (config.maxSize) {        if (item.size > config.maxSize) {            flag = false;            // index = 0 隐式转换为false,这里需要注意            index >= 0 ?             msg = '第' + (index + 1) + '个文件过大,请重新上传':             msg = '文件过大,请重新上传';        }    }    // 返回一个参考对象    return {        flag: flag,        msg: msg    };}复制代码
  • 同时可能需要一些工具方法,比如在还未上传的时候去get和set files的值呀,暂时想到的是这些
get: function () {    return this.fileCached; // 这时候缓存值就有用啦},set: function (files) {    this.fileCached = files; // 简单的处理下...}复制代码

插件使用

var up = $.xupload({    el: '#file', // || $('#file')    uploadUrl: '/test',    uploadParams: {        fileRequestName: 'uploadfile', // || undefined        param1: 1,        param2, 2    },    autoUpload: false, // || true,    maxSize: 2000,    noGif: true, // || false    start: function (files) {        console.dir(files);    },    done: function (res) {        console.dir(res); // 上传成功responce    },    fail: function (error) {        console.error(error);    },    progress: function (loaded, total) {        console.log(Math.round(loaded / total * 100) + '%');    },    checkError: function (errors) {        console.error(errors); // 得到验证失败数组    }});$('#someSubmitBtn').click(function () {     var files = up.get(); // 获取待上传的文件     console.dir(files);     up.triggerUpload(); // 触发异步upload, autoUpload为false时可用});复制代码

总结

第一次写类似的插件,运用的技巧比较简单粗浅,也有很多不足,已经在计划改进了,大牛轻喷,以后会更加努力的(ง •̀_•́)ง。

虽然看到这篇文章的人可能不多,但是刘备也曾经曰过:

勿以善小而不为

我这叫做“善”好像也有点牵强...总之就是那么个意思!

emmm...好像也没啥说的了,大家都是面向工资编程,那就祝大家早日一夜暴富吧。

代码是什么,能吃吗?

Todo

  1. 文件的拖拽上传
  2. 文件的取消上传,重新上传
  3. 一些其他细节和bug处理

转载于:https://juejin.im/post/5adda95c6fb9a07aa541ec2b

你可能感兴趣的文章
BIEE Demo(RPD创建 + 分析 +仪表盘 )
查看>>
Cocos2dx 3.0开发环境的搭建--Eclipse建立在Android工程
查看>>
基本概念复习
查看>>
重构第10天:提取方法(Extract Method)
查看>>
Android Fragment使用(四) Toolbar使用及Fragment中的Toolbar处理
查看>>
解决pycharm在ubuntu下搜狗输入法一直固定在左下角的问题
查看>>
多线程day01
查看>>
react-native 模仿原生 实现下拉刷新/上拉加载更多(RefreshListView)
查看>>
MySQL出现Access denied for user ‘root’@’localhost’ (using password:YES)
查看>>
通过Roslyn构建自己的C#脚本(更新版)(转)
查看>>
红黑树
查看>>
UIImagePickerController拍照与摄像
查看>>
python调用windows api
查看>>
第四章 mybatis批量insert
查看>>
Java并发框架——什么是AQS框架
查看>>
【数据库】
查看>>
Win配置Apache+mod_wsgi+django环境+域名
查看>>
linux清除文件内容
查看>>
WindowManager.LayoutParams 详解
查看>>
find的命令的使用和文件名的后缀
查看>>