在vue2项目实现文件上传

项目实战中手写一个上传方法

解决了一个问题,就是上传的文件,在提交前给改了,再提交就报错。

<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>
<style lang="less" scoped>
</style>

效果图如下:

先创建一个vue的基础项目,方便我们去使用插件

vue cli 创建项目

下面链接,大致需要写以下代码

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>

<style>
  .uploader-example {
    width: 880px;
    padding: 15px;
    margin: 40px auto 0;
    font-size: 12px;
    box-shadow: 0 0 10px rgba(0, 0, 0, .4);
  }
  .uploader-example .uploader-btn {
    margin-right: 4px;
  }
  .uploader-example .uploader-list {
    max-height: 440px;
    overflow: auto;
    overflow-x: hidden;
    overflow-y: auto;
  }
</style>

上面vue代码中有个链接,就是所需上传的接口

上传接口链接

把samples/Node.js 文件夹下的内容下载下来,然后执行下面

npm install
node app.js

在vue2项目实现文件上传_第1张图片
启动服务

上传的效果图

分析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 的使用

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

配置:

  1. 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 的,如果想要带的话需要设置 withCredentialstrue,默认 false
    • method 当上传的时候所使用的是方式,可选 multipartoctet,默认 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)。
  2. new Uploader().xxx其中xxx属性有:

    • .support 当前浏览器是否支持 File API 来上传。
    • .supportDirectory 当前浏览器是否支持文件夹上传。
    • .opts 实例的配置项对象。
    • .filesUploader.File 文件对象组成的数组,纯文件列表。
    • .fileListUploader.File 文件、文件夹对象组成的数组,文件和文件夹共存。

配置项太多,看github文档吧,懒得粘贴

simple-uploader.js 的原理

git clone https://github.com/simple-uploader/Uploader

自己写一个简单版本大文件上传

在vue2项目实现文件上传_第2张图片

  1. 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方法在下面图中

  1. 创建两个空文件夹 ab
  2. 安装包:"express": "^4.17.1""multiparty": "^4.2.2"
  3. 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下放合并后的文件

在这里插入图片描述

你可能感兴趣的:(vue.js,javascript,前端)