文件树组件设计与实现

文件树(File Explorer)是代码编辑器的核心组件之一,本文将介绍文件树的数据结构设计、虚拟文件系统与交互实现。

树节点数据结构

首先定义文件树节点的数据结构:

file-tree-model.js
// 文件树节点类型
const FileType = {
    FILE: 'file',
    DIRECTORY: 'directory'
};

// 树节点数据结构
class TreeNode {
    constructor(name, type, path) {
        this.name = name;
        this.type = type;           // FileType.FILE 或 DIRECTORY
        this.path = path;           // 完整路径
        this.children = [];        // 子节点
        this.parent = null;
        this.expanded = false;   // 展开状态
        this.depth = 0;           // 深度(用于缩进)
        this.icon = this.getIcon();
    }

    getIcon() {
        if (this.type === FileType.DIRECTORY) {
            return this.expanded ? '📂' : '📁';
        }
        // 根据扩展名返回文件图标
        const ext = this.name.split('.').pop();
        const icons = {
            js: '📜', ts: '📘', py: '🐍',
            html: '🌐', css: '🎨', json: '📋'
        };
        return icons[ext] || '📄';
    }

    isFolder() {
        return this.type === FileType.DIRECTORY;
    }
}

虚拟文件系统

在浏览器中实现虚拟文件系统来管理项目文件:

virtual-fs.js
// 虚拟文件系统
class VirtualFileSystem {
    constructor() {
        this.root = new TreeNode('root', FileType.DIRECTORY, '/');
        this.files = new Map();  // path -> TreeNode
        this.files.set('/', this.root);
    }

    // 创建文件或目录
    create(path, type, content = '') {
        const parts = path.split('/').filter(p => p);
        let current = this.root;
        let currentPath = '/';

        for (let i = 0; i < parts.length; i++) {
            const part = parts[i];
            const isLast = i === parts.length - 1;
            currentPath += part + '/';

            let child = current.children.find(c => c.name === part);

            if (!child) {
                child = new TreeNode(
                    part,
                    isLast ? type : FileType.DIRECTORY,
                    currentPath
                );
                child.parent = current;
                child.depth = i;
                current.children.push(child);
                this.files.set(currentPath, child);
            }

            current = child;
        }

        return current;
    }

    // 读取文件内容
    readFile(path) {
        const node = this.files.get(path);
        if (node && node.type === FileType.FILE) {
            return node.content || '';
        }
        return null;
    }

    // 写入文件内容
    writeFile(path, content) {
        const node = this.files.get(path);
        if (node && node.type === FileType.FILE) {
            node.content = content;
        }
    }

    // 获取目录下的所有直接子节点
    getChildren(path) {
        const node = this.files.get(path);
        if (node && node.type === FileType.DIRECTORY) {
            return node.children;
        }
        return [];
    }
}

文件树渲染

使用递归方式渲染文件树:

file-tree-render.js
// 渲染文件树
class FileTreeRenderer {
    constructor(container, vfs) {
        this.container = container;
        this.vfs = vfs;
    }

    render() {
        this.container.innerHTML = '';
        this.renderNode(this.vfs.root, this.container);
    }

    renderNode(node, parent) {
        const element = this.createNodeElement(node);
        parent.appendChild(element);

        // 如果是展开的目录,递归渲染子节点
        if (node.expanded && node.children.length > 0) {
            const childContainer = document.createElement('div');
            childContainer.className = 'tree-children';
            childContainer.style.paddingLeft = '16px';
            parent.appendChild(childContainer);

            for (const child of node.children) {
                this.renderNode(child, childContainer);
            }
        }
    }

    createNodeElement(node) {
        const el = document.createElement('div');
        el.className = 'tree-node';
        el.style.display = 'flex';
        el.style.alignItems = 'center';
        el.style.padding = '6px 8px';
        el.style.cursor = 'pointer';
        el.style.userSelect = 'none';

        // 展开/折叠图标
        const chevron = document.createElement('span');
        chevron.className = 'tree-chevron';
        chevron.textContent = node.isFolder() ? (node.expanded ? '▼' : '▶') : ' ';
        chevron.style.width = '16px';
        chevron.style.fontSize = '10px';
        chevron.style.marginRight = '6px';
        el.appendChild(chevron);

        // 文件/文件夹图标
        const icon = document.createElement('span');
        icon.textContent = node.icon;
        icon.style.marginRight = '8px';
        el.appendChild(icon);

        // 文件名
        const name = document.createElement('span');
        name.textContent = node.name;
        name.className = 'tree-name';
        el.appendChild(name);

        // 绑定点击事件
        el.addEventListener('click', () => this.onNodeClick(node));
        return el;
    }

    onNodeClick(node) {
        if (node.isFolder()) {
            node.expanded = !node.expanded;
            this.render();
        } else {
            // 打开文件
            openFile(node.path);
        }
    }
}

文件操作

实现右键菜单进行文件操作:

file-operations.js
// 文件树上下文菜单
class ContextMenu {
    constructor(tree) {
        this.tree = tree;
    }

    show(x, y, node) {
        const menu = document.createElement('div');
        menu.className = 'context-menu';
        menu.style.position = 'fixed';
        menu.style.left = x + 'px';
        menu.style.top = y + 'px';
        menu.style.zIndex = '1000';

        const actions = [
            { label: '新建文件', action: ()=>this.newFile(node) },
            { label: '新建文件夹', action: ()=>this.newFolder(node) },
            { label: '重命名', action: ()=>this.rename(node) },
            { label: '删除', action: ()=>this.delete(node) }
        ];

        for (const {label, action} of actions) {
            const item = document.createElement('div');
            item.className = 'context-item';
            item.textContent = label;
            item.addEventListener('click', ()=>{ action(); menu.remove(); });
            menu.appendChild(item);
        }

        document.body.appendChild(menu);
    }

    newFile(node) {
        const name = prompt('请输入文件名:');
        if (name) {
            const path = node.isFolder() ? node.path + name : node.path + '/' + name;
            this.tree.vfs.create(path, FileType.FILE);
            this.tree.render();
        }
    }
}

总结

文件树组件的实现关键在于:树形数据结构的清晰设计、虚拟文件系统对文件操作的支持,以及高效的递归渲染机制。结合右键菜单,可以提供完整的项目管理体验。