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,事件绑定// 应该提供销毁APIuploader.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; }}
看完上面的实现,有两点需要说明一下:
- 为了考虑到
destroy()
的实现,我们需要在this
属性上暂存input
标签与绑定的事件。后续方便直接取来,解绑事件与去除dom。 - 其实把
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,否则同一文件不触发changethis.input.value = ''this.input.click() }
removeFile (file) {const id = file.id || fileconst 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
。否则会出现以下这样的提示:
写到这里,我们可以根据已有代码尝试一下,打印upload
时的内部uploadList
,结果正确。
发起请求 - _post
这个是比较关键的函数,我们用原生XHR
实现,因为fetch
并不支持progress
事件。简单描述下要做的事:
- 构建
FormData
,将文件与配置中的data
进行添加。 - 构建
xhr
,设置配置中的header、withCredentials,配置相关事件
- onload事件:处理响应的状态,返回数据并改写文件列表中的状态,响应外部
change
等相关状态事件。 - onerror事件:处理错误状态,改写文件列表,抛出错误,响应外部
error
事件 - onprogress事件:根据返回的事件,计算好百分比,响应外部
onprogress
事件
- 因为xhr的返回格式不太友好,我们需要额外编写两个函数处理http响应:
parseSuccess
、parseError
_post (file) {if (!file.rawFile) return
const { headers, data, withCredentials } = this.settingconst 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 : 0this._callHook('progress', e, file, this.uploadFiles) }
xhr.open('post', this.setting.url, true) xhr.send(formData)}
parseSuccess
将响应体尝试JSON反序列化,失败的话再返回原样文本
const parseSuccess = xhr => {let response = xhr.responseTextif (response) {try {return JSON.parse(response) } catch (error) {} }return response}
parseError
同样的,JSON反序列化,此处还要抛出个错误,记录错误信息。
const parseError = xhr => {let msg = ''let { responseText, responseType, status, statusText } = xhrif (!responseText && responseType === 'text') {try { msg = JSON.parse(responseText) } catch (error) { msg = responseText } } else { msg = `${status} ${statusText}` }
const err = new Error(msg) err.status = statusreturn err}
至此,一个完整的Upload类已经构造完成,整合下来大概200行代码多点,由于篇幅问题,完整的代码已放在个人github里。
测试与实践
写好一个类,当然是上手实践一下,由于测试代码并不是本文关键,所以采用截图的方式呈现。为了呈现良好的效果,把chrome里的network调成自定义降速,并在测试失败重传时,关闭网络。
服务端
这里用node搭建了一个小的http服务器,用multiparty
处理文件接收。
客户端
简单的用html结合vue实现了一下,会发现将业务代码跟基础代码分开实现后,简洁明了不少
拓展拖拽上传
拖拽上传注意两个事情就是
- 监听drop事件,获取
e.dataTransfer.files
- 监听dragover事件,并执行
preventDefault()
,防止浏览器弹窗。
更改客户端代码如下:
效果图GIF
优化与总结
本文涉及的全部源代码以及测试代码均已上传到github仓库中,有兴趣的同学可自行查阅。
代码当中还存在不少需要的优化项以及争论项,等待各位读者去斟酌改良:
- 文件大小判断是否应该结合到类里面?看需求,因为有时候可能会有根据
.zip
压缩包的文件,可以允许更大的体积。 - 是否应该提供可重写ajax函数的配置项?
- 参数是否应该可传入一个函数动态确定?
- ...
源码地址:https://github.com/impeiran/Blog/tree/master/uploader
❤️ 看完三件事
大家好,我是 koala,如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个【在看】,或者分享转发,让更多的人也能看到这篇内容
关注公众号【程序员成长指北】,不定期分享原创&精品技术文章。
添加微信【 coder_qi 】。加入程序员成长指北公众号交流群。
“在看转发”是最大的支持
webbrowser实现input tab事件_如何合理构造一个Uploader工具类(设计到实现)相关推荐
- aspose 转pdf表格大小乱了_自己写了一个小工具类:pdf转word,没有页数和大小限制,保真!...
昨天下午遇到一个问题,想把一个比较大的pdf转化为word,结果使用了各种工具都收费.想着干脆写一个小工具吧,一开始使用的python等等试了好几个网上的代码,结果全都失真.于是乎不得不花了一下午自己 ...
- java百度上传控件_百度Bos上传文件工具类-BosUtils(java)
功能要求 java项目中所有的图片均使用对象存储BOS 准备材料 首先你要又百度bos的账号,找到自己的ak.sk.endpoint.bucketname(这些东西不懂得可以去看bos的文档) 功能代 ...
- qtabwidget切换tab事件_某超超临界机组初压/限压切换过程中扰动原因分析
严寒夕 浙江浙能台州第二发电有限责任公司 [摘要]某火电厂汽轮机在初压/限压切换过程中出现负荷瞬时上升问题.从初压/限压切换的逻辑及切换过程中主要参数的变化分析,确定原因为压力控制器指令上升瞬间和转 ...
- input 输入事件_输入超时为例学习 Python 的线程和协程
需求:做一个程序等待用户输入,3秒内输入则会 echo 这个输入并立即退出.3秒内没输入则自动退出. 实现方法: 1. 线程(错误示范) import 首先启动两个线程,并把等待输入的 get_inp ...
- java如何实现qq截屏代码_基于Java的QQ屏幕截图工具的设计
设计一款基于Java的QQ屏幕截图软件,能实现不联网也能对屏幕截图:截图是由电脑截取显示在屏幕上或其他显示设备上的可视图像,通常截图可以由操作系统或专用截图软件截取,截取的图像会有不同种的文件格式,如 ...
- github用相对路径显示图片_我写了一个开源工具, 让Github的README.md可以正常显示超大图片...
最终效果对比 图片替换前: 图片显示有好有坏,能否显示,全凭运气 图片替换后: 所有大图正常显示! 本项目永久开源地址 痛点: Github的README.md展示图片效果并不完美 为了让项目演示更生 ...
- java 在线rsa解密_通用的Java RSA加密工具类,可在线验证通过
/** * RSA加密工具类 * 使用PKCS1_PADDING填充,密钥长度1024 * 加解密结果在这里测试通过:http://tool.chacuo.net/cryptrsaprikey * 注 ...
- itextpdf api帮助文档_我开源了一个小工具,可以帮你轻松生成 SpringBoot API 文档...
前言 大家好,我叫叶大侠,一名独立开发者.这个文档工具是我17年的一个想法,当时还是在公司里面上班,负责App客户端的开发工作,当时后端童鞋写文档的意愿比较低,总是要等他们开发完接口,然后才在微信上沟 ...
- 加密封装 怎么把_不要再封装各种Util工具类了,这个神级框架值得拥有!
Hutool 谐音 "糊涂",寓意追求 "万事都作糊涂观,无所谓失,无所谓得" 的境界. Hutool 是一个 Java 工具包,也只是一个工具包,它帮助我们简 ...
最新文章
- 取消默认html打开文档,怎么取消mac默认打开文档方式
- eclipse 快捷键汇总
- windows的ftp命令小结
- matlab常用函数——方程函数
- 数据链路层: HDLC
- OpenGL环境配置(超全整合版)
- FlightGear命令行参数
- 计算机报名jpg形式,急!什么叫“jpg格式电子照片”?
- linux串口蜂鸣器报警电路,蜂鸣器报警电路图(简单介绍两种!)
- 运营者想针对公众号吸粉做一些运营活动,怎么做?
- linux遇到hint:num lock on
- 3.3. debug ip igrp
- 进销存管理系统大全【70个进销存系统】
- 使用python生成文字视频
- python数据分析岗位做什么_给力!数据分析岗位内部人的建议,可以少走很多弯路...
- 异常处理(六)--------SpringBoot+Maven项目运行异常:Unable to find a single main class from the following candidat
- Python爬取手机APP
- 电影《寒战1》中的管理知识
- 【scoop 】-【安装与使用】
- 管理学中的知名定律之安慰剂效应(Placebo Effect)
热门文章
- java 保留字符串,如何在Java中保留字符串而不使用反转功能
- colab清理gpu缓存_安卓手机为什么会变卡?强制GPU渲染手机就能变流畅?你真的懂吗...
- 目标检测第8步:如何在Windows10系统下,训练YOLOv5 5.0自定义数据集?(本地)
- codeforces Gargari and Permutations(DAG+BFS)
- poj 1386 Play on Words(有向图欧拉回路)
- 关于mmdetection上手的几点说明
- python 到 poc
- DVWA Cross Site Request Forgery (CSRF) -------WP
- 三星w系列vip服务器,高端人士候机专属特权 三星W2017一张行走的VIP卡
- oracle 超市管理系统,SuperManager 超市账单管理系统
JSP + Servlet + Oracle Jsp/ 240万源代码下载- www.pudn.com...