七、预缓存其他文件夹和文件来加快导航速度

在本章(本节的最后一章)中,我们将通过在应用中引入缓存来进一步加快 Dropbox 文件浏览器的速度。到目前为止,我们已经构建了一个可以查询 Dropbox API 并返回文件和文件夹的应用。从那里,我们添加了文件夹导航,包括更新链接共享的 URL,并能够使用后退和前进按钮。在第 6 章使用 Vuex缓存当前文件夹结构,我们引入了 Vuex 来存储当前文件夹路径和我们访问过的文件夹内容。

本章将介绍:

  • 不仅预缓存用户当前所在的文件夹,而且预缓存子文件夹。这将通过循环当前显示中的文件夹并检查它们是否已被缓存来完成。如果没有,我们可以从 API 收集数据。
  • 如果用户通过直接 URL 输入,则存储父文件夹的内容。这将通过利用面包屑路径向上遍历树来完成。
  • 缓存文件的下载链接。目前,无论我们的代码是否缓存了文件夹,遇到的每个文件都需要一个 API。

通过这些改进,我们可以确保应用只为每个项目联系 API 一次,而不是像最初那样无数次。

缓存子文件夹

使用子文件夹和父文件夹缓存,我们不一定要编写新代码,而是将现有代码重新组织并重新调整用途,使其成为一个更模块化的系统,以便可以分别调用每个部分。

以下流程图应帮助您可视化缓存当前文件夹和子文件夹所需的步骤:

查看流程图时,您可以立即看到应用所需的事件中存在一些重复。在两个点上,应用需要确定缓存中是否存在文件夹,如果不存在,则查询 API 以获取数据并存储结果。虽然它在流程图上只出现两次,但此功能需要多次,对于当前位置的每个文件夹都需要一次。

我们还需要将显示逻辑与查询和存储逻辑分开,因为我们可能需要从 API 和存储中加载,而无需更新视图。

规划应用方法

记住前面的部分,我们可以借此机会修改和重构dropbox-viewer应用上的方法,确保每个操作都有自己的方法。这将允许我们在需要的时候调用每个动作。在开始编写代码之前,让我们根据前面的流程图规划需要创建的方法。

首先要注意的是,每次查询 API 时,我们都需要将结果存储在缓存中。由于我们不需要在缓存中存储任何东西,除非调用了API,所以我们可以用相同的方法将这两个操作组合起来。我们还经常需要检查缓存中是否有特定路径的内容,并从 API 加载或检索它。我们可以将其添加到它自己的返回数据的方法中。

让我们规划出需要创建的方法:

  • getFolderStructure:此方法将接受路径的单个参数,并返回文件夹条目的对象。这将负责检查数据是否在缓存中,如果不在缓存中,则查询 Dropbox API。
  • displayFolderStructure:此方法将触发前面的功能,并使用数据更新组件上的structure对象,以在视图中显示文件和文件夹。
  • cacheFolderStructure:此方法将包括缓存每个子文件夹的getFolderStructure方法。我们将探讨几种触发方法。

我们可能需要创建更多的方法,但这三种方法将是组件的主干。我们将保留路径和段塞计算属性,以及dropbox()方法。删除其余的对象、方法和函数,使您的dropbox-viewer回归基本:

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

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

  computed: {
    path() {
      return this.$store.state.path
    },
    slug() {
      return this.path.toLowerCase()
        .replace(/^\/|\/$/g, '')
        .replace(/ /g,'-')
        .replace(/\//g,'-')
        .replace(/[-]+/g, '-')
        .replace(/[^\w-]+/g,'');
    }
  },

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

创建 getFolderStructure 方法

在组件上创建一个名为getFolderStructure的新方法。如前所述,此方法需要接受单个路径参数。这样我们就可以同时使用当前路径和子路径:

getFolderStructure(path) {

}

此方法需要检查缓存并返回数据。在方法内部创建一个名为output的新变量并返回:

getFolderStructure(path) {
 let output;

 return output;
}

第 6 章中缓存数据时,使用 Vuex缓存当前文件夹结构时,我们使用slug作为存储中的密钥。使用当前路径生成slug;但是,我们不能在新方法中使用它,因为它固定在当前位置。

创建一个名为generateSlug的新方法。这将接受一个参数 path,并使用 slug computed 函数中的替换项返回转换后的字符串:

generateSlug(path) {
  return path.toLowerCase()
    .replace(/^\/|\/$/g, '')
    .replace(/ /g,'-')
    .replace(/\//g,'-')
    .replace(/[-]+/g, '-')
    .replace(/[^\w-]+/g,'');
}

我们现在可以删除计算出的slug函数,因此没有任何重复代码。

回到我们的getFolderStructure方法,使用新方法创建一个新变量来存储路径的 slug 版本。为此,我们将使用const创建一个无法更改的变量:

getFolderStructure(path) {
  let output;

  const slug = this.generateSlug(path);

  return output;
}

我们将创建的最后一个变量是数据路径,正如我们在第 8 章中所做的,介绍了 Vue 路由和加载基于 URL 的组件。这将使用我们刚刚创建的新slug变量:

getFolderStructure(path) {
  let output;

  const slug = this.generateSlug(path),
      data = this.$store.state.structure[slug];

  return output;
}

我们现在可以在这里使用前面代码中的data``if语句,并为 Dropbox 函数调用留出空间。如果店内有data可以直接分配给output

getFolderStructure(path) {
  let output;

  const slug = this.generateSlug(path),
      data = this.$store.state.structure[slug];

 if(data) {
 output = data;
 } else {

 }

  return output;
}

然而,通过 Dropbox API 调用,我们可以调整它以适应新代码。以前,它是从 API 检索数据,然后触发一个方法,然后保存并显示结构。由于我们需要将检索到的数据存储在output变量中,因此我们将改变数据流。我们将利用这个机会首先将响应存储在缓存中,然后将数据返回到output变量,而不是触发一个方法。

由于我们只使用 API 调用中的条目,因此我们还将更新存储,以仅缓存响应的这一部分。这将减少应用的代码和复杂性:

getFolderStructure(path) {
  let output;

  const slug = this.generateSlug(path),
      data = this.$store.state.structure[slug];

  if(data) {
    output = data;
  } else {

    output = this.dropbox().filesListFolder({
 path: path, 
 include_media_info: true
 })
 .then(response => {
 let entries = response.entries;
 this.$store.commit('structure', {
 path: slug,
 data: entries
 });

 return entries;
 })
 .catch(error => {
 this.isLoading = 'error';
 console.log(error);
 });

  }

  return output;
}

DropboxfilesListFolder方法使用传入的path变量,而不是以前使用的全局变量。响应中的条目随后存储在变量中,然后使用相同的变量缓存到 Vuex 存储中。然后从 promise 返回entries变量,该变量将结果存储在output中。catch()功能与之前相同。

通过从缓存或 API 返回数据,我们可以在创建组件和更新路径时触发并处理这些数据。然而,在此之前,我们需要处理多种数据类型的混合。

从 API 返回时,数据仍然是需要解决的承诺;将其分配给变量只会传递稍后解决的承诺。然而,来自存储区的数据是一个处理方式非常不同的普通数组。为了给我们一个要处理的单一数据类型,我们将resolve存储的数组作为承诺,这意味着getFolderStructure返回一个承诺,而不管数据从何处加载:

getFolderStructure(path) {
  let output;

  const slug = this.generateSlug(path),
      data = this.$store.state.structure[slug];

  if(data) {
    output = Promise.resolve(data);
  } else {

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

      this.$store.commit('structure', {
        path: slug,
        data: entries
      });

      return entries;
    })
    .catch(error => {
      this.isLoading = 'error';
      console.log(error);
    });

  }
  return output;
}

通过这个getFolderStructure方法,我们现在可以从 API 加载一些数据,并将结果存储在全局缓存中,而无需更新视图。但是,如果我们希望使用 JavaScript 进一步处理它,该函数确实会返回信息。

我们现在可以继续创建下一个方法displayFolderStructure,它将获取我们刚刚创建的方法的结果并更新我们的视图,因此应用再次可以导航。

使用 displayFolderStructure 方法显示数据

现在,我们的数据已经准备好缓存并从存储中提供,我们可以继续使用我们的新方法来实际地显示数据。在标记为displayFolderStructuredropbox-viewer组件中创建新方法:

displayFolderStructure() {

} 

此方法将从该组件的前一个版本中借用大量代码。请记住,此方法仅用于显示文件夹,与缓存内容无关。

该方法的过程将是:

  1. 在 app 中设置加载状态为active。这让用户知道发生了什么。
  2. 创建一个空的structure对象。
  3. 加载getFolderStructure方法的内容。
  4. 循环遍历结果并将每个项添加到foldersfiles数组中。
  5. 将全局结构对象设置为新创建的对象。
  6. 将加载状态设置为false,以便显示内容。

将加载状态设置为 true 并创建空结构对象

此方法的第一步是隐藏结构树并显示加载消息。这可以通过将isLoading变量设置为true来完成。我们也可以在这里创建空的structure对象,准备由数据填充:

displayFolderStructure() {
 this.isLoading = true;

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

加载 getFolderStructure 方法的内容

由于getFolderStructure方法返回一个承诺,我们需要在继续操作它之前解析结果。这是通过.then()功能完成的;我们已经在 Dropbox 类中使用了它。调用该方法,然后将结果分配给变量:

displayFolderStructure() {
  this.isLoading = true;

  const structure = {
    folders: [],
    files: []
  }
 this.getFolderStructure(this.path).then(data => {

 });
}

此代码将组件的path对象传递到方法中。此路径是用户试图查看的当前路径。一旦返回到函数中,我们就可以把数据分配给它。

循环遍历结果并将每个项添加到文件夹或文件数组中

我们已经熟悉循环遍历条目并检查每个条目的.tag属性的代码。如果生成文件夹,则将其添加到structure.folders数组中,否则将追加到structure.files

我们只将条目存储在缓存中,因此请确保更新for循环以按原样使用数据,而不是访问条目的属性:

displayFolderStructure() {
  this.isLoading = true;

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

  this.getFolderStructure(this.path).then(data => {

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

更新全局结构对象并删除加载状态

此方法中的最后一个任务是更新全局结构并删除加载状态。此代码与以前的代码相同:

displayFolderStructure() {
  this.isLoading = true;

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

  this.getFolderStructure(this.path).then(data => {

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

    this.structure = structure;
 this.isLoading = false;
  });
}

我们现在有了一个显示数据检索结果的方法。

鼓动这种方法

现在可以在创建dropbox-viewer组件时调用此方法。由于全局 Vue 实例上的created函数将 URL 哈希提交到存储区,从而创建 path 变量,因此路径将已经填充。因此,我们不需要向函数传递任何内容。将created函数添加到组件中,并在其中调用新方法:

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

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

  computed: {
    ...
  },

  methods: {

    ...
  },

 created() {
 this.displayFolderStructure();
 }
});

现在刷新应用将加载文件夹内容。更新 URL 哈希并重新加载页面也将显示该文件夹的内容;但是,单击任何文件夹链接都将更新面包屑,但不会更新数据结构。这可以通过观察计算出的path变量来解决。这将在散列更新时更新,因此可以触发watch对象中的函数。添加一个函数,用于监视path变量的更新,并在出现以下情况时触发新方法:

  created() {
    this.displayFolderStructure();
  },

  watch: {
 path() {
 this.displayFolderStructure();
 }
 }

一旦我们访问了此应用,我们将再次使用您创建的任何文件夹进行缓存。第一次点击结构看起来很慢,但是一旦你回到树并重新进入子文件夹,你几乎看不到加载屏幕。

尽管该应用具有与本章开头相同的功能,但我们对代码进行了重构,以分离数据的检索、缓存和显示。让我们继续通过预缓存所选路径的子文件夹来进一步增强我们的应用。

缓存子文件夹

现在我们可以缓存文件夹而不更新 Vue,我们可以使用structure对象来获取子文件夹的内容。使用structure对象中的folders数组,我们可以循环遍历该对象并依次缓存每个文件夹

我们必须确保不会妨碍应用的性能;缓存必须异步完成,因此用户不知道这个过程。我们还需要确保没有不必要地运行缓存。

为了达到这个目的,我们可以观察structure对象。只有在从缓存或 API 加载数据并更新 Vue 后,才会更新此选项。当用户查看文件夹的内容时,我们可以继续循环文件夹以存储其内容。

然而,有一个小问题。如果我们观察structure变量,我们的代码将永远不会运行,因为对象的直接内容不会更新,尽管我们每次都用一个新的对象替换structure对象。从一个文件夹到另一个文件夹,结构对象总是有两个键,filesfolders,这两个键都是数组。就 Vue 和 JavaScript 而言,structure对象永远不会改变。

然而,Vue 可以通过deep变量检测嵌套的更改。这可以在每个变量的基础上启用。与组件上的道具类似,要在 watch 属性上启用更多选项,可以向其传递对象而不是直接函数。

为结构创建一个新的watch键,它是一个具有两个值的对象,deephandlerdeep键将设置为true,而handler将是变量更改时触发的功能:

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

  structure: {
 deep: true,
 handler() {

 }
 }
}

在这个handler中,我们现在可以循环遍历每个文件夹,并使用每个文件夹的path_lower属性作为函数参数,为每个文件夹运行getFolderStructure方法:

structure: {
  deep: true,
  handler() {
    for (let folder of this.structure.folders) {
 this.getFolderStructure(folder.path_lower);
 }
  }
}

通过这段简单的代码,我们的应用的速度似乎提高了十倍。您导航到的每个子文件夹都会立即加载(除非您有一个特别长的文件夹列表,并且您可以很快导航到最后一个文件夹)。为了让您了解缓存的速度和时间,请在您的getFolderStructure方法中添加console.log()并打开浏览器开发工具:

if(data) {
  output = Promise.resolve(data);
} else {

  console.log(`API query for ${path}`);
  output = this.dropbox().filesListFolder({
    path: path, 
    include_media_info: true
  })
  .then(response => {
    console.log(`Response for ${path}`);

    ... 

这使您可以看到所有 API 调用都是异步完成的,应用也不会等待加载和缓存上一个文件夹,然后再移动到下一个文件夹。这样做的好处是允许缓存较小的文件夹,而无需等待从 API 返回较大的文件夹。

替代缓存方法

与所有事情一样,在制作应用时,有许多方法可以达到相同的效果。这种方法的缺点是,即使您的文件夹只包含文件,也会触发此函数,尽管无需执行任何操作。

另一种方法是再次使用created函数,这次是在folder组件本身上,以路径作为参数触发父方法。

一种方法是使用$parent属性。在folder组件中,使用this.$parent将允许访问dropbox-viewer组件上的变量、方法和计算值。

folder组件中添加created函数,并从 Dropbox 组件中删除structure``watch属性。从那里调用父getFolderStructure方法:

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

预览应用证明了该方法的有效性。只有在结构中有文件夹时才会触发,这种更干净的技术将文件夹缓存与文件夹本身联系起来,而不是与 Dropbox 代码混合在一起。

但是,除非必要,否则应避免使用this.$parent,并且仅应在边缘情况下使用。既然我们有机会使用道具,我们就应该这样做。它还让我们有机会在文件夹上下文中为函数指定一个更有意义的名称。

导航到 HTML 视图并更新文件夹组件以接受新道具。我们将调用 prop 缓存并将函数作为值传入。由于属性是动态的,请不要忘记在前面添加冒号:

<folder :f="entry" :cache="getFolderStructure"></folder>

cache关键字添加到 JavaScriptfolder组件中的道具键。通知 Vue 输入将是一个函数:

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

最后,我们可以在created函数中调用cache()

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

可以像以前一样使用控制台日志再次验证缓存。这将创建更清晰的代码,使您自己和其他开发人员更容易阅读。

随着 Dropbox 应用的发展,如果您在 URL 中使用哈希输入子文件夹,我们可以继续缓存父文件夹。

缓存父文件夹

缓存父结构是我们可以做的下一件抢占式事情,以帮助加快我们的应用。假设我们已导航到图像目 img/holiday/summer,并希望与朋友或同事共享。我们会在 URL 散列中向他们发送 URL,在页面加载时,他们会看到内容。例如,如果他们随后使用面包屑导航到树上 img/holiday,他们将需要等待应用检索内容

使用breadcrumb组件,我们可以缓存父目录,因此,当导航到holiday文件夹时,用户将立即看到其内容。当用户浏览此文件夹时,其所有子文件夹都将使用以前的方法进行缓存。

为了缓存父文件夹,我们已经有了一个组件,该组件显示了可以通过面包屑循环访问所有父文件夹段塞的路径

在开始缓存过程之前,我们需要更新组件中的folders计算函数。这是因为当前,我们存储的路径前面有散列,这为 Dropbox API 创建了一个无效路径。移除推送到输出数组的对象的散列,并将其添加到模板中,方式与folder组件类似:

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>',
  computed: {
    folders() {
      let output = [],
        slug = '',
        parts = this.$store.state.path.split('/');

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

      return output;
    }
  }
});

我们现在可以使用输出来显示面包屑和缓存父结构。

第一步是允许breadcrumb组件访问缓存功能。以类似于folder组件的方式,将函数作为道具添加到视图中的breadcrumb组件中:

<breadcrumb :cache="getFolderStructure"></breadcrumb>

props对象添加到 JavaScript 代码中的组件中。将cache道具声明为一个函数,以便 Vue 知道需要什么:

Vue.component('breadcrumb', {
  template: '...',
 props: {
 cache: Function
 },
  computed: {
    folders() {
      ...
  }
});

父结构将创建breadcrumb组件。但是,由于我们不希望这会阻碍加载过程,我们将在组件为mounted而不是created时触发它。

向组件添加一个mounted函数,并将文件夹的计算值分配给一个变量:

Vue.component('breadcrumb', {
  template: '...',
  props: {
    cache: Function
  },
  computed: {
    folders() {
      ...
    }
  },
  mounted() {
 let parents = this.folders;
 }
});

我们现在需要开始缓存文件夹;然而,我们可以按照我们做这件事的顺序来做。我们可以假设用户通常会返回文件夹树,因此理想情况下,我们应该在移动到其父级之前缓存直接父级,以此类推。由于文件夹的变量是自上而下的,我们需要将其反转。

我们可以做的另一件提高性能的事情是删除当前文件夹;因为我们已经在里面了,应用应该已经缓存了它。在组件中,反转阵列并删除第一项:

mounted() {
  let parents = this.folders;
  parents.reverse().shift();
}

如果我们将控制台日志添加到父变量的函数中,我们可以看到它包含我们现在希望缓存的文件夹。我们现在可以循环这个数组,为数组中的每个项目调用cache函数:

mounted() {
  let parents = this.folders;
  parents.reverse().shift();

  for(let parent of parents) {
 this.cache(parent.path);
 }
}

这样,我们的父文件夹和子文件夹就会被应用缓存,从而使树上下的导航速度极快。但是,在mounted函数中运行console.log()会发现每次导航到文件夹时,面包屑都会重新装入。这是因为视图中的v-if语句每次都会删除和添加 HTML

由于我们只需要缓存父文件夹一次,在初始应用加载时,让我们看看如何更改触发父文件夹的位置。我们只需要第一次运行这个函数;一旦用户开始在树上上上下导航,访问的所有文件夹都将被缓存。

缓存父文件夹一次

为了确保使用最少的资源,我们可以将用于面包屑的文件夹阵列保留在存储中。这意味着breadcrumb组件和我们的父缓存函数都可以访问同一个数组。

breadcrumb键添加到存储状态这是我们存储阵列的位置:

const store = new Vuex.Store({
  state: {
    path: '',
    structure: {},
    breadcrumb: []
  },
  mutations: {
    updateHash(state) {
      let hash = window.location.hash.substring(1);
      state.path = (hash || '');
    },
    structure(state, payload) {
      state.structure[payload.path] = payload.data;
    }
  }
});

接下来,将代码从breadcrumb组件移动到updateHash突变中,以便我们可以更新pathbreadcrumb变量:

updateHash(state) {
  let hash = window.location.hash.substring(1);
  state.path = (hash || '');

 let output = [],
 slug = '',
 parts = state.path.split('/');

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

 state.breadcrumb = output;
},

请注意,它不是返回output数组,而是存储在state对象中。我们现在可以在breadcrumb组件上更新文件夹的计算函数,以返回存储数据:

computed: {
  folders() {
 return this.$store.state.breadcrumb;
 }
}

有了现在全球可用的数据,我们可以在dropbox-viewer组件cacheParentFolders上创建一个新方法,触发我们为breadcrumb组件编写的代码。

Dropbox组件上创建一个新方法,并将代码移动到该方法中。更新父级的位置,并确保触发的路径正确:

cacheParentFolders() {
  let parents = this.$store.state.breadcrumb;
  parents.reverse().shift();
  for(let parent of parents) {
    this.getFolderStructure(parent.path);
  }
}

现在,我们可以在创建 Dropbox 组件时触发此方法一次。在created函数中已有方法调用后添加:

created() {
  this.displayFolderStructure();
  this.cacheParentFolders();
}

我们现在可以做一些整理工作,从breadcrumb组件中删除mounted方法,同时从视图中删除props对象和:cache道具。这意味着我们的breadcrumb组件现在比以前更简单:

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>',
  computed: {
    folders() {
      return this.$store.state.breadcrumb;
    }
  }
});

HTML 返回到原来的状态:

<breadcrumb></breadcrumb>

我们也可以将店内的updateHash突变整理得更整洁、更容易理解:

updateHash(state, val) {
  let path = (window.location.hash.substring(1) || ''),
    breadcrumb = [],
    slug = '',
    parts = path.split('/');

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

  state.path = path
  state.breadcrumb = breadcrumb;
}

所有变量现在都在顶部声明,state在底部更新。变量的数量也减少了。

现在查看应用,它似乎工作正常;然而,仔细观察,breadcrumb似乎与初始页面加载时的文件夹结构有点滞后。一旦一个文件夹被导航到,它就会赶上,但在第一次加载时,它似乎少了一个项目,而在查看 Dropbox 的根目录时,一个项目也没有。

这是因为在我们提交updateHash突变之前,存储尚未完全初始化。如果我们回想一下 Vue 实例生命周期,第 4 章使用 Dropbox API获取文件列表,我们可以看到创建的函数很早就启动了。更新主 Vue 实例以触发mounted上的突变可以解决以下问题:

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

  store,
  mounted() {
    store.commit('updateHash');
  }
});

所有的文件夹都被缓存了,我们可以通过存储每个文件的下载链接来缓存更多的 API 调用

我们还可以研究缓存子文件夹的子文件夹,循环遍历每个缓存文件夹的内容,最终缓存整个树。我们不想讨论这个问题,但你可以自己尝试一下。

缓存文件上的下载链接

当用户在文档树中导航时,Dropbox API 的查询仍然超出了需要。这是因为每次显示文件时,我们都会查询 API 以检索下载链接。通过将下载链接响应存储在缓存中并重新显示其导航回的文件夹,可以否定额外的 API 查询。

每次显示文件时,都会使用存储区中的数据初始化一个新组件。我们可以利用这一点,因为这意味着我们只需要更新组件实例,然后缓存结果。

在您的文件组件中,更新 API 响应,不仅保存数据属性的link属性上的结果,还保存文件实例f上的结果。这将被存储为新密钥download_link

在存储数据时,我们可以使用两个等号将它们组合成一个命令,而不是使用两个单独的命令:

Vue.component('file', {
  template: '<li><strong>{{ f.name }}</strong><span v-if="f.size"> - {{ bytesToSize(f.size) }}</span> - <a v-if="link" :href="link">Download</a></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.f.download_link = this.link = data.link;
      });
  }
});

这实质上意味着this.f.download_link等于this.link,也等于data.link,API 的下载链接。当导航到文件夹时,存储并显示该数据,我们可以添加一个if语句来查看数据是否存在,如果不存在,则查询 API 以获取数据。

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

在创建文件时执行此操作可以避免不必要地查询 API。如果我们在缓存文件夹时获得这些信息,我们可能会减慢应用的速度,并存储非必要的信息。假设一个文件夹中有数百张照片,我们不希望在用户可能进入该文件夹的情况下查询 API 中的每一张照片。

这意味着我们应用中的所有内容只需查询一次 API 即可获得信息。用户可以任意多次在文件夹结构上下导航,应用的速度也会随之加快。

完整的代码和添加的文档

随着我们的应用完成,我们现在可以添加一些急需的文档。编写代码文档总是很好的,因为这样可以对代码进行推理和解释。好的文档不应该仅仅说明代码做了什么,还应该说明它为什么做,什么是允许的,什么是不允许的。

一种流行的文档编制方法是 JavaScript DocBlock 标准。这组约定列出了类似样式指南的规则,供您在编写代码文档时遵循。DocBlock 在注释块中格式化,其特征是以@开头的关键字,例如@author@example,或者列出函数可以接受的参数和@param关键字。例如:

/**
 * Displays a folder with a link and cache its contents
 * @example <folder :f="entry" :cache="getFolderStructure"></folder>
 *
 * @param {object} f The folder entry from the tree
 * @param {function} cache The getFolderStructure method from the dropbox-viewer component
 */

从描述开始,DocBlock 有几个关键字来帮助布置文档。我们将浏览完整的 Dropbox 应用,并添加文档。

我们先来看看breadcrumb组件:

/**
 * Displays the folder tree breadcrumb
 * @example <breadcrumb></breadcrumb>
 */
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>',

  computed: {
    folders() {
      return this.$store.state.breadcrumb;
    }
  }
});

转到folder组件:

/**
 * Displays a folder with a link and cache its contents
 * @example <folder :f="entry" :cache="getFolderStructure"></folder>
 *
 * @param {object} f The folder entry from the tree
 * @param {function} cache The getFolderStructure method from the dropbox-viewer component
 */
Vue.component('folder', {
  template: '<li><strong><a :href="\'#\' + f.path_lower">{{ f.name }}</a></strong></li>',
  props: {
    f: Object,
    cache: Function
  },
  created() {
    // Cache the contents of the folder
    this.cache(this.f.path_lower);
  }
});

接下来,我们将看到file组件:

/**
 * File component display size of file and download link
 * @example <file :d="dropbox()" :f="entry"></file>
 * 
 * @param {object} f The file entry from the tree
 * @param {object} d The dropbox instance from the parent component
 */
Vue.component('file', {
  template: '<li><strong>{{ f.name }}</strong><span v-if="f.size"> - {{ bytesToSize(f.size) }}</span> - <a v-if="link" :href="link">Download</a></li>',
  props: {
    f: Object,
    d: Object
  },

  data() {
    return {
      // List of file size
      byteSizes: ['Bytes', 'KB', 'MB', 'GB', 'TB'],

      // The download link
      link: false
    }
  },

  methods: {
    /**
     * Convert an integer to a human readable file size
     * @param {integer} bytes
     * @return {string}
     */
    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() {
    // If the download link has be retrieved from the API, use it
    // if not, aquery the API
    if(this.f.download_link) {
      this.link = this.f.download_link;
    } else {
      this.d.filesGetTemporaryLink({path: this.f.path_lower})
        .then(data => {
          this.f.download_link = this.link = data.link;
        });
    }
  }
});

现在我们来看一下dropbox-viewer组件:

/**
 * The dropbox component
 * @example <dropbox-viewer></dropbox-viewer>
 */
Vue.component('dropbox-viewer', {
  template: '#dropbox-viewer-template',

  data() {
    return {
      // Dropbox API token
      accessToken: 'XXXX',

      // Current folder structure
      structure: {},
      isLoading: true
    }
  },

  computed: {
    // The current folder path
    path() {
      return this.$store.state.path
    }
  },

  methods: {

    /**
     * Dropbox API instance
     * @return {object}
     */
    dropbox() {
      return new Dropbox({
        accessToken: this.accessToken
      });
    },

    /**
     * @param {string} path The path to a folder
     * @return {string} A cache-friendly URL without punctuation/symbals
     */
    generateSlug(path) {
      return path.toLowerCase()
        .replace(/^\/|\/$/g, '')
        .replace(/ /g,'-')
        .replace(/\//g,'-')
        .replace(/[-]+/g, '-')
        .replace(/[^\w-]+/g,'');
    },

    /**
     * Retrieve the folder structure form the cache or Dropbox API
     * @param {string} path The folder path
     * @return {Promise} A promise containing the folder data
     */
    getFolderStructure(path) {
      let output;

      const slug = this.generateSlug(path),
          data = this.$store.state.structure[slug];

      if(data) {
        output = Promise.resolve(data);
      } else {
        output = this.dropbox().filesListFolder({
          path: path, 
          include_media_info: true
        })
        .then(response => {
          let entries = response.entries;

          this.$store.commit('structure', {
            path: slug,
            data: entries
          });

          return entries;
        })
        .catch(error => {
          this.isLoading = 'error';
          console.log(error);
        });

      }
      return output;
    },

    /**
     * Display the contents of getFolderStructure
     * Updates the output to display the folders and folders
     */
    displayFolderStructure() {
      // Set the app to loading
      this.isLoading = true;

      // Create an empty object
      const structure = {
        folders: [],
        files: []
      }

      // Get the structure
      this.getFolderStructure(this.path).then(data => {

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

        // Update the data object
        this.structure = structure;
        this.isLoading = false;
      });
    },

    /**
     * Loop through the breadcrumb and cache parent folders
     */
    cacheParentFolders() {
      let parents = this.$store.state.breadcrumb;
      parents.reverse().shift();

      for(let parent of parents) {
        this.getFolderStructure(parent.path);
      }
    }
  },

  created() {
    // Display the current path & cache parent folders
    this.displayFolderStructure();
    this.cacheParentFolders();
  },

  watch: {
    // Update the view when the path gets updated
    path() {
      this.displayFolderStructure();
    }
  }
});

我们也来看看 Vuex 商店:

/**
 * The Vuex Store
 */
const store = new Vuex.Store({
  state: {
    // Current folder path
    path: '',

    // The current breadcrumb
    breadcrumb: [],

    // The cached folder contents
    structure: {},
  },
  mutations: {
    /**
     * Update the path & breadcrumb components
     * @param {object} state The state object of the store
     */
    updateHash(state) {

      let path = (window.location.hash.substring(1) || ''),
        breadcrumb = [],
        slug = '',
        parts = path.split('/');

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

      state.path = path
      state.breadcrumb = breadcrumb;
    },

    /**
     * Cache a folder structure
     * @param {object} state The state objet of the store
     * @param {object} payload An object containing the slug and data to store
     */
    structure(state, payload) {
      state.structure[payload.path] = payload.data;
    }
  }
});

我们进一步转向 Vue 应用

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

  // Initialize the store
  store,

  // Update the current path on page load
  mounted() {
    store.commit('updateHash');
  }
});

最后,我们通过window.onhashchange函数:

/**
 * Update the path & store when the URL hash changes
 */
window.onhashchange = () => {
  app.$store.commit('updateHash');
}

最后,视图中的 HTML 如下所示:

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

Dropbox 查看器的模板如下所示:

<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></breadcrumb>
        <ul>
          <template v-for="entry in structure.folders">
            <folder :f="entry" :cache="getFolderStructure"></folder>
          </template>

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

  </div>
</script>

您会注意到,并非所有的都已记录在案。一个简单的函数或变量赋值不需要重新解释它的作用,但对主要变量的注释将有助于任何人在将来查看它。

总结

在本书的这一部分中,我们涵盖了很多内容!我们首先查询 Dropbox API 以获得文件和文件夹的列表。然后我们继续添加导航,允许用户单击文件夹并下载文件。然后我们将 Vuex 和商店引入我们的应用,这意味着我们可以集中路径、面包屑,最重要的是,缓存文件夹内容。最后,我们查看了缓存子文件夹和文件下载链接。

在本书的下一节中,我们将介绍如何创建一个商店。这将包括使用名为 Vue 路由的新 Vue 插件浏览类别中的产品和产品页面。我们还将考虑将产品添加到购物篮中,并将产品列表和首选项存储在 Vuex 商店中。