VueАктуальный бой: компонент загрузки файлов
Загрузка файлов — требование, с которым мы часто сталкиваемся в повседневной работе разработчиков.В различных популярных библиотеках компонентов есть готовые компоненты, которые можно легко вызвать напрямую. Конкретное использование не будет повторяться, и друзья смогут просмотреть его в библиотеке компонентов.demo, здесь автор в основном рассказывает о том, как добитьсяUploadкомпоненты.
Функции, поддерживаемые компонентом, следующие:
- Отображение индикатора выполнения загрузки
- Предварительный просмотр изображения
- Пользовательский метод запроса на загрузку
- Лимит изображений
- Поддержка перетаскивания загрузки
- использовать
koaИ соответствующее промежуточное ПО для реализации интерфейса сервера загрузки изображений.
Последний рендеринг выглядит следующим образом:

Релевантная информация:
Создайте сервер загрузки файлов
перед письмомUploadПеред компонентом сначала нужно пройтиNode.jsпостроить сервер. Вот некоторые из используемых библиотек:
-
Koa:Node.jsсерверная структура -
koa-static:KoaПО промежуточного слоя статического сервера -
@koa/multer:Koaпромежуточное ПО для загрузки файлов -
@koa/router:Koaпромежуточное ПО маршрутизации -
@koa/cors: Решение междоменных проблем
Код на стороне сервера относительно прост, исходный код выглядит следующим образом:
const path = require('path');
const Koa = require('koa');
const Router = require('@koa/router');
const multer = require('@koa/multer');
const cors = require('@koa/cors');
const serve = require('koa-static');
const app = new Koa();
// deploy to heroku will use environment variable
const PORT = process.env.PORT || 3000;
const router = new Router();
// set upload files store directory
const upload = multer({ dest: 'uploads' });
app.use(serve('.'));
app.use(cors());
app.use(router.routes());
app.use(router.allowedMethods());
router.post('/upload', upload.single('file'), async (ctx) => {
const domain = ctx.protocol + '://' + ctx.host + '/';
const { path, originalname } = ctx.file;
const data = { path: domain + path, filename: originalname };
ctx.body = {
code: 200,
data,
message: '成功'
};
})
app.listen(PORT, () => {
console.log(`server is listening on port ${PORT}`);
});
Мы используем корневой каталог проекта в качестве корневого каталога статического сервера, и тогда мы можем получить доступ к ресурсам в проекте напрямую через путь. Такие как:http://localhost:3000/uploads/xxxx(запуск службы локально) можно получить напрямуюuploadsНиже представлены фотографии, которые мы загрузили.
Основная логика приведенного выше кода заключается в том, чтобы помочь нам реализовать интерфейс загрузки, и после завершения загрузки путь доступа к изображению склеивается, а затем возвращается в качестве ответа. Внешний интерфейс может предварительно просмотреть изображение по пути после получения ответа.
Наконец, мы развертываем сервер наherokuПодробнее о процессе развертывания см. здесь:портал
дизайн компонентов
поддерживаемый компонентAPIследующим образом (ссылкаelement ui):
-
name: когда файл загружается, внешний интерфейс должен согласиться с сервером.key -
limit: количество загрузок файлов -
fileList: Список загруженных файлов -
action: адрес загрузки файла -
beforeUpload: Функция хука перед загрузкой -
onChange: Обратный вызов срабатывает при изменении информации о файле во время загрузки. -
onSuccess/onError/onProgress: загрузить функцию обратного вызова успеха/ошибки/прогресса -
onExceed: Обратный вызов срабатывает при превышении максимального количества загрузок. -
data: объект, состоящий из дополнительных параметров, который в конечном итоге будет пройден с помощьюkey/valueпара ключ-значениеappendприбытьformDataсередина -
multiple: Поддерживать ли загрузку нескольких файлов -
accept: Загрузить полученный тип файла -
customHttpRequest: Поддержка функции пользовательского запроса -
drag: включить ли загрузку методом перетаскивания
Перед кодированием давайте посмотрим на основное использование компонентов:
<template>
<go-upload name="file" :limit="3" multiple on-exceed="onExceed" :action="action">
<go-button color="primary">click to upload</go-button>
</go-upload>
</template>
<script>
export default {
data () {
return {
action: 'https://afternoon-dawn-09444.herokuapp.com/upload'
};
},
methods: {
onExceed() {
}
}
};
</script>
Компонент размещает триггерную кнопку загрузки файла в виде слота и получаетnameа такжеactionсвойства и черезmultipleАтрибуты поддерживают загрузку нескольких файлов
Теперь приступаем к реализации компонента.
вызыватьInputизchangeмероприятие
В зависимости от того, как используется компонент, мы можем написать следующий код:
<template>
<div class="go-upload">
<input
class="go-upload-input"
ref="input"
type="file"
>
<div class="go-upload-trigger" >
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
name: { type: String, default: 'file' },
action: {
type: String,
required: true
},
}
}
</script>
Просто нажмитеtype=fileсоответствующийinput, появится соответствующее окно загрузки файла. Но из-за оригиналаinputНекрасиво, это можно скрыть(display:none), затем вручную запуститьinputизclickСобытие для срабатыванияinputизchangeмероприятие.
Конкретные подробности вmdnПредставлено:
В компоненте, когда пользователь нажимаетslotкнопка вclickСобытия будут пузыми доgo-upload-triggerсоответствующийdiv, мы можем контролироватьgo-upload-triggerизclickсобытие, а затем позвонитеinputизclickЗатем всплывает окно загрузки:
<template>
<div class="go-upload">
<input
class="go-upload-input"
ref="input"
type="file"
>
<div class="go-upload-trigger" @click="onClickTrigger">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
// ...
methods: {
onClickTrigger () {
this.$refs.input.click();
}
}
}
</script>
Таким образом, мы можем реализовать более красивую кнопку для загрузки файлов.
Перед загрузкой разберемся, что делать:
- Реализовать инкапсуляцию метода загрузки
- Получить файл, выбранный пользователем (
e.target.files) - аккуратный
XMLHttpRequestЗагрузите необходимые параметры и вызовите метод загрузки - Обновление во время загрузки
fileListСтатус для простого отображения
выполнитьXMLHttpRequestСпособ загрузки файла
XMLHttpRequestПодробное использование можно найти здесь:XMLHttpRequest
Загрузка файла может использоватьFormDataреализовать,FormDataМножество представителей может быть легко построеноformполя и их ценностиkey/valueпары ключ-значение вместо того, чтобы писать на страницеformформа.
Для удобства пользовательский вводdataявляется объектом, нам нужно перебрать каждый элемент в нем, а затемkey/valueпройти черезappendметод добавления кformData, переданный на сервер. Исходный код выглядит следующим образом:
// XMLHttpRequest常用的三个事件: error/load/progress
import { entries } from '@/shared/util';
const processResponse = (response) => {
if (typeof response === 'string') {
try {
return JSON.parse(response);
} catch (e) {
return response;
}
}
return response;
};
const request = ({
url,
name,
file,
data,
onSuccess,
onError,
onProgress
}) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
// 传入文件key,value对
formData.append(name, file);
// 遍历对象方便为formData添加key,value对
entries(data, (key, val) => formData.append(key, val));
// 监听上传的progress事件来监听上传过程,显示上传进度
xhr.upload.addEventListener('progress', (e) => {
e.percent = e.loaded / e.total * 100;
onProgress(e);
});
xhr.open('POST', url);
xhr.send(formData);
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
const response = processResponse(xhr.response);
onSuccess(response);
} else {
onError(new Error('upload request failed!'));
}
});
xhr.addEventListener('error', (e) => {
onError(e);
});
return xhr;
};
export default request;
Соответствующие параметры имеют следующие значения:
- URL-адрес загрузки
- имя загруженного файла
keyзначение, необходимо согласовать с серверной частью - файл загруженного объекта File
- данные другие параметры, кроме файлов, тип - объект
- Обратный вызов onSuccess после успешной загрузки
- Обратный вызов onError после сбоя загрузки
- Обратный вызов прогресса загрузки onProgress
наконецrequestфункция вернетсяxhr, который удобно звонить позжеxhrсвойства и методы, такие как:xhr.abort()чтобы отменить запрос.
Уведомление:
xhr.uploadизprogressсобытие должно быть вызваноxhr.openМониторьте раньше, иначе не подействует(xhr.upload.onprogress doesn't work)
Начать загрузку
Выбранный пользователем файл находится вinputизchangeВ объекте события события:e.target.files, то черезuploadFilesзагружать файлы
<script>
export default {
methods: {
onInputChange (e) {
// e.target.files is pseudo array, need to convert to real array
const rawFiles = Array.from(e.target.files);
this.uploadFiles(rawFiles);
},
uploadFiles (rawFiles) {
const filesLen = rawFiles.length + this.files.length;
if (this.limit && this.limit < filesLen) {
return this.onExceed(rawFiles, this.files);
}
this.startUpload(rawFiles);
}
}
}
</script>
пройдет перед загрузкойlimitЧтобы судить, превышает ли количество загрузок лимит, если это так, он будет вызывать входящие сообщения пользователя.onExceedметод и прекратите загрузку.
Уведомление:
e.target.filesТо, что получается, является псевдомассивом, нам нужно преобразовать его в реальный массив, чтобы вызвать метод массива.
существуетstartUploadметод, мы перебираем каждый файл, который выбирает пользователь, и загружаем их по очереди. Перед фактической загрузкой он выполнитnormalizeFilesроднойfileОбъект форматируется в формат, удобный для использования в компоненте:
<script>
export default {
methods: {
startUpload (rawFiles) {
rawFiles.forEach(rawFile => {
const file = this.normalizeFiles(rawFile);
if (!this.beforeUpload || this.beforeUpload()) {
this.upload(file);
}
});
},
normalizeFiles (rawFile) {
const file = {
name: rawFile.name, // 文件名
size: rawFile.size, // 文件尺寸
type: rawFile.type, // 文件类型
percent: 0, // 上传进度
uid: Date.now() + this.tempIndex++, // 唯一标识
status: 'init', // value list: init pending success failure
raw: rawFile // 原生文件对象
};
// concat does not change the existing arrays, but instead returns a new array
this.files.push(file);
return file;
},
}
}
</script>
в форматированииfileобъект, нам нужно сгенерировать уникальныйuidдля идентификации информации о каждом загруженном файле. Здесь мы используемDate.now()Для создания уникального значения, но во время загрузки файла будетв то же времязагрузка нескольких файлов, что приводит кuidповторить. Итак, мы добавляем автоинкрементtempIndex, чтобы предотвратить повторение.
После получения отформатированного файла он войдет в настоящую ссылку для загрузки:
<script>
export default {
methods: {
upload (file) {
const options = {
url: this.action,
name: this.name,
file: file.raw,
data: this.data,
onSuccess: this.handleSuccess.bind(this, file),
onError: this.handleError.bind(this, file),
onProgress: this.handleProgress.bind(this, file)
};
file.status = 'pending';
this.onChange(file, this.files);
const req = this.customHttpRequest(options);
if (req instanceof Promise) {
req.then(options.onSuccess, options.onError);
}
},
handleSuccess (file, response) {
file.status = 'success';
this.$set(file, 'response', response);
// Not only front end can implement picture preview but also back end can do it. Here make use of back end api
this.$set(file, 'url', response.data.path);
this.onChange(file, this.files);
this.onSuccess(response, file, this.files);
},
handleError (file, error) {
file.status = 'failure';
this.onError(error, file, this.files);
},
handleProgress (file, event) {
file.percent = event.percent;
this.onChange(file, this.files);
this.onProgress(event, file, this.files);
},
}
}
</script>
uploadДля функции, которая фактически инициирует запрос, она сначала собирает все параметры, необходимые для инициирования запроса, а затем изменяет статус файла наpendingПосле загрузки. Функции, реализованные соответствующими функциями обратного вызова, следующие:
-
handleSuccess: изменить статус файла наsuccess;Добавить кresponseатрибут — ответ на запрос; добавитьurlАтрибут представляет собой адрес предварительного просмотра файла. -
handleProgress: установить процент выполнения загрузки файла. -
handleError: изменить статус файла наfailure
Теперь, когда мы можем успешно обрабатывать различные состояния файла во время загрузки, мы покажем его на странице.
список загрузки
Все загружаемые и загруженные файлы будут помещены вfilesПриходите показать:
<script>
export default {
data () {
return {
files: [],
};
},
}
</script>
упаковкаupload-listкомпонент и передатьfilesПоказать весь процесс загрузки файла:
<template>
<div class="go-upload-list">
<div :class="['go-upload-list-item',file.status]" v-for="(file,i) in files" :key="file.uid">
<!-- FIXME:code in here is so chaos, can it become more elegance? -->
<div class="go-upload-list-item-img">
<go-icon v-if="file.status === 'pending'" class="go-upload-item-img-loading" name="loading"></go-icon>
<template v-else-if="file.status === 'success'">
<img v-if="isImage(file.type)" class="go-upload-list-item-img" :src="file.url" alt="">
<go-icon v-else class="go-upload-item-file" name="file"></go-icon>
</template>
<go-icon v-else class="go-upload-item-img-error" name="picture"></go-icon>
</div>
<div class="go-upload-list-item-name">
<span>{{ file.name }}</span>
<my-progress v-if="file.status === 'pending'" :percent="file.percent"></my-progress>
</div>
</div>
</div>
</template>
Здесь ранее отформатированная информация о файле будет использоваться для отображения состояния файла, индикатора загрузки и т. д., а также эскиза изображения после успешной загрузки изображения.
перетащить загрузить
Мы также поддерживаем пользователей, которые могут перетаскивать файлы в область перетаскивания для загрузки. пройти черезdropв объекте события событияe.dataTransfer.filesчтобы получить загруженный файловый объект, а затем обработать и отправить файловый объект, как при обычной загрузкеXMLHttpRequestпросить. код показывает, как показано ниже:
<template>
<div
class="go-upload-dragger"
:class="{dragging}"
@dragenter="onDragenter"
@dragleave="onDragleave"
@dragover="onDragover"
@drop="onDrop"
@click="onClick"
>
<go-icon class="go-upload-dragger-icon" name="upload"></go-icon>
<div class="go-upload-dragger-describe">
<span>Drop file here or click to upload</span>
</div>
</div>
</template>
<script>
export default {
name: 'UploadDragger',
data () {
return {
dragging: false
};
},
methods: {
onDragenter (e) {
this.dragging = true;
e.stopPropagation();
e.preventDefault();
},
onDragleave (e) {
this.dragging = false;
e.stopPropagation();
e.preventDefault();
},
onDragover (e) {
e.stopPropagation();
e.preventDefault();
},
onDrop (e) {
this.dragging = false;
e.stopPropagation();
e.preventDefault();
const files = e.dataTransfer.files;
this.$emit('handle-files', files);
},
onClick () {
this.$emit('on-click');
}
}
};
</script>
После входа в область перетаскивания мы установим логическое значениеdraggingДля динамического управленияclass, чтобы реализовать интерактивный эффект после того, как пользователь перетащит файл в область перетаскивания. Вызывается, когда пользователь отпускает файл в области перетаскиванияdropсобытие, файловый объект будетfilesпройти черезthis.$emit('handle-files',files)Отправитьuploadкомпоненты для обработки.
<template>
<div class="go-upload">
<!-- omit other code ... -->
<upload-dragger v-if="drag" @on-click="onClickTrigger" @handle-files="uploadFiles"></upload-dragger>
<div
v-else
class="go-upload-trigger"
@click="onClickTrigger"
>
<slot></slot>
</div>
</div>
</template>
<script>
export default{
methods: {
onClickTrigger () {
this.$refs.input.click();
},
uploadFiles (rawFiles) {
const filesLen = rawFiles.length + this.files.length;
if (this.limit && this.limit < filesLen) {
return this.onExceed(rawFiles, this.files);
}
this.startUpload(rawFiles);
},
}
}
</script>
будет передан в родительский компонент@контролироватьhandle-filesВызывается соответствующий метод и вызывается предыдущая функция загрузки.Последующий процесс загрузки точно такой же, как и при обычной загрузке. В приведенном выше коде область перетаскивания также может выступать в качестве триггерной кнопки для операции загрузки.После нажатия на область она вызоветonClickTriggerметод для вызова интерфейса выбора файлов.
существуетmdnСуществует введение в загрузку с помощью перетаскивания, и заинтересованные друзья могут его проверить:
На данный момент мы реализовали некоторые общие функции компонента загрузки, и у нас есть несколько удачных попыток 🤣!
Эпилог
Когда компонент готов, его можно развернуть наGitHub PagesЧтобы поделиться в сети, конкретный процесс развертывания:Разверните проект Vue на страницах GitHub.
Я надеюсь, что после прочтения этой статьи я смогу помочь партнерам по чтению понять конкретную логику реализации компонента загрузки и лучше использовать популярный в сообществе фреймворк.Uploadкомпоненты и инкапсулировать их дважды.
Использованная литература: