element-ui Upload 上传组件源码分析整理笔记(十四)
程序员文章站
2022-07-21 13:42:17
简单写了部分注释,upload dragger.vue(拖拽上传时显示此组件)、upload list.vue(已上传文件列表)源码暂未添加多少注释,等有空再补充,先记下来... index.vue upload.vue pythod import ajax from './ajax'; impor ......
简单写了部分注释,upload-dragger.vue(拖拽上传时显示此组件)、upload-list.vue(已上传文件列表)源码暂未添加多少注释,等有空再补充,先记下来...
index.vue
<script> import uploadlist from './upload-list'; import upload from './upload'; import elprogress from 'element-ui/packages/progress'; import migrating from 'element-ui/src/mixins/migrating'; function noop() {} export default { name: 'elupload', mixins: [migrating], components: { elprogress, uploadlist, upload }, provide() { return { uploader: this }; }, inject: { elform: { default: '' } }, props: { action: { //必选参数,上传的地址 type: string, required: true }, headers: { //设置上传的请求头部 type: object, default() { return {}; } }, data: object, //上传时附带的额外参数 multiple: boolean, //是否支持多选文件 name: { //上传的文件字段名 type: string, default: 'file' }, drag: boolean, //是否启用拖拽上传 dragger: boolean, withcredentials: boolean, //支持发送 cookie 凭证信息 showfilelist: { //是否显示已上传文件列表 type: boolean, default: true }, accept: string, //接受上传的文件类型(thumbnail-mode 模式下此参数无效) type: { type: string, default: 'select' }, beforeupload: function, //上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 promise 且被 reject,则停止上传。 beforeremove: function, //删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 promise 且被 reject,则停止上传。 onremove: { //文件列表移除文件时的钩子 type: function, default: noop }, onchange: { //文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 type: function, default: noop }, onpreview: { //点击文件列表中已上传的文件时的钩子 type: function }, onsuccess: { //文件上传成功时的钩子 type: function, default: noop }, onprogress: { //文件上传时的钩子 type: function, default: noop }, onerror: { //文件上传失败时的钩子 type: function, default: noop }, filelist: { //上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] type: array, default() { return []; } }, autoupload: { //是否在选取文件后立即进行上传 type: boolean, default: true }, listtype: { //文件列表的类型 type: string, default: 'text' // text,picture,picture-card }, httprequest: function, //覆盖默认的上传行为,可以自定义上传的实现 disabled: boolean, //是否禁用 limit: number, //最大允许上传个数 onexceed: { //文件超出个数限制时的钩子 type: function, default: noop } }, data() { return { uploadfiles: [], dragover: false, draging: false, tempindex: 1 }; }, computed: { uploaddisabled() { return this.disabled || (this.elform || {}).disabled; } }, watch: { filelist: { immediate: true, handler(filelist) { this.uploadfiles = filelist.map(item => { item.uid = item.uid || (date.now() + this.tempindex++); item.status = item.status || 'success'; return item; }); } } }, methods: { //文件上传之前调用的方法 handlestart(rawfile) { rawfile.uid = date.now() + this.tempindex++; let file = { status: 'ready', name: rawfile.name, size: rawfile.size, percentage: 0, uid: rawfile.uid, raw: rawfile }; //判断文件列表类型 if (this.listtype === 'picture-card' || this.listtype === 'picture') { try { file.url = url.createobjecturl(rawfile); } catch (err) { console.error('[element error][upload]', err); return; } } this.uploadfiles.push(file); this.onchange(file, this.uploadfiles); }, handleprogress(ev, rawfile) { const file = this.getfile(rawfile); this.onprogress(ev, file, this.uploadfiles); file.status = 'uploading'; file.percentage = ev.percent || 0; }, //文件上传成功后改用该方法,在该方法中调用用户设置的on-success和on-change方法,并将对应的参数传递出去 handlesuccess(res, rawfile) { const file = this.getfile(rawfile); if (file) { file.status = 'success'; file.response = res; this.onsuccess(res, file, this.uploadfiles); this.onchange(file, this.uploadfiles); } }, //文件上传失败后改用该方法,在该方法中调用用户设置的on-error和on-change方法,并将对应的参数传递出去 handleerror(err, rawfile) { const file = this.getfile(rawfile); const filelist = this.uploadfiles; file.status = 'fail'; filelist.splice(filelist.indexof(file), 1); this.onerror(err, file, this.uploadfiles); this.onchange(file, this.uploadfiles); }, //文件列表移除文件时调用该方法 handleremove(file, raw) { if (raw) { file = this.getfile(raw); } let doremove = () => { this.abort(file); let filelist = this.uploadfiles; filelist.splice(filelist.indexof(file), 1); this.onremove(file, filelist); }; if (!this.beforeremove) { doremove(); } else if (typeof this.beforeremove === 'function') { const before = this.beforeremove(file, this.uploadfiles); if (before && before.then) { before.then(() => { doremove(); }, noop); } else if (before !== false) { doremove(); } } }, getfile(rawfile) { let filelist = this.uploadfiles; let target; filelist.every(item => { target = rawfile.uid === item.uid ? item : null; return !target; }); return target; }, abort(file) { this.$refs['upload-inner'].abort(file); }, clearfiles() { this.uploadfiles = []; }, submit() { this.uploadfiles .filter(file => file.status === 'ready') .foreach(file => { this.$refs['upload-inner'].upload(file.raw); }); }, getmigratingconfig() { return { props: { 'default-file-list': 'default-file-list is renamed to file-list.', 'show-upload-list': 'show-upload-list is renamed to show-file-list.', 'thumbnail-mode': 'thumbnail-mode has been deprecated, you can implement the same effect according to this case: http://element.eleme.io/#/zh-cn/component/upload#yong-hu-tou-xiang-shang-chuan' } }; } }, beforedestroy() { this.uploadfiles.foreach(file => { if (file.url && file.url.indexof('blob:') === 0) { url.revokeobjecturl(file.url); } }); }, render(h) { let uploadlist; //如果用户设置showfilelist为true,则显示上传文件列表 if (this.showfilelist) { uploadlist = ( <uploadlist disabled={this.uploaddisabled} listtype={this.listtype} files={this.uploadfiles} on-remove={this.handleremove} handlepreview={this.onpreview}> </uploadlist> ); } const uploaddata = { props: { type: this.type, drag: this.drag, action: this.action, multiple: this.multiple, 'before-upload': this.beforeupload, 'with-credentials': this.withcredentials, headers: this.headers, name: this.name, data: this.data, accept: this.accept, filelist: this.uploadfiles, autoupload: this.autoupload, listtype: this.listtype, disabled: this.uploaddisabled, limit: this.limit, 'on-exceed': this.onexceed, 'on-start': this.handlestart, 'on-progress': this.handleprogress, 'on-success': this.handlesuccess, 'on-error': this.handleerror, 'on-preview': this.onpreview, 'on-remove': this.handleremove, 'http-request': this.httprequest }, ref: 'upload-inner' }; const trigger = this.$slots.trigger || this.$slots.default; const uploadcomponent = <upload {...uploaddata}>{trigger}</upload>; return ( <div> { this.listtype === 'picture-card' ? uploadlist : ''} { this.$slots.trigger ? [uploadcomponent, this.$slots.default] : uploadcomponent } {this.$slots.tip} { this.listtype !== 'picture-card' ? uploadlist : ''} </div> ); } }; </script>
upload.vue
<script> import ajax from './ajax'; import uploaddragger from './upload-dragger.vue'; export default { inject: ['uploader'], components: { uploaddragger }, props: { type: string, action: { //必选参数,上传的地址 type: string, required: true }, name: { //上传的文件字段名 type: string, default: 'file' }, data: object, //上传时附带的额外参数 headers: object, //设置上传的请求头部 withcredentials: boolean, //支持发送 cookie 凭证信息 multiple: boolean, //是否支持多选文件 accept: string, //接受上传的文件类型(thumbnail-mode 模式下此参数无效) onstart: function, onprogress: function, //文件上传时的钩子 onsuccess: function, //文件上传成功时的钩子 onerror: function, //文件上传失败时的钩子 beforeupload: function, //上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 promise 且被 reject,则停止上传。 drag: boolean, //是否启用拖拽上传 onpreview: { //点击文件列表中已上传的文件时的钩子 type: function, default: function() {} }, onremove: { //文件列表移除文件时的钩子 type: function, default: function() {} }, filelist: array, //上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] autoupload: boolean, //是否在选取文件后立即进行上传 listtype: string, //文件列表的类型 httprequest: { //覆盖默认的上传行为,可以自定义上传的实现 type: function, default: ajax }, disabled: boolean,//是否禁用 limit: number,//最大允许上传个数 onexceed: function //文件超出个数限制时的钩子 }, data() { return { mouseover: false, reqs: {} }; }, methods: { isimage(str) { return str.indexof('image') !== -1; }, handlechange(ev) { const files = ev.target.files; if (!files) return; this.uploadfiles(files); }, uploadfiles(files) { //文件超出个数限制时,调用onexceed钩子函数 if (this.limit && this.filelist.length + files.length > this.limit) { this.onexceed && this.onexceed(files, this.filelist); return; } //将files转成数组 let postfiles = array.prototype.slice.call(files); if (!this.multiple) { postfiles = postfiles.slice(0, 1); } if (postfiles.length === 0) { return; } postfiles.foreach(rawfile => { this.onstart(rawfile); //选取文件后调用upload方法立即进行上传文件 if (this.autoupload) this.upload(rawfile); }); }, upload(rawfile) { this.$refs.input.value = null; //beforeupload 上传文件之前的钩子不存在就直接调用post上传文件 if (!this.beforeupload) { return this.post(rawfile); } // beforeupload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 promise 且被 reject,则停止上传 const before = this.beforeupload(rawfile); // 在调用beforeupload钩子后返回的是true,则继续上传 if (before && before.then) { before.then(processedfile => { //processedfile转成对象 const filetype = object.prototype.tostring.call(processedfile); if (filetype === '[object file]' || filetype === '[object blob]') { if (filetype === '[object blob]') { processedfile = new file([processedfile], rawfile.name, { type: rawfile.type }); } for (const p in rawfile) { if (rawfile.hasownproperty(p)) { processedfile[p] = rawfile[p]; } } this.post(processedfile); } else { this.post(rawfile); } }, () => { this.onremove(null, rawfile); }); } else if (before !== false) { //调用beforeupload之后没有返回值,此时before为undefined,继续上传 this.post(rawfile); } else { //调用beforeupload之后返回值为false,则不再继续上传并移除文件 this.onremove(null, rawfile); } }, abort(file) { const { reqs } = this; if (file) { let uid = file; if (file.uid) uid = file.uid; if (reqs[uid]) { reqs[uid].abort(); } } else { object.keys(reqs).foreach((uid) => { if (reqs[uid]) reqs[uid].abort(); delete reqs[uid]; }); } }, //上传文件过程的方法 post(rawfile) { const { uid } = rawfile; const options = { headers: this.headers, withcredentials: this.withcredentials, file: rawfile, data: this.data, filename: this.name, action: this.action, onprogress: e => { //文件上传时的钩子函数 this.onprogress(e, rawfile); }, onsuccess: res => { //文件上传成功的钩子函数 //上传成功调用onsuccess方法,即index.vue中的handlesuccess方法 this.onsuccess(res, rawfile); delete this.reqs[uid]; }, onerror: err => { //文件上传失败的钩子函数 this.onerror(err, rawfile); delete this.reqs[uid]; } }; //httprequest可以自定义上传文件,如果没有定义,默认通过ajax文件中的方法上传 const req = this.httprequest(options); this.reqs[uid] = req; if (req && req.then) { req.then(options.onsuccess, options.onerror); } }, handleclick() { //点击组件时调用input的click方法 if (!this.disabled) { this.$refs.input.value = null; this.$refs.input.click(); } }, handlekeydown(e) { if (e.target !== e.currenttarget) return; //如果当前按下的是回车键和空格键,调用handleclick事件 if (e.keycode === 13 || e.keycode === 32) { this.handleclick(); } } }, render(h) { let { handleclick, drag, name, handlechange, multiple, accept, listtype, uploadfiles, disabled, handlekeydown } = this; const data = { class: { 'el-upload': true }, on: { click: handleclick, keydown: handlekeydown } }; data.class[`el-upload--${listtype}`] = true; return ( //判断是否允许拖拽,允许的话显示upload-dragger组件,不允许就显示所有插槽中的节点 <div {...data} tabindex="0" > { drag ? <upload-dragger disabled={disabled} on-file={uploadfiles}>{this.$slots.default}</upload-dragger> : this.$slots.default } <input class="el-upload__input" type="file" ref="input" name={name} on-change={handlechange} multiple={multiple} accept={accept}></input> </div> ); } }; </script>
ajax.js
function geterror(action, option, xhr) { let msg; if (xhr.response) { msg = `${xhr.response.error || xhr.response}`; } else if (xhr.responsetext) { msg = `${xhr.responsetext}`; } else { msg = `fail to post ${action} ${xhr.status}`; } const err = new error(msg); err.status = xhr.status; err.method = 'post'; err.url = action; return err; } function getbody(xhr) { const text = xhr.responsetext || xhr.response; if (!text) { return text; } try { return json.parse(text); } catch (e) { return text; } } //默认的上传文件的方法 export default function upload(option) { //xmlhttprequest 对象用于在后台与服务器交换数据。 if (typeof xmlhttprequest === 'undefined') { return; } //创建xmlhttprequest对象 const xhr = new xmlhttprequest(); const action = option.action; //上传的地址 //xmlhttprequest.upload 属性返回一个 xmlhttprequestupload对象,用来表示上传的进度。这个对象是不透明的,但是作为一个xmlhttprequesteventtarget,可以通过对其绑定事件来追踪它的进度。 if (xhr.upload) { //上传进度调用方法,上传过程中会频繁调用该方法 xhr.upload.onprogress = function progress(e) { if (e.total > 0) { // e.total是需要传输的总字节,e.loaded是已经传输的字节 e.percent = e.loaded / e.total * 100; } //调文件上传时的钩子函数 option.onprogress(e); }; } // 创建一个formdata 对象 const formdata = new formdata(); //用户设置了上传时附带的额外参数时 if (option.data) { object.keys(option.data).foreach(key => { // 添加一个新值到 formdata 对象内的一个已存在的键中,如果键不存在则会添加该键。 formdata.append(key, option.data[key]); }); } formdata.append(option.filename, option.file, option.file.name); //请求出错 xhr.onerror = function error(e) { option.onerror(e); }; //请求成功回调函数 xhr.onload = function onload() { if (xhr.status < 200 || xhr.status >= 300) { return option.onerror(geterror(action, option, xhr)); } //调用upload.vue文件中的onsuccess方法,将上传接口返回值作为参数传递 option.onsuccess(getbody(xhr)); }; //初始化请求 xhr.open('post', action, true); if (option.withcredentials && 'withcredentials' in xhr) { xhr.withcredentials = true; } const headers = option.headers || {}; for (let item in headers) { if (headers.hasownproperty(item) && headers[item] !== null) { //设置请求头 xhr.setrequestheader(item, headers[item]); } } //发送请求 xhr.send(formdata); return xhr; }
upload-dragger.vue
<template> <!--拖拽上传时显示此组件--> <div class="el-upload-dragger" :class="{ 'is-dragover': dragover }" @drop.prevent="ondrop" @dragover.prevent="ondragover" @dragleave.prevent="dragover = false" > <slot></slot> </div> </template> <script> export default { name: 'eluploaddrag', props: { disabled: boolean }, inject: { uploader: { default: '' } }, data() { return { dragover: false }; }, methods: { ondragover() { if (!this.disabled) { this.dragover = true; } }, ondrop(e) { if (this.disabled || !this.uploader) return; //接受上传的文件类型(thumbnail-mode 模式下此参数无效),此处判断该文件是都符合能上传的类型 const accept = this.uploader.accept; this.dragover = false; if (!accept) { this.$emit('file', e.datatransfer.files); return; } this.$emit('file', [].slice.call(e.datatransfer.files).filter(file => { const { type, name } = file; //获取文件名后缀,与设置的文件类型进行对比 const extension = name.indexof('.') > -1 ? `.${ name.split('.').pop() }` : ''; const basetype = type.replace(/\/.*$/, ''); return accept.split(',') .map(type => type.trim()) .filter(type => type) .some(acceptedtype => { if (/\..+$/.test(acceptedtype)) { //文件名后缀与设置的文件类型进行对比 return extension === acceptedtype; } if (/\/\*$/.test(acceptedtype)) { return basetype === acceptedtype.replace(/\/\*$/, ''); } if (/^[^\/]+\/[^\/]+$/.test(acceptedtype)) { return type === acceptedtype; } return false; }); })); } } }; </script>
upload-list.vue
<template> <!--这里主要显示已上传文件列表--> <transition-group tag="ul" :class="[ 'el-upload-list', 'el-upload-list--' + listtype, { 'is-disabled': disabled } ]" name="el-list"> <li v-for="file in files" :class="['el-upload-list__item', 'is-' + file.status, focusing ? 'focusing' : '']" :key="file.uid" tabindex="0" @keydown.delete="!disabled && $emit('remove', file)" @focus="focusing = true" @blur="focusing = false" @click="focusing = false" > <img class="el-upload-list__item-thumbnail" v-if="file.status !== 'uploading' && ['picture-card', 'picture'].indexof(listtype) > -1" :src="file.url" alt="" > <a class="el-upload-list__item-name" @click="handleclick(file)"> <i class="el-icon-document"></i>{{file.name}} </a> <label class="el-upload-list__item-status-label"> <i :class="{ 'el-icon-upload-success': true, 'el-icon-circle-check': listtype === 'text', 'el-icon-check': ['picture-card', 'picture'].indexof(listtype) > -1 }"></i> </label> <i class="el-icon-close" v-if="!disabled" @click="$emit('remove', file)"></i> <i class="el-icon-close-tip" v-if="!disabled">{{ t('el.upload.deletetip') }}</i> <!--因为close按钮只在li:focus的时候 display, li blur后就不存在了,所以键盘导航时永远无法 focus到 close按钮上--> <el-progress v-if="file.status === 'uploading'" :type="listtype === 'picture-card' ? 'circle' : 'line'" :stroke-width="listtype === 'picture-card' ? 6 : 2" :percentage="parsepercentage(file.percentage)"> </el-progress> <span class="el-upload-list__item-actions" v-if="listtype === 'picture-card'"> <span class="el-upload-list__item-preview" v-if="handlepreview && listtype === 'picture-card'" @click="handlepreview(file)" > <i class="el-icon-zoom-in"></i> </span> <span v-if="!disabled" class="el-upload-list__item-delete" @click="$emit('remove', file)" > <i class="el-icon-delete"></i> </span> </span> </li> </transition-group> </template> <script> import locale from 'element-ui/src/mixins/locale'; import elprogress from 'element-ui/packages/progress'; export default { name: 'eluploadlist', mixins: [locale], data() { return { focusing: false }; }, components: { elprogress }, props: { files: { type: array, default() { return []; } }, disabled: { type: boolean, default: false }, handlepreview: function, listtype: string }, methods: { parsepercentage(val) { return parseint(val, 10); }, handleclick(file) { this.handlepreview && this.handlepreview(file); } } }; </script>
上一篇: 京东“长跑”,生鲜受益?
下一篇: 详解微信UnionID作用
推荐阅读
-
element-ui Tag、Dialog组件源码分析整理笔记(五)
-
element-ui button组件 radio组件源码分析整理笔记(一)
-
element-ui Carousel 走马灯源码分析整理笔记(十一)
-
element-ui Upload 上传组件源码分析整理笔记(十四)
-
element-ui input组件源码分析整理笔记(六)
-
element-ui Steps步骤条组件源码分析整理笔记(九)
-
element-ui switch组件源码分析整理笔记(二)
-
element-ui inputNumber、Card 、Breadcrumb组件源码分析整理笔记(三)
-
element-ui Rate组件源码分析整理笔记(十三)
-
element-ui Message组件源码分析整理笔记(八)