前言
今天带大家一起学习一下webkitRelativePath这个api
兼容性
这个api对于浏览器要求还是比较高的,大家可以去can i use网站看看兼容性。
我这里贴一张图
常规使用
我写一个最普通的上传,我们只需要在input
上添加webkitdirectory
属性和multiple
属性即可
代码
这里我用vue3的代码写了个简单的逻辑
1 2 3 4 5 6 7 8 9 10 11
| <template> <input type="file" webkitdirectory multiple @change="handleUploadChange" /> </template> <script setup> import { ref } from "vue"; const handleUploadChange=(event)=>{ let files = event.target.files; for (let i = 0; i < files.length; i++) { console.log(files[i]) } }
|
文件目录
我新建了一个文件,目录结构差不多是这样的,后续我也将一直使用这个目录
测试效果
此时,效果如下
他就可以得到我们要的file格式
优化显示
代码
我此时就有了个想法,优化一下我的列表显示,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| <template> <t-space> <t-button @click="handleUploadFile">上传文件</t-button> <t-button @click="handleUploadFolder">上传文件夹</t-button> </t-space> <input type="file" style="width:0" webkitdirectory multiple @change="handleUploadChange" ref="folderRef" v-show="false" /> <input type="file" style="width:0" @change="handleUploadChange" ref="fileRef" v-show="false" /> <t-table :columns="tableColumns" :data="fileList"> <!-- size栏 --> <template #size="{row}"> {{getFileSize(row.size)}} </template> <!-- 操作栏 --> <template #action="{rowIndex}"> <t-button @click="handleDeleteFile(rowIndex)" size="small" theme="danger">删除</t-button> </template> </t-table> </template> <script setup> import {ref} from "vue";
const folderRef = ref(null); // 文件值改变 const handleUploadChange=(event)=>{ let files = event.target.files; for (let i = 0; i < files.length; i++) { Object.assign(files[i],{time:new Date().toLocaleString()}) fileList.value.push(files[i]) } } // 上传文件夹 const handleUploadFolder=()=>{ folderRef.value.click(); }
const fileRef = ref(null);
// 上传文件 const handleUploadFile=()=>{ fileRef.value.click(); }
// 展示的文件列表 const fileList=ref([]);
// 删除某个文件 const handleDeleteFile=(rowIndex)=>{ fileList.value.splice(rowIndex,1); }
// 列表信息 const tableColumns=[ { title: "文件名", colKey: "name", width: 200, }, { title: "大小", colKey:'size', width: 100, }, { title: "上传时间", colKey:'time', width: 200, }, { title: "操作", colKey:'action', width: 100, }, ]
// 得到文件大小 const getFileSize = (size) => { let initNum=0; const unit=[ 'Bytes', 'KB', 'MB', 'GB', 'TB' ] const returnValFunc=()=>{ return size.toFixed(2)+' '+unit[initNum]; } if(size<=1024){ return returnValFunc(); } while(size>1024){ size/=1024; initNum++; } return returnValFunc(); } </script>
|
这里有点细节,首先是我将input设置的宽度为0了,这样他就不会显示了,然后点击按钮的时候调用file类型input的click事件,就可以调用我们的选取文件弹窗,文件大小我就用了简单的逻辑,如果是不到1024的那就用Bytes
,如果大于1024的那就先根据1024的进制来计算大小,然后那个时间我尝试过{...}
的方式进行对象合并,发现会有错误,于是就使用了Object.assign
来进行对象合并
效果展示
此时展示就是这样
实现拖拽上传
禁用默认行为
修改一下table的样式,然后加个ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <t-space> <t-button @click="handleUploadFile">上传文件</t-button> <t-button @click="handleUploadFolder">上传文件夹</t-button> </t-space> <input type="file" style="width:0" webkitdirectory multiple @change="handleUploadChange" ref="folderRef" v-show="false" /> <input type="file" style="width:0" @change="handleUploadChange" ref="fileRef" v-show="false" /> <t-table :columns="tableColumns" :data="fileList" @drop="handleDrop" @dragenter="handleDragenter" ref="tableRef" > <!-- 自定义没有内容的情况 --> <template #empty> <p v-if="dragActive">释放鼠标</p> <p v-else>将 <span class="font-bold">文件</span> 或 <span class="font-bold">文件夹</span> 拖拽 到此处上传</p> </template> <!-- size栏 --> <template #size="{row}"> {{getFileSize(row.size)}} </template> <!-- 操作栏 --> <template #action="{rowIndex}"> <t-button @click="handleDeleteFile(rowIndex)" size="small" theme="danger">删除</t-button> </template> </t-table> </template>
|
js中添加默认行为禁用,避免拖拽的时候文件进行下载
1 2 3 4 5 6 7 8 9 10
| const tableRef=ref(null);
const handlePreventDefault=()=>{ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event=>{ tableRef.value.$el.addEventListener(event,preventDefault,false); }) } onMounted(()=>{ handlePreventDefault(); })
|
处理普通文件
这里我将for循环添加文件的代码单独封装成了一个方法handleFileAdd
,这样在选中内容和拖拽内容的时候都可以进行同一段代码
1 2 3 4 5 6 7 8 9 10 11 12
| const dragActive=ref(false);
const handleDrop=(e)=>{ dragActive.value=false; let files = e.dataTransfer.files; handleFileAdd(files); }
const handleDragenter=()=>{ dragActive.value=true; }
|
此时普通文件拖拽的效果
处理文件夹拖拽
直接使用的bug
此时如果我们直接拖拽文件夹,可以看到效果,emm,完全没用
使用webkitGetAsEntry判断拖拽类型
这里,我使用了这个api来判断拖拽的类型
地址:https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransferItem/webkitGetAsEntry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const handleFileFolderAdd=(items)=>{ for(let j=0;j<items.length;j++){ const item=items[j]; const fileEntry=item.webkitGetAsEntry() if(fileEntry.isDirectory){ } else if(fileEntry.isFile){ } } }
const handleDrop=(e)=>{ dragActive.value=false; let items = e.dataTransfer.items; handleFileFolderAdd(items); }
|
处理文件类型的
我们可以通过文档的file方法,知道这个fileEntry可以进行文件格式转化
地址:https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileEntry
1 2 3 4 5 6
| else if(fileEntry.isFile){ fileEntry.file((file)=>{ handleFileAdd([file]); }) }
|
因此我们处理文件就可以这样简单的完成,效果如下
处理文件夹类型的
我们可以通过createReader
和readEntries
来实现文件夹的读取:
地址:https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry/createReader
地址:https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const readEntriesAsync = (reader) => new Promise((resolve) => { reader.readEntries((results) => { const cacheRes = results.map((item) => ({ entry: item, })); resolve(cacheRes); }); });
const handleFileFolderAdd=(items)=>{ for(let j=0;j<items.length;j++){ const item=items[j]; const fileEntry=item.webkitGetAsEntry(); if(fileEntry.isDirectory){ const reader = fileEntry.createReader(); const readEntries = async (reader) => { const results = await readEntriesAsync(reader); for(let i=0;i<results.length;i++){ const curItem=results[i]; const curFileEntry=curItem.entry; if(curFileEntry.isFile){ curFileEntry.file((file)=>{ handleFileAdd([file]); }) } else{ readEntries(curFileEntry.createReader()); } } }; readEntries(reader); } else if(fileEntry.isFile){ fileEntry.file((file)=>{ handleFileAdd([file]); }) } } }
|
效果
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
| <template> <t-space> <t-button @click="handleUploadFile">上传文件</t-button> <t-button @click="handleUploadFolder">上传文件夹</t-button> </t-space> <input type="file" style="width:0" webkitdirectory multiple @change="handleUploadChange" ref="folderRef" v-show="false" /> <input type="file" style="width:0" @change="handleUploadChange" ref="fileRef" v-show="false" /> <t-table :columns="tableColumns" :data="fileList" @drop="handleDrop" @dragenter="handleDragenter" ref="tableRef" > <!-- 自定义没有内容的情况 --> <template #empty> <p v-if="dragActive">释放鼠标</p> <p v-else>将 <span class="font-bold">文件</span> 或 <span class="font-bold">文件夹</span> 拖拽 到此处上传</p> </template> <!-- size栏 --> <template #size="{row}"> {{getFileSize(row.size)}} </template> <!-- 操作栏 --> <template #action="{rowIndex}"> <t-button @click="handleDeleteFile(rowIndex)" size="small" theme="danger">删除</t-button> </template> </t-table> </template> <script setup> import {onMounted, ref} from "vue";
const folderRef = ref(null); // 获取文件夹下文件的entries const readEntriesAsync = (reader) => new Promise((resolve) => { reader.readEntries((results) => { const cacheRes = results.map((item) => ({ entry: item, })); resolve(cacheRes); }); });
// 处理拖拽文件(夹)添加 const handleFileFolderAdd=(items)=>{ for(let j=0;j<items.length;j++){ const item=items[j]; const fileEntry=item.webkitGetAsEntry(); // 如果是文件夹类型 if(fileEntry.isDirectory){ // 文件夹类型,递归处理 const reader = fileEntry.createReader(); const readEntries = async (reader) => { const results = await readEntriesAsync(reader); for(let i=0;i<results.length;i++){ const curItem=results[i]; const curFileEntry=curItem.entry; if(curFileEntry.isFile){ curFileEntry.file((file)=>{ handleFileAdd([file]); }) } else{ readEntries(curFileEntry.createReader()); } } }; readEntries(reader); } // 如果是文件类型 else if(fileEntry.isFile){ fileEntry.file((file)=>{ handleFileAdd([file]); }) } } } // 处理文件添加 const handleFileAdd=(files)=>{ for(let i=0;i<files.length;i++){ const file=files[i]; Object.assign(file,{time:new Date().toLocaleString()}) fileList.value.push(file) } console.log(fileList.value,'fileList') } // 文件值改变 const handleUploadChange=(event)=>{ let files = event.target.files; handleFileAdd(files); } // 上传文件夹 const handleUploadFolder=()=>{ folderRef.value.click(); }
const fileRef = ref(null);
// 上传文件 const handleUploadFile=()=>{ fileRef.value.click(); }
// 展示的文件列表 const fileList=ref([]);
// 删除某个文件 const handleDeleteFile=(rowIndex)=>{ fileList.value.splice(rowIndex,1); }
// 列表信息 const tableColumns=[ { title: "文件名", colKey: "name", width: 200, }, { title: "大小", colKey:'size', width: 100, }, { title: "上传时间", colKey:'time', width: 200, }, { title: "操作", colKey:'action', width: 100, }, ]
// 得到文件大小 const getFileSize = (size) => { let initNum=0; const unit=[ 'Bytes', 'KB', 'MB', 'GB', 'TB' ] const returnValFunc=()=>{ return size.toFixed(2)+' '+unit[initNum]; } if(size<=1000){ return returnValFunc(); } size/=1000; while(size>1024){ size/=1024; initNum++; } return returnValFunc(); }
const tableRef=ref(null); // 阻止默认行为 const handlePreventDefault=()=>{ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event=>{ tableRef.value.$el.addEventListener(event,preventDefault,false); }) } onMounted(()=>{ handlePreventDefault(); })
const preventDefault=(e)=>{ e.preventDefault(); e.stopPropagation(); }
// 是否在拖拽 const dragActive=ref(false); // 鼠标放下 const handleDrop=(e)=>{ dragActive.value=false; let items = e.dataTransfer.items; handleFileFolderAdd(items); } // 鼠标进入 const handleDragenter=()=>{ dragActive.value=true; } </script> <style scoped> .font-bold{ font-weight: bold; } </style>
|
结语
这样一个简单的上传文件夹展开内容的功能就实现了,大家也可以根据这个逻辑继续完善,比如实现树形展示,文件夹一层嵌套一层展示,这样功能就高级多了。更多内容,敬请期待~