用Vue编写一个简单的仿Explorer文件管理器

本文详细介绍了如何使用Vue构建一个Web版本的Windows资源管理器,包括导航原理、数据结构设计、文件夹跳转、导航堆栈与历史管理,以及问题修复和优化过程。

大家一定很熟悉你桌面左上角那个小电脑吧,学名Windows资源管理器,几乎所有的工作都从这里开始,文件云端化是一种趋势。怎样用浏览器实现一个Web版本的Windows资源管理器呢?今天来用Vue好好盘一盘它。

一、导航原理

首先操作和仔细观察导航栏,我们有几个操作途径:

  • 点击“向上”按钮回到上一个目录,点击地址栏的文件夹名称返回任意一个目录
  • 双击文件夹进入新目录
  • 点击“前进”,“后退”按钮操作导航

其中前进,后退操作,可以点击小三角查看一个列表,点击进入文件夹,列表会记录导航历史,哪怕反复进入同一个文件夹,列表仍然会记录下来,如下图:

那么我们就能分析并抽象出两个变量:

  1. 一个用于存储实际导航的变量(navigationStack)
  2. 另一个用于存储导航历史的变量(navigationHistoryStack)

导航堆栈用于存储每一个浏览文件夹的信息,拼接起这些文件夹就形成了当前路径, 一组简单的<li>元素通过绑定导航堆栈,就能形成地址栏(web世界里也叫面包屑导航)了。

navigationStack实际上是一个堆栈,用的是先进后出(FILO)原则

导航历史则是单纯记录了用户的操作轨迹,不会收到导航目标的影响,如刚才所述,哪怕反复进入同一个文件夹,列表仍然会记录下来

navigationHistoryStack实际上是一个队列,用的是先进先出(FIFO)原则

接下来我们开始码代码

我们先新建一个Vue项目(Typescript),打开App.vue文件

script标签里编写代码如下:

<script lang='ts'>
export default {
  name: "App",
  data: () => {
    return {
      navigationStack: new Array<FileDto>(),
      navigationHistoryStack: new Array<FileDto>(),
    };
  }
}
</script>

二、文件夹跳转原理

我们先来看如下数据结构

export class FileDto {
  id: number;        //唯一id
  parentId: number;    //父id
  fileName: string;    //文件名称
  fileType: number;    //文件类型:1-文件夹,2-常规文件
  byteSize: number;    //文件大小
}

FileDto是定义的文件描述类,这是描述一整个树形结构的基本单元,通过唯一id和指定它的上级parentId,通过递归就可以描述你的某一文件,某一文件夹具体在哪一层级的哪一个分支中。现在假设我们有一堆的文件树长这样:

定义查询函数checkMessage和当前目录层级的文件集合listMessage:

      listMessage: new Array<FileDto>(),
      checkMessage: {},

再定义一个目录访问器gotoList函数,通过传入查询条件,更新当前目录层级的文件列表:

gotoList() {
      this.listMessage = Enumerable.from(FileList)
        .where((c) => c.parentId == (this.checkMessage as any).parentId)
        .toArray();
    },

 编写UI部分,简单定义一个table,并绑定文件集合listMessage来显示所有文件:

      <table border="1">
        <tr>
          <th>id</th>
          <th>文件名</th>
          <th>类型</th>
          <th>大小</th>
        </tr>
        <tr v-for="item in listMessage" :key="item.id">
          <td>{{ item.id }}</td>
          <td>
            <a href="javascript:void(0)" @click="open(item)">{{
              item.fileName
            }}</a>
          </td>
          <td>{{ item.fileType == 1 ? "目录" : "文件" }}</td>
          <td>{{ item.fileType == 1 ? "/" : `${item.byteSize}M` }}</td>
        </tr>
      </table>

当调用gotoList函数的时候,相当与“刷新”功能,获取了当前查询条件下的所有文件

三、编写导航逻辑

导航堆栈处理函数

刚刚我们分析了导航原理,导航堆栈的作用是形成地址,我们定义一个导航堆栈处理逻辑:

  1. 判断当前页面是否在导航堆栈中
  2. 若是,则弹出至目标在导航堆栈中所在的位置
  3. 若否,则压入导航堆栈

 其中toFolder函数用于实际导航并刷新页面的,稍后介绍

navigationTo(folder: FileBriefWithThumbnailDto) {
    var toIndex = Enumerable.from(this.NavigationStack).indexOf(folder);
    if (toIndex >= 0) {
      this.NavigationStack.splice(
        toIndex + 1,
        this.NavigationStack.length - toIndex - 1
      );
    } else {
      this.NavigationStack.push(folder);
    }
    if (this.toFolder(folder)) {
      this.navigationHistoryStack.unshift(folder);
    }
  }

“向上”导航函数:

向上的作用属于一个特定的导航堆栈处理:

  1. 直接弹出最上的条目,
  2. 拿到最上层条目并导航
  navigationBack() {
    this.NavigationStack.pop();
    var lastItem = Enumerable.from(this.NavigationStack).lastOrDefault();
    if (this.getIsNull(lastItem)) {
      return;
    }
    if (this.toFolder(lastItem)) {
      this.NavigationHistoryStack.push(lastItem);
    }
  }

定义跳转函数toFolder,之后许多函数引用此函数,这个函数单纯执行跳转,传入文件描述对象,执行导航,刷新页面,返回bool值代表成功与否:

toFolder(folder: FileDto) {
      if ((this.checkMessage as any).parentId == folder.id) {
        return false;
      }

      (this.checkMessage as any).parentId = folder.id;

      this.gotoList();
      return true;
    },

简单的写一下导航操作区域和地址栏的Ui界面: 

    <div class="crumbs">
      <ul>
        <li v-for="(item, index) in navigationStack" :key="item.id">
          {{ index > 0 ? " /" : "" }}
          <a href="javascript:void(0)" @click="navigationTo(item)">{{
            item.fileName
          }}</a>
        </li>
      </ul>
    </div>

四、编写历史导航处理逻辑

“后退”函数

  1. 首先确定当前页面在历史导航的哪个位置
  2. 拿到角标后+1(因为是队列,所以越早的角标越大),拿到历史导航队列中后一个页面条目,并执行导航函数
navigationHistoryBack() {
    var currentIndex = Enumerable.from(this.NavigationHistoryStack).indexOf(
      (c) => c.id == (this.checkMessage as any).parentId
    );
    if (currentIndex < this.NavigationHistoryStack.length - 1) {
      var forwardIndex = currentIndex + 1;
      var folder= this.NavigationHistoryStack[forwardIndex]     
      this.toFolder(folder);
    }
  }

“前进”函数

  1. 首先确定当前页面在历史导航的哪个位置
  2. 拿到角标后-1(因为是队列,所以越晚的角标越小),拿到历史导航队列中前一个页面条目,并执行导航函数


  navigationHistoryForward() {
    var currentIndex = Enumerable.from(this.NavigationHistoryStack).indexOf(
      (c) => c.id == (this.checkMessage as any).parentId
    );
    if (currentIndex > 0) {
      var forwardIndex = currentIndex - 1;
      var folder= this.NavigationHistoryStack[forwardIndex]
      this.toFolder(folder);
    }
  }

然后我们需要一个函数,用于显示历史队列中(当前)标签:

getIsCurrentHistoryNavigationItem(item) {
    var itemIndex = Enumerable.from(this.NavigationHistoryStack).indexOf(
      (c) => c.id == item.id
    );
    var result = (this.checkMessage as any).parentId == itemIndex;
    return result;
  }

简单的写一下导航操作区域:

导航按钮以及历史列表:

代码如下: 

<div class="buttons">
      <div>
        <button @click="navigationHistoryBack">
          <img
            style="transform: rotate(180deg)"
            :src="require('@/assets/arr.png')"
          />
        </button>
      </div>
      <div>
        <button @click="navigationHistoryForward">
          <img :src="require('@/assets/arr.png')" />
        </button>
      </div>
      <div>
        <a @click="show">
          <img
            :src="require('@/assets/arr2.png')"
            :style="{
              transform: showHistory ? 'rotate(0deg)' : 'rotate(-180deg)',
            }"
          />
        </a>
      </div>
      <ul class="history" v-show="showHistory">
        <li v-for="(item, index) in navigationHistoryStack" :key="index">
          <span>{{ item.fileName }}</span>

          <span v-if="getIsCurrentHistoryNavigationItem(item)"> (当前)</span>
        </li>
      </ul>

      <div>
        <button @click="navigationBack">
          <img
            style="transform: rotate(-90deg)"
            :src="require('@/assets/arr.png')"
          />
        </button>
      </div>
    </div>

五、问题修复与优化

问题1:历史条目判断错误

测试的时候会发现一个问题,用id判断当前页面所在的堆栈位置,会始终定位到最近一次,相当于FirstOrDefault,因为历史队列可以重复添加,所以需要引入一个isCurrent的bool值属性,来作为判断依据。

这相当于是增加了状态变量,从“无状态”变换成“有状态”,意味着我们要维护这个状态。好处是可以简单的从isCurrent就能判断状态,坏处就是要另写代码维护状态,增加了代码的复杂性。

将navigationTo函数改写成如下:


navigationTo(folder: FileBriefWithThumbnailDto) {
    var toIndex = Enumerable.from(this.NavigationStack).indexOf(folder);
    if (toIndex >= 0) {
      this.NavigationStack.splice(
        toIndex + 1,
        this.NavigationStack.length - toIndex - 1
      );
    } else {
      this.NavigationStack.push(folder);
    }
    if (this.toFolder(folder)) {
        this.navigationHistoryStack.forEach((element) => {
          element["isCurrent"] = false;
        });
        folder["isCurrent"] = true;
        this.navigationHistoryStack.unshift(folder);
      }
  }

判断是否为当前的函数则简化为如下:

    getIsCurrentHistoryNavigationItem(item) {
      var result = item["isCurrent"];
      return result;
    },

从导航历史队列跳转的目录,也需要处理导航堆栈,因此从navigationTo函数中将这一部分剥离出来单独形成函数命名为dealWithNavigationStack:

dealWithNavigationStack(folder) {
      var toIndex = Enumerable.from(this.navigationStack).indexOf(
        (c) => c.id == folder.id
      );
      if (toIndex >= 0) {
        this.navigationStack.splice(
          toIndex + 1,
          this.navigationStack.length - toIndex - 1
        );
      } else {
        this.navigationStack.push(folder);
      }
    },

“前进”函数与“后退”函数分别改写为: 

navigationHistoryForward() {
      var currentIndex = Enumerable.from(this.navigationHistoryStack).indexOf(
        (c) => c["isCurrent"]
      );
      if (currentIndex > 0) {
        var forwardIndex = currentIndex - 1;

        var folder = this.navigationHistoryStack[forwardIndex];
        this.dealWithNavigationStack(folder);

        if (this.toFolder(folder)) {
          this.navigationHistoryStack.forEach((element) => {
            element["isCurrent"] = false;
          });
          this.navigationHistoryStack[forwardIndex]["isCurrent"] = true;
        }
      }
    },
navigationHistoryBack() {
      var currentIndex = Enumerable.from(this.navigationHistoryStack).indexOf(
        (c) => c["isCurrent"]
      );
      if (currentIndex < this.navigationHistoryStack.length - 1) {
        var forwardIndex = currentIndex + 1;

        var folder = this.navigationHistoryStack[forwardIndex];
        this.dealWithNavigationStack(folder);

        if (this.toFolder(folder)) {
          this.navigationHistoryStack.forEach((element) => {
            element["isCurrent"] = false;
          });
          this.navigationHistoryStack[forwardIndex]["isCurrent"] = true;
        }
      }
    },

问题2:文件描述对象重叠

先看现象,重复进入“文件夹A”的时候,都标记为(当前),这显然是错误的

请留意navigationTo中的这一段代码:

 if (this.toFolder(folder)) {
        this.navigationHistoryStack.forEach((element) => {
          element["isCurrent"] = false;
        });
        folder["isCurrent"] = true;
        this.navigationHistoryStack.unshift(folder);
      }

这里隐藏了一个bug,逻辑是将所有的历史队列条目去除当前标记,然后将最新的目标标记为当前并压入历史队列,这里的 folder这一对象来自于listMessages,

JavaScript在5中基本数据类型(Undefined、Null、Boolean、Number和String)之外的类型,都是按地址访问的,因此赋值的是对象的引用而不是对象本身,当重复进入文件夹时,folder与上一次进入添加到队列中的folder,实际上是同一个对象!

因此所有的“文件夹A”都被标记为“(当前)”了

我们需要将 this.navigationHistoryStack.unshift(folder);改写,提取出一个名称为pushNavigationHistoryStack的入队函数:

   pushNavigationHistoryStack(item) {
      var newItem = Object.assign({}, item);

      if (this.navigationHistoryStack.length > 10) {
        this.navigationHistoryStack.pop();
      }
      this.navigationHistoryStack.unshift(newItem);
    },

这里加入了一个控制,历史队列最多容纳10个条目,大于10个有新的条目入队列时,将剔除最后一条(也就是最早的一条记录,记录越早角标越大)。

接下来运行yarn serve来看看最终效果:

 代码仓库:

jevonsflash/vue-explorer-sample (github.com)

jevonsflash/vue-explorer-sample (gitee.com)

<think>我们正在讨论的是使用GoGoCode插件在VSCode中编写转换规则,将Vue2代码转换为Vue3代码。根据用户的问题,我们需要指导用户如何在VSCode中使用GoGoCode插件来编写这样的转换规则。首先,我们需要明确几个步骤:1.安装GoGoCode插件(如果尚未安装)。2.创建或打开一个转换规则文件(通常是一个JavaScript文件)。3.编写转换规则,利用GoGoCode提供的API来匹配和转换代码。4.测试转换规则。参考引用[2]中提到了GoGoCode的特点,包括直观的API(类似jQuery选择器)、语义化操作(基于AST)等,这些将帮助我们编写规则。具体步骤:第一步:安装GoGoCode插件-在VSCode的扩展市场中搜索GoGoCode并安装。第二步:创建转换规则文件-在项目中创建一个新的JavaScript文件,例如`vue2-to-vue3.js`。第三步:编写转换规则-在规则文件中,我们将使用GoGoCode的API来定义转换逻辑。-例如,Vue2到Vue3的一个重大变化是`v-model`的用法改变。在Vue2中,一个组件上可以使用多个`v-model`,而在Vue3中,我们需要为每个`v-model`指定参数。-另一个例子是事件API的改变:`$on`、`$off`和`$once`被移除。下面是一个简单的转换规则示例,用于转换`v-model`:假设我们有一个Vue2组件,使用了`v-model`,在Vue3中我们需要将其转换为带有参数的`v-model`。在Vue2中:```html<child-componentv-model="value"></child-component>```在Vue3中,如果我们希望保持相同的逻辑,并且子组件中仍然使用`value`作为prop和`input`事件,那么我们可以将其转换为:```html<child-componentv-model:value="value"></child-component>```但是,Vue3的默认`v-model`使用`modelValue`作为prop和`update:modelValue`作为事件。因此,如果我们要保持与Vue2相同的prop和事件名,我们需要使用带参数的`v-model`。我们可以编写一个规则来转换所有`v-model`,为它们添加一个参数(比如使用原来的prop名,但这里需要根据具体情况判断,通常Vue2中默认的prop是`value`,事件是`input`)。然而,由于每个项目可能不同,我们需要根据实际情况编写规则。使用GoGoCode,我们可以这样写:```javascriptmodule.exports=function(fileInfo,api){//GoGoCode的APIconst$=api.gogocode;//将源代码解析为ASTconstroot=$(fileInfo.source);//查找所有元素中具有v-model属性的节点root.find('<$_$1v-model=$_$2></$_$1>').each(function(node){//获取节点constelement=node.match[0];//获取v-model的值(表达式)constmodelValue=element.attributes['v-model'];//获取标签名consttagName=element.name;//移除原来的v-modeldeleteelement.attributes['v-model'];//添加新的带参数的v-model,参数使用'value'(因为Vue2默认是value)element.attributes['v-model:value']=modelValue;});//返回转换后的代码returnroot.generate();};```注意:这只是一个非常简单的示例,实际转换要复杂得多,因为需要考虑多种情况(比如组件可能自定义了model选项,即prop和event)。第四步:测试转换规则-我们可以使用GoGoCode提供的命令行工具或在VSCode中通过插件提供的功能来测试转换。-或者,我们可以使用GoGoCode的API在Node.js环境中运行转换。另外,引用[1]中提到VSCode的自动完成设置,确保在编写规则时,VSCode能够提供代码提示,这对于编写规则非常有帮助。由于转换规则的编写需要深入理解AST和GoGoCode的API,建议参考GoGoCode的官方文档和示例。相关问题:1.GoGoCode插件在VSCode中具体如何操作来转换代码?2.除了`v-model`,Vue2到Vue3还有哪些常见的需要转换的地方?3.如何测试我们编写的转换规则?4.如何处理转换过程中的复杂情况,例如自定义事件或第三方组件的转换?</think>### 如何在VSCode中使用GoGoCode编写Vue2到Vue3的转换规则 #### 步骤1:环境准备 1. **安装GoGoCode插件** 在VSCode扩展商店搜索"GoGoCode"并安装[^2] ![安装示意图](https://example.com/gogocode-install.png) *(示意图)* 2. **创建规则文件** 新建`.js`文件(如`vue2-to-vue3-rule.js`),GoGoCode会自动识别为转换规则文件。 #### 步骤2:编写转换规则(核心) 使用GoGoCode的AST操作API编写规则: ```javascript module.exports = function (fileInfo, api) { const $ = api.gogocode; const source = $(fileInfo.source); // 示例1:转换Vue选项API → Composition API source.find(`export default { $_$ }`).replace( `export default { $$$ }`, `import { ref, reactive } from 'vue'; export default { $$$, setup() { $_$ } }` ); // 示例2:转换v-model语法 source.find(`<$_$ v-model="$_$1" />`).replace( `<$_$ v-model="$_$1" />`, `<$_$ modelValue="$_$1" @update:modelValue="val => $_$1 = val" />` ); // 示例3:迁移事件总线($on/$off) source.find(`this.$on(`).replaceBy(`// 事件总线需替换为mitt库`); return source.generate(); } ``` #### 步骤3:调试与测试 1. **实时预览** 在VSCode中打开`.vue`文件,右键选择 **"Preview Transformation"** ![预览功能](https://example.com/preview-transform.png) *(示意图)* 2. **批量转换** 终端执行命令(需全局安装GoGoCode CLI): ```bash gogocode -s ./src -t ./vue2-to-vue3-rule.js -o ./vue3-src ``` #### 关键转换场景参考表 | Vue2特性 | Vue3对应转换 | GoGoCode匹配模式示例 | |------------------|----------------------------------|-----------------------------------| | `this.$refs` | `ref()` + `setup()` | `find('this.$refs.$_$')` | | 事件总线 | 迁移到`mitt`库 | `find('this.$on($_$)')` | | 过滤器(filter) | 改用计算属性/方法 | `find('{{ $_$ \| filterFn }}')` | | `Vue.extend` | `defineComponent` | `find('Vue.extend($_$)')` | #### 最佳实践建议 1. **增量转换** 建议按组件粒度逐个转换,使用`// @gogocode-ignore`注释跳过未完成文件 2. **自定义复杂规则** 对于特定组件库(如ElementUI→Element Plus): ```javascript // 转换ElementUI按钮 source.find(`<el-button @click="$_$">`) .replace(`<el-button @click="$_$">`, `<el-button type="primary" @click="$_$">`) ``` 3. **AST可视化辅助** 使用GoGoCode AST Explorer插件查看代码AST结构[^2] --- ### 相关问题 1. **如何处理Vue2到Vue3迁移中的`$children` API移除问题?** 2. **GoGoCode能否自动转换Vuex 3到Pinia的代码?需要哪些额外配置?** 3. **转换过程中如何处理第三方库的兼容性问题?** 4. **如何验证转换后的Vue3代码功能完整性?** [^1]: VSCode的自动完成功能对编写转换规则至关重要,确保在设置中开启相关功能 [^2]: GoGoCode提供丰富的AST操作API和跨文件处理能力,适合复杂迁移场景
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林晓lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值