六、使用 Vuex 缓存当前文件夹结构

在本章中,我们将介绍一个名为 Vuex 的官方 Vue 插件。Vuex 是一种状态管理模式和库,它允许您为所有 Vue 组件建立一个集中存储,而与它们是子组件还是 Vue 实例无关。它为我们提供了一种集中、简单的方法,使我们的数据在整个应用中保持同步。

本章将涵盖:

  • 开始使用 Vuex
  • 从 Vuex 存储区存储和检索数据
  • 将 Vuex 与我们的 Dropbox 应用集成
  • 缓存当前 Dropbox 文件夹内容并在需要时从存储加载数据

Vue 应用的每个部分都可以更新中央存储,而其他部分可以根据这些信息做出反应并更新其数据和状态,而不是要求每个组件上都有自定义事件和$emit功能,并试图使组件和子组件保持最新。它还为我们提供了一个存储数据的公共位置,因此,我们可以使用 Vuex 存储,而不是试图确定将数据对象放置在组件、父组件或 Vue 实例上是否更具语义。

Vuex 还集成到 Vue 开发工具中第 12 章使用 Vue 开发工具和测试 SPA中介绍了这一点。通过集成库,可以轻松调试和查看存储的当前和过去状态。开发工具反映状态更改或数据更新,并允许您检查存储的每个部分。

如前所述,Vuex 是一种状态管理模式,它是 Vue 应用的真实来源。例如,跟踪购物篮或登录用户对某些应用至关重要,如果这些数据在组件之间不同步,可能会造成严重破坏。如果不使用父组件来处理交换,也不可能在子组件之间传递数据。Vuex 通过处理数据的存储、变异和操作,消除了这种复杂性。

当最初使用 Vuex 时,它可能看起来非常冗长,并且对于所需的内容来说似乎有些过分;然而,这是一个很好的例子,可以让你了解图书馆。有关 Vuex 的更多信息,请参见其文档。

对于我们的 Dropbox 应用,Vuex 应用商店可用于存储文件夹结构、文件列表和下载链接。这意味着,如果用户多次访问同一文件夹,则无需查询 API,因为所有信息都已存储。这将加快文件夹的导航速度。

包括并初始化 Vuex

Vuex 库的包含方式与 Vue 本身相同。您可以通过使用前面提到的 unpkg 服务(来使用托管版本 https://unpkg.com/vuex 或您可以从他们的下载 JavaScript 库 https://github.com/vuejs/vuex

在 HTML 文件的底部添加一个新的<script>块。确保 Vuex 库包含在vue.js库之后,但在应用 JavaScript 之前:

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript" src="js/vuex.js"></script>
<script type="text/javascript" src="js/dropbox.js"></script>
<script type="text/javascript" src="js/app.js"></script>

If you are deploying an app with several JavaScript files, it is worth investigating whether it is more efficient to combine and compress them into one file or configure your server to use HTTP/2 push. 

包含库后,我们可以初始化应用并将其包含在应用中。创建一个名为store的新变量,并初始化Vuex.Store类,将其分配给变量:

const store = new Vuex.Store({

});

Vuex 存储区初始化后,我们现在可以通过store变量利用其功能。使用store,我们可以访问其中的数据,并通过突变改变该数据。通过一个独立的store,很多 Vue 实例可以更新相同的store;这在某些情况下可能是需要的,但在其他情况下可能是不希望的副作用。

为了避免这种情况,我们可以将存储与特定的 Vue 实例相关联。这是通过将store变量传递给我们的 Vue 类来完成的。这样做还会将store实例注入到所有子组件中。虽然我们的应用并非严格要求,但养成将商店与应用关联的习惯是一种良好的做法:

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

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

添加了store变量后,我们现在可以使用this.$store变量访问组件中的store

利用商店

为了帮助我们掌握如何使用存储,让我们移动当前存储在父 Vue 实例上的path变量。在开始编写和移动代码之前,在使用 Vuex store 时,有一些短语和单词是不同的,我们应该熟悉它们:

  • state:这是存储区与数据对象的等价物;原始数据存储在此对象中。
  • getters:这些是计算值的 Vuex 等价物;store的功能,可在返回原始状态值供组件使用之前对其进行处理。
  • mutations:Vuex 不允许直接在store之外修改状态对象,这必须通过变异处理程序完成;这些是store上允许更新状态的功能。他们总是把state作为第一个参数。

这些对象直接属于store中。然而,更新store并不像调用store.mutationName()那么简单。相反,我们必须使用新的commit()函数调用该方法。此函数接受两个参数:突变的名称和传递给它的数据。

虽然最初很难理解,但 Vuex 存储区的冗长特性允许强大的功能。行动商店的一个示例,改编自第 1 章Vue.js入门的原始示例,如下所示:

const store = new Vuex.Store({
  state: {
    message: 'HelLO Vue!'
  },

  getters: {
    message: state => {
      return state.message.toLowerCase();
    }
  },

  mutations: {
    updateMessage(state, msg) {
      state.message = msg;
    }
  }
});

前面的store示例包括state对象,它是我们的原始数据存储;一个getters对象,包括我们对状态的处理;最后是一个mutations对象,它允许我们更新消息。注意messagegetter 和updateMessage突变如何将存储的状态作为第一个参数。

要使用此store,您可以执行以下操作:

new Vue({
  el: '#app',

  store,
  computed: {
    message() {
      return this.$store.state.message
    },
    formatted() {
      return this.$store.getters.message
    }
  }
});

检索消息

{{ message }}计算函数中,我们从状态对象检索到原始的未处理消息,并使用以下路径:

this.$store.state.message

这实际上是访问store,然后是状态对象,然后是消息对象键。

类似地,{{ formatted }}计算值使用store中的 getter,该 getter 将字符串小写。取而代之的是访问getters对象:

this.$store.getters.message

更新消息

要更新消息,需要调用commit函数。这接受方法名作为第一个参数,有效负载或数据作为第二个参数。有效负载可以是简单变量、数组,如果需要传递多个变量,也可以是对象

store中的updateMessage突变接受单个参数,并将消息设置为等于该参数,因此要更新我们的消息,代码为:

store.commit('updateMessage', 'VUEX Store');

这可以在应用中的任何位置运行,并将自动更新以前使用的值,因为它们都依赖于相同的store

现在返回我们的消息 getter 将返回 VUEX 存储,因为我们已经更新了状态。记住这一点,让我们更新我们的应用,在store中使用 path 变量,而不是 Vue 实例。

将 Vuex 存储区用于文件夹路径

为全局 Dropbox path 变量使用 Vue store 的第一步是将数据对象从 Vue 实例移动到Store,并将其重命名为state

const store = new Vuex.Store({
  state: {
 path: ''
 }
});

我们还需要创建一个变异,以允许从 URL 的哈希更新路径。向存储添加一个mutations对象,并从 Vue 实例中移动updateHash函数。别忘了更新函数以接受存储作为第一个参数。另外,更改方法,使其更新state.path而不是this.path

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

通过将路径变量和变异移动到存储区,它使 Vue 实例显著变小,同时删除了methodsdata对象:

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

  store,
  created() {
    this.updateHash()
  }
});

我们现在需要更新我们的应用,以使用来自store的 path 变量,而不是 Vue 实例。我们还需要确保调用store``mutation函数来更新 path 变量,而不是 Vue 实例上的方法。

更新路径方法以使用存储提交

从 Vue 实例开始,将this.Updatehash改为store.commit('updateHash')。别忘了更新onhashchange函数中的方法。第二个函数应该引用 Vue 实例上的store对象,而不是直接引用store。这是通过访问 Vue 实例变量app,然后在此实例中引用 Vuex 存储来实现的。

在 Vue 实例上引用 Vuex 存储时,无论最初声明的变量名是什么,都会将其保存在变量$store下:

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

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

window.onhashchange = () => {
  app.$store.commit('updateHash');
}

使用 path 变量

我们现在需要更新组件以使用来自store的路径,而不是通过组件传递的路径。breadcrumbdropbox-viewer都需要更新才能接受这个新变量。我们还可以从组件中移除不必要的道具。

更新面包屑组件

从 HTML 中删除:p道具,留下一个简单的面包屑 HTML 标记:

<breadcrumb></breadcrumb>

接下来,从 JavaScript 文件中的组件中删除props对象。parts变量也需要更新为使用this.$store.state.path,而不是this.p

Vue.component('breadcrumb', {
  template: '<div>' +
    '<span v-for="(f, i) in folders">' +
      '<a :href="f.path">[F] {{ f.name }}</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;
    }
  }
});

更新 dropbox 查看器组件以使用 Vuex

breadcrumb组件一样,第一步是从视图中删除 HTML 道具。这将进一步简化应用的视图,并为您留下一些 HTML 标记:

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

下一步是清理 JavaScript,删除任何不必要的函数参数。从dropbox-viewer组件上移除props对象。接下来,更新位于getFolderStructure内部的filesListFolderDropbox 方法以使用存储路径,而不是使用 path 变量:

this.dropbox().filesListFolder({
  path: this.$store.state.path, 
  include_media_info: true
})

由于此方法现在使用的是store,而不是函数参数,因此我们可以从方法声明本身中删除变量,同时从updateStructure方法中以及在调用这两个函数时删除变量。例如:

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

这将成为以下内容:

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

但是,我们仍然需要将路径作为变量存储在此组件上。这是因为我们的watch方法调用了updateStructure函数。为此,我们需要将路径存储为计算值,而不是固定变量。这是因为它可以在store更新时动态更新,而不是在组件初始化时使用固定值。

使用名为path的方法在dropbox-viewer组件上创建一个计算对象-这应该只返回store路径:

computed: {
  path() {
    return this.$store.state.path
  }
}

我们现在将其作为局部变量,因此 DropboxfilesListFolder方法可以更新为再次使用this.path

新更新的dropbox-viewer组件应该如下所示。在浏览器中查看应用时,应该看起来好像什么都没有改变。但是,应用的内部工作现在依赖于新的 Vuex 存储,而不是存储在 Vue 实例上的变量:

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

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

  computed: {
 path() {
 return this.$store.state.path
 }
 },

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

    getFolderStructure() { 
      this.dropbox().filesListFolder({
        path: this.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() {
      this.isLoading = true;
      this.getFolderStructure();
    }
  },

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

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

缓存文件夹内容

现在我们的应用中有了 Vuex,并将其用于路径,我们可以开始考虑存储当前显示文件夹的内容,这样,如果用户返回到相同的位置,就不需要查询 API 来检索结果。我们将通过将 API 返回的对象存储到 Vuex 存储区来实现这一点。

当文件夹被请求时,应用将检查存储中是否存在数据。如果这样做,API 调用将被忽略,数据将从存储器中加载。如果它不存在,将查询 API 并将结果保存在 Vuex 存储中。

第一步是将数据处理分离为自己的方法。这是因为无论数据来自存储还是 API,都需要拆分文件和文件夹。

在名为createFolderStructure()dropbox-viewer组件中创建一个新方法,并按照 DropboxfilesListFolder方法从then()函数内部移动代码。改为在该函数内调用新方法。

您的两种方法现在应该如下所示,并且您的应用仍应像以前一样工作:

createFolderStructure(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;
},

getFolderStructure() { 
  this.dropbox().filesListFolder({
    path: this.path, 
    include_media_info: true
  })
  .then(this.createFolderStructure)
  .catch(error => {
    this.isLoading = 'error';
    console.log(error);
  });
}

使用承诺,我们可以使用createFolderStructure作为 API 调用的操作

下一步是存储我们正在处理的数据。为此,我们将利用将对象传递给storecommit函数的能力,并使用路径作为存储对象中的键。我们不是嵌套文件结构,而是将信息存储在平面结构中。例如,在浏览了几个文件夹后,我们的商店将如下所示:

structure: {
  'images': [{...}],
  'images-holiday': [{...}],
  'images-holiday-summer': [{...}]
}

将对路径进行若干转换,以使其对对象键友好。它将被小写,任何标点符号都将被删除。我们还将用连字符替换所有空格和斜杠。

首先,在 Vuex 存储状态对象中创建一个名为structure的空对象;这就是我们要存储数据的地方:

state: {
  path: '',
  structure: {}
}

我们现在需要创建一个新的mutation,以便在加载数据时存储数据。在mutations对象内创建一个新函数。称之为structure;它需要接受state作为参数,加上payload变量,该变量将作为传入的对象:

structure(state, payload) {
}

path 对象将由一个path变量和从 API 返回的data组成。例如:

{
  path: 'images-holiday',
  data: [{...}]
}

传入此对象后,我们可以使用路径作为键,使用数据作为值。使用变异内的路径键存储数据:

structure(state, payload) {
  state.structure[payload.path] = payload.data;
}

我们现在可以在组件中的新createFolderStructure方法末尾提交这些数据:

createFolderStructure(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;

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

这将在应用中导航时存储每个文件夹的数据。这可以通过在结构突变中添加一个console.log(state.structure)来验证

尽管这样做可以按原样工作,但最好在将路径用作对象中的键时对其进行清理。为此,我们将删除任何标点符号,用连字符替换任何空格和斜杠,并将路径更改为小写。

dropbox-viewer组件上创建一个名为slug的新计算函数。slug 一词通常用于经过净化的 url,它起源于报纸以及编辑如何引用故事。此函数将运行多个 JavaScriptreplace方法来创建安全对象密钥:

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

slug 功能执行以下操作 img/iPhone/mom's Birthday - 40th`的示例路径将受到以下方式的影响:

  • 将字符串转换为小写 img/iphone/mom's birthday - 40th`
  • 删除路径开始和结束处的所有斜线:img/iphone/mom birthday - 40th
  • 用连字符替换任何空格:img/iphone/mom-birthday---40th
  • 用连字符替换任何斜杠:images-iphone-mom-birthday---40th
  • 将任何多个连字符替换为单个连字符:images-iphone-mom-birthday-40th
  • 最后,删除任何标点符号:images-iphone-moms-birthday-40th

现在创建 slug 后,我们可以将其用作存储数据时的密钥:

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

现在我们的文件夹内容缓存在 Vuex 存储中,我们可以添加一个检查,查看存储中是否存在数据,如果存在,则从那里加载数据。

从存储加载数据(如果存在)

从存储加载数据需要对代码进行几处更改。第一步是检查结构是否存在于store中,如果存在,则加载。第二步是仅在数据是新数据时才将数据提交到存储器。调用现有的createFolderStructure方法将更新结构,但也会将数据重新提交到存储器。尽管目前对用户无害,但不必要地将数据写入store可能会在应用增长时引发问题。这也将有助于我们对文件夹和文件进行预处理。

从存储加载数据

由于store是一个 JavaScript 对象,而我们的slug变量是我们组件上一致的计算值,我们可以用if语句检查对象键是否存在:

if(this.$store.state.structure[this.slug]) {
  // The data exists
}

这使我们能够灵活地使用createFolderStructure方法从store加载数据(如果存在),如果没有,则触发 Dropbox API 调用。

更新getFolderStructure方法以包含if语句,如果数据存在,则添加方法调用:

getFolderStructure() {
  if(this.$store.state.structure[this.slug]) {
 this.createFolderStructure(this.$store.state.structure[this.slug]);
 } else {
    this.dropbox().filesListFolder({
      path: this.path, 
      include_media_info: true
    })
    .then(this.createFolderStructure)
    .catch(error => {
      this.isLoading = 'error';
      console.log(error);
    });
  }
}

数据的路径相当长,可能使我们的代码无法读取。为了便于理解,将数据分配给一个变量,这允许我们检查它是否存在,并使用更干净、更小、可重复性更少的代码返回数据。这还意味着,如果数据路径发生变化,我们只需更新一行:

getFolderStructure() {
  let data = this.$store.state.structure[this.slug]; 
  if(data) {
    this.createFolderStructure(data);
  } else {
    this.dropbox().filesListFolder({
      path: this.path, 
      include_media_info: true
    })
    .then(this.createFolderStructure)
    .catch(error => {
      this.isLoading = 'error';
      console.log(error);
    });
  }
}

仅存储新数据

如前所述,当前的createFolderStructure方法既显示结构,又将响应缓存在store中,因此即使从缓存加载数据,也会重新保存结构。

创建一个新方法,一旦加载数据,Dropbox API 将触发该方法。叫它createStructureAndSave。这应接受响应变量作为其唯一参数:

createStructureAndSave(response) {

}

现在,我们可以将store``commit函数从createFolderStructure方法移动到这个新方法中,同时调用以使用数据触发现有方法:

createStructureAndSave(response) {

  this.createFolderStructure(response)

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

最后,更新 Dropbox API 函数以调用此方法:

getFolderStructure() {
  let data = this.$store.state.structure[this.slug]; 
  if(data) {
    this.createFolderStructure(data);
  } else {
    this.dropbox().filesListFolder({
      path: this.path, 
      include_media_info: true
    })
    .then(this.createStructureAndSave)
    .catch(error => {
      this.isLoading = 'error';
      console.log(error);
    });
  }

},

在浏览器中打开应用并在文件夹中导航。当您使用 breadcrumb 返回时,响应应该会快得多,因为它现在从您创建的缓存加载,而不是每次都查询 API。

第 7 章中,我们将对其他文件夹和文件进行预缓存,以便更快地导航,我们将研究如何预缓存文件夹,以尝试抢占用户下一步的前进方向。我们还将研究缓存文件的下载链接。

我们的完整应用 JavaScript 现在应该如下所示:

Vue.component('breadcrumb', {
  template: '<div>' +
    '<span v-for="(f, i) in folders">' +
      '<a :href="f.path">[F] {{ f.name }}</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;
    }
  }
});

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> - <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.link = data.link;
    });
  },
});

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
      });
    },

    createFolderStructure(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;

    },

    createStructureAndSave(response) {

      this.createFolderStructure(response)

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

    getFolderStructure() {
      let data = this.$store.state.structure[this.slug]; 
      if(data) {
        this.createFolderStructure(data);
      } else {
        this.dropbox().filesListFolder({
          path: this.path, 
          include_media_info: true
        })
        .then(this.createStructureAndSave)
        .catch(error => {
          this.isLoading = 'error';
          console.log(error);
        });
      }

    },

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

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

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

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

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

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

window.onhashchange = () => {
  app.$store.commit('updateHash');
}

总结

在本章之后,您的应用现在应该与 Vuex 集成,并缓存 Dropbox 文件夹的内容。Dropbox 文件夹路径也应该利用store来提高应用的效率。我们也只在需要时查询 API。

第 7 章预缓存其他文件夹和文件以实现更快的导航中,我们将关注提前主动查询 API 的文件夹预缓存,以加快应用的导航和可用性。