介绍
在本文中,我们将讨论如何使用 VueJs 处理文件上传。我们将创建一个图像上传器,允许用户通过拖放或选择文件对话框上传单个或多个图像文件。
然后我们将上传选定的图像并相应地显示它们。我们还将学习过滤上传文件类型,例如,我们只允许图像,不允许文件类型如 PDF。
文件上传 UI 和 API
文件上传由两部分组成:UI(前端)和API(后端)。我们将使用VueJs来处理 UI 部分。我们需要一个后端应用程序来接受上传的文件。您可以按照后端教程或下载并运行这些服务器端应用程序之一来处理后端的文件上传:-
- 使用 Hapi.js 上传文件:https://scotch.io/bar-talk/handling-file-uploads-with-hapi-js,或
- 使用 Express + Multer 上传文件:https://scotch.io/tutorials/express-file-uploads-with-multer,或
- 切换到您选择的任何云解决方案(Amazon S3、Google Drive 等)。
在本文中,我们将使用带有 Hapi.js 的文件上传作为我们的后端。我们还将学习在前端启用虚假上传的技巧。
使用 Vue-Cli 设置项目
我们将使用vue-cli来搭建 Vue.js 项目。我们将使用webpack-simple
项目模板。
# install cli
npm install vue-cli -g
# then create project, with sass
# follow the instructions to install all necessary dependencies
vue init webpack-simple file-upload-vue
好了,一切就绪。让我们继续创建我们的组件。
文件上传组件
我们将在 App.vue
. 删除文件中所有自动生成的代码。
<!-- App.vue -->
<!-- HTML Template -->
<template>
<div id="app">
<div class="container">
<!--UPLOAD-->
<form enctype="multipart/form-data" novalidate v-if="isInitial || isSaving">
<h1>Upload images</h1>
<div class="dropbox">
<input type="file" multiple :name="uploadFieldName" :disabled="isSaving" @change="filesChange($event.target.name, $event.target.files); fileCount = $event.target.files.length"
accept="image/*" class="input-file">
<p v-if="isInitial">
Drag your file(s) here to begin<br> or click to browse
</p>
<p v-if="isSaving">
Uploading {{ fileCount }} files...
</p>
</div>
</form>
</div>
</template>
<!-- Javascript -->
<script>
</script>
<!-- SASS styling -->
<style lang="scss">
</style>
笔记:-
- 我们的
App.vue
组件由 3 部分组成:模板 (HTML)、脚本 (Javascript) 和样式 (SASS)。 - 我们的模板有一个上传表单。
- 表单属性
enctype="multipart/form-data"
很重要。要启用文件上传,必须设置此属性。在此处了解有关 enctype 的更多信息。 - 我们有一个文件输入
<input type="file" />
来接受文件上传。该属性multiple
表明它允许多个文件上传。删除它以进行单个文件上传。 - 我们将处理文件输入
change
事件。每当文件输入发生变化(有人删除或选择文件)时,我们将触发该filesChange
功能并传入控件名称和所选文件$event.target.files
,然后上传到服务器。 - 我们将文件输入限制为仅接受具有属性的图像
accept="image/*"
。 - 上传时文件输入将被禁用,因此用户只能在上传完成后再次删除/选择文件。
- 我们捕获
fileCount
文件更改时的 。我们使用fileCount
变量来显示上传文件的数量Uploading {{ fileCount }} files...
。
为我们的文件上传组件设计样式
现在,这就是有趣的部分。目前,我们的组件如下所示:
我们需要将它转换成这样:
让我们来设计它!
<!-- App.vue -->
...
<!-- SASS styling -->
<style lang="scss">
.dropbox {
outline: 2px dashed grey; /* the dash box */
outline-offset: -10px;
background: lightcyan;
color: dimgray;
padding: 10px 10px;
min-height: 200px; /* minimum height */
position: relative;
cursor: pointer;
}
.input-file {
opacity: 0; /* invisible but it's there! */
width: 100%;
height: 200px;
position: absolute;
cursor: pointer;
}
.dropbox:hover {
background: lightblue; /* when mouse over to the drop zone, change color */
}
.dropbox p {
font-size: 1.2em;
text-align: center;
padding: 50px 0;
}
</style>
只需几行 scss,我们的组件现在看起来更漂亮了。
笔记:-
- 我们通过应用
opacity: 0
样式使文件输入不可见。这不会隐藏文件输入,只是使其不可见。 - 然后,我们为文件输入父元素设置样式,即
dropbox
css 类。我们使它看起来像一个带有破折号的放置文件区。 - 然后,我们将 dropbox 内的文本居中对齐。
文件上传组件代码
让我们继续编写我们的组件。
<!-- App.vue -->
...
<!-- Javascript -->
<script>
import { upload } from './file-upload.service';
const STATUS_INITIAL = 0, STATUS_SAVING = 1, STATUS_SUCCESS = 2, STATUS_FAILED = 3;
export default {
name: 'app',
data() {
return {
uploadedFiles: [],
uploadError: null,
currentStatus: null,
uploadFieldName: 'photos'
}
},
computed: {
isInitial() {
return this.currentStatus === STATUS_INITIAL;
},
isSaving() {
return this.currentStatus === STATUS_SAVING;
},
isSuccess() {
return this.currentStatus === STATUS_SUCCESS;
},
isFailed() {
return this.currentStatus === STATUS_FAILED;
}
},
methods: {
reset() {
// reset form to initial state
this.currentStatus = STATUS_INITIAL;
this.uploadedFiles = [];
this.uploadError = null;
},
save(formData) {
// upload data to the server
this.currentStatus = STATUS_SAVING;
upload(formData)
.then(x => {
this.uploadedFiles = [].concat(x);
this.currentStatus = STATUS_SUCCESS;
})
.catch(err => {
this.uploadError = err.response;
this.currentStatus = STATUS_FAILED;
});
},
filesChange(fieldName, fileList) {
// handle file changes
const formData = new FormData();
if (!fileList.length) return;
// append the files to FormData
Array
.from(Array(fileList.length).keys())
.map(x => {
formData.append(fieldName, fileList[x], fileList[x].name);
});
// save it
this.save(formData);
}
},
mounted() {
this.reset();
},
}
</script>
笔记:-
- 我们的组件将有几个状态:STATUS INITIAL、STATUS SAVING、STATUS SUCCESS、STATUS FAILED,变量名本身就很有表现力。
- 稍后,我们将调用Hapi.js 文件上传API 上传图片,该 API 接受字段调用
photos
。那是我们的文件输入字段名称。 - 我们使用
filesChange
函数处理文件更改。FileList
是由 HTML <input> 元素的 files 属性返回的对象。它允许我们访问使用 <input type=”file”> 元素选择的文件列表。了解更多 [此处](( https://developer.mozilla.org/en/docs/Web/API/FileList )。 - 然后我们创建一个新的
FormData
,并将我们所有的photos
文件附加到它。FormData
接口提供了一种轻松构建一组表示表单字段及其值的键/值对的方法。在此处了解更多信息。 - 该
save
函数将调用我们的文件上传服务(等等,我们接下来将创建该服务!)。我们还根据结果设置状态。 mount()
是 vue 组件生命周期钩子。在此期间,我们将组件状态设置为初始状态。
文件上传服务
让我们继续创建我们的服务。我们将使用axios进行 HTTP 调用。
安装 axios
# install axios
npm install axios --save
服务
// file-upload.service.js
import * as axios from 'axios';
const BASE_URL = 'http://localhost:3001';
function upload(formData) {
const url = `${BASE_URL}/photos/upload`;
return axios.post(url, formData)
// get data
.then(x => x.data)
// add url field
.then(x => x.map(img => Object.assign({},
img, { url: `${BASE_URL}/images/${img.id}` })));
}
export { upload }
没什么,代码本身就很有表现力。我们上传文件,等待结果,相应地映射它。
您现在可以使用npm run dev
命令运行该应用程序。尝试上传几张图片,它正在工作!(记得启动你的后端服务器)
显示成功和失败的结果
我们现在可以成功上传文件了。但是,UI 中没有任何指示。让我们更新我们的 HTML 模板。
<!-- App.vue -->
<!-- HTML Template -->
<template>
<div id="app">
<div class="container">
...form...
<!--SUCCESS-->
<div v-if="isSuccess">
<h2>Uploaded {{ uploadedFiles.length }} file(s) successfully.</h2>
<p>
<a href="javascript:void(0)" @click="reset()">Upload again</a>
</p>
<ul class="list-unstyled">
<li v-for="item in uploadedFiles">
<img :src="item.url" class="img-responsive img-thumbnail" :alt="item.originalName">
</li>
</ul>
</div>
<!--FAILED-->
<div v-if="isFailed">
<h2>Uploaded failed.</h2>
<p>
<a href="javascript:void(0)" @click="reset()">Try again</a>
</p>
<pre>{{ uploadError }}</pre>
</div>
</div>
</div>
</template>
笔记:-
- 上传成功时显示上传的图片。
- 上传失败时显示错误信息。
在前端伪造上传
如果你懒得启动后端应用程序(Hapi、Express 等)来处理文件上传。这里有一个假的服务来代替文件上传服务。
// file-upload.fake.service.js
function upload(formData) {
const photos = formData.getAll('photos');
const promises = photos.map((x) => getImage(x)
.then(img => ({
id: img,
originalName: x.name,
fileName: x.name,
url: img
})));
return Promise.all(promises);
}
function getImage(file) {
return new Promise((resolve, reject) => {
const fReader = new FileReader();
const img = document.createElement('img');
fReader.onload = () => {
img.src = fReader.result;
resolve(getBase64Image(img));
}
fReader.readAsDataURL(file);
})
}
function getBase64Image(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL('image/png');
return dataURL;
}
export { upload }
在这篇Stackoverflow 帖子中遇到了这个解决方案。挺有用的。我的在线演示正在使用此服务。
基本上,代码所做的是读取源代码,将其绘制在画布中,然后使用画布toDataURL
功能将其保存为数据 url 。在此处了解有关画布的更多信息。
现在你可以用假的服务交换真正的服务。
<!-- App.vue -->
...
<!-- Javascript -->
<script>
// swap as you need
import { upload } from './file-upload.fake.service'; // fake service
// import { upload } from './file-upload.service'; // real service
</script>
...
完毕!停止你的后端 API,刷新你的浏览器,你应该看到我们的应用程序仍在工作,而是调用假服务。
奖励:延迟你的承诺
有时,您可能希望延迟 Promise 以查看状态更改。在我们的例子中,文件上传可能完成得太快。让我们为此编写一个辅助函数。
// utils.js
// utils to delay promise
function wait(ms) {
return (x) => {
return new Promise(resolve => setTimeout(() => resolve(x), ms));
};
}
export { wait }
然后,您可以在您的组件中使用它
<!-- App.vue -->
...
<!-- Javascript -->
<script>
import { wait } from './utils';
...
save(formData) {
....
upload(formData)
.then(wait(1500)) // DEV ONLY: wait for 1.5s
.then(x => {
this.uploadedFiles = [].concat(x);
this.currentStatus = STATUS_SUCCESS;
})
...
},
</script>
结论
就是这样。这是您在不使用 Vue 中的任何 3rd 方库和插件的情况下处理文件上传的方式。没那么难吧?
快乐编码!
用户界面(前端)
API(后端)教程和源代码
- 使用 Hapi.js 上传文件:https://scotch.io/bar-talk/handling-file-uploads-with-hapi-js,或
- 使用 Express + Multer 上传文件:https://scotch.io/tutorials/express-file-uploads-with-multer,或
- 切换到您选择的任何云解决方案(Amazon S3、Google Drive 等)。