Vue инкапсулирует простой и легкий компонент загружаемого файла.

внешний интерфейс Element Vue.js iView

1. Некоторые проблемы, с которыми приходилось сталкиваться ранее

Требований к загрузке файлов в проекте много, в процессе использования существующего UI фреймворка всегда будут какие-то необъяснимые баги по какой-то причине. Например, если вы используете определенный компонент загрузки, он четко помечен (:multiple="false"), но на самом деле вы все равно можете выбрать несколько, и при загрузке по-прежнему отправляется несколько файлов; например, пока вы добавить атрибут (:file-list="fileList" "), если вы хотите вручную управлять списком загрузки, событие загрузки this.refs.[upload (component ref)].submit() не будет работать и не может быть загружено. Короче, мне лень смотреть как это реализовано.Я использую функции, а сам интерфейс еще надо переписать.Если настаивать на его использовании, то это добавит в проект много лишней логики и стилевых кодов ...

Фреймворки представлений, использовавшиеся ранее для проектов Vue, включают element-ui, zp-ui в качестве дополнения внутри команды и iview. Фреймворк прост в использовании, но использовать его весь для собственных проектов часто невозможно, тем более интерфейс, созданный нашей девушкой-дизайнером, сильно отличается от существующего фреймворка, эффективность изменения исходного кода невысока и это легко чтобы привести к неизвестным ошибкам, поэтому я не тороплюсь Инкапсулирует этот компонент загрузки.

2. Код и введение

родительский компонент


<template>
  <div class="content">
		<label for="my-upload">
			<span>上传</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: [],//上传文件列表,无论单选还是支持多选,文件都以列表格式保存
  		param: {param1: '', param2: '' },//携带参数列表
  	}
  },
  methods: {
  	onChange(fileList){//监听文件变化,增减文件时都会被子组件调用
  		this.fileList = [...fileList];
  	},
  	uploadSuccess(index, response){//某个文件上传成功都会执行该方法,index代表列表中第index个文件
  		console.log(index, response);
  	},
  	upload(){//触发子组件的上传方法
  		this.$refs.myUpload.submit();
  	},
  	removeFile(index){//移除某文件
  		this.$refs.myUpload.remove(index);
  	},
  	uploadProgress(index, progress){//上传进度,上传时会不断被触发,需要进度指示时会很有用
  		const{ percent } = progress;
  		console.log(index, percent);
  	},
  	uploadFailed(index, err){//某文件上传失败会执行,index代表列表中第index个文件
  		console.log(index, err);
  	},
  	onFinished(result){//所有文件上传完毕后(无论成败)执行,result: { success: 成功数目, failed: 失败数目 }
  		console.log(result);
  	}
  },
  components: {
  	myUpload
  }
}
</script>

Родительский компонент обрабатывает бизнес-логику.Я специально добавил параметры индекса, чтобы интерфейс мог напрямую манипулировать первым значением при отображении результата загрузки.Не все методы обязательны, и их можно использовать по мере необходимости.

Подсборка

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

Загрузите файл, часть html — это просто пара тегов, я не люблю сложного и многословного

<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: {}//下文主要是methods的介绍,此处先省略
}
</script>

Это определяет значения атрибутов, которые необходимо передать от родительского компонента к дочернему компоненту.Обратите внимание, что здесь можно передать метод как атрибут.

Компоненты, написанные мной, не такие полные и исчерпывающие, как выпущенные популярными фреймворками.Кроме того, я также хочу сделать все возможное, чтобы решить проблему, что список файлов не может быть загружен, как было сказано в начале (скорее потому, что мой позиция неверна). Я столкнулся с этой проблемой, поэтому надеюсь иметь абсолютный контроль над списком файлов. Помимо действия, список файлов также используется как свойство, которое должно быть передано родительским компонентом. (Родительский компонент имени свойства связан с «-», что соответствует имени случая верблюда в опоре дочернего компонента)

Три, основная функция загрузки

methods: {
    addFile, remove, submit, checkIfCanUpload
}

Всего в методах есть 4 метода: добавление файлов, удаление файлов, отправка и тестирование (проверка перед загрузкой), а именно:

1. Добавьте файлы

addFile({target: {files}}){//input标签触发onchange事件时,将文件加入待上传列表
	for(let i = 0, l = files.length; i < l; i++){
		files[i].url = URL.createObjectURL(files[i]);//创建blob地址,不然图片怎么展示?
		files[i].status = 'ready';//开始想给文件一个字段表示上传进行的步骤的,后面好像也没去用......
	}
	let fileList = [...this.fileList];
	if(this.multiple){//多选时,文件全部压如列表末尾
		fileList = [...fileList, ...files];
		let l = fileList.length;
		let limit = this.limit;
		if(limit && typeof limit === "number" && Math.ceil(limit) > 0 && l > limit){//有数目限制时,取后面limit个文件
			limit = Math.ceil(limit);
//			limit = limit > 10 ? 10 : limit;
			fileList = fileList.slice(l - limit);
		}
	}else{//单选时,只取最后一个文件。注意这里没写成fileList = files;是因为files本身就有多个元素(比如选择文件时一下子框了一堆)时,也只要一个
		fileList = [files[0]];
	}
	this.onChange(fileList);//调用父组件方法,将列表缓存到上一级data中的fileList属性
	},
	

2. Удалить файлы

Это просто, иногда, когда родительский компонент разветвляет файл, просто передайте index.

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

3. Отправить загрузку

Здесь используются два метода: выборка и собственные методы.Поскольку выборка не поддерживает получение хода загрузки, если вам не нужен индикатор выполнения или вы сами имитируете ход выполнения или объект XMLHttpRequest не существует, будет проще использовать выборку запросить логику загрузки

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

4. На основе двух наборов логики загрузки здесь инкапсулированы два метода xhrSubmit и fetchSubmit.

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()是根据返回值类型使用的,应该视情况而定
	});
	Promise.all(promises).then(resArray => {//多线程同时开始,如果并发数有限制,可以使用同步的方式一个一个传,这里不再赘述。
		let success = 0, failed = 0;
		resArray.forEach((res, index) => {
			if(res.code == 1){
				success++;                  //统计上传成功的个数,由索引可以知道哪些成功了
				this.onSuccess(index, res);
			}else if(res.code == 520){      //约定失败的返回值是520
				failed++;                  //统计上传失败的个数,由索引可以知道哪些失败了
				this.onFailed(index, res);
			}
		});
		return { success, failed };     //上传结束,将结果传递到下文
	}).then(this.onFinished);           //把上传总结果返回
},


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);//闭包,将index存住
        },
        onSuccess(res){
          _this.onSuccess(index, res);
        },
        onError(err){
          _this.onFailed(index, err);
        }
    }));
	let l = this.fileList.length;
	let send = async options => {
		for(let i = 0; i < l; i++){
			await _this.sendRequest(options[i]);//这里用了个异步方法,按次序执行this.sendRequest方法,参数为文件列表包装的每个对象,this.sendRequest下面紧接着介绍
		}
	};
	send(options);
},

Здесь мы используем исходный код загрузки element-ui.

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) {
	    return text;
	  }
	}

	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);
	  return xhr;
	}
}


Наконец, добавьте проверку перед запросом

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

Если родительский компонент определяет метод onBefore и возвращает false или список файлов пуст, запрос не будет отправлен.

Кодовая часть закончена, пока доступен атрибут on-progress и доступен объект XMLHttpRequest, запрос будет отправлен нативным способом, в противном случае запрос будет отправлен с выборкой (прогресс отображаться не будет).