相信很多同学都使用过element的上传组件,那它是怎么实现的呢?我们知道,涉及到文件上传的话都要使用到FileList
对象,而FileList
通常又来自于input[type='file']
控件。我们从这个控件入手,一步一步展开。
我使用的element是"2.13.2"
的,我们找到/packages/upload/src/upload.vue
,我们可以看到,组件使用的是render
函数,在函数中有这样一行代码:
<input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
render中使用的是JSX,可以看出这实际就是渲染了一个input[type='file']
控件,并且它绑定了关键的事件处理函数handleChange
。我们跳到handleChange
看一下它的源码:
handleChange(ev) {
const files = ev.target.files;
if (!files) return;
this.uploadFiles(files);
}
handleChange
只做了一件事情,在拿到FileList
对象的时候,将FileList
传到uploadFiles
函数进行一些前置条件到处理。
uploadFiles(files) {
//判断上传到文件数量是否超出限制
if (this.limit && this.fileList.length + files.length > this.limit) {
this.onExceed && this.onExceed(files, this.fileList);
return;
}
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);
// 判断是否自动上传
if (this.autoUpload) this.upload(rawFile);
});
},
我们可以看到函数之中对文件对上传数量做了判断,然后又对每个File对象单独处理了一遍。看postFiles.forEach
内的onStart
调用,这是父组件传递过来的函数,我们找到/packages/upload/src/index.vue
,并在322
行找到组件的渲染部分:
const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
我们看到upload
组件接收了一个参数uploadData
,往上翻可以看到对应的值:
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'
};
可以看到onStart
的值就是this.handleStart
,this.handleStart
处理了三件事:
- 使用当前时间加计数器变量生成uid
- 将File对象加工成组件自定义对象
- 将处理过的FileList传递给组件上层的钩子onChange(文件状态改变时的钩子)
// 使用当前时间加计数器变量生成uid
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);
我们看到上面的代码在使用了URL.createObjectURL
,URL.createObjectURL()
静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL对象表示指定的 File 对象或 Blob 对象。什么意思呢,你可以简单的认为这是用来将File对像转换成预览图的。需要注意的是,URL.createObjectURL
在使用完后并不会主动销毁,我们需要在组件销毁的时候调用 URL.revokeObjectURL()
方法来释放。可以看到组件在beforeDestroy
时做了这步操作:
beforeDestroy() {
this.uploadFiles.forEach(file => {
if (file.url && file.url.indexOf('blob:') === 0) {
URL.revokeObjectURL(file.url);
}
});
}
onStart
方法最主要的目的是为了将每次上传后的FileList与当前上传的File传递给上层使用方使用,通过调用上层传递的onChange
事件,将File、FileList传递给上层调用方。
到了这里我们可以看出element的上传组件其实更多的是在处理File、FileList来给到上层调用方使用。有了File对象,我们自己其实就已经可以将文件上传到服务器了,不过element帮我们将这部分也做好了。我们回到upload.vue
,在84行:
upload(rawFile) {
this.$refs.input.value = null;
if (!this.beforeUpload) {
return this.post(rawFile);
}
const before = this.beforeUpload(rawFile);
if (before && before.then) {
before.then(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) {
this.post(rawFile);
} else {
this.onRemove(null, rawFile);
}
}
如果对照官方使用文档,我们可以看到许多钩子函数到身影beforeUpload、onRemove
,这些我们不细讲。我们主要看看element是如何帮我们将File对象处理并上传到服务器的。将所有的条件判断忽略,我们可以看到最终的上传使用的是this.post
方法。post代码如下:
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 => {
this.onSuccess(res, rawFile);
delete this.reqs[uid];
},
onError: err => {
this.onError(err, rawFile);
delete this.reqs[uid];
}
};
const req = this.httpRequest(options);
this.reqs[uid] = req;
if (req && req.then) {
req.then(options.onSuccess, options.onError);
}
}
post处理了一些使用到的参数,并将这些参数传递给this.httpRequest
,这就是我们最后要讲的上传步骤。找到/packages/upload/src/ajax.js
:
// 错误消息提示处理
function getError(action, option, xhr){}
//返回内容处理
function getBody(xhr) {}
//上传
function upload(option) {}
ajax.js一共有三个函数,upload处理了主要的上传逻辑:
function upload(option) {
if (typeof XMLHttpRequest === 'undefined') {
return;
}
const xhr = new XMLHttpRequest();
const action = option.action;
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
option.onProgress(e);
};
}
const formData = new FormData();
if (option.data) {
Object.keys(option.data).forEach(key => {
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));
}
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;
}
可以看到函数中主要使用了XMLHttpRequest、FormData
XMLHttpRequest
- XMLHttpRequest.withCredentials 属性是一个Boolean类型,它指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。在同一个站点下使用withCredentials属性是无效的。
- XMLHttpRequest.upload 属性返回一个 XMLHttpRequestUpload对象,用来表示上传的进度。这个对象是不透明的,但是作为一个XMLHttpRequestEventTarget,可以通过对其绑定事件来追踪它的进度。
- XMLHttpRequestEventTarget.onerror 是XMLHttpRequest 事务由于错误而失败时调用的函数。
- XMLHttpRequestEventTarget.onload 是 XMLHttpRequest 请求成功完成时调用的函数。
- 只读属性 XMLHttpRequest.status 返回了XMLHttpRequest 响应中的数字状态码。status码是标准的HTTP status codes。
- XMLHttpRequest.setRequestHeader() 是设置HTTP请求头部的方法。此方法必须在 open() 方法和 send() 之间调用。如果多次对同一个请求头赋值,只会生成一个合并了多个值的请求头。
FormData
- FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式
- FormData.set 和 append() 的区别在于,如果指定的键已经存在, FormData.set 会使用新值覆盖已有的值,而 append() 会把新值添加到已有值集合的后面。
element通过XMLHttpRequest发起请求,通过FormData组装File最后达到上传目的。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!