封装个【文件夹】上传组件,顺便手把手教你们发布个npm包;(附源码)

134 阅读5分钟

1.前言

文件上传对大家来说已经不新鲜了,什么断点上传,秒传,分片上传无外乎就那些东西。这里推荐个博客给你们看吃透大文件上传这篇博客;今天来跟大家说个意思的如何实现文件夹拖拽上传

2.前置项

2.1 找个靠谱的拖拽库

点击按钮上传局限性太大,所以我们用多拽的方式上传文件夹。至于如何实现拖拽,我这里用的是dropzone.js这个库,这个库非常强大基本上你能想到的拖拽方式这里全都有。不展开细说,文件夹上传的源码会有体现。

2.2 遍历文件夹思路

文件夹上传肯定不能像文件一样单个上传,文件夹中可能包含很多层级,一层套一层非常繁琐。所以我们要理清思路

  1. 首先读取到文件夹的名称,包括文件夹下面的文件夹,组成树形结构
  2. 组成文件夹树形结构之后 遍历上传文件夹
  3. 在服务器上创建同名文件夹
  4. 再把文件夹的图片上传到同名文件夹下

思路看着很清晰,但是真的实现起来还是有点麻烦的::>_<::

2.3 核心API

  1. 通过webkitGetAsEntry() 来返回文件判断是文件夹还是文件 通过 isDirectory 方法和 isFile 来判断,如果是文件夹目录。就需要递归得到该目录。
  2. 通过FileSystemDirectoryReader来处理目录内容,通过createReder()方法来完成。
  3. 通过readEntries()调用读取目录中的所有文件。

举个例子


 <div id="app">
  <div class="upload upload-box" id="upload">
   拖动文件夹到此处上传
  </div>
 </div>
 

const upload = document.getElementById('upload');

upload.addEventListener('dragover', uploadFunc, false);
upload.addEventListener('drop', uploadFunc, false);

function uploadFunc(event) {
  event.preventDefault();
  
  let files = [];
  if (event.type === 'drop') {
    let fileList = event.dataTransfer.files;
    let len = fileList.length;
   
    for (let i = 0; i < len; i++) {
      files[i] = fileList[i];
    }
   
    if (files.length) {
      let items = event.dataTransfer.items;
      if (items && items.length && items[0].webkitGetAsEntry != null) {
        addFilesItems(items);
      }
    }
  }
}

  function addFilesItems(items) {
    return function() {
      var ret = [];
      for (var i = 0; i < items.length; i++) {
        var item = items[i];
        var entry;

        if (item.webkitGetAsEntry && (entry = item.webkitGetAsEntry())) {
          if (entry.isFile) {
            // 把文件对象放到结果数组中  这里的addFile方法要单独写 我没有写上 这个不是重点
            ret.push(addFile(item.getAsFiles()));
          } else if (entry.isDirectory) {
            ret.push(addFilesFormDirectory(entry, entry.name));
          }
        }
      }
    }();
 } 

// 读取文件夹下的文件
function addFilesFormDirectory(directory, path) {
  const dirReader = directory.createReader();

  const readEntries = function readEntries() {
    return dirReader.readEntries(function(entries) {
      entries.forEach(function(entry){
        if (entry.isFile) {
          // 如果是文件
          entry.file(function(file){
            file.fullPath = path + '/' + file.name;
            // 那么暴露出去的就是 '文件夹/q.jpg' 这种形式 
            return addFile(file);
          });
          return addFile(file);
        } else if (entry.isDirectory) {
          // 递归处理
          addFilesFormDirectory(entry, path + '/' + entry.name);
        }
      });
    });
  };
  return readEntries();
}

上述代码中readEntries结果如下:

1678774e6c47bdc1~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.png

3 实现

3.1 组装一个文件夹的树形结构

通过dropzone的API 依次读取每个文件的信息 来组装一个文件夹的树形结构

在上传图片之前我们要先保证文件夹创建完毕才能上传图片不然我不知道把图片上传到何处。所以组件传参数的时候 给dropzone传options下的autoProcessQueue属性为false,表示文件会被添加到上传队列里面但是不会自动上传,需要等到所有的文件夹创建之后才开始上传。树形结构的组装我说一下思路 代码在github上会有。

组装文件夹树形结构思路
  • 首先根据路径来拆分开 比如( 添加文件API里面会有个file参数,file里面有个filePath。例如 '文件夹A/文件夹B/图片.png'。这样我们就可以拆开成['文件夹A', '文件夹B', '图片.png'] ) 画个图说明一下。就是类似下面的结构

16787c3f9eb07dd4~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.png

结构体如下:


[
  // 第一层
  {
      name: '文件夹A',
      level: 0, // 第一层
      isDir: true, // 是否是文件夹 图片就是false 图片也没有children属性
      children: [
        {
          name: '文件夹B',
          level: 1, // 第二层
          isDir: true, // 是否是文件夹
          children: [
            {
                name: '图片',
                level: 2, // 第三层
                isDir: false
            }
          ]
        }
      ]
  }
]
    

递归组装函数实现思路就是 根据上面的 ['文件夹A', '文件夹B', '图片.png'] 值来判断是文件还是文件夹。如果是文件夹就判断是不是第一次添加 因为如果已经有该层有这个文件夹 就不能在添加这个文件夹了 所以要分开处理。当已经有文件夹之后 就循环对比名字是否相同

代码如下:


/**
     * @param { newPath 文件路径拆开的数组 }
     * @param { level 层级 表示是第几层 }
     * @param { folder 传进来的文件夹树形结构 首次为空数组 }
     */
    singleFolder(newPath, level, folder) {
      // 计数 如果循环完毕没有相等的我们才添加在这一层 接着继续递归
      let sum = 0;
      
      // newPath[level] 不能跟folder名字相同
      if (!newPath[level].includes('.')) {
        // 如果不是空文件夹 
        if (folder.length) {
          // 如果里面有就添加到当前下面 没有就添加到这一层
          for (let i = 0; i < folder.length; i++) {
            if (newPath[level] == folder[i].name) {
              if (newPath[level + 1]) {
                folder[i].children = this.singleFolder(newPath, level + 1, folder[i].children);
                return folder;
              }
            } else {
              sum++;
            }
          }
          // 如果能走到这里就表示不相等
          if (sum == folder.length) {
            const node = {
              children: [],
              isDir: true,
              name: newPath[level],
              level
            };
            folder.push(node);
            if (newPath[level + 1]) {
              node.children = this.singleFolder(newPath, level + 1, node.children);
            }
          }
        } else {
          const node = {
            children: [],
            isDir: true,
            name: newPath[level],
            level
          };
          folder.push(node);
          // 看后面还有没有元素
          if (newPath[level + 1]) {
            node.children = this.singleFolder(newPath, level + 1, node.children);
          }
        }
      } else {
          const node = {
            isDir: false,
            name: newPath[level],
            level
          };
          folder.push(node);
      }
      return folder;
    },

3.2 创建文件夹

组装好文件夹树形结构之后开始调用创建文件夹的接口。传刚才创建好的树形结构 以及要传在哪个文件夹的id。这里还有个要处理的 就是上传成功返回的id我们要添加到对应的树形结构下。之前的level我们就可以使用了。


deepUpload(folder, returnParentId) {
      // 如果存在文件夹
      folder.forEach(item => {
        if (item.isDir) {
          // 这里加id 
          this.uploadFolder[item.level] = this.uploadFolder[item.level] || [];
            const id = item.parentId ? item.parentId : returnParentId;
            // 不存在就开始上传
            this.axios.get(`xxx`).then(res => {
                if (res.data) {
                  if (res.data.errorCode === ERR_OK)) {
                  // 成功之后返回的 parentId 添加到 
                    const returnParentId = res.data.result.id;
                    item.parentId = returnParentId;
                    if (item.children && item.children.length) {
                        this.deepUpload(item.children, returnParentId);
                    }
                  } else if (res.data.errorCode === 1) {
                    this.$message.error('已存在该文件夹!');
                    this.removeAllFiles(true);
                    return;
                  }
                }
            });
        }
      });
    }


3.3 上传图片

等文件夹创建成功之后。就要开始上传文件了。这时候使用dropzone的send方法。里面有个回调函数 函数有个formData参数。我们这里通过append方法加上id即可。

    this.dropzone.on('sending', (file, xhr, formData) => {
      // 判断是文件夹下的图片还是外层的图片
      if (file.fullPath) {
        const pathArr = file.fullPath.split('/');
        const LEN = pathArr.length - 2;
        const id = this.findId(this.folder, pathArr, 0, LEN);
        // 这里加上id
        formData.append('parentId', id);
      } else {
        // 直接点击上传或者拖动图片上传
        formData.append('parentId', this.parentId);
      }
    });

文件夹下Id查找函数也贴出来。

    /** 查找id
     * @param list 树形结构
     * @param arr 文件的路径
     * @param 文件的层级
     * @param 路径的length
     * return 返回的是 这个图片是哪个文件夹下对应的id
     */
    findId(list, arr, level, len) {
      let id;
      let i = 0;
      while(i < list.length) {
        if (list[i].name === arr[level]) {
          if (level === len) {
            id = list[i].parentId;
            return id;
          }
          if (arr[level + 1] && !arr[level + 1].includes('.')) {
            id = this.findId(list[i].children, arr, level + 1, len);
          }
        }
        i++;
      }
      return id;
    }

4.注意点

不同业务需求需进行代码微调,比如说这里限制了只能批量上传上传文件夹中的图片。

5. 发布成npm组件

5.1 npm包目录结构

图片.png src/lib文件夹下的vue-upload-folder就是我们的npm包源码了。每个文件具体的代码如下:

vue-upload-folder-hy/lib/index.js

略 太长了,看github的源码吧

vue-upload-folder-hy/lib/index.js

import vueUploadFolderHy from './vue-upload-folder-hy.vue';

vueUploadFolderHy.install = function(Vue) {
  Vue.component(vueUploadFolderHy.name, vueGicSpace);
};

export default vueUploadFolderHy;

vue-upload-folder-hy/index.js

module.exports = require('./lib')

5.2 发布过程 (以下操作都在vue-upload-folder\src\lib\vue-upload-folder-hy目录)

  • npm init 命令来初始化一个npm包配置文件,此时他会问你一系列问题来完成配置文件:
-   version:包的版本,默认是1.0.0, 你可以更换,例如0.1.0或2.0.1等。

-   description:包的描述。主要描述你的包是用来做什么的。

-   entry point:包入口文件,默认是`Index.js`,可以自定义。

-   test command:测试命令,这个直接回车就好了,因为目前还不需要这个。

-   git repository:包的git仓库地址,npm自动读取`.git`目录作为这一项的默认值。没使用则回车略过。

-   keyword:包的关键词。该项会影响到用户怎样才能搜到你的包,可以理解为搜索引擎悠哈的关键词。建议关键词要能准确描述你的包,例如:"vpay vue-pay vue-password password"

-   author:作者。例如你的npm账号或者github账号

-   license:开源协议,回车就好。

  • npm login 没什么好说的,输入自己的账号密码就行了

  • npm publish publish成功之后就可以用npm下载自己的包了,我这里的vue-upload-folder包名被占用了,所以我发布的包名字叫vue-upload-folder-hy

5.3 使用

安装依赖

npm i vue-upload-folder-hy

引入依赖

import Vue from 'vue';
import VueUploadFolderHy from 'vue-upload-folder-hy';

Vue.use(VueUploadFolderHy);

使用依赖

    <vue-upload-folder-hy
      id="dropzone"
      ref="myDropzone"
      :options="dropzoneOptions"
      :action="folderUrl"
      :parentId="parentId"
      :useCustomSlot="true">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将图片或文件夹拖到此处上传,或点击<b>上传</b>图片</div>
      <div class="upload-tips">仅支持5M以内的jpg、png、gif格式的图片</div>
    </vue-upload-folder-hy>
    

<script>
export default {

  data() {
    return {
      // 传给dropzone的配置项
      dropzoneOptions: {
        autoProcessQueue: false, // 不立即上传
        url: 'xx', // 上传图片地址
      },
      parentId: 'xxx', // 如果你拖动文件夹里面还有图片 那要传一个id 
      folderUrl: 'xx' // 创建文件夹的地址
    };
  }
};
</script>

Documentation 更多参数可看dropzone的文档

props

此组件基于 dropzone 库实现的。 相关配置参数可看官网

prop nametypedefaultdescriptionrequired
idstringdropzone组件定义的id名必传
optionsobject{ }dropzone 配置对象 可看dropzone官网必传
destoryDropzonebooleantrue组件销毁时会销毁dropzone实例不是必传
useCustomeSlotbooleanfalse用户自定义的分发内容 (dropzone库有默认 一般我们自己添加)不是必传
folderUrlString''文件夹上传地址(如果有文件夹要上传)选传

events

Event NameDescription
file-added(file)添加文件到dropzone
upload-success(file, response)文件上传成功 获取服务端返回响应
upload-complete(file)上传完成之后被调用 无论上传成功或失败
upload-error(file, message, xhr)上传失败后被调用
upload-progress(file, progress)文件上传进度 上传发生进度发生变化 都会定期调用 获取progress参数 至少调用一次

methods

方法描述
removeAllFiles()移除所有文件 正在上传的文件不会删除 如果要取消在上传的文件 翻看dropzone
removeFile(file)移除dropzone区域的文件
getUploadingFiles()获取所有上传的文件
getQueuedFiles()获取所有队列的文件
getRejectedFiles()失败的文件
getAcceptedFiles()成功的文件

methods: {
	// 调用方法
	this.$refs.myDropzone.xxxMethod();
}


6.演示页面、源码

演示页面点击查看

github源码点击查看,求⭐⭐⭐