在vue2项目实现文件上传
vue cli 创建项目vue-sample-loader上面vue代码中有个链接,就是所需上传的接口上传接口链接把samples/Node.js 文件夹下的内容下载下来,然后执行下面启动服务
·
项目实战中手写一个上传方法
解决了一个问题,就是上传的文件,在提交前给改了,再提交就报错。
<template>
<a-card
size="small"
:body-style="{ padding: '40px' }"
style="width: 100%;"
:bordered="false"
:form-container="true"
class="header-filter"
:no-border-radius="true"
>
<a-form-model
:model="pageData.formData"
layout="horizontal"
vertical-space="no"
:rules="pageData.rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 6 }"
>
<a-form-model-item label="上传文件" prop="file" style="width: 100%;" size="small">
<input ref="iptUpload" type="file" style="display:none" @change="onChange" />
<a-tag
v-if="pageData.name"
color="green"
style="margin-right:5px;"
closable
@close="deleteChoose"
>{{pageData.name}}</a-tag>
<a-button type="primary" icon="plus" size="mini" ghost @click="chooseFile">选择文件</a-button>
</a-form-model-item>
</a-form-model>
<a-row style="padding-top:20px;">
<a-col span="6"></a-col>
<a-space :size="10">
<a-button type="primary" :loading="pageData.loading" @click="butHandle('upload')">上传</a-button>
<a-button @click="butHandle('reset')">重置</a-button>
<a-button @click="fasdfasd">发发大方的说</a-button>
</a-space>
</a-row>
</a-card>
</template>
<script>
import { onMounted, reactive } from '@vue/composition-api';
import { getDispatch } from '@/utils/service-dispatch.js';
import { actionName } from '../../store/actions';
import { async } from '@pa/pawm-portal-sdk';
export default {
name: 'CollateFilter',
props: {
queryButton: {
type: Object,
default: () => {},
},
},
setup(_, { root, refs }) {
const dispatch = getDispatch(root, root.$route.path);
// 系统平台日
//初始化数据
let formInitData = {
file: undefined,
};
let pageData = reactive({
loading: false,
name: '',
rules: {
file: [
{
required: true,
message: '不能为空',
trigger: 'change',
},
],
},
formData: Object.assign({}, formInitData),
});
const clearSelect = () => {
refs.iptUpload.value = '';
pageData.name = '';
pageData.formData.file = undefined;
};
//查询、重置点击
const butHandle = async state => {
if (state === 'upload') {
const { file } = pageData.formData;
if (!file || !file.get('file')) {
root.$message.error('请选择文件');
return;
}
在上传之前,读一下文件,如果发现文件读取错误,就清空之前的上传数据,提示重新上传。
const reader = new FileReader();
reader.readAsText(file.get('file'), 'UTF-8');
reader.onload = async () => {
pageData.loading = true;
let res = await dispatch(actionName.UPDATE_PLAT_DATE, file);
if (res.responseCode === '000000') {
root.$message.success('上传成功!');
clearSelect();
pageData.formData = Object.assign({}, formInitData);
} else {
root.$message.error(res.responseMsg || '系统异常');
}
pageData.loading = false;
};
reader.onerror = () => {
root.$message.error('文件有变更,请重新上传。');
clearSelect();
};
}
if (state === 'reset') {
clearSelect();
pageData.formData = Object.assign({}, formInitData);
}
};
const onChange = arg => {
const file = arg.target.files[0];
const name = file.name;
pageData.name = name;
let formData = new FormData();
formData.append('file', file);
pageData.formData.file = formData;
};
const chooseFile = () => {
refs.iptUpload.click();
};
const deleteChoose = e => {
e.preventDefault();
clearSelect();
};
const fasdfasd = () => {
console.dir(refs.iptUpload);
};
onMounted(() => {});
return {
pageData,
butHandle,
chooseFile,
onChange,
deleteChoose,
fasdfasd,
};
},
};
</script>
vue-sample-loader
开源包是怎么用的
安装一下
npm install vue-simple-uploader --save
import Vue from 'vue'
import uploader from 'vue-simple-uploader'
import App from './App.vue'
Vue.use(uploader)
/* eslint-disable no-new */
new Vue({
render(createElement) {
return createElement(App)
}
}).$mount('#app')
<template>
<uploader :options="options" class="uploader-example">
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<p>Drop files here to upload or</p>
<uploader-btn>select files</uploader-btn>
<uploader-btn :attrs="attrs">select images</uploader-btn>
<uploader-btn :directory="true">select folder</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>
</template>
<script>
export default {
data () {
return {
options: {
// https://github.com/simple-uploader/Uploader/tree/develop/samples/Node.js
target: '//localhost:3000/upload',
testChunks: false
},
attrs: {
accept: 'image/*'
}
}
}
}
</script>
把samples/Node.js 文件夹下的内容下载下来,然后执行下面
npm install
node app.js
目录如下:
最后需要启动一下服务才行
综合上面的执行结果,上传文件后效果如下:
分析vue-sample-loader 实现原理
git clone https://gitcode.net/mirrors/simple-uploader/vue-uploader
cd vue-uploader
cnpm i
uploader.vue 文件下有下面代码,说明本库是基于simple-uploader.js
二次封装的vue插件库
import Uploader from 'simple-uploader.js'
const uploader = new Uploader(this.options)
simple-uploader.js 的使用
插件代码在这里
npm install simple-uploader.js
var uploader = new Uploader({
target: '/api/photo/redeem-upload-token',
query: { upload_token: 'my_token' }
})
// 如果不支持 需要降级的地方
if (!uploader.support) location.href = '/some-old-crappy-uploader'
如果想要选择文件或者拖拽文件的话,你可以通过如下两个 API 来指定哪些 DOM 节点:
uploader.assignBrowse(document.getElementById('browseButton')) 单击选择文件
uploader.assignDrop(document.getElementById('dropTarget')) 拖拽文件
实例化后你还可以选择监听一些事件:
// 文件添加 单个文件
uploader.on('fileAdded', function (file, event) {
console.log(file, event)
})
// 单个文件上传成功
uploader.on('fileSuccess', function (rootFile, file, message) {
console.log(rootFile, file, message)
})
// 根下的单个文件(文件夹)上传完成
uploader.on('fileComplete', function (rootFile) {
console.log(rootFile)
})
// 某个文件上传失败了
uploader.on('fileError', function (rootFile, file, message) {
console.log(rootFile, file, message)
})
配置:
-
new Uploader(...)
内部的配置有:target
目标上传 URL,可以是字符串也可以是函数,如果是函数的话,则会传入Uploader.File
实例、当前块Uploader.Chunk
以及是否是测试模式,默认值为'/'
。singleFile
单文件上传。覆盖式,如果选择了多个会把之前的取消掉。默认false
。chunkSize
分块时按照该值来分。最后一个上传块的大小是可能是大于等于1倍的这个值但是小于两倍的这个值大小,可见这个 Issue #51,默认1*1024*1024
。forceChunkSize
是否强制所有的块都是小于等于chunkSize
的值。默认是false
。simultaneousUploads
并发上传数,默认3
。fileParameterName
上传文件时文件的参数名,默认file
。query
其他额外的参数,这个可以是一个对象或者是一个函数,如果是函数的话,则会传入Uploader.File
实例、当前块Uploader.Chunk
以及是否是测试模式,默认为{}
。headers
额外的一些请求头,如果是函数的话,则会传入Uploader.File
实例、当前块Uploader.Chunk
以及是否是测试模式,默认{}
。withCredentials
标准的 CORS 请求是不会带上 cookie 的,如果想要带的话需要设置withCredentials
为true
,默认false
。method
当上传的时候所使用的是方式,可选multipart
、octet
,默认multipart
,参考 multipart vs octet。testMethod
测试的时候使用的 HTTP 方法,可以是字符串或者函数,如果是函数的话,则会传入Uploader.File
实例、当前块Uploader.Chunk
,默认GET
。uploadMethod
真正上传的时候使用的 HTTP 方法,可以是字符串或者函数,如果是函数的话,则会传入Uploader.File
实例、当前块Uploader.Chunk
,默认POST
。allowDuplicateUploads
如果说一个文件已经上传过了是否还允许再次上传。默认的话如果已经上传了,除非你移除了否则是不会再次重新上传的,所以也就是默认值为false
。prioritizeFirstAndLastChunk
对于文件而言是否高优先级发送第一个和最后一个块。一般用来发送到服务端,然后判断是否是合法文件;例如图片或者视频的 meta 数据一般放在文件第一部分,这样可以根据第一个块就能知道是否支持;默认false
。testChunks
是否测试每个块是否在服务端已经上传了,主要用来实现秒传、跨浏览器上传等,默认true
。preprocess
可选的函数,每个块在测试以及上传前会被调用,参数就是当前上传块实例Uploader.Chunk
,注意在这个函数中你需要调用当前上传块实例的preprocessFinished
方法,默认null
。initFileFn
可选函数用于初始化文件对象,传入的参数就是Uploader.File
实例。readFileFn
可选的函数用于原始文件的读取操作,传入的参数就是Uploader.File
实例、文件类型、开始字节位置 startByte,结束字节位置 endByte、以及当前块Uploader.Chunk
实例。并且当完成后应该调用当前块实例的readFinished
方法,且带参数-已读取的 bytes。checkChunkUploadedByResponse
可选的函数用于根据 XHR 响应内容检测每个块是否上传成功了,传入的参数是:Uploader.Chunk
实例以及请求响应信息。这样就没必要上传(测试)所有的块了,具体细节原因参考 Issue #1,使用示例.generateUniqueIdentifier
可覆盖默认的生成文件唯一标示的函数,默认null
。maxChunkRetries
最大自动失败重试上传次数,值可以是任意正整数,如果是undefined
则代表无限次,默认0
。chunkRetryInterval
重试间隔,值可以是任意正整数,如果是null
则代表立即重试,默认null
。progressCallbacksInterval
进度回调间隔,默认是500
。speedSmoothingFactor
主要用于计算平均速度,值就是从 0 到 1,如果是 1 那么上传的平均速度就等于当前上传速度,如果说长时间上传的话,建议设置为0.02
,这样剩余时间预估会更精确,这个参数是需要和progressCallbacksInterval
一起调整的,默认是0.1
。successStatuses
认为响应式成功的响应码,默认[200, 201, 202]
。permanentErrors
认为是出错的响应码,默认[404, 415, 500, 501]
。initialPaused
初始文件 paused 状态,默认false
。processResponse
处理请求结果,默认function (response, cb) { cb(null, response) }
。 0.5.2版本后,processResponse
会传入更多参数:(response, cb, Uploader.File, Uploader.Chunk)。processParams
处理请求参数,默认function (params) {return params}
,一般用于修改参数名字或者删除参数。0.5.2版本后,processParams
会有更多参数:(params, Uploader.File, Uploader.Chunk, isTest)。
-
new Uploader().xxx
其中xxx属性有:.support
当前浏览器是否支持 File API 来上传。.supportDirectory
当前浏览器是否支持文件夹上传。.opts
实例的配置项对象。.files
由Uploader.File
文件对象组成的数组,纯文件列表。.fileList
由Uploader.File
文件、文件夹对象组成的数组,文件和文件夹共存。
配置项太多,看github文档吧,懒得粘贴
看看simple-uploader.js 的原理
git clone https://github.com/simple-uploader/Uploader
手写大文件上传
index.html代码:
引入axios
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
写入标签内容
<input type="file" name="" id="fileInput">
<button id="uploadBtn">上传</button>
修改文件时,触发fileInput
上传文件时,触发uploadBtn
document.getElementById('fileInput').addEventListener('change', handleFileChange);
document.getElementById('uploadBtn').addEventListener('click', handleUpload);
var file = null;
axios.defaults.baseURL = `http://localhost:3000`;
// 文件修改后,触发函数
function handleFileChange(event) {
const file = event.target.files[0];
if (!file) return;
window.file = file;
}
// 大文件上传
async function handleUpload(event) {
const file = window.file;
// 切片
const createFileChunks = function (file, size = 1024 * 1024 * 100) {// 每个切片 10MB
const fileChunks = [];
let cur = 0;
while (cur < file.size) {
fileChunks.push( file.slice(cur, cur + size) );
cur += size;
}
return fileChunks;
}
// 上传 切片
const uploadFileChunks = async function (fileChunks, filename) {
const formDataList = fileChunks.map((chunk, index) => {
const formData = new FormData();
console.log(`${filename}-${index} is instanceof Blob?`, chunk instanceof Blob);
formData.append('filename', filename);
formData.append('hash', index);
formData.append('chunk', chunk);
return formData
});
const requestList = formDataList.map(( formData ) =>
axios({
method: 'post',
url: `/upload`,
data: formData
})
);
await Promise.all(requestList);
}
// 合并切片
async function mergeFileChunks(filename) {
await axios({
method: 'get',
url: `/merge`,
params: {
filename
}
})
}
await uploadFileChunks(createFileChunks(file), file.name);
await mergeFileChunks(file.name);
}
看下切片的slice方法从何而来:
const file = event.target.files[0];
console.log(file.slice(0 , 1024 * 1024 * 100));// slice方法在下面图中
- 创建两个空文件夹
a
和b
- 安装包:
"express": "^4.17.1"
和"multiparty": "^4.2.2"
index.js
代码
const express = require('express');
const multiparty = require('multiparty');
const path = require('path');
const fs = require('fs');
const EventEmitter = require('events');
const { Buffer } = require('buffer');
const server = express();
const STATIC_TEMPORARY = path.resolve(__dirname, './a');
const STATIC_FILES = path.resolve(__dirname, './b');
function deleteFolder(filepath) {
if (fs.existsSync(filepath)) {
fs.readdirSync(filepath).forEach(filename => {
const fp = `${filepath}/${filename}`;
if (fs.statSync(fp).isDirectory()) deleteFolder(fp);
else fs.unlinkSync(fp);
});
fs.rmdirSync(filepath);
}
}
// 设置响应头处理跨域
server.use((request, response, next) => {
response.header('Access-Control-Allow-Origin', '*');
next();
})
// 上传切片
server.post('/upload', async (req, res) => {
const multipart = new multiparty.Form();
try {
let formData = {
filename: '',
hash: '',
chunk: null,
};
let isFileOk = false,
isFieldOk = false;
const myEmitter = new EventEmitter();
// 获取参数
multipart.parse(req, function (err, fields, files) {
formData.filename = fields['filename'];
formData.hash = fields['hash'];
isFieldOk = true;
myEmitter.emit('start');
});
// 获取文件
multipart.on('file', async function (name, file) {
formData['chunk'] = file;
isFileOk = true;
myEmitter.emit('start');
});
// 保存文件
myEmitter.on('start', function () {
if (isFileOk && isFieldOk) {
const {
filename,
hash,
chunk,
} = formData;
console.log(STATIC_TEMPORARY);
// 如果没有文件夹则新建
const chunkDir = `${STATIC_TEMPORARY}/${filename}`;
if (!fs.existsSync(chunkDir)) fs.mkdirSync(chunkDir);
// 获取 Buffer File
const buffer = fs.readFileSync(chunk.path);
// 创建写入流 写入Buffer
const ws = fs.createWriteStream(`${chunkDir}/${hash}`);
ws.end(buffer);
isFileOk = false;
isFieldOk = false;
res.send(`${chunk} 上传完成!`);
}
});
} catch (error) {
console.error(error);
}
});
// 合并切片
server.get('/merge', async (req, res) => {
const { filename } = req.query;
try {
const wsPath = `${STATIC_FILES}/${filename}`;
const temporaryPath = `${STATIC_TEMPORARY}/${filename}`;
let len = 0;
const bufferList = fs.readdirSync(temporaryPath).map(name => {
const buffer = fs.readFileSync(`${temporaryPath}/${name}`);
len += buffer.length;
return buffer;
});
// 合并写入
const buffer = Buffer.concat(bufferList, len);
const ws = fs.createWriteStream(wsPath);
ws.end(buffer);
// 删除切片缓存
deleteFolder(temporaryPath);
res.send(`${filename} 合并成功`);
} catch (error) {
console.error(error);
}
});
server.listen(3000, _ => {
console.log("http://localhost:3000/");
});
结果:a文件夹下放切片
,b下放合并后的文件
更多推荐
已为社区贡献4条内容
所有评论(0)