Bootstrap

js使用input上传文件夹、拖拽上传文件夹并将文件夹结构展示为树形结构

一、实现效果

左侧区域支持选择一个系统中的文件夹,或者将文件夹拖拽到这个区域进行上传,右侧区域可以将文件夹的结构展示为树形结构。

二、代码实现

由于需要使用树形插件zTree,这个插件是依赖于jquery的,所以在项目中我们需要引入:

1、jquery

2、zTree:官网链接API Document [zTree -- jQuery tree plug-ins.]

项目结构很简单,一个zTree源码的文件夹,一个index.html文件

下载完zTree源码之后,解压放到index.html平级的位置

3、在index.html的head标签中引入相关依赖

<script type="text/javascript" src="zTree_v3-master/js/jquery-1.4.4.min.js"></script>
<script type="text/javascript" src="zTree_v3-master/js/jquery.ztree.core.js"></script>
<script type="text/javascript" src="zTree_v3-master/js/jquery.ztree.excheck.js"></script>
<script type="text/javascript" src="zTree_v3-master/js/jquery.ztree.exedit.js"></script>
<link rel="stylesheet" href="zTree_v3-master/css/zTreeStyle/zTreeStyle.css" type="text/css">

4、搭建html结构,左边放置拖拽框和input输入框,右边放置树结构

<style>
        li {
            list-style: none;
        }

        a {
            cursor: pointer;
        }

        .icon-download {
            font-size: 30px;
            cursor: pointer;
        }

        #drop {
            width: 500px;
            height: 500px;
            border: 1px solid black;
            float: left;
        }

        #folder_container {
            float: left;
        }

        
    </style>
    <!--文件夹下所有文件的信息 -->
    <div id="drop">
        <input type="file" id="file_input" name="folder" webkitdirectory />
        <div style="text-indent: 10px"> 将文件夹拖到这里进行上传</div>
    </div>
    <!-- 树 -->
    <ul id="folder_container">
        <ul id="fileTree" class="ztree"></ul>
    </ul>

5、input框上传文件夹实现

input框可以增加一个属性webkitdirectory实现上传文件夹,不过这个功能过低版本的浏览器是不兼容的。使用onchange事件监听这个input框的输入事件,就可以在上传文件夹完毕通过event.target.files获取文件夹中的所有文件。

 获取的是一个fileList,是一个类数组对象,每一个元素是一个file信息,其中包含文件更新时间、文件名、大小、类型、相对路径。

我们需要解析文件的相对路径,从而获取真正的文件夹结构。

首先需要使用Array.from()方法将类数组对象转换为数组,这样就可以使用数组的迭代方法。

具体的解析过程放在createTree方法中

<script>
    let files = []
    const fileInput = $('#file_input');
    fileInput.bind('change', function (e) {
       files = Array.from(e.target.files)
       createTree();
    })
    function createTree() {

    }
</script>

我们对文件的相对路径进行解析,最终是要放在zTree树上。zTree的节点之间是通过parentId这个属性来确定层级关系的。如果一个节点的parentId为null,证明这个节点就是根节点;一个节点的parentId等于其父节点的Id。所以在解析相对路径的时候,首先需要获取层数,知道这个文件的结构总共有几层。通过split(‘/’)就可以获取从外到内具体的名字。

(1)在设置根节点之前,要先初始化一棵树

let zNodes = [];
function initNodes() {
    zNodes = [];
    $.fn.zTree.init($("#fileTree"), setting, zNodes);
}
const setting = {}

function createTree() {
    initNodes();
}

(2)确认根节点,也就是根目录的名字。每一个文件的头头都带着根目录的名字。粘贴一下完整的js代码

let files = [];
let zNodes = [];
const treeId = 'fileTree'
const fileInput = $('#file_input');
fileInput.bind('change', function (e) {
    files = Array.from(e.target.files)
    createTree();
})
function initNodes() {
    zNodes = [];
    $.fn.zTree.init($("#fileTree"), setting, zNodes);
}
const setting = {}

function createTree() {
    initNodes();
    const zTree = $.fn.zTree.getZTreeObj(treeId);
    let nodes = [];
    files.forEach(file => {
        nodes = zTree.transformToArray(zTree.getNodes());
        const names = file.webkitRelativePath.split('/')
        if (nodes.length == 0) {
            zTree.addNodes(null, 0, {
                id: names[0],
                parentId: null,
                name: names[0],
                filePath: names[0],
            })
        }
    })
}

当前实现效果,有一个根节点了:

(3)处理其他节点。

对names进行循环,相当于从外到内逐层添加节点,先找到父节点,就可以使用addNodes方法添加当前节点。如果是文件(即在最后一层),需要加上filePath记录相对路径的信息。

files.forEach(file => {
    nodes = zTree.transformToArray(zTree.getNodes());
    const names = file.webkitRelativePath.split('/')
    if (nodes.length == 0) {
        zTree.addNodes(null, 0, {
            id: names[0],
            parentId: null,
            name: names[0],
            filePath: names[0],
        })
    }
    names.forEach((name, index) => {
        // index==0时就是name就是根节点
        if (index >= 1) {
            nodes = zTree.transformToArray(zTree.getNodes());
            // 找父节点
            const parentId = names[index - 1]
            const pNode = nodes.find(node => node.id == parentId)
            let newNode = {
                id: name,
                parentId: parentId,
                name: name
            }
            if (name == names[names.length - 1]) {
                newNode.filePath = file.webkitRelativePath
            }
            zTree.addNodes(pNode, 0, newNode)
        }
    })
})

实现效果:

6、使用input框上传文件夹并且展示成树形结构的功能已经实现了,下边来做拖拽上传文件夹。 

 (1)先了解几个拖拽相关API:

拖拽容器相关事件:

 (2)在dragenter的时候,可以把容器中的内容显示为“请释放鼠标”,这样会有比较好的体验效果;dragleave的时候,内容显示为“请将文件夹拖拽到此”;drop的时候,要阻止默认事件,否则浏览器会尝试打开文件夹,并且需要恢复原有内容。

const drop = $('#drop');
const originHTML = drop.html();
drop.bind('dragenter', function (e) {
    drop.html('请释放鼠标')
})
drop.bind('dragleave', function (e) {
    drop.html('请将文件夹拖拽到此')
})

$(document).bind('dragover', function (e) {
    e.preventDefault();
    return false
})
$(document).bind('drop', function (e) {
    e.preventDefault();
    drop.html(originHTML)
    return false
}) 

(3)如果是在drop容器中发生drop事件,获取拖拽携带的信息。通过event.originalEvent.dataTransfer.items可以获取到所有的传输对象的信息。在此需要将files清空。

drop.bind('drop', function (e) {
    files = [];
    const items = e.originalEvent.dataTransfer.items;
    for (let i = 0; i < items.length; i++) {
        const item = items[i]
        console.log(item);
    }
})

如果是文件夹的话,item的信息长这样:

 (4)对于文件夹使用item.webkitGetAsEntry()

可以查看一下关于这一方法的解释

 DataTransferItem.webkitGetAsEntry() - Web API 接口参考 | MDN

drop.bind('drop', function (e) {
    files = [];
    const items = e.originalEvent.dataTransfer.items;
    for (let i = 0; i < items.length; i++) {
        const item = items[i]
        if (item.kind == 'file') {
            let entry = item.webkitGetAsEntry();
            console.log(entry);
        }
    }
})

打印出来长这样:

 可以通过isFile判断是不是文件,通过isDirectory判断是不是文件夹

(5)这里如果不是文件夹,需要提示错误(可以最后再加)

drop.bind('drop', function (e) {
    files = [];
    const items = e.originalEvent.dataTransfer.items;
    for (let i = 0; i < items.length; i++) {
        const item = items[i]
        if (item.kind == 'file') {
            let entry = item.webkitGetAsEntry();
            if (!entry.isDirectory) {
                alert('请上传文件夹')
                return
            }
        }
    }
})

(6)接下来就需要解析这个文件夹了。解析文件夹需要放到一个递归方法中。

我们可以通过__proto__看一下这个entry的原型是什么:

文件entry的原型:

文件夹entry的原型:

 

(7)如果是文件的话,可以通过file()方法, 来创建一个拥有当前文件信息的文件

可以查看一下官方解释:FileSystemFileEntry - Web APIs | MDN

function getFilesFromEntry(entry) {
    if (entry.isFile) {
        entry.file(
            file => {
                console.log(file);
            },
            err => {
                console.log(err);
            }
        )
    } else {
        console.log(entry.__proto__);
    }
}

 可以看到当前file是没有相对路径的。这个属性是不能手动加上的,所以加一个filePath属性指向相对路径,并且push到files数组中。

function getFilesFromEntry(entry) {
    if (entry.isFile) {
        entry.file(
            file => {
                file.filePath = entry.fullPath.slice(1)
                files.push(file)
            },
            err => {
                console.log(err);
            }
        )
    } else {
        console.log(entry.__proto__);
    }
}

(8)如果是文件夹可以使用createReader()方法来解析这个文件夹

FileSystemDirectoryEntry.createReader() - Web APIs | MDN

 这个方法会返回一个对象,这个对象可以用来读文件夹中所有的entries

FileSystemDirectoryReader - Web APIs | MDN

readEntries这个方法就可以返回文件夹里的所有entries

function getFilesFromEntry(entry) {
    if (entry.isFile) {
        entry.file(
            file => {
                file.filePath = entry.fullPath.slice(1)
                files.push(file)
            },
            err => {
                console.log(err);
            }
        )
    } else {
        const entryReader = entry.createReader()
        entryReader.readEntries(
            (results) => {
                results.forEach(result => {
                    getFilesFromEntry(result);
                })
            },
            (error) => {
                console.log(error);
            }
        );
    }
}

(9)打印一下files:

 当解析完所有文件的时候需要调用createTree方法。怎么判断解析完了所有文件呢?

可能需要先遍历一边所有的entry先算一下count

function getCount(entry) {
    if (entry.isFile) {
        entry.file(
            file => {
                count++
            },
            err => {
                console.log(err);
            }
        )
    } else {
        const entryReader = entry.createReader()
        entryReader.readEntries(
            (results) => {
                results.forEach(result => {
                    getCount(result);
                })
            },
            (error) => {
                console.log(error);
            }
        );
    }
}

在第二次遍历的时候比较count和files.length如果相等,则调用createTree方法创建文件结构树。

至此拖拽功能实现

完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="zTree_v3-master/js/jquery-1.4.4.min.js"></script>
    <script type="text/javascript" src="zTree_v3-master/js/jquery.ztree.core.js"></script>
    <script type="text/javascript" src="zTree_v3-master/js/jquery.ztree.excheck.js"></script>
    <script type="text/javascript" src="zTree_v3-master/js/jquery.ztree.exedit.js"></script>
    <link rel="stylesheet" href="zTree_v3-master/css/zTreeStyle/zTreeStyle.css" type="text/css">
    <title>Document</title>
    <style>
        li {
            list-style: none;
        }

        a {
            cursor: pointer;
        }

        .icon-download {
            font-size: 30px;
            cursor: pointer;
        }

        #drop {
            width: 500px;
            height: 500px;
            border: 1px solid black;
            float: left;
        }

        #folder_container {
            float: left;
        }
    </style>
</head>

<body>
    <!--文件夹下所有文件的信息 -->
    <div id="drop">

        <input type="file" id="file_input" name="folder" webkitdirectory />

        <div style="text-indent: 10px"> 将文件夹拖到这里进行上传</div>
    </div>
    <!-- 树 -->
    <ul id="folder_container">
        <ul id="fileTree" class="ztree"></ul>
    </ul>
    <script>
        let files = [];
        let zNodes = [];
        const treeId = 'fileTree'
        const fileInput = $('#file_input');
        fileInput.bind('change', function (e) {
            files = Array.from(e.target.files)
            createTree();
        })
        function initNodes() {
            zNodes = [];
            $.fn.zTree.init($("#fileTree"), setting, zNodes);
        }
        const setting = {}

        function createTree() {
            console.log(files);
            initNodes();
            const zTree = $.fn.zTree.getZTreeObj(treeId);
            let nodes = [];
            files.forEach(file => {
                const filePath = file.webkitRelativePath == '' ? file.filePath : file.webkitRelativePath
                nodes = zTree.transformToArray(zTree.getNodes());
                const names = filePath.split('/')
                if (nodes.length == 0) {
                    zTree.addNodes(null, 0, {
                        id: names[0],
                        parentId: null,
                        name: names[0],
                        filePath: names[0],
                    })
                }
                names.forEach((name, index) => {
                    // index==0时就是name就是根节点
                    if (index >= 1) {
                        nodes = zTree.transformToArray(zTree.getNodes());
                        // 找父节点
                        const parentId = names[index - 1]
                        const pNode = nodes.find(node => node.id == parentId)
                        let newNode = {
                            id: name,
                            parentId: parentId,
                            name: name
                        }
                        if (name == names[names.length - 1]) {
                            newNode.filePath = filePath
                        }
                        zTree.addNodes(pNode, 0, newNode)
                    }
                })
            })
        }


        // 拖拽
        const drop = $('#drop');
        const originHTML = drop.html();
        drop.bind('dragenter', function (e) {
            drop.html('请释放鼠标')
        })
        drop.bind('dragleave', function (e) {
            drop.html('请将文件夹拖拽到此')
        })

        $(document).bind('dragover', function (e) {
            e.preventDefault();
            return false
        })
        $(document).bind('drop', function (e) {
            e.preventDefault();
            drop.html(originHTML)
            return false
        })
        let count = 0
        drop.bind('drop', function (e) {
            files = [];
            const items = e.originalEvent.dataTransfer.items;
            for (let i = 0; i < items.length; i++) {
                const item = items[i]
                if (item.kind == 'file') {
                    let entry = item.webkitGetAsEntry();
                    if (!entry.isDirectory) {
                        alert('请上传文件夹')
                        return
                    }
                    //递归解析文件夹
                    getCount(entry)
                    setTimeout(() => {
                        getFilesFromEntry(entry)
                    }, 300)
                }
            }
        })

        function getCount(entry) {
            if (entry.isFile) {
                entry.file(
                    file => {
                        count++
                    },
                    err => {
                        console.log(err);
                    }
                )
            } else {
                const entryReader = entry.createReader()
                entryReader.readEntries(
                    (results) => {
                        results.forEach(result => {
                            getCount(result);
                        })
                    },
                    (error) => {
                        console.log(error);
                    }
                );
            }
        }
        function getFilesFromEntry(entry) {
            if (entry.isFile) {
                entry.file(
                    file => {
                        file.filePath = entry.fullPath.slice(1)
                        files.push(file)
                        if (files.length == count) createTree();
                    },
                    err => {
                        console.log(err);
                    }
                )
            } else {
                const entryReader = entry.createReader()
                entryReader.readEntries(
                    (results) => {
                        results.forEach(result => {
                            getFilesFromEntry(result);
                        })
                    },
                    (error) => {
                        console.log(error);
                    }
                );
            }
        }
    </script>
</body>
</html>

;