五、浏览文件树并从 URL 加载文件夹

第 4 章使用 Dropbox API获取文件列表,我们创建了一个应用,列出指定 Dropbox 文件夹的文件和文件夹内容。我们现在需要使我们的应用易于导航。这意味着用户将能够单击文件夹名称以导航到并列出其内容,同时还允许用户下载文件。

继续之前,请确保 HTML 中包含 Vue 和 Dropbox JavaScript 文件。

在本章中,我们将:

  • 为文件和文件夹创建组件
  • 添加指向文件夹组件的链接以更新目录列表
  • 向文件组件添加下载按钮
  • 创建一个面包屑组件,这样用户就可以轻松地导航到树的后面
  • 动态更新浏览器 URL,以便在文件夹已添加书签或链接已共享时加载正确的文件夹

分离文件和文件夹

在创建组件之前,我们需要在结构中分离文件和文件夹,以便轻松识别和显示不同的类型。由于每个项目的.tag属性,我们可以拆分文件夹和文件。

首先,我们需要将structure数据属性更新为同时包含filesfolders数组的对象:

      data() {
        return {
          accessToken: 'XXXX',
          structure: {
 files: [],
 folders: []
 },
          byteSizes: ['Bytes', 'KB', 'MB', 'GB', 'TB'],
          isLoading: true
        }
      }

这使我们能够将文件和文件夹附加到不同的数组中,这意味着我们可以在视图中以不同的方式显示它们。

下一步是用当前文件夹的数据填充这些数组。以下所有代码都发生在getFolderStructure方法的第一个then()函数中

创建一个 JavaScript 循环,循环遍历条目并检查项目的.tag属性。如果等于folder则追加到structure.folder数组中,否则追加到structure.files数组中:

      getFolderStructure(path) {
        this.dropbox().filesListFolder({
          path: path, 
          include_media_info: true
        })
        .then(response => {
          for (let entry of response.entries) {
 // Check ".tag" prop for type
 if(entry['.tag'] === 'folder') {
 this.structure.folders.push(entry);
 } else {
 this.structure.files.push(entry);
 }
 }
          this.isLoading = false;
        })
        .catch(error => {
          console.log(error);
        });
      },

这段代码循环遍历条目,就像我们在视图中一样,并检查.tag属性。由于属性本身以.开头,因此我们无法使用对象样式表示法来访问属性,就像我们对名称entry.name所做的那样。然后,我们使用 JavaScript 推送将条目附加到filesfolders数组中,具体取决于类型。

要显示这些新数据,我们需要更新视图以循环两种类型的数组。这是使用<template>标记的完美用例,因为我们希望将两个数组附加到同一个无序列表中。

更新视图以分别列出这两个阵列。我们可以从“文件夹显示”部分删除“大小”选项,因为它永远不会具有size属性:

      <ul v-if="!isLoading">
        <template v-for="entry in structure.folders">
 <li>
 <strong>{{entry.name }}</strong>
 </li>
 </template>
 <template v-for="entry in structure.files">
 <li>
 <strong>{{ entry.name }}</strong>
 <span v-if="entry.size">- {{ bytesToSize(entry.size)       }}</span>
 </li>
 </template>
      </ul>

现在,我们有机会为这两种类型创建组件。

制作文件和文件夹组件

通过分离数据类型,我们可以创建单独的组件来划分数据和方法。创建一个接受单个属性的folder组件,允许传递folder对象变量。由于模板太小,不需要视图或基于<script>块的模板;相反,我们可以将其作为字符串传递到组件:

      Vue.component('folder', {
        template: '<li><strong>{{ f.name }}</strong>      
        </li>',
        props: {
          f: Object
        },
      });

为了使我们的代码更小、重复性更少,这个道具被称为f。这将整理视图,并让组件名称确定显示类型,而无需多次重复单词folder

更新视图以使用文件夹组件,并将entry变量传递给f属性:

      <template v-for="entry in structure.folders">
        <folder :f="entry"></folder>
      </template>

通过创建file组件,对文件重复该过程。在创建file组件时,我们可以从父dropbox-viewer组件中移动bytesToSize方法和byteSizes数据属性,因为它只会在显示文件时使用:

      Vue.component('file', {
        template: '<li><strong>{{ f.name }}</strong><span       v-if="f.size"> - {{ bytesToSize(f.size) }}</span>         </li>',
        props: {
          f: Object
        },
        data() {
          return {
            byteSizes: ['Bytes', 'KB', 'MB', 'GB', 'TB']
          }
        }, 
        methods: {
          bytesToSize(bytes) {
            // Set a default
            let output = '0 Byte';      
            // If the bytes are bigger than 0
            if (bytes > 0) {
              // Divide by 1024 and make an int
              let i = parseInt(Math.floor(Math.log(bytes) 
              / Math.log(1024)));
             // Round to 2 decimal places and select the 
            appropriate unit from the array
            output = Math.round(bytes / Math.pow(1024, i), 
             2) + ' ' + this.byteSizes[i];
            }   
            return output
          }
        }
      });

再一次,我们可以使用f作为道具名称,以减少重复(以及我们应用的文件大小)。再次更新视图以使用此新组件:

      <template v-for="entry in structure.files">
        <file :f="entry"></file>
      </template>

链接文件夹和更新结构

现在我们已经将文件夹和文件分开,我们可以将文件夹名称转换为链接。然后,这些链接将更新结构以显示所选文件夹的内容。为此,我们将使用每个文件夹中的path_lower属性来构建链接目标

创建到每个文件夹name的动态链接,链接到文件夹的path_lower。随着我们对 Vue 越来越熟悉,v-bind属性已缩短为冒号符号:

      Vue.component('folder', {
        template: '<li><strong><a :href="f.path_lower">{{ 
        f.name }}</a></strong></li>',
        props: {
          f: Object
        },
      });

我们现在需要为这个链接添加一个click侦听器。单击时,我们需要触发dropbox-viewer组件上的getFolderStructure方法。尽管 click 方法将在每个实例上使用f变量来获取数据,但最好将href属性设置为文件夹 URL。

使用我们在本书早期章节中所学的内容,在folder组件上创建一个方法,当触发该方法时,会将文件夹路径发送到父组件。dropbox-viewer组件还需要一种新方法,在点火时用给定参数更新结构。

folder组件上创建新方法,并将click事件添加到文件夹链接。与v-bind指令一样,我们现在使用v-on的简写符号,由@符号表示:

      Vue.component('folder', {
        template: '<li><strong><a          
 @click.prevent="navigate()" :href="f.path_lower">{{ 
       f.name }}</a></strong></li>',
        props: {
          f: Object
        },
        methods: {
          navigate() {
 this.$emit('path', this.f.path_lower);
 }
        }
      });

除了定义click事件外,还添加了事件修饰符。在点击事件之后使用.preventpreventDefault添加到链接操作中,这会阻止链接实际到达指定的 URL,而让click方法处理所有事情。在 Vue 文档中可以找到更多事件修饰符及其详细信息。

单击时,将触发 navigate 方法,该方法使用path变量发出文件夹的下部路径。

现在我们有了click处理程序和正在发出的变量,我们需要更新视图以触发父dropbox-viewer组件上的方法:

      <template v-for="entry in structure.folders">
        <folder :f="entry" @path="updateStructure">      
        </folder>
      </template>

在 Dropbox 组件上创建一个与v-on属性值同名的新方法,在本例中为updateStructure。此方法将有一个参数,即我们前面发出的路径。从这里,我们可以使用 path 变量触发我们原来的getFolderStructure方法:

      updateStructure(path) {
        this.getFolderStructure(path);
      }

在浏览器中查看我们的应用时,现在应该列出文件夹和链接,并在单击时显示新文件夹的内容。

然而,在这样做的时候,会提出几个问题。首先,文件和文件夹将附加到现有列表中,而不是替换它。其次,没有向用户反馈应用正在加载下一个文件夹。

第一个问题可以通过在附加新结构之前清除文件夹和文件数组来解决。第二个问题可以通过使用我们在应用开始时使用的加载屏幕来解决-这将给用户一些反馈。

为了解决第一个问题,在getFolderStructure方法的成功承诺函数中创建一个新的structure对象。此对象应复制data对象中的structure对象。这应该为文件和文件夹设置空白数组。更新for循环以使用本地结构数组而不是组件数组。最后,用新版本更新组件structure对象,包括更新的文件和文件夹:

      getFolderStructure(path) {
        this.dropbox().filesListFolder({
          path: path, 
          include_media_info: true
        })
        .then(response => {  
          const structure = {
 folders: [],
 files: []
 }
          for (let entry of response.entries) {
            // Check ".tag" prop for type
            if(entry['.tag'] == 'folder') {
              structure.folders.push(entry);
            } else {
              structure.files.push(entry);
            }
          } 
          this.structure = structure;
          this.isLoading = false;
        })
        .catch(error => {
          console.log(error);
        });
      }

由于此方法在安装应用并创建其自身版本的 structure 对象时被调用,因此无需在data函数中声明数组。更新数据对象以将structure属性初始化为对象:

      data() {
        return {
          accessToken: 'XXXX',
          structure: {},
          isLoading: true
        }
      }

现在运行应用将呈现文件列表,当单击新文件夹进入时,文件列表将被清除并更新。为了给用户一些反馈并让他们知道应用正在工作,让我们在每次单击后切换加载屏幕。

但是,在我们这样做之前,让我们充分了解延迟的来源以及触发加载屏幕的最佳位置。

点击链接是瞬时的,这会触发文件夹组件上的导航方法,进而触发 Dropbox 组件上的updateStructure方法。当应用到达getFolderStructure方法内 Dropbox 实例上的filesListFolder函数时,会出现延迟。由于我们可能希望在稍后的日期触发getFolderStucture方法而不触发加载屏幕,因此在updateStructure方法内将isLoading变量设置为true

      updateStructure(path) {
        this.isLoading = true;
        this.getFolderStructure(path);
      }

动画就位后,在文件夹中导航时,应用会在加载屏幕和文件夹结构之间淡出。

从当前路径创建面包屑

在浏览文件夹或任何类型的嵌套结构时,最好有一个面包屑,这样用户就可以知道它们在哪里,走了多远,还可以方便地返回到以前的文件夹。我们将为面包屑制作一个组件,因为它将具有各种属性、计算函数和方法。

面包屑组件将列出每个文件夹深度,作为指向文件夹图标的链接。单击链接将直接将用户带到该文件夹-即使它是多层的。为了实现这一点,我们需要有一个可以循环的链接列表,每个链接都有两个属性——一个是文件夹的完整路径,另一个只是文件夹名。

例如,如果我们 img/holiday/summer/iphone的文件夹结构,我们希望能够点击Holiday并让应用导航 img/holiday

创建面包屑组件-现在,在模板属性中添加一个空的<div>

      Vue.component('breadcrumb', {
        template: '<div></div>'
      });

将组件添加到视图中。我们希望 breadcrumb 随结构列表淡入淡出,因此我们需要调整 HTML 以将列表和 breadcrumb 组件包装在一个具有v-if声明的容器中:

      <transition name="fade">
        <div v-if="!isLoading">
          <breadcrumb></breadcrumb>
          <ul>
            <template v-for="entry in structure.folders">
              <folder :f="entry" @path="updateStructure">              </folder>
            </template>  
            <template v-for="entry in structure.files">
              <file :f="entry"></file>
            </template>
          </ul>
        </div>
      </transition>

我们现在需要为我们提供一个存储当前文件夹路径的变量。然后,我们可以在 breadcrumb 组件中操纵这个变量。这将在 Dropbox 组件上存储和更新,并传递给面包屑组件。

dropbox-viewer组件上创建名为path的新属性:

      data() {
        return {
          accessToken: 'XXXXX',
          structure: {},
          isLoading: true,
          path: ''
        }
      }

现在,我们需要确保每当从 Dropbox API 检索结构时,该路径都会得到更新。在禁用isLoading变量之前,在getFolderStructure方法中执行此操作。这可确保仅在加载结构后,但在显示文件和文件夹之前,才会对其进行更新:

      getFolderStructure(path) {
        this.dropbox().filesListFolder({
          path: path, 
          include_media_info: true
        })
        .then(response => {    
          const structure = {
            folders: [],
            files: []
          }  
          for (let entry of response.entries) {
            // Check ".tag" prop for type
            if(entry['.tag'] == 'folder') {
              structure.folders.push(entry);
            } else {
              structure.files.push(entry);
            }
          } 
          this.path = path;
          this.structure = structure;
          this.isLoading = false;
        })
        .catch(error => {
          console.log(error);
        });
      },

现在我们有了一个用当前路径填充的变量,我们可以将它作为一个道具传递给 breadcrumb 组件。将路径变量作为值添加到面包屑中的新属性:

      <breadcrumb :p="path"></breadcrumb>

更新组件以将道具作为字符串接受:

      Vue.component('breadcrumb', {
        template: '<div></div>',
        props: {
 p: String
 }
      });

p属性现在包含我们所在位置的完整路径(例 img/holiday/summer`。我们希望分解这个字符串,以便识别文件夹名称,并为要渲染的组件构建面包屑。

在组件上创建一个computed对象,并创建一个名为folders()的新函数。这将创建面包屑数组,以便我们在模板中循环:

      computed: {
       folders() {   
        }
      }

我们现在需要设置一些变量供我们使用。创建一个名为output的新空数组。这就是我们要建立面包屑的地方。我们还需要一个名为slug的空变量作为字符串。slug变量是 URL 的一部分,WordPress 很流行它的用法。最后一个变量是作为数组创建的路径。正如我们所知,每个文件夹由一个/分隔,我们可以使用它将字符串分解或拆分为不同的部分:

      computed: {
        folders() {
 let output = [],
 slug = '',
 parts = this.p.split('/');
        }
      }

如果我们查看Summer文件夹的 parts 变量,它将如下所示:

      ['images', 'holiday', 'summer']

现在,我们可以通过循环数组来创建面包屑。每个面包屑项目将是一个对象,其中包含单个文件夹的name,例如holidaysummer,以及slug,前者 img/holiday,后者 img/holiday/summer

将构造每个对象,然后将其添加到output数组中。然后,我们可以返回模板使用的输出:

      folders() {
        let output = [],
          slug = '',
          parts = this.p.split('/'); 
 for (let item of parts) {
 slug += item;
 output.push({'name': item, 'path': slug});
 slug += '/';
 }  
        return output;
      }

这个循环通过以下步骤创建面包屑。对于本例,我们假设我们 img/holiday`文件夹中:

  1. parts现在将是一个包含三项的数组['', 'images', holiday']。如果拆分的字符串以要拆分的项目开头,则会将空项目作为第一个项目
  2. 在循环开始时,第一个 slug 变量将等于'',因为它是第一项。
  3. output数组将添加一个新项,其对象为{'name': '', 'path': ''}
  4. slug变量的末尾添加了一个/
  5. 循环遍历下一个项,slug变量获取添加到其中的项的名称(images
  6. output现在添加了一个新对象,值为{'name': 'images', 'path': '/images'}
  7. 对于最后一项,将添加另一个/以及下一个名称holiday
  8. output获取添加的最后一个对象,值为{'name': 'holiday', 'path':img/holiday'}-注意路径正在建立,而名称仍然是单一的文件夹名称。

现在我们有了面包屑输出数组,可以在视图中循环

The reason we add the slash after we've appended to the output array is that the API states that to get the root of the Dropbox we pass in an empty string, whereas all other paths must begin with a /.

下一步是将面包屑输出到视图中。由于这个模板很小,我们将使用多行 JavaScript 表示法。循环通过folders计算变量中的项目,输出每个项目的链接。不要忘记在所有链接周围保留一个包含元素:

      template: '<div>' +
 '<span v-for="f in folders">' +
 '<a :href="f.path">{{ f.name }}</a>' +
 '</span>' + 
      '</div>'

在浏览器中呈现这个应用应该会显示一个面包屑-尽管有点挤在一起,并且缺少一个主链接(因为第一个项目没有名称)。返回folders函数并添加if语句-检查项目是否有名称,如果没有,则添加硬编码值:

      folders() {
        let output = [],
          slug = '',
          parts = this.p.split('/');
        console.log(parts);
        for (let item of parts) {
          slug += item;
          output.push({'name': item || 'home', 'path':      
            slug});
          slug += '/';
        }  
        return output;
      }

另一个选项是在模板本身中添加if语句:

      template: '<div>' +
        '<span v-for="f in folders">' +
          '<a :href="f.path">{{ f.name || 'Home' }}</a>' +
        '</span>' + 
      '</div>'

如果我们想在文件夹名称之间显示分隔符,例如斜杠或 V 形,可以很容易地添加分隔符。然而,当我们想要显示链接之间的分隔符,而不是在开始或结束时,会出现一个小小的障碍。为了解决这个问题,我们将在执行循环时使用可用的index关键字。然后我们将把它与数组的长度进行比较,并对一个元素执行一个v-if声明。

在数组中循环时,Vue 允许您使用另一个变量。默认情况下,这是索引(项目在数组中的位置);但是,如果数组是以键/值方式构造的,则可以将索引设置为值。如果是这种情况,您仍然可以通过添加第三个变量来访问索引。由于我们的数组是一个简单的列表,因此我们可以轻松地使用此变量:

      template: '<div>' +
        '<span v-for="(f, i) in folders">' +
          '<a :href="f.path">{{ f.name || 'Home' }}</a>' +
          '<span v-if="i !== (folders.length - 1)"> » 
            </span>' +
        '</span>' + 
      '</div>',

f变量更新为一对括号,其中包含一个f和一个i,逗号分隔。f变量是循环中的当前文件夹,而已创建的i变量是项目的索引。请记住,数组索引从0开始,而不是从1开始。

我们添加的分隔符包含在一个带有v-if属性的 span 标记中,其内容可能会让人感到困惑。这会将当前索引与folders数组的长度(它有多少项)减去 1 相混淆。- 1是因为索引从 0 开始,而不是像您预期的那样从 1 开始。如果数字不匹配,则显示span元素。

我们需要做的最后一件事是让面包屑导航到选定的文件夹。我们可以通过调整为folder组件编写的导航函数来实现这一点。但是,因为我们的整个组件是面包屑,而不是每个单独的链接,所以我们需要修改它,使其接受一个参数。

首先将click事件添加到链接中,传入folder对象:

      template: '<div>' +
        '<span v-for="(f, i) in folders">' +
          '<a @click.prevent="navigate(f)"          
            :href="f.path"> 
            {{ f.name || 'Home' }}</a>' +
          '<i v-if="i !== (folders.length - 1)"> &raquo; 
           </i>' +
        '</span>' + 
      '</div>',

接下来,在面包屑组件上创建navigate方法,确保接受folder参数并发出路径:

      methods: {
        navigate(folder) {
          this.$emit('path', folder.path);
        }
      }

最后一步是在发出路径时触发父方法。为此,我们可以在dropbox-viewer组件上使用相同的updateStructure方法:

      <breadcrumb :p="path" @path="updateStructure">      
      </breadcrumb>

我们现在有了一个完全可操作的 breadcrumb,它允许用户使用文件夹链接向下导航文件夹结构,并通过 breadcrumb 链接进行备份。

我们的完整面包屑组件如下所示:

      Vue.component('breadcrumb', {
        template: '<div>' +
          '<span v-for="(f, i) in folders">' +
            '<a @click.prevent="navigate(f)" 
             :href="f.path">{{ 
              f.name || 'Home' }}</a>' +
              '<i v-if="i !== (folders.length - 1)"> » 
              </i>' + '</span>' + 
             '</div>',

        props: {
    p: String
  },

  computed: {
    folders() {
      let output = [],
        slug = '',
        parts = this.p.split('/');
      console.log(parts);
      for (let item of parts) {
        slug += item;
        output.push({'name': item || 'home', 'path':   
        slug});
        slug += '/';
      }

      return output;
    }
  },

   methods: {
    navigate(folder) {
      this.$emit('path', folder.path);
    }
  }
});

添加下载文件的功能

现在我们的用户可以浏览文件夹结构,我们需要添加下载文件的功能。不幸的是,这并不像访问文件上的链接属性那么简单。要获得下载链接,我们必须查询每个文件的 Dropbox API。

我们将在创建文件组件时查询 API,这将异步获取下载链接,并在可用时显示它。在此之前,我们需要使 Dropbox 实例可用于文件组件。

向视图中的文件组件添加新属性,并将 Dropbox 方法作为值传递:

      <file :d="dropbox()" :f="entry"></file>

d变量添加到接受对象的组件的props对象中:

    props: {
      f: Object,
      d: Object
    },

我们现在要添加一个数据属性link。默认情况下,这应该设置为false,这样我们就可以隐藏链接,一旦 API 返回值,我们就会用下载链接填充它。

created()函数添加到文件组件中,并在内部添加 API 调用:

     created() {
      this.d.filesGetTemporaryLink({path:    
       this.f.path_lower}).then(data => {
        this.link = data.link;
     });
    }

此 API 方法接受一个对象,类似于filesListFolder函数。我们正在传递当前文件的路径。返回数据后,我们可以将组件的link属性设置为下载链接。

我们现在可以向组件的模板添加下载链接。添加一个v-if以仅在检索下载链接后显示<a>

   template: '<li><strong>{{ f.name }}</strong><span v-  
    if="f.size"> - {{ bytesToSize(f.size) }}</span><span    
    v-if="link"> - <a :href="link">Download</a></span>  
   </li>'

浏览这些文件,我们现在可以看到每个文件旁边都会出现一个下载链接,下载速度取决于您的 internet 连接和 API 速度。

添加了下载链接的完整文件组件现在看起来像:

    Vue.component('file', {
     template: '<li><strong>{{ f.name }}</strong><span v-   
     if="f.size"> - {{ bytesToSize(f.size) }}</span><span 
     v-if="link"> - <a :href="link">Download</a></span>
     </li>',
    props: {
      f: Object,
      d: Object
      },

   data() {
     return {
       byteSizes: ['Bytes', 'KB', 'MB', 'GB', 'TB'],
      link: false
      }
   },

    methods: {
     bytesToSize(bytes) {
      // Set a default
      let output = '0 Byte';

      // If the bytes are bigger than 0
      if (bytes > 0) {
        // Divide by 1024 and make an int
        let i = parseInt(Math.floor(Math.log(bytes) / 
          Math.log(1024)));
        // Round to 2 decimal places and select the 
         appropriate unit from the array
        output = Math.round(bytes / Math.pow(1024, i), 2) 
         + ' ' + this.byteSizes[i];
      }

      return output
      }
     },

     created() {
    this.d.filesGetTemporaryLink({path:    
     this.f.path_lower}).then(data => {
      this.link = data.link;
      });
    },
  });

更新 URL 哈希并使用它在文件夹中导航

我们的 Dropbox web 应用现在可以通过结构列表和面包屑完全导航,我们现在可以添加和更新浏览器 URL,以便快速访问和共享文件夹。我们可以通过两种方式实现这一点:要么更新散列,例如,www.domain.comimg/holiday/summer,要么将所有路径重定向到单个页面,并在 URL 中不使用散列的情况下处理路由

对于此应用,我们将在 URL 中使用#方法。在介绍vue-router时,我们将在本书的第三部分介绍 URL 路由技术。

在让应用显示 URL 的相应文件夹之前,我们首先需要在导航到新文件夹时获取要更新的 URL。我们可以使用本机的window.location.hashJavaScript 对象来实现这一点。我们希望在用户单击链接时立即更新 URL,而不是等待数据加载更新。

因为每当我们更新结构时,getFolderStructure方法就会被激发,所以将代码添加到此函数的顶部。这意味着 URL 得到更新,然后调用 Dropbox API 来更新结构:

    getFolderStructure(path) {
      window.location.hash = path;

      this.dropbox().filesListFolder({
       path: path, 
        include_media_info: true
      })
     .then(response => {

       const structure = {
        folders: [],
        files: []
       }

      for (let entry of response.entries) {
        // Check ".tag" prop for type
         if(entry['.tag'] == 'folder') {
          structure.folders.push(entry);
         } else {
          structure.files.push(entry);
         }
       }

      this.path = path;
      this.structure = structure;
      this.isLoading = false;
   })
     .catch(error => {
      console.log(error);
   });
 }

当您在应用中导航时,应用应更新 URL 以包含当前文件夹路径。

但是,当您使用文件夹按“刷新”时会看到什么;URL 被重置为只有一个散列,之后没有文件夹,因为它被通过created()函数中的方法传入的空路径重置。

我们可以通过将当前哈希传递给created函数中的getFolderStructure来解决这个问题,但是,如果我们这样做,我们需要进行一些检查和错误捕获。

首先,当调用window.location.hash时,您还将得到作为字符串一部分返回的哈希值,因此我们需要删除它。其次,如果用户输入了错误的路径或文件夹被移动,我们需要处理错误 URL 的实例。最后,我们需要让用户在浏览器中使用后退和前进按钮(或键盘快捷键)。

基于 URL 显示文件夹

当我们的应用挂载时,它已经调用一个函数来请求基本文件夹的结构。我们编写此函数是为了允许传入路径,并且在created()函数中,我们将该值固定为''的根文件夹。这使我们能够灵活地调整此函数,以从 URL 传入哈希,而不是固定字符串。

更新函数以接受 URL 的哈希值,如果没有,则接受原始固定字符串:

  created() {
    let hash = window.location.hash.substring(1);
    this.getFolderStructure(hash || '');
  }

创建一个名为hash的新变量,并为其分配window.location.hash。因为变量以#开头,我们的应用不需要它,所以使用substring函数从字符串中删除第一个字符。然后,我们可以使用一个逻辑运算符来使用哈希变量,或者如果这等于零,则使用原始的固定字符串。

你现在应该能够通过 URL 更新在你的应用中导航。如果要将 URL 复制或粘贴到其他浏览器中,请按“刷新”或“粘贴”按钮。

显示错误消息

当我们的应用接受 URL 时,我们需要处理一种情况,即有人输入 URL 并出错,或者共享一个文件夹,该文件夹后来被移动。

由于此错误是一个边缘情况,如果加载数据时出错,我们将劫持isLoading参数。在getFolderStructure函数中,我们有一个catch函数作为承诺返回,如果 API 调用出现错误,它将被触发。在此功能中,将isLoading变量设置为'error'

   getFolderStructure(path) {
     window.location.hash = path;

     this.dropbox().filesListFolder({
      path: path, 
      include_media_info: true
    })
    .then(response => {

      const structure = {
        folders: [],
        files: []
      }

      for (let entry of response.entries) {
        // Check ".tag" prop for type
        if(entry['.tag'] == 'folder') {
         structure.folders.push(entry);
       } else {
         structure.files.push(entry);
       }
     }

     this.path = path;
     this.structure = structure;
     this.isLoading = false;
   })
    .catch(error => {
      this.isLoading = 'error';
      console.log(error);
    });
  }

如果我们需要诊断错误文件路径以外的问题,console.log已被保留。虽然 API 可能抛出几个不同的错误,但我们将假定此应用的错误是由于错误的路径造成的。如果您想处理应用中的其他错误,您可以通过其status_code属性来识别错误类型。有关这方面的更多详细信息,请参阅 Dropbox API 文档。

更新视图以处理此新的isLoading变量属性。当设置为错误时,isLoading变量仍为“真”,因此在加载元素中,添加一个新的v-if以检查加载变量是否设置为error

   <transition name="fade">
    <div v-if="isLoading">
      <div v-if="isLoading === 'error'">
 <p>There seems to be an issue with the URL entered.  
       </p>
 <p><a href="">Go home</a></p>
 </div>
 <div v-else>
 Loading...
 </div>
    </div>
  </transition>

设置为显示isLoading变量的第一个元素设置为error;否则,显示加载文本。在错误文本中,包含一个链接,用于将用户发送回当前 URL,而不使用任何 URL 哈希。这将“重置”它们回到文档树的顶部,以便它们可以向下导航。一个改进可能是将当前 URL 分解,并建议删除最后一个文件夹时使用相同的 URL。

通过在 URL 末尾添加不存在的路径并确保显示错误消息,验证是否正在加载错误代码。请记住,您的用户可能会遇到此错误消息的误报,即如果 Dropbox API 抛出任何类型的错误,将显示此消息。

使用浏览器中的“后退”和“前进”按钮

要使用浏览器中的后退和前进按钮,我们需要显著地更新代码。目前,当用户从结构或面包屑中单击文件夹时,我们通过在click处理程序上使用.prevent来防止浏览器的默认行为。然后,在处理文件夹之前,我们会立即更新 URL

但是,如果我们允许应用使用本机行为更新 URL,那么我们就可以监视哈希 URL 更新,并使用它来检索我们的新结构。使用这种方法,后退和前进按钮将在没有任何进一步干预的情况下工作,因为它们将更新 URL 哈希。

这也将提高我们应用的可读性,并减少代码重量,因为我们将能够删除链接上的navigate方法和click处理程序。

删除不需要的代码

在添加更多代码之前,第一步是从组件中删除不必要的代码。从面包屑开始,从组件中删除navigate方法,从模板中的链接中删除@click.prevent属性。

我们还需要更新每个项目的slug以前置#-这确保应用在单击时不会尝试导航到全新页面。当我们在 folderscomputed函数中循环浏览面包屑项目时,将对象推送到output数组时,为每个slug添加一个散列:

 Vue.component('breadcrumb', {
   template: '<div>' +
     '<span v-for="(f, i) in folders">' +
       '<a :href="f.path">{{ f.name || 'Home' }}</a>' +
       '<i v-if="i !== (folders.length - 1)"> &raquo;   
       </i>' + '</span>' + 
       '</div>',
    props: {
      p: String
     },
    computed: {
      folders() {
        let output = [],
          slug = '',
          parts = this.p.split('/');

         for (let item of parts) {
          slug += item;
            output.push({'name': item || 'home', 'path': '#' + slug});
            slug += '/';
         }

         return output;
       }
     }
   });

我们也可以删除dropbox-viewer-template中面包屑成分的v-on声明。它应该只有作为道具传入的路径:

    <breadcrumb :p="path"></breadcrumb>

现在,我们可以对文件夹组件重复相同的模式。从链接中删除@click.prevent声明并删除navigate方法。

由于在显示文件夹对象之前,我们不会循环或编辑文件夹对象,因此我们可以在模板中预先添加#。正如我们告诉 Vue 的那样,href绑定到一个 JavaScript 对象(带有冒号),我们需要用引号封装哈希,并使用 JavaScript+符号将其与文件夹路径连接起来。

我们已经在单引号和双引号中,所以我们需要通知 JavaScript 我们字面上的是指单引号,这是通过在单引号字符前面使用反斜杠来完成的:

   Vue.component('folder', {
    template: '<li><strong><a :href="\'#\' +   
    f.path_lower">{{ f.name }}</a></strong></li>',
     props: {
      f: Object
     }
   });

我们还可以从视图中的<folder>组件中删除@path属性:

   <template v-for="entry in structure.folders">
     <folder :f="entry"></folder>
   </template>

我们的代码看起来已经更干净、更不凌乱、文件更小了。在浏览器中查看应用将呈现您所在文件夹的结构;但是,单击链接将更新 URL,但不会更改显示内容。

使用 URL 更改更新结构并在实例外部设置 Vue 数据

现在我们已经正确地更新了 URL,每当哈希值发生变化时,我们就可以得到新的结构。这可以通过带有onhashchange函数的 JavaScript 本机实现。

我们将创建一个函数,该函数在 URL 的哈希更新时触发,这反过来将更新父 Vue 实例上的 path 变量。此变量将作为道具传递给子dropbox-viewer组件。此组件将监视变量中的更改,并在更新时检索新结构。

首先,更新父 Vue 实例,使其数据对象的路径键设置为空字符串属性。我们还将把我们的 Vue 实例分配给一个常量变量app——这允许我们在实例之外设置数据和调用方法:

 const app = new Vue({
    el: '#app',
 data: {
 path: ''
 }
 });

下一步是在每次更新 URL 时更新此数据属性。这是通过使用window.onhashchange完成的,这是一个本机 JavaScript 函数,每当 URL 中的哈希值发生变化时都会触发。

复制并粘贴 Dropbox 组件上的created函数中的哈希修饰符,并使用该修饰符修改哈希并将值存储在 Vue 实例上。如果哈希不存在,我们将向 path 变量传递一个空字符串:

   window.onhashchange = () => {
    let hash = window.location.hash.substring(1);
    app.path = (hash || '');
   }

我们现在需要将这个 path 变量传递给 Dropbox 组件。添加一个p属性,并将path变量作为视图中的值:

   <div id="app">
    <dropbox-viewer :p="path"></dropbox-viewer>
   </div>

props对象添加到 Dropbox 组件以接受字符串:

   props: {
     p: String
    },

我们现在要在dropbox-viewer组件中添加一个 watch 函数。此函数将监视p道具,并在更新时使用修改后的路径调用updateStructure()方法:

   watch: {
     p() {
      this.updateStructure(this.p);
     }
   }

回到浏览器,我们现在应该能够像以前一样,使用文件夹链接和面包屑作为导航,在 Dropbox 结构中导航。我们现在应该能够使用后退和前进浏览器按钮,加上任何键盘快捷键,也可以在文件夹中导航回来。

在我们进入第 6 章之前,使用 Vuex缓存当前文件夹结构,并使用vuex将文件夹缓存引入我们的应用之前,我们可以对 Dropbox 组件进行一些优化。

首先,在getFolderStructure函数中,我们可以删除 URL 哈希设置为路径的第一行。这是因为 URL 在使用链接时已经更新。从代码中删除此行:

   window.location.hash = path;

第二,现在 Dropbox 组件中有重复的this.path变量和p属性。消除这需要一些轻微的返工,因为你不允许直接修改道具,因为你是与路径;但是,它需要保持同步,以便正确渲染面包屑。

从 Dropbox 组件中的数据对象中删除path属性,并从getFolderStructure函数中删除this.path=路径行。

接下来,将道具更新为等于path,而不是p。这还需要更新watch功能,以查看path变量,而不是p()

将创建的方法更新为只使用this.path作为函数的参数。Dropbox 组件现在应该如下所示:

   Vue.component('dropbox-viewer', {
     template: '#dropbox-viewer-template',

     props: {
      path: String
     },

     data() {
       return {
         accessToken: 'XXXX',
        structure: {},
         isLoading: true
       }
      },

     methods: {
       dropbox() {
         return new Dropbox({
            accessToken: this.accessToken
         });
       },

       getFolderStructure(path) { 
         this.dropbox().filesListFolder({
           path: path, 
          include_media_info: true
         })
          .then(response => {

           const structure = {
            folders: [],
            files: []
           }

          for (let entry of response.entries) {
            // Check ".tag" prop for type
            if(entry['.tag'] == 'folder') {
             structure.folders.push(entry);
             } else {
           }
          }

         this.structure = structure;
         this.isLoading = false;
       })
        .catch(error => {
         this.isLoading = 'error';
         console.log(error);
        });
      },

       updateStructure(path) {
        this.isLoading = true;
        this.getFolderStructure(path);
       }
    },

     created() {
       this.getFolderStructure(this.path);
     },

      watch: {
      path() {
        this.updateStructure(this.path);
      }
     },
   });

更新视图以接受proppath

   <dropbox-viewer :path="path"></dropbox-viewer>

我们现在需要确保父Vue实例在页面加载和哈希更改上都有正确的路径。为了避免重复,我们将用一个方法和一个函数来扩展我们的Vue实例。

将 path 变量设置为空字符串。创建一个名为updateHash()的新方法,从窗口散列中删除第一个字符,然后将path变量设置为散列或空字符串。接下来,创建一个运行updateHash方法的created()函数。

Vue实例现在如下所示:

  const app = new Vue({
    el: '#app',

    data: {
      path: ''
    }, 
    methods: {
 updateHash() {
 let hash = window.location.hash.substring(1);
 this.path = (hash || '');
 }
 },
 created() {
 this.updateHash()
 }
  });

最后,为了消除重复,我们可以在地址栏中的哈希改变时触发updateHash方法:

   window.onhashchange = () => {
     app.updateHash();
   }

最终代码

现在完成我们的代码后,您的视图和 JavaScript 文件应该如下所示。首先,视图应该是这样的:

   <div id="app">
      <dropbox-viewer :path="path"></dropbox-viewer>
    </div>

   <script type="text/x-template" id="dropbox-viewer- 
     template">
    <div>
      <h1>Dropbox</h1>

      <transition name="fade">
        <div v-if="isLoading">
          <div v-if="isLoading == 'error'">
            <p>There seems to be an issue with the URL 
            entered.</p>
            <p><a href="">Go home</a></p>
          </div>
          <div v-else>
            Loading...
          </div>
        </div>
      </transition>

      <transition name="fade">
        <div v-if="!isLoading">
          <breadcrumb :p="path"></breadcrumb>
          <ul>
            <template v-for="entry in structure.folders">
             <folder :f="entry"></folder>
            </template>

           <template v-for="entry in structure.files">
             <file :d="dropbox()" :f="entry"></file>
           </template>
         </ul>
       </div>
      </transition>

     </div>
    </script>

附带的 JavaScript 应用应如下所示:

   Vue.component('breadcrumb', {
        template: '<div>' +
        '<span v-for="(f, i) in folders">' +
         '<a :href="f.path">{{ f.name || 'Home' }}</a>' +
          '<i v-if="i !== (folders.length - 1)"> &raquo; 
           </i>' + '</span>' + 
        '</div>',
      props: {
      p: String
     },
     computed: {
        folders() {
          let output = [],
           slug = '',
           parts = this.p.split('/');

        for (let item of parts) {
          slug += item;
            output.push({'name': item || 'home', 'path': 
            '#' + slug});
          slug += '/';
         }

         return output;
        }
      }
    });

    Vue.component('folder', {
       template: '<li><strong><a :href="\'#\' + 
       f.path_lower">{{ f.name }}</a></strong></li>',
      props: {
       f: Object
      }
   });

   Vue.component('file', {
         template: '<li><strong>{{ f.name }}</strong><span 
         v-if="f.size"> - {{ bytesToSize(f.size) }}</span>
         <span v-if="link"> - <a :href="link">Download</a>
         </span></li>',
        props: {
        f: Object,
         d: Object
       },

     data() {
      return {
        byteSizes: ['Bytes', 'KB', 'MB', 'GB', 'TB'],
        link: false
       }
      },

    methods: {
       bytesToSize(bytes) {
        // Set a default
        let output = '0 Byte';

        // If the bytes are bigger than 0
         if (bytes > 0) {
          // Divide by 1024 and make an int
          let i = parseInt(Math.floor(Math.log(bytes) / 
           Math.log(1024)));
        // Round to 2 decimal places and select the 
           appropriate unit from the array
         output = Math.round(bytes / Math.pow(1024, i), 2)   
         + ' ' + this.byteSizes[i];
       }

       return output
      }
    },

     created() {
       this.d.filesGetTemporaryLink({path:   
       this.f.path_lower}).then(data => {
         this.link = data.link;
       });
      },
    });

     Vue.component('dropbox-viewer', {
       template: '#dropbox-viewer-template',

     props: {
       path: String
      },

     data() {
       return {
       accessToken: 'XXXX',
       structure: {},
       isLoading: true
     }
    },

     methods: {
      dropbox() {
        return new Dropbox({
          accessToken: this.accessToken
        });
      },

     getFolderStructure(path) { 
      this.dropbox().filesListFolder({
        path: path, 
        include_media_info: true
      })
      .then(response => {

        const structure = {
          folders: [],
          files: []
        }

        for (let entry of response.entries) {
          // Check ".tag" prop for type
          if(entry['.tag'] == 'folder') {
            structure.folders.push(entry);
          } else {
            structure.files.push(entry);
          }
        }

          this.structure = structure;
          this.isLoading = false;
        })
        .catch(error => {
         this.isLoading = 'error';
         console.log(error);
        });
     },

     updateStructure(path) {
       this.isLoading = true;
       this.getFolderStructure(path);
      }
    },

    created() {
     this.getFolderStructure(this.path);
    },

   watch: {
     path() {
       this.updateStructure(this.path);
       }
     },
  });

     const app = new Vue({
      el: '#app',

       data: {
       path: ''
      }, 
    methods: {
     updateHash() {
        let hash = window.location.hash.substring(1);
        this.path = (hash || '');
      }
    },
     created() {
      this.updateHash()
     }
  });

   window.onhashchange = () => {
   app.updateHash();
 }

总结

我们现在有一个功能齐全的 Dropbox viewer 应用,具有文件夹导航和文件下载链接。我们可以使用文件夹链接或面包屑进行导航,并使用后退和/或前进按钮。我们还可以共享或添加链接书签,并加载该文件夹的内容

第 6 章使用 Vuex缓存当前文件夹结构,我们将通过使用 Vuex 缓存当前文件夹内容来加快导航过程。