First, some problems encountered before

There is a need to upload files in the project. For some unknown reason, there are always some inexplicable bugs in the implementation process of using the existing UI framework. For example, if you use a upload component and specify (:multiple=”false”), you can select multiple files and still send multiple files when uploading. Refs.[upload (component ref)].submit() does not support the upload event this.refs.[upload (component ref)].submit(). In short, I don’t want to see how it is implemented, I use the function, the interface itself or to rewrite, if insist on using will also make the project a lot of unnecessary logic, style code……

Previous view frameworks for Vue projects include Element-UI, zP-UI as a complement within the team, and iView. Framework is easy to use, but for their own projects are often not all used, especially our design sister out of the interface and the existing framework is very different, change the source code is inefficient and easy to cause unknown bugs, so I took the time to encapsulate the upload component.

Ii. Code and introduction

The parent component


<template>
  <div class="content">
		<label for="my-upload"> <span> upload </span> </label> <my-upload ref="myUpload"
	    :file-list="fileList"
	    action="/uploadPicture"
	    :data="param"
	    :on-change="onChange"
	    :on-progress="uploadProgress"
	    :on-success="uploadSuccess"
	    :on-failed="uploadFailed"
	    multiple
	    :limit="5"
	    :on-finished="onFinished">
    </my-upload>
    <button @click="upload" class="btn btn-xs btn-primary">Upload</button>
  </div>
</template>

<script>
import myUpload from './components/my-upload'
export default {
  name: 'test'.data() {return{fileList: [],// Upload a list of files. Files are stored in a list format, whether single or multiple options are supported param: {param1:' ', param2: ' '}, {onChange(fileList){}, methods: {onChange(fileList){this.fileList = [...fileList]; }, uploadSuccess(index, response){// uploadSuccess(index, response){console.log(index, response); },upload(){// Trigger the child component's upload method this.$refs.myUpload.submit(); }, removeFile(index){// Remove this file.$refs.myUpload.remove(index); }, uploadProgress(index, progress){// uploadProgress(index, progress) const{percent} = progress; console.log(index, percent); }, uploadFailed(index, err){// uploadFailed(index, err){console.log(index, err); }, onFinished(result){result: {success: number of successes, failed: number of failures} console.log(result); } }, components: { myUpload } } </script>Copy the code

The parent component deals with business-related logic. I specially add index parameters to facilitate the interface to display the uploaded results and directly manipulate the number of values. Not all methods are necessary, depending on the need to use.

Child components

<template>
<div>
	<input style="display:none" @change="addFile" :multiple="multiple" type="file" :name="name" id="my-upload"/>
</div>
</template>

Copy the code

Upload a file, the HTML part is just a pair of tags, don’t like complicated wordy

<script>
export default {
	name: 'my-upload',
	props: {
		name: String,
		action: {
			type: String,
			required: true
		},
		fileList: {
			type: Array,
			default: []
		},
		data: Object,
		multiple: Boolean,
		limit: Number, onChange: Function, onBefore: Function, onProgress: Function, onSuccess: Function, onFailed: Function, onFinished: Function}, methods: {}Copy the code

This defines the property values that the parent component needs to pass to the child component. Note that methods are also passed as property values, which is fine.

The component written by myself is not as complete and comprehensive as that published by the popular framework. In addition, I also want to solve the problem that the binding file-list cannot be uploaded (more likely because of my bad posture). Therefore, I hope to have absolute control over the file list, except for action. Make file-list an attribute that the parent component must pass as well. (The property name parent component uses a “-” connection, corresponding to the hump name in the child component Prop)

Three, the main upload function

methods: {
    addFile, remove, submit, checkIfCanUpload
}
Copy the code

There are four methods in Methods: add file, remove file, submit file, and check (check before upload), which are described below:

1. Add files

AddFile ({target: {files}}){// Input adds the file to the list when the onchange event is triggeredfor(leti = 0, l = files.length; i < l; i++){ files[i].url = URL.createObjectURL(files[i]); // create a blob address. files[i].status ='ready'; // We want to give the file a field to indicate the upload process, but we don't use...... }let fileList = [...this.fileList];
	ifFileList = [...fileList,...files]; (this.multiple){// fileList = [...fileList,...files];let l = fileList.length;
		let limit = this.limit;
		if(limit && typeof limit= = ="number" && Math.ceil(limit) > 0 && l > limit){// if there is a limit on the number of numbers, take the followinglimitA filelimit = Math.ceil(limit);
//			limit = limit> 10? 10:limit;
			fileList = fileList.slice(l - limit); }}else{// Only the last file is selected. Notice I didn't write fileList = files; FileList = [files[0]]; fileList = [files[0]]; } this.onChange(fileList); // Call the parent component method to cache the list in the fileList property} in the data level above,Copy the code

2. Remove files

This is simple, sometimes when the parent component forks a file, just pass an index.

remove(index){
	let fileList = [...this.fileList];
	if(fileList.length){ fileList.splice(index, 1); this.onChange(fileList); }},Copy the code

3. Submit and upload

There are two methods used here, FETCH and native. Since FETCH does not support getting the progress of the upload, it is easier to use the fetch request uploading logic if you do not need the progress bar or emulate the progress yourself, or the XMLHttpRequest object does not exist

submit() {if(this.checkIfCanUpload()){
		if(this.onProgress && typeof XMLHttpRequest ! = ='undefined')
			this.xhrSubmit();
		elsethis.fetchSubmit(); }},Copy the code

4. Two methods xhrSubmit and fetchSubmit are encapsulated based on two sets of upload logic

fetchSubmit
fetchSubmit() {let keys = Object.keys(this.data), values = Object.values(this.data), action = this.action;
	const promises = this.fileList.map(each => {
		each.status = "uploading";
		let data = new FormData();
		data.append(this.name || 'file', each);
		keys.forEach((one, index) => data.append(one, values[index]));
		return fetch(action, {
		  method: 'POST',
		  headers: {
		  	"Content-Type" : "application/x-www-form-urlencoded"}, body: data }).then(res => res.text()).then(res => JSON.parse(res)); // res.text() is used according to the return value type and should depend on the case}); Promise.all(promises).then(resArray => {// Multiple threads start at the same time, if there is a limit to the number of concurrent threads, you can use the synchronous way one by one, I will not repeat here.let success = 0, failed = 0;
		resArray.forEach((res, index) => {
			if(res.code == 1){ success++; This.onsuccess (index, res); this.onsuccess (index, res); }else if(res.code == 520){// the return value for convention failure is 520 failed++; This.onfailed (index, res); this.onfailed (index, res); }});return{ success, failed }; }).then(this.onfinished); // return the total upload result},Copy the code
xhrSubmit
xhrSubmit(){
    const _this = this;
	let options = this.fileList.map((rawFile, index) => ({
		file: rawFile,
		data: _this.data,
        filename: _this.name || "file", action: _this.action, onProgress(e){ _this.onProgress(index, e); }, onSuccess(res){_this.onsuccess (index, res); }, onError(err){ _this.onFailed(index, err); }}));let l = this.fileList.length;
	let send = async options => {
		for(leti = 0; i < l; i++){ await _this.sendRequest(options[i]); // This. SendRequest is executed in sequence with each object wrapped in a list of files. This. send(options); },Copy the code

Here is a reference to the element- UI upload source

sendRequest(option){

	const _this = this;
    upload(option);

	function getError(action, option, xhr) {
	  var msg = void 0;
	  if (xhr.response) {
	    msg = xhr.status + ' ' + (xhr.response.error || xhr.response);
	  } else if (xhr.responseText) {
	    msg = xhr.status + ' ' + xhr.responseText;
	  } else {
	    msg = 'fail to post ' + action + ' ' + xhr.status;
	  }

	  var err = new Error(msg);
	  err.status = xhr.status;
	  err.method = 'post';
	  err.url = action;
	  return err;
	}

	function getBody(xhr) {
	  var text = xhr.responseText || xhr.response;
	  if(! text) {return text;
	  }

	  try {
	    return JSON.parse(text);
	  } catch (e) {
	    returntext; }}function upload(option) {
	  if (typeof XMLHttpRequest === 'undefined') {
	    return;
	  }

	  var xhr = new XMLHttpRequest();
	  var 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);
	    };
	  }

	  var formData = new FormData();

	  if (option.data) {
	    Object.keys(option.data).map(function (key) {
	      formData.append(key, option.data[key]);
	    });
	  }

	  formData.append(option.filename, option.file);

	  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;
	  }

	  var headers = option.headers || {};

	  for (var item in headers) {
	    if(headers.hasOwnProperty(item) && headers[item] ! == null) { xhr.setRequestHeader(item, headers[item]); } } xhr.send(formData);returnxhr; }}Copy the code

Finally, add the pre-request checksum

checkIfCanUpload() {returnthis.fileList.length ? (this.onBefore && this.onBefore() || ! this.onBefore) :false;
},

Copy the code

If the parent component defines the onBefore method and returns false, or if the file list is empty, the request will not be sent.

That’s the end of the code, as long as you have the on-progress attribute and the XMLHttpRequest object is accessible, the request is sent natively, otherwise fetch is sent (without showing progress).