webbrowser实现input tab事件_如何合理构造一个Uploader工具类(设计到实现)

作者:Chaser (本文来自作者投稿)     

原文地址:https://juejin.im/post/5e5badce51882549652d55c2

源码地址:https://github.com/impeiran/Blog/tree/master/uploader

前言

本文将带你基于ES6的面向对象,脱离框架使用原生JS,从设计到代码实现一个Uploader基础类,再到实际投入使用。通过本文,你可以了解到一般情况下根据需求是如何合理构造出一个工具类lib。

需求描述

相信很多人都用过/写过上传的逻辑,无非就是创建input[type=file]标签,监听onchange事件,添加到FormData发起请求。

但是,想引入开源的工具时觉得增加了许多体积且定制性不满足,每次写上传逻辑又会写很多冗余性代码。在不同的toC业务上,还要重新编写自己的上传组件样式。

此时编写一个Uploader基础类,供于业务组件二次封装,就显得很有必要。

下面我们来分析下使用场景与功能:

  • 选择文件后可根据配置,自动/手动上传,定制化传参数据,接收返回。
  • 可对选择的文件进行控制,如:文件个数,格式不符,超出大小限制等等。
  • 操作已有文件,如:二次添加、失败重传、删除等等。
  • 提供上传状态反馈,如:上传中的进度、上传成功/失败。
  • 可用于拓展更多功能,如:拖拽上传、图片预览、大文件分片等。

然后,我们可以根据需求,大概设计出想要的API效果,再根据API推导出内部实现。

可通过配置实例化

const uploader = new Uploader({
url: '',
// 用于自动添加input标签的容器
wrapper: null,

// 配置化的功能,多选、接受文件类型、自动上传等等
multiple: true,
accept: '*',
limit: -1, // 文件个数
autoUpload: false

// xhr配置
header: {}, // 适用于JWT校验
data: {} // 添加额外参数
withCredentials: false
});

状态/事件监听

// 链式调用更优雅
uploader
.on('choose', files => {
// 用于接受选择的文件,根据业务规则过滤
})
.on('change', files => {
// 添加、删除文件时的触发钩子,用于更新视图
// 发起请求后状态改变也会触发
})
.on('progress', e => {
// 回传上传进度
})
.on('success', ret => {/*...*/})
.on('error', ret => {/*...*/})

外部调用方法

这里主要暴露一些可能通过交互才触发的功能,如选择文件、手动上传等

uploader.chooseFile();

// 独立出添加文件函数,方便拓展
// 可传入slice大文件后的数组、拖拽添加文件
uploader.loadFiles(files);

// 相关操作
uploader.removeFile(file);
uploader.clearFiles()

// 凡是涉及到动态添加dom,事件绑定
// 应该提供销毁API
uploader.destroy();

至此,可以大概设计完我们想要的uploader的大致效果,接着根据API进行内部实现。

内部实现

使用ES6的class构建uploader类,把功能进行内部方法拆分,使用下划线开头标识内部方法。

然后可以给出以下大概的内部接口:

class Uploader {
// 构造器,new的时候,合并默认配置
constructor (option = {}) {}
// 根据配置初始化,绑定事件
_init () {}

// 绑定钩子与触发
on (evt) {}
_callHook (evt) {}

// 交互方法
chooseFile () {}
loadFiles (files) {}
removeFile (file) {}
clear () {}

// 上传处理
upload (file) {}
// 核心ajax发起请求
_post (file) {}
}

构造器 - constructor

代码比较简单,这里目标主要是定义默认参数,进行参数合并,然后调用初始化函数

class Uploader {
constructor (option = {}) {
const defaultOption = {
url: '',
// 若无声明wrapper, 默认为body元素
wrapper: document.body,
multiple: false,
limit: -1,
autoUpload: true,
accept: '*',

headers: {},
data: {},
withCredentials: false
}
this.setting = Object.assign(defaultOption, option)
this._init()
}
}

初始化 - _init

这里初始化做了几件事:维护一个内部文件数组uploadFiles,构建input标签,绑定input标签的事件,挂载dom。

为什么需要用一个数组去维护文件,因为从需求上看,我们的每个文件需要一个状态去追踪,所以我们选择内部维护一个数组,而不是直接将文件对象交给上层逻辑。

由于逻辑比较混杂,分多了一个函数_initInputElement进行初始化input的属性。

class Uploader {
// ...

_init () {
this.uploadFiles = [];
this.input = this._initInputElement(this.setting);
// input的onchange事件处理函数
this.changeHandler = e => {
// ...
};
this.input.addEventListener('change', this.changeHandler);
this.setting.wrapper.appendChild(this.input);
}

_initInputElement (setting) {
const el = document.createElement('input');
Object.entries({
type: 'file',
accept: setting.accept,
multiple: setting.multiple,
hidden: true
}).forEach(([key, value]) => {
el[key] = value;
})''
return el;
}
}

看完上面的实现,有两点需要说明一下:

  1. 为了考虑到destroy()的实现,我们需要在this属性上暂存input标签与绑定的事件。后续方便直接取来,解绑事件与去除dom。
  2. 其实把input事件函数changeHandler单独抽离出去也可以,更方便维护。但是会有this指向问题,因为handler里我们希望将this指向本身实例,若抽离出去就需要使用bind绑定一下当前上下文。

上文中的changeHanler,来单独分析实现,这里我们要读取文件,响应实例choose事件,将文件列表作为参数传递给loadFiles

为了更加贴合业务需求,可以通过事件返回结果来判断是中断,还是进入下一流程。

this.changeHandler = e => {
const files = e.target.files;
const ret = this._callHook('choose', files);
if (ret !== false) {
this.loadFiles(ret || e.target.files);
}
};

通过这样的实现,如果显式返回false,我们则不响应下一流程,否则拿返回结果||文件列表。这样我们就将判断格式不符,超出大小限制等等这样的逻辑交给上层实现,响应样式控制。如以下例子:

uploader.on('choose', files => {
const overSize = [].some.call(files, item => item.size > 1024 * 1024 * 10)
if (overSize) {
setTips('有文件超出大小限制')
return false;
}
return files;
});

状态事件绑定与响应

简单实现上文提到的_callHook,将事件挂载在实例属性上。因为要涉及到单个choose事件结果控制。没有按照标准的发布/订阅模式的事件中心来做,有兴趣的同学可以看看tiny-emitter的实现。

class Uploader {
// ...
on (evt, cb) {
if (evt && typeof cb === 'function') {
this['on' + evt] = cb;
}
return this;
}

_callHook (evt, ...args) {
if (evt && this['on' + evt]) {
return this['on' + evt].apply(this, args);
}
return;
}
}

装载文件列表 - loadFiles

传进来文件列表参数,判断个数响应事件,其次就是要封装出内部列表的数据格式,方便追踪状态和对应对象,这里我们要用一个外部变量生成id,再根据autoUpload参数选择是否自动上传。

let uid = 1

class Uploader {
// ...
loadFiles (files) {
if (!files) return false;

if (this.limit !== -1 &&
files.length &&
files.length + this.uploadFiles.length > this.limit
) {
this._callHook('exceed', files);
return false;
}
// 构建约定的数据格式
this.uploadFiles = this.uploadFiles.concat([].map.call(files, file => {
return {
uid: uid++,
rawFile: file,
fileName: file.name,
size: file.size,
status: 'ready'
}
}))

this._callHook('change', this.uploadFiles);
this.setting.autoUpload && this.upload()

return true
}
}

到这里其实还没完善,因为loadFiles可以用于别的场景下添加文件,我们再增加些许类型判断代码。

class Uploader {
// ...
loadFiles (files) {
if (!files) return false;

+ const type = Object.prototype.toString.call(files)
+ if (type === '[object FileList]') {
+ files = [].slice.call(files)
+ } else if (type === '[object Object]' || type === '[object File]') {
+ files = [files]
+ }

if (this.limit !== -1 &&
files.length &&
files.length + this.uploadFiles.length > this.limit
) {
this._callHook('exceed', files);
return false;
}

+ this.uploadFiles = this.uploadFiles.concat(files.map(file => {
+ if (file.uid && file.rawFile) {
+ return file
+ } else {
return {
uid: uid++,
rawFile: file,
fileName: file.name,
size: file.size,
status: 'ready'
}
}
}))

this._callHook('change', this.uploadFiles);
this.setting.autoUpload && this.upload()

return true
}
}

上传文件列表 - upload

这里可根据传进来的参数,判断是上传当前列表,还是单独重传一个,建议是每一个文件单独走一次接口(有助于失败时的文件追踪)。

upload (file) {
if (!this.uploadFiles.length && !file) return;

if (file) {
const target = this.uploadFiles.find(
item => item.uid === file.uid || item.uid === file
)
target && target.status !== 'success' && this._post(target)
} else {
this.uploadFiles.forEach(file => {
file.status === 'ready' && this._post(file)
})
}
}

当中涉及到的_post函数,我们往下再单独实现。

交互方法

这里都是些供给外部操作的方法,实现比较简单就直接上代码了。

class Uploader {
// ...
chooseFile () {
// 每次都需要清空value,否则同一文件不触发change
this.input.value = ''
this.input.click()
}

removeFile (file) {
const id = file.id || file
const index = this.uploadFiles.findIndex(item => item.id === id)
if (index > -1) {
this.uploadFiles.splice(index, 1)
this._callHook('change', this.uploadFiles);
}
}

clear () {
this.uploadFiles = []
this._callHook('change', this.uploadFiles);
}

destroy () {
this.input.removeEventHandler('change', this.changeHandler)
this.setting.wrapper.removeChild(this.input)
}
// ...
}

有一点要注意的是,主动调用chooseFile,需要在用户交互之下才会触发选择文件框,就是说要在某个按钮点击事件回调里,进行调用chooseFile。否则会出现以下这样的提示:

c329e6e563c82afbb559fc92e059983f.png

写到这里,我们可以根据已有代码尝试一下,打印upload时的内部uploadList,结果正确。

dd8f4695a90fbdc6bd94520f8d84cffb.png

发起请求 - _post

这个是比较关键的函数,我们用原生XHR实现,因为fetch并不支持progress事件。简单描述下要做的事:

  1. 构建FormData,将文件与配置中的data进行添加。
  2. 构建xhr,设置配置中的header、withCredentials,配置相关事件
  • onload事件:处理响应的状态,返回数据并改写文件列表中的状态,响应外部change等相关状态事件。
  • onerror事件:处理错误状态,改写文件列表,抛出错误,响应外部error事件
  • onprogress事件:根据返回的事件,计算好百分比,响应外部onprogress事件
  1. 因为xhr的返回格式不太友好,我们需要额外编写两个函数处理http响应:parseSuccessparseError
_post (file) {
if (!file.rawFile) return

const { headers, data, withCredentials } = this.setting
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append('file', file.rawFile, file.fileName)

Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key])
})

file.status = 'uploading'

xhr.withCredentials = !!withCredentials
xhr.onload = () => {
/* 处理响应 */
if (xhr.status < 200 || xhr.status >= 300) {
file.status = 'error'
this._callHook('error', parseError(xhr), file, this.uploadFiles)
} else {
file.status = 'success'
this._callHook('success', parseSuccess(xhr), file, this.uploadFiles)
}
}

xhr.onerror = e => {
/* 处理失败 */
file.status = 'error'
this._callHook('error', parseError(xhr), file, this.uploadFiles)
}

xhr.upload.onprogress = e => {
/* 处理上传进度 */
const { total, loaded } = e
e.percent = total > 0 ? loaded / total * 100 : 0
this._callHook('progress', e, file, this.uploadFiles)
}

xhr.open('post', this.setting.url, true)
xhr.send(formData)
}
parseSuccess

将响应体尝试JSON反序列化,失败的话再返回原样文本

const parseSuccess = xhr => {
let response = xhr.responseText
if (response) {
try {
return JSON.parse(response)
} catch (error) {}
}
return response
}
parseError

同样的,JSON反序列化,此处还要抛出个错误,记录错误信息。

const parseError = xhr => {
let msg = ''
let { responseText, responseType, status, statusText } = xhr
if (!responseText && responseType === 'text') {
try {
msg = JSON.parse(responseText)
} catch (error) {
msg = responseText
}
} else {
msg = `${status} ${statusText}`
}

const err = new Error(msg)
err.status = status
return err
}

至此,一个完整的Upload类已经构造完成,整合下来大概200行代码多点,由于篇幅问题,完整的代码已放在个人github里。

测试与实践

写好一个类,当然是上手实践一下,由于测试代码并不是本文关键,所以采用截图的方式呈现。为了呈现良好的效果,把chrome里的network调成自定义降速,并在测试失败重传时,关闭网络。

447528f6f9d804fdce2d3b3f13504684.gif

服务端

这里用node搭建了一个小的http服务器,用multiparty处理文件接收。

bf1d0c10dddfe9bf75a5d2c317d87271.png

客户端

简单的用html结合vue实现了一下,会发现将业务代码跟基础代码分开实现后,简洁明了不少

15922e8d6c27564949af9625be1c3786.png

拓展拖拽上传

拖拽上传注意两个事情就是

  1. 监听drop事件,获取e.dataTransfer.files
  2. 监听dragover事件,并执行preventDefault(),防止浏览器弹窗。
更改客户端代码如下:
cdb016a3027b86a865b196566164ab91.png
效果图GIF
8fd81fb485e6ccd12440260a5105e879.gif

优化与总结

本文涉及的全部源代码以及测试代码均已上传到github仓库中,有兴趣的同学可自行查阅。

代码当中还存在不少需要的优化项以及争论项,等待各位读者去斟酌改良:

  • 文件大小判断是否应该结合到类里面?看需求,因为有时候可能会有根据.zip压缩包的文件,可以允许更大的体积。
  • 是否应该提供可重写ajax函数的配置项?
  • 参数是否应该可传入一个函数动态确定?
  • ...

源码地址:https://github.com/impeiran/Blog/tree/master/uploader

❤️ 看完三件事

大家好,我是 koala,如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  • 点个【在看】,或者分享转发,让更多的人也能看到这篇内容

  • 关注公众号【程序员成长指北】,不定期分享原创&精品技术文章。

  • 添加微信【 coder_qi 】。加入程序员成长指北公众号交流群。

41a3790709cdf9cb6ea3d844b8d34559.png

“在看转发”是最大的支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/537518.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小达人点读笔的任我贴贴纸怎么使用?

小达人点读笔是一款高性能、高容量点读笔。其自身的开放性、共享性、传播性让可读书籍及音频资源真正的达到了海量且优质有用。 下面我们来认识一下标配中任我贴上的贴纸&#xff1a; 智能贴 用于已经布有二维码的有声图书的封面&#xff0c;比如律动英语&#xff0c;成长一线&…

vscode必备插件_10个必备的Visual Studio Code (VS code)插件

我基本上每天都用vs code&#xff0c;我喜欢的小巧&#xff0c;开源&#xff0c;免费并且非常强大。尤其它非常多的插件可以免费使用。这里我把经常用插件介绍给大家&#xff0c;希望大家留言讨论我把VS code 插件分两类&#xff0c;一类是处理可视化的&#xff0c;比如说颜色&…

软考官方教材:信息系统项目管理师教程(第三版)

2020年下半年信息系统项目管理师考试官方教材将继续使用清华大学出版社出版的信息系统项目管理师教程第3版&#xff08;特别注意&#xff0c;有某些辅导资料也打着信息系统项目管理师教程第3版的旗号&#xff0c;各位考生一定要看准官方指定教材是由全国计算机专业技术资格考试…

centos挂载windows共享目录

2019独角兽企业重金招聘Python工程师标准>>> 在windows中创建一个共享文件夹记住这个网络路径&#xff0c; 在centos上新建文件夹/mnt/MyShare $> mkdir /mnt/MyShare挂载 username用户名.password登录密码 $> Sudo mount -t cifs -o username用户名,pass…

博阅likebook alita专用pdf制作

适合博阅likebook alita带手写的。 先上效果图&#xff0c;左边是正文&#xff0c;右边有留白可以写读书笔记 这样做有什么好处&#xff1f; pdf从阅读器里复制出来&#xff0c;在电脑打开&#xff0c;笔记也会有。笔记是直接记录在pdf上的&#xff0c;相当于pdf上的批注。 …

esc键没反应_有机人名反应——Brown 硼氢化反应(Brown Hydroboration)

Brown 硼氢化反应&#xff08;Brown Hydroboration&#xff09;反应机理链接&#xff1a;http://chem.kingdraw.cn/Shortlink?id20200624161301Brown硼氢化反应&#xff0c;是指乙硼烷在醚类溶液中离解成的甲硼烷以B-H键与烯烃、炔烃的不饱和键加成&#xff0c;生成有机硼化合…

修改Navicat数据库自动备份目录

1.右键连接&#xff0c;选择“编辑连接” 2. 选择“高级”&#xff0c;设置位置

python图形界面编程库_Python支持哪些图形界面的第三方库

Python支持哪些图形界面的第三方库 发布时间&#xff1a;2020-11-09 10:37:56 来源&#xff1a;亿速云 阅读&#xff1a;58 作者&#xff1a;小新 这篇文章给大家分享的是有关Python支持哪些图形界面的第三方库的内容。小编觉得挺实用的&#xff0c;因此分享给大家做个参考。一…

成为中国特色项目经理,走上人生巅峰

今天是秋分&#xff0c;写在项目经理6周年的总结 落叶知秋&#xff0c;情谊如酒&#xff0c;风渐凉时有喜无忧&#xff1b; 岁月流走&#xff0c;蓦然回首&#xff0c;一声问候醇绵依旧&#xff1b; 有情相守&#xff0c;不离左右&#xff0c;含笑送出这份问候。 -----------…

项目经理到底要不要懂技术?

不难发现&#xff0c;高薪项目经理岗位&#xff0c;往往对项目经理有技术要求。为什么会这样&#xff1f; 存在即合理&#xff0c;一定是现实中需要&#xff0c;项目实施过程中有必要。 想起了自己以前有次面试&#xff0c;二面是公司总经理&#xff0c;总经理说&#xff1a;…

git 可视化工具_Git的基本使用(二)

通过前文Git的基本使用(一)的学习&#xff0c;相信大家对如何将iOS项目通过Git传到GitHub账户上有了一个基本的了解&#xff0c;其过程是相对繁琐和容易出错的。本文将告诉大家借助工具来帮助我们实现这些操作&#xff0c;并对前文进行进一步补充。一、两个软件Visual Studio C…

小米8配哪个版本的MIUI?

小米8之前我是忠实的魅粉&#xff0c;魅族手机用过M8&#xff0c;MX3&#xff0c;note5&#xff0c;MX6 pro。 魅族真的是被联发科的cpu坑了&#xff0c;一核有难&#xff0c;八核围观。 入手小米8&#xff0c;miui是9&#xff0c;一路跟着系统自动更新&#xff0c;更新到了12。…

3D设计桌面云

榕力3D设计桌面云方案&#xff0c;为使用GPU高负载应用的设计师们提供强大支持&#xff0c;如3D建模、光线追踪、渲染计算等领域&#xff0c;带来极致流畅的3D设计桌面体验&#xff0c;同时提升工作效率、可管理性和安全性。 3D设计桌面云方案满足了工业客户在专业设计场景中的…

PDF图片文字识别

工具 adobe acrobat XI PRO 没有安装该软件的&#xff0c;需下载安装&#xff0c;有了此软件&#xff0c;pdf随意改&#xff0c;必备&#xff01; 步骤 1.打开pdf文件&#xff0c;点击工具---文本识别---在本文件中 2.选择“所有页面”&#xff0c;点击“编辑” 3.选择语言&a…

python中import的作用_python使用import报错是什么原因

在练习Python中package的相对导入时&#xff0c;即from . import XXX 或者from .. import XXX 有时会遇到这样两个错误&#xff1a;SystemError: Parent module not loaded, cannot perform relative import 和ValueError: attempted relative import beyond top-level packag…

win10卓越性能模式,提升电脑性能

Win10隐藏了一个电源模式“卓越性能”&#xff0c;是比“高性能”模式更强性能的电源模式。 这个模式开启后&#xff0c;电脑到底有没有变快呢&#xff1f;心理上是快了。 开启步骤 1.鼠标移到开始按钮图标上&#xff0c;别动&#xff01;&#xff01;&#xff01;&#xff…

Redis Sentinel 模拟故障迁移

什么是redis sentinel 参考文档:https://redis.io/topics/sentinel 简单的来说&#xff0c;就是Redis Sentinel 为redis 提供高可用性&#xff0c;主要体现在下面几个方面:1.监控:redis sentinel会不间断的监控主服务器和从服务器是否正常工作2.通知:当出现问题时&#xff0c;…

erp生产管理系统流程_企业生产管理好帮手——ERP智能管理系统

ERP企业管理软件是为企业决策层及员工提供决策运行手段的管理平台。在生产制造制造业中&#xff0c;尽管加工早已用于机械自动化&#xff0c;但在仓库管理、人工管理工作&#xff0c;还必须erp公司智能管理系统协助管理方法。erp公司管理软件是公司供应链管理的综合性管理软件&…

神经网络的基本工作原理

神经网络的基本工作原理 一、总结 一句话总结&#xff1a;先给一个初始值&#xff0c;然后依赖正确值&#xff08;真实值&#xff09;进行修复模型&#xff08;训练模型&#xff09;&#xff0c;直到模型和真实值的误差可接受 初始值 真实值 修复模型 1、神经网络由基本的神经元…

jsonrpc aria2_抛弃迅雷,Aria2 新手入门

迅雷已经用了 10 年&#xff0c;一直把它看作是速度最快也最方便的下载工具。迅雷会员也是我必续的服务。但&#xff0c;迅雷堕落了。thunder: 迅雷专属链接越来越少&#xff0c;基本都是磁力、BT 的天下迅雷会员加速不再给力&#xff0c;大量资源速度为 0。会员虽然还有一年多…