Vue2 Web 开发完全手册(三)
原文:
zh.annas-archive.org/md5/E8B4B21F7ACD89D5DD2A27CD73B2E070译者:飞龙
第七章:预缓存其他文件夹和文件以实现更快的导航
在本章中,本节的最后一章,我们将通过引入更多的缓存来进一步加快 Dropbox 文件浏览器的速度。到目前为止,我们已经构建了一个可以查询 Dropbox API 并返回文件和文件夹的应用程序。从那里开始,我们添加了文件夹导航,包括更新用于链接共享的 URL 以及能够使用后退和前进按钮。有了这个功能,我们在第六章《使用 Vuex 缓存当前文件夹结构》中引入了 Vuex 来存储当前文件夹路径和我们访问过的文件夹的内容。
本章将讨论以下内容:
-
预缓存不仅用户当前所在的文件夹,还包括子文件夹。这将通过循环遍历当前显示的文件夹并检查它们是否已被缓存来完成。如果没有,我们可以从 API 中获取数据。
-
如果用户通过直接 URL 进入,存储父文件夹的内容。这将通过利用面包屑路径向上遍历来完成。
-
缓存文件的下载链接。目前,这需要为遇到的每个文件都进行一次 API 调用,无论该文件夹是否已被我们的代码缓存。
通过这些改进,我们可以确保应用程序每个项目只与 API 联系一次,而不是像原来那样无数次。
缓存子文件夹
通过对子文件夹和父文件夹进行缓存,我们不一定需要编写新代码,而是将现有代码重新组织和重新用途化为一个更模块化的系统,以便可以单独调用每个部分。
以下流程图应该能帮助您可视化缓存当前文件夹和子文件夹所需的步骤:
在查看流程图时,您可以立即看到应用程序所需的事件中存在一些重复。在两个点上,应用程序需要决定缓存中是否存在一个文件夹,如果不存在,则查询 API 以获取数据并存储结果。尽管在流程图上只出现两次,但这个功能需要多次,每个当前位置的文件夹都需要一次。
我们还需要将显示逻辑与查询和存储逻辑分开,因为我们可能需要从 API 加载和存储,而不更新视图。
规划应用程序方法
在考虑前一节的内容时,我们可以借此机会修订和重构我们的dropbox-viewer应用程序中的方法,确保每个操作都有自己的方法。这将允许我们在需要时调用每个操作。在进入代码之前,让我们根据前面的流程图规划需要创建的方法。
首先要注意的是,每次查询 API 时,我们都需要将结果存储在缓存中。由于我们不需要在缓存中存储任何东西,除非调用了 API,所以我们可以将这两个操作合并在同一个方法中。我们还经常需要检查特定路径的缓存中是否有内容,并加载它或从 API 中检索它。我们可以将此添加到自己的方法中,并返回数据。
让我们列出我们需要创建的方法:
-
getFolderStructure:此方法将接受一个路径参数,并返回一个包含文件夹条目的对象。它将负责检查数据是否在缓存中,如果不在,则查询 Dropbox API。 -
displayFolderStructure:这个方法将调用前面的函数,并使用数据来更新组件上的structure对象,以在视图中显示文件和文件夹。 -
cacheFolderStructure:这个方法将包含getFolderStructure方法来缓存每个子文件夹,我们将探讨几种触发它的方式。
我们可能需要创建更多的方法,但这三个方法将是组件的主干。我们将保留路径和 slug-computed 属性,以及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;
}
在第六章中缓存数据时,我们使用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;
}
我们将创建的最后一个变量是数据路径,就像在第八章“介绍 Vue-Router 和加载基于 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;
}
Dropbox 的filesListFolder方法使用传入的path变量,而不是之前使用的全局变量。然后将响应中的条目存储在一个变量中,然后使用相同的 mutation 将其缓存在 Vuex 存储中。然后,entries变量从 promise 中返回,该 promise 将结果存储在output中。catch()函数与之前相同。
通过从缓存或 API 返回的数据,我们可以在组件创建和路径更新时触发和处理这些数据。然而,在此之前,我们需要处理各种数据类型的混合。
从 API 返回的数据仍然是一个需要解析的 promise;将其分配给一个变量只是将 promise 传递给以后解析。然而,来自存储的数据是一个处理方式非常不同的普通数组。为了给我们一个单一的数据类型来处理,我们将把存储的数组作为 promise 来resolve,这意味着getFolderStructure无论数据从何处加载,都返回一个 promise:
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 promise 进一步处理它。
现在我们可以继续创建我们的下一个方法displayFolderStructure,该方法将使用我们刚刚创建的方法的结果来更新我们的视图,以便应用程序可以再次导航。
使用displayFolderStructure方法显示数据
现在我们的数据已经准备好可以缓存并从存储中提供,我们可以继续使用我们的新方法显示数据。在你的dropbox-viewer组件中创建一个名为displayFolderStructure的新方法:
displayFolderStructure() {
}
该方法将从此组件的先前版本中借用很多代码。请记住,该方法仅用于显示文件夹,与缓存内容无关。
该方法的过程将是:
-
在应用程序中将加载状态设置为
active。这让用户知道有事情正在发生。 -
创建一个空的
structure对象。 -
加载
getFolderStructure方法的内容。 -
循环遍历结果,并将每个项目添加到
folders或files数组中。 -
将全局结构对象设置为新创建的对象。
-
将加载状态设置为
false,以便可以显示内容。
将加载状态设置为 true,并创建一个空的结构对象
该方法的第一步是隐藏结构树并显示加载消息。这可以像之前一样通过将isLoading变量设置为true来完成。我们还可以在这里创建一个空的structure对象,准备好由数据填充:
displayFolderStructure() {
this.isLoading = true;
const structure = {
folders: [],
files: []
}
}
加载getFolderStructure方法的内容
由于getFolderStructure方法返回一个 promise,我们需要在继续操作之前解析结果。这可以通过.then()函数来完成;我们已经在 Dropbox 类中使用过这个函数。调用该方法,然后将结果分配给一个变量:
displayFolderStructure() {
this.isLoading = true;
const structure = {
folders: [],
files: []
}
this.getFolderStructure(this.path).then(data => {
});
}
这段代码将组件的path对象传递给方法。这个路径是用户正在尝试查看的当前路径。一旦数据返回,我们可以将其赋值给data变量,然后在函数内部使用它。
循环遍历结果,并将每个项添加到文件夹或文件数组中。
我们已经熟悉了循环遍历条目并检查每个条目的.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 哈希提交到存储中,从而创建了路径变量,因此路径已经被填充。因此,我们不需要向函数传递任何内容。将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对象。从一个文件夹到另一个文件夹,结构对象始终有两个键,即files和folders,它们都是数组。就 Vue 和 JavaScript 而言,structure对象从不改变。
然而,Vue 可以检测到deep变量的嵌套更改。这可以在每个变量的基础上启用。与组件上的 props 类似,要在 watch 属性上启用更多选项,您需要将其传递给一个对象而不是直接函数。
为structure创建一个新的watch键,它是一个包含两个值的对象,deep和handler。deep键将设置为true,而handler将是在变量改变时触发的函数:
watch: {
path() {
this.displayFolderStructure();
},
structure: {
deep: true,
handler() {
}
}
}
在这个handler中,我们现在可以循环遍历每个文件夹,并对每个文件夹运行getFolderStructure方法,使用每个文件夹的path_lower属性作为函数参数:
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,并且只应在特殊情况下使用。由于我们有机会使用 props,我们应该这样做。这还给了我们在文件夹上下文中给函数一个更有意义的名称的机会。
导航到 HTML 视图并更新文件夹组件以接受一个新的 prop。我们将称之为 cache,并将函数作为值传递。由于属性是动态的,请不要忘记添加一个前导冒号:
<folder :f="entry" :cache="getFolderStructure"></folder>
在 JavaScript 的folder组件中将cache关键字添加到 props 键中。告诉 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 中的哈希进入子文件夹,我们可以继续缓存父文件夹。
缓存父文件夹
缓存父结构是我们可以采取的下一个预防措施,以帮助加快应用程序的速度。假设我们导航到了我们的图像目录/images/holiday/summer,并希望与朋友或同事共享。我们会将带有此 URL 的 URL 哈希发送给他们,在页面加载时,他们将看到内容。如果他们然后使用面包屑向上导航到/images/holiday,例如,他们需要等待应用程序检索内容。
使用breadcrumb组件,我们可以缓存父目录,因此当用户导航到holiday文件夹时,将立即显示其内容。当用户浏览此文件夹时,所有子文件夹都将使用先前的方法进行缓存。
为了缓存父文件夹,我们已经有一个组件显示具有访问所有父文件夹的 slug 的路径,我们可以通过面包屑循环遍历。
在开始缓存过程之前,我们需要更新组件内的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)"> » </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组件,将函数作为 prop 添加到你的视图中的breadcrumb组件中:
<breadcrumb :cache="getFolderStructure"></breadcrumb>
在 JavaScript 代码中,将props对象添加到组件中。将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()会发现,每次导航到一个文件夹时,面包屑都会重新挂载。这是因为 View 中的v-if语句会每次删除和添加 HTML。
由于我们只需要在初始应用加载时缓存父文件夹一次,让我们看看如何改变触发它的位置。我们只需要在第一次运行此函数时运行它;一旦用户开始在树中向上和向下导航,所有访问过的文件夹都将被缓存。
缓存父文件夹一次
为了确保我们使用的资源最少,我们可以将用于面包屑的文件夹数组保留在 store 中。这意味着breadcrumb组件和我们的父级缓存函数都可以访问相同的数组。
在你的 store 状态中添加一个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组件中的代码移动到updateHashmutation 中,这样我们就可以同时更新path和breadcrumb变量:
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)"> » </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 实例的生命周期,在第四章“使用 Dropbox API 获取文件列表”中介绍过,我们可以看到 created 函数在非常早期就被触发了。将主 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,也等于来自 API 的data.link下载链接。通过在导航到文件夹时存储和显示此信息,我们可以添加一个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)"> » </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 和 store 到我们的应用程序中,这意味着我们可以集中路径、面包屑,最重要的是,缓存文件夹内容。最后,我们看了一下缓存子文件夹和文件下载链接。
在本书的下一部分,我们将看看如何创建一个商店。这将包括使用一个名为 Vue router 的新 Vue 插件浏览类别和产品页面。我们还将研究如何将产品添加到购物篮中,并将产品列表和偏好存储在 Vuex store 中。
第八章:介绍 Vue-Router 和加载基于 URL 的组件
在接下来的几章中,我们将创建一个商店界面。这个商店将结合我们迄今为止学到的所有知识,同时引入一些更多的技术、插件和功能。我们将学习如何从 CSV 文件中获取产品列表,显示它们及其变体,并按制造商或标签对产品进行过滤。我们还将学习如何创建产品详细视图,并允许用户向其在线购物篮中添加和删除产品和产品变体,例如尺寸或颜色。
所有这些都将使用 Vue、Vuex 和一个新的 Vue 插件 Vue-router 来实现。Vue-router 用于构建单页应用程序(SPA),它允许您将组件映射到 URL,或者在 VueRouter 术语中称为路由和路径。这是一个非常强大的插件,处理了许多处理 URL 所需的复杂细节。
本章将涵盖以下内容:
-
初始化 Vue-router 及其选项
-
使用 Vue-router 创建链接
-
创建动态路由以根据 URL 更新视图
-
在 URL 中使用 props
-
嵌套和命名路由
-
如何使用 Vue-router 进行编程导航
安装和初始化 Vue-router
与我们向应用程序添加 Vue 和 Vuex 的方式类似,您可以直接从 unpkg 中包含该库,或者转到以下 URL 并下载一个本地副本:unpkg.com/Vue-router。将 JavaScript 添加到新的 HTML 文档中,以及 Vue 和应用程序的 JavaScript。还要创建一个应用程序容器元素作为您的视图。在下面的示例中,我将 Vue-router 的 JavaScript 文件保存为router.js:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript" src="js/router.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
在应用程序的 JavaScript 中初始化一个新的 Vue 实例:
new Vue({
el: '#app'
});
现在我们已经准备好添加 VueRouter 并利用其功能了。然而,在此之前,我们需要创建一些非常简单的组件,根据 URL 加载和显示它们。由于我们将使用路由器加载组件,因此不需要使用Vue.component注册它们,而是创建具有与 Vue 组件相同属性的 JavaScript 对象。
在这个第一个练习中,我们将创建两个页面——主页和关于页面。在大多数网站上都可以找到这些页面,它们应该帮助您了解加载的内容以及何时加载。在您的 HTML 页面中创建两个模板供我们使用:
<script type="text/x-template" id="homepage">
<div>
<h1>Hello & Welcome</h1>
<p>Welcome to my website. Feel free to browse around.</p>
</div>
</script>
<script type="text/x-template" id="about">
<div>
<h1>About Me</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed metus magna. Vivamus eget est nisi. Phasellus vitae nisi sagittis, ornare dui quis, pharetra leo. Nullam eget tellus velit. Sed tempor lorem augue, vitae luctus urna ultricies nec. Curabitur luctus sapien elit, non pretium ante sagittis blandit. Nulla egestas nunc sit amet tellus rhoncus, a ultrices nisl varius. Nam scelerisque lacus id justo congue maximus. Etiam rhoncus, libero at facilisis gravida, nibh nisi venenatis ante, sit amet viverra justo urna vel neque.</p>
<p>Curabitur et arcu fermentum, viverra lorem ut, pulvinar arcu. Fusce ex massa, vehicula id eros vel, feugiat commodo leo. Etiam in sem rutrum, porttitor velit in, sollicitudin tortor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec ac sapien efficitur, pretium massa at, vehicula ligula. Vestibulum turpis quam, feugiat sed orci id, eleifend pretium urna. Nullam faucibus arcu eget odio venenatis ornare.</p>
</div>
</script>
不要忘记将所有内容封装在一个“根”元素中(在这里用包裹的<div>标签表示)。您还需要确保在加载应用程序 JavaScript 之前声明模板。
我们创建了一个 Home 页面模板,其id为homepage,以及一个 About 页面,其中包含一些来自lorem ipsum的占位文本,其id为about。在您的 JavaScript 中创建两个引用这两个模板的组件:
const Home = {
template: '#homepage'
};
const About = {
template: '#about'
};
下一步是为路由器提供一个占位符来渲染视图中的组件。这可以通过使用自定义的<router-view> HTML 元素来完成。使用此元素可以控制内容的渲染位置。它允许我们在应用程序视图中拥有一个头部和页脚,而无需处理混乱的模板或包含组件本身。
在您的应用程序中添加一个header、main和footer元素。在 header 中放置一个 logo,在 footer 中放置 credits;在main HTML 元素中,放置router-view占位符:
<div id="app">
<header>
<div>LOGO</div>
</header>
<main>
<router-view></router-view>
</main>
<footer>
<small>© Myself</small>
</footer>
</div>
应用程序视图中的所有内容都是可选的,除了router-view之外,但它可以让您了解如何将路由器 HTML 元素实现到站点结构中。
下一步是初始化 Vue-router 并指示 Vue 使用它。创建一个VueRouter的新实例并将其添加到Vue实例中,类似于我们在前一节中添加Vuex的方式:
const router = new VueRouter();
new Vue({
el: '#app',
router
});
现在我们需要告诉路由器我们的路由(或路径)以及在遇到每个路由时应加载的组件。在 Vue-router 实例内部创建一个具有routes键和数组值的对象。此数组需要为每个路由包含一个对象:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
}
]
});
每个路由对象都包含一个path和component键。path是您要在其上加载component的 URL 字符串。Vue-router 根据先到先得的原则提供组件。例如,如果有多个具有相同路径的路由,则使用遇到的第一个路由。确保每个路由都有开始斜杠-这告诉路由器它是一个根页面而不是子页面,我们将在本章后面介绍子页面。
按下保存并在浏览器中查看你的应用程序。你应该看到Home模板组件的内容。如果你观察 URL,你会注意到在页面加载时,路径后面会添加一个哈希和斜杠(#/)。这是路由器创建的一种浏览组件和利用地址栏的方法。如果你将其更改为第二个路由的路径,#/about,你将看到About组件的内容。
Vue-router 还可以使用 JavaScript 历史 API 来创建更漂亮的 URL。例如,yourdomain.com/index.html#about将变成yourdomain.com/about。这是通过在你的VueRouter实例中添加mode: 'history'来激活的:
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
}
]
});
然而,这也需要一些服务器配置来捕获所有请求并将它们重定向到你的index.html页面,这超出了本书的范围,但在 Vue-router 文档中有详细说明。
更改 Vue-router 的文件夹
有时候你可能想要将 Vue 应用程序托管在网站的子文件夹中。在这种情况下,你需要声明项目的基本文件夹,以便 Vue-router 可以构建和监听正确的 URL。
例如,如果你的应用程序基于一个/shop/文件夹,你可以使用 Vue-router 实例上的base参数来声明它:
const router = new VueRouter({
base: '/shop/',
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
}
]
});
这个值需要在开头和结尾都有斜杠。
除了base之外,Vue-router 还有其他几个配置选项可用,值得熟悉它们,因为它们可能会解决你以后遇到的问题。
链接到不同的路由
路由器正常工作后,我们现在可以继续向应用程序中添加链接,允许用户在网站中导航。链接可以通过两种方式实现:我们可以使用传统的<a href="#/about">标签,或者我们可以利用路由器提供的新的 HTML 元素<router-link to="/about">。当使用router-link元素时,它的工作方式与<a>标签相同,在浏览器中运行时实际上会被转换为<a>标签,但允许更多的自定义和与路由器的集成。
强烈建议在可能的情况下使用router-link元素,因为它比标准链接具有几个优点:
-
模式更改:第一个优点与路由器的
mode相关。使用路由链接可以更改路由器的模式,例如从哈希模式更改为历史模式,而不必更改应用程序中的每个链接。 -
CSS 类:使用路由链接的另一个优点是应用于“树”中活动链接和当前正在查看的页面的 CSS 类。树中的链接是父页面,也包括根页面(例如,任何链接到
/的链接将始终具有活动类)。这是使用路由的一个重要优势,因为手动添加和删除这些类将需要复杂的编码。这些类可以进行自定义,我们将在稍后进行。 -
URL 参数和命名路由:使用路由元素的另一个优点是它使您能够使用命名路由和传递 URL 参数。这进一步允许您在页面的 URL 上拥有一个真实的来源,并使用名称和快捷方式引用路由。关于这一点,稍后在本章中将进行更详细的介绍。
在视图中添加页面链接,以便您可以在页面之间导航。在您的网站的<header>中,创建一个新的<nav>元素,其中包含一个无序列表。对于每个页面,添加一个包含router-link元素的新列表项。在链接路径上添加一个to属性:
<nav>
<ul>
<li>
<router-link to="/">Home</router-link>
</li>
<li>
<router-link to="/about">About</router-link>
</li>
</ul>
</nav>
在浏览器中查看应用程序应该显示两个链接,允许您在两个内容页面之间切换。您还会注意到,通过点击链接,URL 也会更新。
如果您使用浏览器的 HTML 检查器检查链接,您会注意到 CSS 类的变化。主页链接始终具有router-link-active类 - 这是因为它本身处于活动状态,或者它有一个活动的子页面,例如关于页面。当您在两个页面之间导航时,还会添加和删除另一个 CSS 类 - router-link-exact-active。这个类仅适用于当前活动页面上的链接。
让我们自定义应用于视图的类。在 JavaScript 中的路由初始化部分,向对象添加两个新键 - linkActiveClass和linkExactActiveClass:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
这些键应该相当容易理解,但是linkExactActiveClass应用于当前页面,即正在查看的页面,而linkActiveClass是当页面或其子页面之一处于活动状态时应用的类。
链接到子路由
有时您可能希望链接到子页面。例如/about/meet-the-team。幸运的是,不需要太多工作来实现这个功能。在routes数组中创建一个指向具有模板的新组件的新对象:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/about/meet-the-team',
component: MeetTheTeam
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
当导航到这个页面时,你会注意到 Home 和 About 链接都有active类,而没有我们创建的current类。如果你在导航中创建一个链接到这个页面,那么一个current类将被应用到它上面。
带参数的动态路由
Vue 路由器很容易让你拥有动态 URL。动态 URL 允许你使用相同的组件来显示不同的数据,同时使用相同的模板。一个例子是商店,所有的类别页面看起来都一样,但根据 URL 显示不同的数据。另一个例子是产品详情页面,你不想为每个产品都创建一个组件,所以你可以使用一个带有 URL 参数的组件。
URL 参数可以出现在路径的任何位置,可以有一个或多个。每个参数都被分配一个键,因此可以一致地创建和访问它们。我们将在第九章“使用 Vue-Router 动态路由加载数据”中更详细地介绍动态路由和参数。现在,我们将构建一个基本的示例。
在我们开始创建组件之前,让我们来看一下一个新的可用变量——this.$route。类似于我们如何使用 Vuex 访问全局存储一样,这个变量允许我们访问关于路由、URL 和参数的许多信息。
在你的 Vue 实例中,作为一个测试,添加一个mounted()函数。在console.log中插入this.$route参数:
new Vue({
el: '#app',
router,
mounted() {
console.log(this.$route);
}
});
如果你打开浏览器并查看开发者工具,你应该会看到一个对象被输出。查看这个对象将显示一些信息,比如路径和与当前路径匹配的组件。前往/about URL 将显示关于该对象的不同信息:
让我们创建一个使用这个对象参数的组件。在你的路由数组中创建一个新对象:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/user/:name',
component: User
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
你会注意到这个路径与之前的路径不同的地方是在路径中name之前有一个冒号。这告诉 Vue-router 这个 URL 的这部分是动态的,但该部分的变量名是name。
现在创建一个名为User的新组件,并为其创建一个模板。在这个例子中,我们的模板将是内联的,并且我们将使用 ES2015 模板语法。这种语法使用反引号,可以直接将变量和换行符传递到模板中,而无需对它们进行转义:
const User = {
template: `<h1>Hello {{ $route.params.name }}</h1>`
};
模板中输出的变量来自全局路由实例,并且是参数对象中的name变量。变量name引用路由路径中冒号前面的变量,在routes数组中。在组件模板中,我们还可以省略$route中的this变量。
返回浏览器,然后在 URL 末尾输入#/user/sarah。您应该在网页的主体中看到 Hello sarah。查看 JavaScript 浏览器控制台,您应该看到params对象中有一个键/值对name: sarah:
此变量也可在组件本身中使用。例如,如果我们想要将用户姓名的第一个字母大写,我们可以创建一个计算变量,该变量接受路由参数并进行转换:
const User = {
template: `<h1>Hello {{ name }}</h1>`,
computed: {
name() {
let name = this.$route.params.name;
return name.charAt(0).toUpperCase() + name.slice(1);
}
}
};
如果您不熟悉前面的代码在做什么,它会获取字符串的第一个字符并将其大写。然后,它在大写字母后拆分字符串(即,单词的其余部分)并将其附加到大写字母上。
添加此computed函数并刷新应用程序将产生 Hello sarah。
如前所述,路由可以接受任意数量的参数,并且可以由静态或动态变量分隔。
将路径更改为以下内容(保持组件名称相同):
/:name/user/:emotion
这意味着您需要转到/sarah/user/happy才能看到用户组件。但是,您将可以访问一个名为emotion的新参数,这意味着您可以使用以下模板来呈现 sarah is happy!:
const User = {
template: `<h1>{{ name }} is {{ $route.params.emotion }}</h1>`,
computed: {
name() {
let name = this.$route.params.name;
return name.charAt(0).toUpperCase() + name.slice(1);
}
}
};
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/:name/user/:emotion',
component: User
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
在接下来的几章中,当我们构建商店时,动态路由将非常有用,因为我们将同时用于产品和类别。
GET 参数
除了动态路由,Vue-router 还以一种非常简单的方式处理 GET 参数。GET 参数是您可以传递给网页的额外 URL 参数,它们显示为键/值对。使用 GET 参数,第一个参数前面有一个?-这告诉浏览器要期望参数。任何其他参数都由和号分隔。例如:
example.com/?name=sarah&emotion=happy
此 URL 将返回name的值为sarah,emotion的值为happy。它们通常用于过滤或搜索-下次在 Google 上搜索时,请查看 URL,您会注意到地址栏中的?q=Your+search+query。
Vue 路由器将这些参数在this.$route变量的query对象中提供给开发者。尝试在 URL 末尾添加?name=sarah并打开 JavaScript 开发者工具。检查查询对象将显示一个以name为键,sarah为值的对象:
在构建商店类别的过滤器时,我们将使用查询对象。
使用 props
尽管直接在组件中使用路由参数完全可以正常工作,但这不是一个好的做法,因为它将组件直接与路由绑定在一起。相反,应该使用props,就像我们在本书中之前为 HTML 组件使用它们一样。当启用和声明后,通过 URL 传递的参数可以像通过 HTML 属性传递的参数一样使用。
使用 props 作为路由组件传递选项和参数是一种更好的方式,因为它有很多好处。首先,它将组件与特定的 URL 结构解耦-正如您将看到的,我们可以直接将 props 传递给组件本身。它还有助于使您的路由组件更清晰;传入的参数在组件本身中清晰地列出,并且整个组件的代码更清晰。
props 仅适用于动态路由-GET 参数仍然可以通过前面的技术访问。
使用前面的示例,为name和emotion参数声明props。当使用基于 URL 的变量时,您将希望使用String数据类型:
const User = {
template: `<h1>{{ name }} is {{ $route.params.emotion }}</h1>`,
props: {
name: String,
emotion: String
},
computed: {
name() {
let name = this.$route.params.name;
return name.charAt(0).toUpperCase() + name.slice(1);
}
}
};
现在,我们有了this.name可以通过props和计算值两次使用。然而,由于我们通过props有了this.name和this.emotion,我们可以更新我们的组件来使用这些变量,而不是$route参数。
为了避免与 prop 冲突,将计算函数更新为formattedName()。我们还可以从函数中删除变量声明,因为新变量更易读:
const User = {
template: `<h1>{{ formattedName }} is {{ this.emotion }}</h1>`,
props: {
name: String,
emotion: String
},
computed: {
formattedName() {
return this.name.charAt(0).toUpperCase() + this.name.slice(1);
}
}
};
在props起作用之前,需要告诉 Vue-router 在特定路由中使用它们。这在routes数组中启用,逐个路由设置,并且最初设置为props: true:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/:name/user/:emotion',
component: User,
props: true
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
设置 prop 默认值
现在,由于路由参数可用作props,这使我们可以轻松创建默认值。如果我们想要使参数可选,我们需要添加几个if()语句来检查变量的存在。
然而,使用 props,我们可以像之前一样声明默认值。为情感变量添加一个默认值:
const User = {
template: `<h1>{{ formattedName }} is {{ this.emotion }}</h1>`,
props: {
name: String,
emotion: {
type: String,
default: 'happy'
}
},
computed: {
formattedName() {
return this.name.charAt(0).toUpperCase() + this.name.slice(1);
}
}
};
我们现在可以在路由器中创建一个新的路由,该路由使用相同的组件,但没有最后的变量。不要忘记为新路由启用props:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/:name/user',
component: User,
props: true
},
{
path: '/:name/user/:emotion',
component: User,
props: true
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
现在,通过访问/sarah/user,我们应该看到声明 sarah 很开心的文本。
使用静态 props
除了布尔值之外,路由中的 props 参数还可以接受一个带有要传递的 props 列表的对象。这使您可以使用相同的组件并根据 URL 更改其状态,而无需通过路径传递变量,例如,如果您想要激活或停用模板的一部分。
当通过 URL 传递 props 对象时,它会覆盖整个 props 对象,这意味着您必须声明全部或全部。props 变量也将优先于动态的基于 URL 的变量。
将您的新的/:name/user路径更新为在路由中包含props - 从路径中删除:name变量,使其变为/user:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/user',
component: User,
props: {
name: 'Sarah',
emotion: 'happy'
}
},
{
path: '/:name/user/:emotion',
component: User,
props: true
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
导航到/user应该显示与之前相同的句子。在某些情况下,通过“幕后”传递props(而不是使用 URL)是理想的,因为您可能不希望用户共享特定的 URL 或根据易于更改的参数更改应用程序的状态。
嵌套路由
嵌套路由与子路由不同,因为它们存在于已经匹配路由开始部分的组件中。这使您可以在现有视图中显示不同的内容。
一个很好的例子是 Twitter。如果您访问 Twitter 用户的个人资料页面,您可以查看他们关注的人,关注他们的人以及他们创建的列表。如果您在浏览页面时观察 URL,您会注意到一个重复的模式:用户名后跟不同的页面。嵌套路由和子路由之间的区别在于,嵌套路由允许您在不同的子页面中保持组件相同(例如,标题和侧边栏)。
这样做的优点是用户可以收藏和分享链接,使页面更易访问,并且有利于 SEO。使用简单的切换或选项卡框来在视图中显示不同内容,很难实现这些优点。
要将 Twitter 模式复制到 Vue 路由中,可以按照以下方式进行设置:
https://twitter.com/:user/:page
如果我们使用之前的路由方法来创建这个,我们将不得不为每个页面构建组件,在它们的模板中包含侧边栏中的标题和用户信息——如果您需要更新代码,这将是一件麻烦的事情!
让我们为我们的 About 页面创建一些嵌套路由。我们不会在我们的购物应用程序中使用嵌套路由,但了解 Vue 路由的功能是很重要的。
创建两个新组件——AboutContact,用于显示联系信息,以及AboutFood,用于详细介绍您喜欢吃的食物!虽然不是必需的,但在组件名称中保留对父组件(在本例中为 About)的引用是一个好主意——这样可以在以后查看它们时将组件联系在一起!为每个组件添加一个带有一些固定内容的模板:
const AboutContact = {
template: `<div>
<h2>This is some contact information about me</h2>
<p>Find me online, in person or on the phone</p>
</div>`
};
const AboutFood = {
template: `<div>
<h2>Food</h2>
<p>I really like chocolate, sweets and apples.</p>
</div>`
};
下一步是在您的#about模板中创建用于渲染嵌套路由的占位符。该元素与我们之前看到的元素完全相同——<router-view>元素。为了证明它可以放置在任何位置,在模板的两个段落之间添加它:
<script type="text/x-template" id="about">
<div>
<h1>About Me</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed metus magna. Vivamus eget est nisi. Phasellus vitae nisi sagittis, ornare dui quis, pharetra leo. Nullam eget tellus velit. Sed tempor lorem augue, vitae luctus urna ultricies nec. Curabitur luctus sapien elit, non pretium ante sagittis blandit. Nulla egestas nunc sit amet tellus rhoncus, a ultrices nisl varius. Nam scelerisque lacus id justo congue maximus. Etiam rhoncus, libero at facilisis gravida, nibh nisi venenatis ante, sit amet viverra justo urna vel neque.</p>
<router-view></router-view>
<p>Curabitur et arcu fermentum, viverra lorem ut, pulvinar arcu. Fusce ex massa, vehicula id eros vel, feugiat commodo leo. Etiam in sem rutrum, porttitor velit in, sollicitudin tortor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec ac sapien efficitur, pretium massa at, vehicula ligula. Vestibulum turpis quam, feugiat sed orci id, eleifend pretium urna. Nullam faucibus arcu eget odio venenatis ornare.</p>
</div>
</script>
在浏览器中查看 About 页面不会渲染任何内容,也不会破坏应用程序。下一步是为这些组件添加嵌套路由到路由器中。我们不是将它们添加到顶级routes数组中,而是在/about路由内创建一个数组,键为children。该数组的语法与主数组完全相同,即路由对象的数组。
为每个routes添加一个包含path和component键的对象。需要注意的是,如果希望路径添加到父路径的末尾,路径不应以/开头。
例如,如果您希望 URL 为/about/contact来渲染AboutContact组件,您可以将路由组件设置如下:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About,
children: [
{
path: 'contact',
component: AboutContact
},
{
path: 'food',
component: AboutFood
}
]
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
然而,如果你希望 URL 只是简单地为/contact,但仍然在About组件内渲染AboutContact组件,你可以添加前导斜杠。尝试在没有斜杠的情况下查看应用程序,然后添加斜杠,看看它所产生的差异。如果你希望在父级加载时显示一个子路由,而 URL 没有第二部分,你可以使用空路径——path: ''。
现在,保留没有斜杠,并添加前导的children数组。转到浏览器并导航到 About 页面。在 URL 的末尾添加/contact或/food,注意新内容出现在您之前添加到模板中的<router-link>元素的位置。
可以从任何地方创建到这些组件的链接,方式与您链接 Home 和 About 页面的方式相同。您可以将它们添加到about模板中,这样它们只会在导航到该页面时出现,或者将它们添加到应用程序视图中的主导航中。
创建一个 404 页面
在构建应用程序或网站时,尽管有着良好的意图,问题、错误和失误仍然会发生。因此,设置错误页面是一个好主意。最常见的页面是 404 页面——当链接不正确或页面已移动时显示的消息。404 是页面未找到的官方 HTTP 代码。
如前所述,Vue-router 将根据先到先得的原则匹配路由。我们可以利用这一点,将通配符(*)字符作为最后一个路由。由于通配符匹配每个路由,只有未匹配到先前路由的 URL 才会被此路由捕获。
创建一个名为PageNotFound的新组件,使用简单的模板,并添加一个新的路由,使用通配符作为路径:
const PageNotFound = {
template: `<h1>404: Page Not Found</h1>`
};
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About,
children: [
{
path: 'contact',
component: AboutContact
},
{
path: 'food',
component: AboutFood
}
]
},
{
path: '*',
component: PageNotFound
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
在浏览器中打开应用程序,输入 URL 的末尾的任何内容(除了about),然后按下Enter键,您应该看到 404 标题。
尽管这是模拟一个页面未找到的请求,但它实际上没有向浏览器发送正确的 HTTP 代码。如果您在生产中使用 Vue Web 应用程序,建议设置服务器端错误检查,以便在 URL 不正确的情况下可以正确通知浏览器。
命名组件、路由和视图
在使用Vue-router时,不需要为路由和组件添加名称,但这是一个好的实践和一个好习惯。
命名组件
具有名称的组件使您能够更轻松地调试错误。在 Vue 中,当从组件中抛出 JavaScript 错误时,它将给出该组件的名称,而不是将Anonymous列为组件。
例如,如果您尝试在食品组件中输出一个不可用的变量{{ test }},默认情况下,JavaScript 控制台错误将如下所示:
请注意堆栈中的两个<Anonymous>组件。
通过为我们的组件添加名称,我们可以轻松地确定问题所在。在下面的示例中,已经为About和AboutFood组件添加了名称:
您可以轻松地看到错误在<AboutFood>组件中。
为组件添加名称就像在对象中添加一个名为name的键,并将名称作为值一样简单。这些名称遵循与创建 HTML 元素组件时相同的规则:不允许空格,但允许连字符和字母。为了让我能够快速识别代码,我选择将组件命名为与定义它的变量相同:
const About = {
name: 'About',
template: '#about'
};
const AboutFood = {
name: 'AboutFood',
template: `<div>
<h2>Food</h2>
<p>I really like chocolate, sweets and apples.</p>
</div>`
}
命名路由
在使用VueRouter时,您还可以为路由本身命名。这使您能够简化路由的位置并更新路径,而无需在应用程序中查找和替换所有实例。
请按照以下示例将name键添加到您的routes中:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About,
children: [
{
name: 'contact',
path: 'contact',
component: AboutContact
},
{
name: 'food',
path: 'food',
component: AboutFood
}
]
},
{
path: '*',
component: PageNotFound
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
现在,您可以在创建router-link组件时使用该名称,如下所示:
<router-link :to="{name: 'food'}">Food</router-link>
注意to属性前面的冒号。这确保内容被解析为对象,而不是字面字符串。使用命名路由的另一个优点是能够向我们的动态路径传递特定属性。使用本章前面的示例,我们可以以编程方式构建 URL,将数据从路径构建中抽象出来。这就是命名路由真正发挥作用的地方。假设我们有以下路径:
{ name: 'user', path: '/:name/user/:emotion', component: User }
我们需要向 URL 传递一个名称和情感变量以供组件渲染。我们可以像之前那样直接传递给 URL,或者使用带有命名路由的to对象表示法:
<router-link :to="{name: 'user', params: { name: 'sarah', emotion: 'happy' }}">
Sarah is Happy
</router-link>
在浏览器中查看将显示生成的锚链接:
/sarah/user/happy
这使我们能够重新排列 URL,使用变量,而无需更新应用的其余部分。如果您想在 URL 末尾传递参数(例如?name=sarah),则可以将params键更改为query,因为它遵循相同的格式:
<router-link :to="{name: 'user', query: { name: 'sarah', emotion: 'happy' }}">
Sarah is Happy
</router-link>
通过重新配置路径以不接受参数,将生成以下链接:
/user?name=sarah&emotion=happy
在交换params和query时要小心-它们可能会影响您使用path还是name。当使用path时,params对象将被忽略,而query对象不会被忽略。要使用params对象,您需要使用命名路由。或者,使用$变量将参数传递到path中。
命名视图
Vue 路由还允许您为视图命名,从而可以将不同的组件传递给应用程序的不同部分。一个例子是商店,其中有侧边栏和主要内容区域。不同的页面可以以不同的方式利用这些区域。
About 页面可以使用主要内容显示关于内容,同时使用侧边栏显示联系方式。然而,商店页面将使用主要内容列出产品,并使用侧边栏显示过滤器。
为此,请创建第二个router-view元素作为原始元素的兄弟元素。保留原始元素的位置,但是给第二个元素添加一个name属性,并给它一个适当的标题:
<main>
<router-view></router-view>
</main>
<aside>
<router-view name="sidebar"></router-view>
</aside>
在路由实例中声明路由时,我们现在将使用一个新的键components,并删除之前的单数component键。这个键接受一个对象,其中包含视图的名称和组件的名称的键值对。
建议将主路由保持未命名,这样您就不需要更新每个路由。如果您决定给主路由命名,那么您需要为应用中的每个路由执行下一步操作。
更新About路由以使用这个新的键,并将其转换为一个对象。下一步是告诉代码每个组件将放在哪里。
使用default作为键,将About组件设置为值。这将把 About 组件的内容放在未命名的router-view中,即主要的那个。这也是使用单数component键的简写方式:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
components: {
default: About
}
},
{
path: '*',
component: PageNotFound
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
接下来,添加第二个键值对,指定第二个router-view的名称为sidebar。当导航到/about URL 时,将使用您想要填充此区域的组件,我们将使用AboutContact组件:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
components: {
default: About,
sidebar: AboutContact
}
},
{
path: '*',
component: PageNotFound
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
在浏览器中运行应用程序将呈现两个组件,联系组件的内容将显示在侧边栏中。
以编程方式导航、重定向和添加别名
在构建应用程序时,可能会遇到一些需要不同导航技术的情况。这些可能是以编程方式导航,例如在组件或主 Vue 实例中,当用户访问特定 URL 时重定向用户,或者使用不同的 URL 加载相同的组件。
以编程方式导航
您可能希望从代码、组件或操作中更改路径、URL 或用户流程。一个例子是在用户添加了一个项目后将其发送到购物篮。
为此,您可以在路由器实例上使用push()函数。push 的值可以是直接 URL 的字符串,也可以是接受命名路由或路由参数的对象。push函数的允许内容与router-link元素上的to=""属性完全相同。例如:
const About = {
name: 'About',
template: '#about',
methods: {
someAction() {
/* Some code here */
// direct user to contact page
this.$router.push('/contact');
}
}
};
或者,您可以使用带参数的命名路由进行重定向:
this.$router.push({name: 'user', params: { name: 'sarah', emotion: 'happy' }});
重定向
使用VueRouter进行重定向非常简单。重定向的一个例子可能是将您的/about页面移动到/about-us URL。您将希望将第一个 URL 重定向到第二个 URL,以防有人分享或收藏了您的链接,或者搜索引擎缓存了 URL。
您可能会想要创建一个基本组件,当创建时使用router.push()函数将用户发送到新的 URL。
相反,您可以添加一个路由并在其中指定重定向:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
redirect: '/about-us'
},
{
path: '/about-us',
component: About
},
{
path: '*',
component: PageNotFound
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
再次强调,重定向键的内容可以是一个字面字符串或一个对象,就像push()函数一样。在上述示例中,如果用户访问/about,他们将立即重定向到/about-us并显示About组件。
别名路由
可能有一些情况下,您希望在两个 URL 下显示相同的组件。虽然不推荐作为标准做法,但在某些边缘情况下,这是必需的。
别名键会添加到现有路由中,并接受一个路径的字符串。使用上述示例,无论用户访问/about还是/about-us,都将显示About组件:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
alias: '/about-us',
component: About,
},
{
path: '*',
component: PageNotFound
}
],
linkActiveClass: 'active',
linkExactActiveClass: 'current'
});
摘要
现在,您应该熟悉 Vue-router,知道如何初始化它,了解可用的选项,以及如何创建静态和动态的新路由。在接下来的几章中,我们将开始创建我们的商店,首先加载一些商店数据并创建一个产品页面。
第九章:使用 Vue-Router 动态路由加载数据
在第八章《介绍 Vue-Router 和加载基于 URL 的组件》中,我们探索了 Vue-router 及其功能和功能。有了这些知识,我们现在可以继续使用 Vue 制作我们的商店。在我们进入代码并开始创建之前,我们应该首先计划我们的商店将如何工作,我们需要哪些 URL 以及我们需要制作哪些组件。一旦我们计划好我们的应用程序,我们就可以继续创建一个产品页面。
在本章中,我们将:
-
概述我们的组件和路由,并创建占位符文件
-
加载产品 CSV 文件,处理并缓存在 Vuex 中
-
创建一个带有图像和产品变体的单独产品页面
概述和计划您的应用程序
首先,让我们考虑整个应用程序和用户流程。
我们将创建一个没有支付处理网关的商店。商店首页将显示一系列精选产品。用户将能够使用类别浏览产品,并使用我们制作的筛选器缩小选择范围。他们将能够选择一个产品并查看更多详细信息。产品将具有变体(大小、颜色等),并且可能有多个产品图像。用户将能够将变体添加到购物篮中。从那里,他们可以继续浏览产品并添加更多产品到购物篮中,或者进入结账流程,在那里他们将被要求提供姓名和地址,并进行支付。将显示订单确认屏幕。
整个商店应用程序将在 Vue 中创建并在客户端运行。这不涵盖任何用于付款、用户帐户、库存管理或验证的服务器端代码。
该应用程序将使用 Vue-router 处理 URL 和 Vuex 存储产品、购物篮内容和用户详细信息。
组件
在确定用户流程后,我们需要计划我们的商店需要制作哪些组件以及它们的名称。这有助于开发应用程序,因为我们清楚地知道需要创建哪些组件。我们还将决定组件的名称。根据 Vue 风格指南(vuejs.org/v2/style-guide/index.html),我们的所有组件都将由两个名称组成。
路由组件
以下组件将与 Vue-router 一起使用,形成我们应用程序的页面:
-
商店首页—
HomePage:商店首页将显示由商店所有者精选的产品列表。这将使用预先选择的产品句柄列表进行显示。 -
分类页面—
CategoryPage:这将列出特定类别的产品。类别列表页面还将具有过滤器。 -
产品页面—
ProductPage:产品页面将显示产品的详细信息、图片和产品的变体。 -
购物篮—
OrderBasket:在购物篮中,用户可以查看已添加的产品,删除不需要的项目,并更改每个项目的数量。它还将显示订单的总成本。 -
结账—
OrderCheckout:结账将锁定购物篮,禁止删除和更新产品,并提供一个表单供用户输入地址。 -
订单确认—
OrderConfirmation:下单后将显示此组件,确认购买的产品、交付地址和总价格。 -
404页面—PageNotFound:当输入错误的 URL 时显示的错误页面。
HTML 组件
HTML 组件将在页面组件中使用,以帮助减少我们的代码中的重复布局。
-
产品列表—
ListProducts:在列表视图中查看时,将显示产品的分页列表,例如在HomePage或CategoryPage组件中。 -
类别列表—
ListCategories:这将创建一个用于导航的类别列表。 -
购买列表—
ListPurchases:此组件将出现在购物篮、结账和订单确认页面中;它将以表格形式列出产品的变体、价格和数量。它还将显示购物篮中所有产品的总价格。 -
过滤—
ProductFiltering:在类别页面的侧边使用的组件将提供用户过滤的能力,并且将使用我们在第八章中介绍的 GET 参数更新 URL,介绍 Vue-Router 和加载基于 URL 的组件。
路径
在我们概述了组件之后,我们可以规划商店的路径和 URL,以及它们将采取的组件或操作。我们还需要考虑错误的 URL 以及是否应该将用户重定向到适当的位置或显示错误消息:
-
/:Home -
/category/:slug:CategoryPage,使用:slug唯一标识符来确定要显示的产品 -
/category:这将重定向到/ -
/product/:slug:ProductPage- 再次使用:slug来标识产品 -
/product:这将重定向到/ -
/basket:OrderBasket -
/checkout:OrderCheckout- 如果没有产品,它将重定向用户到/basket -
/complete:OrderConfirmation- 如果用户没有从OrderCheckout组件进入,则会重定向到/basket -
*:PageNotFound- 这将捕获任何未指定的路由
随着我们确定了路由和组件,我们可以开始创建我们的应用程序。
创建初始文件
在前一节中概述的应用程序中,我们可以为文件结构和组件创建框架。由于这个应用程序是一个大型应用程序,我们将把文件拆分为每个组件的单独文件。这意味着我们的文件更易管理,我们的主应用程序 JavaScript 文件不会变得无法控制。
尽管在开发过程中可以接受,但是部署具有这么多文件的应用程序可能会增加加载时间,这取决于服务器的设置方式。使用传统的 HTTP/1.1,浏览器必须请求和加载每个文件 - 如果有多个文件,这将是一个阻碍。然而,使用 HTTP/2,您可以同时向用户推送多个文件 - 在这种情况下,多个文件可以在一定程度上提高应用程序的性能。
无论您选择使用哪种部署方法,强烈建议在将代码部署到生产环境时对 JavaScript 进行缩小。这样可以确保在为用户提供服务时,代码尽可能小:
为每个组件、视图和库(如 Vue、Vuex 和 Vue-router)创建一个文件。然后,为每种类型的文件创建一个文件夹。最后,添加一个app.js - 这是初始化库的地方。
您还可以考虑使用 vue-cli (https://github.com/vuejs/vue-cli)来构建您的应用程序。超出了本书的范围,因为我们只涵盖了使用包含的 JavaScript 文件构建 Vue 应用程序,vue-cli 应用程序允许您以更模块化的方式开发应用程序,并在开发完成后以类似我们一直在开发应用程序的方式部署它。
创建一个index.html文件,并包含您的 JavaScript 文件,确保首先加载 Vue,最后加载您的应用程序的 JavaScript。添加一个容器来形成我们商店的视图:
<!DOCTYPE html>
<html>
<head>
<title>Vue Shop</title>
</head>
<body>
<div id="app"></div>
<!-- Libraries -->
<script type="text/javascript" src="js/libs/vue.js"></script>
<script type="text/javascript" src="js/libs/vuex.js"></script>
<script type="text/javascript" src="js/libs/router.js"></script>
<!-- Components -->
<script src="js/components/ListCategories.js"></script>
<script src="js/components/ListProducts.js"></script>
<script src="js/components/ListPurchases.js"></script>
<script src="js/components/ProductFiltering.js"></script>
<!-- Views -->
<script src="js/views/PageNotFound.js"></script>
<script src="js/views/CategoryPage.js"></script>
<script src="js/views/HomePage.js"></script>
<script src="js/views/OrderBasket.js"></script>
<script src="js/views/OrderCheckout.js"></script>
<script src="js/views/OrderConfirmation.js"></script>
<script src="js/views/ProductPage.js"></script>
<!-- App -->
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
确保首先加载PageNotFound组件,因为我们将在其他组件中使用它,并将其指定为我们的 404 页面。
在每个文件中,通过声明变量或使用Vue.component来初始化组件的类型。对于视图,还要添加一个name属性,以便以后调试时使用。
例如,位于js/components/文件夹中的所有文件应该像下面这样初始化。确保这些组件是小写的,并且使用连字符分隔:
Vue.component('list-products', {
});
而位于js/views中的路由和视图组件应该如下所示:
const OrderConfirmation = {
name: 'OrderConfirmation'
};
最后一步是初始化我们的 Vuex 存储、Vue-router 和 Vue 应用程序。打开app.js并初始化这些库:
const store = new Vuex.Store({});
const router = new VueRouter({});
new Vue({
el: '#app',
store,
router
});
准备好 Vue 组件和路由,初始化我们的存储、路由和应用程序后,让我们来看看如何设置服务器(如果需要)并加载数据。
服务器设置
在我们的商店中,我们将在页面加载时加载一个产品的 CSV 文件。这将模拟从数据库或 API 中收集库存和产品数据的过程,这是在线商店与实体店可能需要处理的事情。
与本书前面的 Dropbox 应用程序类似,我们将加载外部数据并将其保存到 Vuex 存储中。然而,我们将面临一个问题,即在通过 JavaScript 加载资源时,浏览器要求请求的文件的协议必须是 HTTP、HTTPS 或 CORS 请求。
这意味着我们无法使用我们在 Dropbox API 中使用的fetch()技术来加载本地文件,因为在浏览器中查看我们的应用程序时,我们是通过file://协议加载本地资源的。
我们可以通过几种不同的方式解决这个问题-你选择哪种方式取决于你的情况。我们将加载一个 CSV 文件,并使用两个插件将其转换为可用的 JSON 对象。你有三个选项:
-
将文件存储在本地
-
使用远程服务器或
-
使用本地服务器
让我们逐个讨论每个选项的优缺点。
将文件存储在本地
第一种选择是将 CSV 适当地转换为 JSON,然后将输出保存在文件中。您需要将其分配给文件中的变量,并在加载库之前加载 JSON。一个示例可能是创建一个data.json并将其更新为分配给变量:
const products = {...}
然后可以在您的 HTML 中加载 JSON 文件:
<script type="text/javascript" src="data.json"></script>
然后,在您的app.js中可以使用products变量。
优点:
-
代码负载较少
-
无需加载处理 CSV 所需的额外文件
-
不需要额外的步骤
缺点:
-
无法模拟真实世界
-
如果要更新 CSV 数据,需要进行转换、保存并分配给变量
使用远程服务器
另一个选项是将文件上传到远程现有服务器并在那里开发您的应用程序。
优点:
-
模拟真实世界中加载 CSV 的开发
-
可以在任何地方、任何机器上开发
缺点:
-
可能会很慢
-
需要连接到互联网
-
需要设置部署过程或在实时服务器上编辑文件
设置本地服务器
最后一种选择是在您的计算机上设置本地服务器。有几个小型、轻量级、零配置模块和应用程序,也有更大、更强大的应用程序。如果您的计算机上安装了 npm,则建议使用 Node HTTP 服务器。如果没有,还有其他选项可用。
另一种选择是使用更重量级的应用程序,它可以为您提供 SQL 数据库和运行 PHP 应用程序的能力。这种情况的一个例子是 MAMP 或 XAMPP。
优点:
-
模拟真实世界中加载 CSV 的开发
-
快速、即时更新
-
可以离线开发
缺点:
-
需要安装软件
-
可能需要一些配置和/或命令行知识
我们将选择的选项是使用 HTTP 服务器。让我们加载和处理 CSV 文件,以便开始创建我们的商店。
加载 CSV
为了模拟从商店数据库或销售点收集数据,我们的应用程序将从 CSV 加载产品数据。CSV(逗号分隔值)是一种常用的文件格式,用于以数据库样式的方式共享数据。想象一下如何在 Excel 或 Numbers 中布置产品列表:这就是 CSV 文件的格式。
下一步需要下载并包含几个 JavaScript 文件。如果您在“服务器设置”部分选择了选项 1-将文件存储在本地的 JSON 文件中-则可以跳过此步骤。
我们将使用 Shopify 的示例商店数据。这些 CSV 文件有各种产品类型和不同的数据,这将测试我们的 Vue 技能。Shopify 已经将他们的示例数据放在了一个 GitHub 仓库中(github.com/shopifypartners/shopify-product-csvs-and-images)。下载任何你感兴趣的 CSV 文件,并将其保存在你的文件系统中的data/文件夹中。对于这个应用程序,我将使用bicycles.csv文件。
JavaScript 不能本地加载和处理 CSV 文件,除非进行大量的编码和处理逗号分隔和引号封装的值。为了避免本书偏离主题,介绍如何加载、解析和处理 CSV 文件,我们将使用一个库来完成这些繁重的工作。有两个值得注意的库,CSV 解析器(github.com/okfn/csv.js)和 d3(d3js.org/)。CSV 解析器只做 CSV 解析,而 d3 有生成图表和数据可视化的能力。
值得考虑哪个适合你;CSV 解析器只会给你的应用程序增加 3KB 多的负担,而 d3 大约是 60KB。除非你预计以后会添加可视化效果,否则建议你选择更小的库-尤其是它们执行相同的功能。然而,我们将为两个库运行示例。
我们希望在应用程序加载时加载我们的产品数据,这样我们的组件在需要数据时就可以加载和解析 CSV。因此,我们将在 Vue 的created()方法中加载数据。
使用 d3 加载 CSV
这两个插件以非常相似的方式加载数据,但返回的数据有所不同-然而,我们将在加载数据后处理这个问题。
加载 d3 库-如果你想尝试一下,你可以使用托管的版本:
<script src="https://d3js.org/d3.v4.min.js"></script>
使用 d3,我们使用d3对象上的csv()函数,它接受一个参数-CSV 文件的路径。将created()函数添加到你的 Vue 实例中,并初始化 CSV 加载器:
new Vue({
el: '#app',
store,
router,
created() {
d3.csv('./data/csv-files/bicycles.csv', (error, data) => {
console.log(data);
});
}
});
请记住,文件的路径是相对于包含 JavaScript 文件的 HTML 文件的路径-在这种情况下是index.html。
在浏览器中打开文件不会显示任何输出。然而,如果你打开 Javascript 控制台并展开输出的对象,你会看到类似于这样的内容:
这将以key: value的格式为每个产品提供所有可用属性的详细信息。这使我们可以使用每个产品上的一致的key来访问每个value。例如,如果我们想要获取上面产品的15mm-combo-wrench,我们可以使用Handle键。稍后将详细介绍这个。
使用 CSV 解析器加载 CSV 文件
CSV 解析器的工作方式略有不同,它可以接受许多不同的参数,并且库包含几种不同的方法和函数。数据输出也是以不同的格式,以表格/CSV 样式的结构返回,包含headers和fields对象:
new Vue({
el: '#app',
store,
router,
created() {
CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
console.log(data);
});
}
});
这次查看输出将显示一个非常不同的结构,并需要将字段的key与headers对象的索引进行匹配。
统一 Shopify CSV 数据
在保存和使用 Shopify 数据之前,我们需要统一数据并将其转换为更易处理的状态。如果检查任一库输出的数据,您会注意到每个变体或产品的附加图像都有一个条目,而链接因子是 handle。例如,有大约 12 个 handle 为pure-fix-bar-tape的条目,每个条目都是不同的颜色。理想情况下,我们希望将每个变体分组到同一项下,并将图像显示为一个产品的列表。
Shopify CSV 数据的另一个问题是字段标题的标点符号和语法不适合作为对象键。理想情况下,对象键应该像 URL 的 slug 一样,小写且不包含空格。例如,Variant Inventory Qty理想情况下应该是variant-inventory-qty。
为了避免手动处理数据并更新键,我们可以使用一个 Vue 插件来处理加载库的输出,并返回一个格式完全符合我们要求的产品对象。该插件是vue-shopify-products,可以从 unpkg 获取:
https://unpkg.com/vue-shopify-products
下载并将该库包含到您的index.html文件中。下一步是告诉 Vue 使用这个插件 - 在您的app.js文件的顶部,包含以下行:
Vue.use(ShopifyProducts);
这将在 Vue 实例的$formatProducts()上暴露一个新的方法,允许我们传入 CSV 加载库的输出,并获得一个更有用的对象集合:
Vue.use(ShopifyProducts);
const store = new Vuex.Store({});
const router = new VueRouter({});
new Vue({
el: '#app',
store,
router,
created() {
CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
let products = this.$formatProducts(data);
console.log(products);
});
}
});
现在检查输出,可以看到按handle分组的集合,其中变体和图像作为对象:
通过更有效地分组我们的产品,我们可以按需存储和调用。
存储产品
一旦我们检索和格式化了 CSV 数据,我们就可以将内容缓存到 Vuex store 中。这将通过一个简单的 mutation 来完成,该 mutation 接受一个 payload 并将其存储在不进行任何修改的情况下。
在你的 store 中创建一个state和mutations对象。在state中添加一个products键作为一个对象,并在mutations对象中创建一个名为products的函数。该 mutation 应该接受两个参数 - state 和 payload:
const store = new Vuex.Store({
state: {
products: {}
},
mutations: {
products(state, payload) {
}
}
});
更新state.products对象为payload的内容:
const store = new Vuex.Store({
state: {
products: {}
},
mutations: {
products(state, payload) {
state.products = payload;
}
}
});
用一个 commit 函数替换主 Vue 实例中的console.log,调用新的 mutation 并传入格式化的产品数据:
new Vue({
el: '#app',
store,
router,
created() {
CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
let products = this.$formatProducts(data);
this.store.commit('products', products);
});
}
});
可以通过直接将$formatProducts函数传递给 store 的commit()函数来减少一些代码,而不是将其存储为一个变量:
new Vue({
el: '#app',
store,
router,
created() {
CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
this.$store.commit('products', this.$formatProducts(data));
});
}
});
显示单个产品
现在我们的数据已经存储好了,我们可以开始制作组件并在前端显示内容了。我们将从制作产品视图开始 - 显示产品详情、变体和图片。接下来我们将在第十章中创建分类列表页面,构建电子商务商店 - 浏览产品。
创建产品视图的第一步是创建路由,以允许通过 URL 显示组件。回顾一下本章开头的笔记,产品组件将加载在/product/:slug路径上。
在你的 Vue-router 中创建一个routes数组,指定路径和组件:
const router = new VueRouter({
routes: [
{
path: '/product/:slug',
component: ProductPage
}
]
});
通过解释products对象的布局,我们可以开始理解路由和产品之间的关联。我们将把产品的句柄传递到 URL 中。这将选择具有该句柄的产品并显示数据。这意味着我们不需要显式地将slug与products关联起来。
页面未找到
创建第一个路由后,我们还应该创建我们的PageNotFound路由,以捕获任何不存在的 URL。当没有与之匹配的产品时,我们也可以重定向到此页面。
我们将以稍微不同的方式创建PageNotFound组件。我们将创建一个/404路径作为一个命名路由,而不是将组件放在*上。这样可以根据需要对多个不同的路由进行别名和重定向。
向路由数组添加一个新对象,将/404作为路径,将PageNotFound组件作为指定的组件。为您的路由添加一个名称,以便在需要时使用,并最后添加一个alias属性,其中包含我们的全局捕获所有路由。
不要忘记将此放在路由数组的末尾,以捕获任何先前未指定的路由。添加新路由时,始终记得将它们放在PageNotFound路由之前:
const router = new VueRouter({
routes: [
{
path: '/product/:slug',
component: ProductPage
},
{
path: '/404',
alias: '*',
component: PageNotFound
}
]
});
为您的PageNotFound组件添加一个模板。现在,给它一些基本内容 - 一旦我们设置好应用程序的其余部分,我们可以稍后改进它:
const PageNotFound = {
name: 'PageNotFound',
template: `<div>
<h1>404 Page Not Found</h1>
<p>Head back to the <router-link to="/">home page</router-link> and start again.</p>
</div>`
};
注意内容中使用的路由链接。让我们启动应用程序的最后一件事是在我们的应用程序中添加<router-view>元素。转到视图,并将其包含在应用程序空间中:
<div id="app">
<router-view></router-view>
</div>
在浏览器中加载应用程序,不要忘记在需要时启动 HTTP 服务器。首先,您应该看到PageNotFound组件的内容。导航到以下产品 URL 应该导致 JavaScript 错误,而不是404页面。这表明路由正确地捕获了 URL,但错误是因为我们的ProductPage组件不包含模板:
#/product/15mm-combo-wrench
如果您看到PageNotFound组件,请检查您的路由代码,因为这意味着ProductPage路由没有被捕获。
选择正确的产品
设置初始路由后,我们现在可以继续加载所需的产品并显示存储中的信息。打开views/Product.js并创建一个模板键。首先,创建一个简单的<div>容器,显示产品的标题:
const ProductPage = {
name: 'ProductPage',
template: `<div>{{ product.title }}</div>`
};
在浏览器中查看此内容将立即引发 JavaScript 错误,因为 Vue 期望product变量是一个对象 - 但由于我们尚未声明它,它目前是未定义的。尽管此问题的修复目前似乎非常简单,但我们需要考虑产品尚未定义的情况。
我们的商店应用程序异步加载数据 CSV。这意味着在产品加载时,应用程序的其余部分不会停止执行。总体而言,这增加了我们应用程序的速度,一旦我们有了产品,我们就可以开始操作和显示列表,而不需要等待应用程序的其余部分启动。
因此,有可能用户在没有加载产品列表的情况下访问产品详情页面,无论是通过共享的链接还是搜索结果。为了防止应用程序在没有完全初始化的情况下尝试显示产品数据,可以在模板中添加一个条件属性,检查产品变量是否存在,然后再尝试显示其任何属性。
在加载产品数据时,可以确保产品变量设置为false,直到所有内容都加载完成。在模板中的包含元素上添加v-if属性:
const ProductPage = {
name: 'ProductPage',
template: `<div v-if="product">{{ product.title }}</div>`
};
现在我们可以从存储中加载正确的产品并将其赋值给一个变量。
创建一个带有product()函数的computed对象。在函数内部,创建一个空的变量来存储产品,并在之后返回它。现在默认返回false,这意味着我们的模板不会生成<div>标签:
const ProductPage = {
name: 'ProductPage',
template: `<div v-if="product">{{ product.title }}</div>`,
computed: {
product() {
let product;
return product;
}
}
};
由于我们有格式良好的产品存储和可在Product组件中使用的slug变量的帮助,选择产品现在变得相当简单。存储中的products对象以句柄作为键,以product details对象作为值进行格式化。有了这个想法,我们可以使用方括号格式来选择所需的产品。例如:
products[handle]
使用路由器的params对象,从存储中加载所需的产品并将其赋值给product变量以返回:
const ProductPage = {
name: 'ProductPage',
template: `<div v-if="product">{{ product.title }}</div>`,
computed: {
product() {
let product;
product = this.$store.state.products[this.$route.params.slug];
return product;
}
}
};
我们不直接赋值给product的原因是我们可以添加一些条件语句。为了确保只有在存储中有可用数据时才加载产品,我们可以添加一个if()语句来确保产品对象有可用的键;换句话说,是否已加载数据?
添加一个if语句来检查存储产品键的长度。如果它们存在,则将存储中的数据赋值给product变量以返回。
const ProductPage = {
name: 'ProductPage',
template: `<div v-if="product">{{ product.title }}</div>`,
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
}
return product;
}
}
};
现在在浏览器中查看应用程序,一旦数据加载完成,您将看到产品的标题。这应该只需要一瞬间就能加载完成,并且应该由我们的if语句优雅地处理。
在继续显示所有产品数据之前,我们需要处理 URL 中不存在产品句柄的情况。因为我们的ProductPage路由会捕捉 URL 中/product之后的任何内容,所以无法使用PageNotFound通配符路径 - 因为我们的ProductPage组件正在加载数据并确定产品是否存在。
捕捉找不到的产品
为了在产品不可用时显示PageNotFound页面,我们将使用我们的ProductPage组件加载组件并有条件地显示它。
为了做到这一点,我们需要注册组件,以便在模板中使用它。我们需要注册它,因为我们的PageNotFound组件当前是一个对象,而不是一个 Vue 组件(例如,当我们使用Vue.component时)。
在ProductPage组件中添加一个components对象,并包含PageNotFound:
const ProductPage = {
name: 'ProductPage',
template: `<div v-if="product"><h1>{{ product.title }}</h1></div>`,
components: {
PageNotFound
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
}
return product;
}
}
};
现在,我们有了一个新的 HTML 元素可以在模板中使用,即<page-not-found>。在现有的<div>之后,在模板中添加此元素。由于我们的模板需要一个根元素,所以将它们都包装在一个额外的容器中:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product"><h1>{{ product.title }}</h1></div>
<page-not-found></page-not-found>
</div>`,
components: {
PageNotFound
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
}
return product;
}
}
};
在浏览器中查看时,将呈现404页面模板,一旦数据加载完成,就会显示在其上方的产品标题。现在我们需要更新组件,只有在没有数据可显示时才显示PageNotFound组件。我们可以使用现有的产品变量和v-if属性,如果为 false,则显示错误消息,如下所示:
<page-not-found v-if="!product"></page-not-found>
然而,这意味着如果用户在产品数据加载之前访问产品页面,他们会先看到 404 信息,然后再被替换为产品信息。这不是一个很好的用户体验,所以我们应该只在确定产品数据已加载并且没有匹配项时显示错误。
为了解决这个问题,我们将创建一个新的变量来确定组件是否显示。在ProductPage组件中创建一个数据函数,返回一个键为productNotFound的对象,将其设置为 false。在<page-not-found>元素中添加一个v-if条件,检查新的productNotFound变量:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product"><h1>{{ product.title }}</h1></div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
}
return product;
}
}
};
最后一步是将变量设置为true,如果产品不存在。由于我们只想在数据加载完成后执行此操作,因此将代码添加到$store.state.products检查中。我们已经将数据分配给了product变量,因此我们可以添加一个检查以查看此变量是否存在-如果不存在,则更改我们的productNotFound变量的极性:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product"><h1>{{ product.title }}</h1></div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
if(!product) {
this.productNotFound = true;
}
}
return product;
}
}
};
尝试在 URL 末尾输入一个错误的字符串-您应该看到我们现在熟悉的404错误页面。
显示产品信息
有了我们的产品加载、过滤和错误捕捉,我们可以继续显示我们所需的产品信息。每个产品可能包含一个或多个图像,一个或多个变体以及任何组合之间的任何组合-因此我们需要确保我们为每种情况提供支持。
在return之前添加一个console.log(product)以查看我们可用的数据:
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
打开 JavaScript 控制台并检查应该存在的对象。熟悉可用的键和值。请注意,images键是一个数组,而variations是一个对象,包含一个字符串和一个进一步的数组。
在处理变体和图像之前-让我们输出简单的内容。我们需要记住的是,我们输出的每个字段可能不会存在于每个产品上-因此最好在必要时将其包装在条件标签中。
从产品详细信息中输出body,type和vendor.title。在供应商标题和类型之前添加它们的描述,但请确保仅在产品详细信息中存在时才呈现该文本:
template: `<div>
<div v-if="product">
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
{{ product.body }}
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
请注意,我们可以在类型和供应商之前添加更友好的名称。一旦我们设置好了我们的类别和过滤器,我们就可以将供应商和类型链接到适当的产品列表。
在浏览器中查看此内容将显示将所有 HTML 标签输出为文本-这意味着我们可以在页面上看到它们。如果你回想起我们在本书开头讨论输出类型时,我们需要使用v-html告诉 Vue 将该块呈现为原始 HTML:
template: `<div>
<div v-if="product">
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
产品图像
下一步是输出我们产品的图像。如果您使用的是自行车的 CSV 文件,则可以使用650c-micro-wheelset进行测试-导航到此产品,因为它有四个图像。不要忘记返回到原始产品以检查它是否适用于一个图像。
无论是一个图像还是 100 个图像,images 的值始终是一个数组,因此要显示它们,我们始终需要使用v-for。添加一个新的容器并循环遍历图像。为每个图像添加一个宽度,以免占用整个页面。
images 数组包含每个图像的对象。该对象具有alt和source键,可以直接输入到 HTML 中。然而,有些情况下alt值是缺失的 - 如果缺失,将产品标题插入其中:
template: `<div>
<div v-if="product">
<div class="images" v-if="product.images.length">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100">
</template>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
在显示图像的同时,创建一个画廊会是一个不错的补充。商店通常会显示一张大图像,下面是一组缩略图。点击每个缩略图,然后替换主图像,以便用户可以更好地查看更大的图像。让我们重新创建这个功能。我们还需要确保如果只有一个图像,不显示缩略图。
我们通过将图像变量设置为 images 数组中的第一个图像来实现这一点,这是将形成大图像的图像。如果数组中有多个图像,我们将显示缩略图。然后,我们将创建一个点击方法,用选定的图像更新图像变量。
在数据对象中创建一个新变量,并在产品加载完成时使用 images 数组的第一项进行更新。在尝试分配值之前,确保images键实际上是一个项目数组是一个好的做法:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product">
<div class="images" v-if="product.images.length">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100">
</template>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
}
};
接下来,在模板中更新现有的图像循环,只有在数组中有多个图像时才显示。还要将第一个图像添加为模板中的主图像 - 不要忘记先检查它是否存在:
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100">
</template>
</div>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
最后一步是为每个缩略图像添加一个点击处理程序,以在与之交互时更新图像变量。由于图像本身不会具有cursor: pointer的 CSS 属性,因此考虑添加这个属性可能是值得的。
点击处理程序将是一个接受缩略图循环中的每个图像作为参数的方法。点击时,它将简单地使用传递的对象更新图像变量:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
methods: {
updateImage(img) {
this.image = img;
}
}
};
在浏览器中加载产品,并尝试点击任何缩略图 - 您应该能够更新主图像。不要忘记在只有一个图像甚至零个图像的产品上验证您的代码,以确保用户不会遇到任何错误。
不要害怕使用空格和添加新行以提高可读性。能够轻松理解您的代码比在文件加载时节省几个字节更重要。在部署到生产环境时,文件应该被压缩,但在开发过程中,空白占据主导地位。
产品变体
对于这个特定的数据集,我们的每个产品至少包含一个变体,但可以包含多个变体。这通常与图像数量相对应,但并不总是相关的。变体可以是颜色或尺寸等。
在我们的Product对象上,我们有两个键将帮助我们显示变体。它们是variationTypes,列出了变体的名称,如尺寸和颜色,以及variationProducts,其中包含所有的变体。variationProducts对象中的每个产品都有一个更进一步的variant对象,列出了所有可变的属性。例如,如果一件夹克有两种颜色,每种颜色有三种尺寸,那么将有六个variationProducts,每个产品都有两个variant属性。
每个产品都将包含至少一个变体,尽管如果只有一个变体,我们可能需要考虑产品页面的用户体验。我们将在表格和下拉菜单中显示产品的变体,这样您就可以体验创建这两个元素。
变体显示表格
在产品模板中创建一个新的容器,用于显示变体。在此容器中,我们可以创建一个表格来显示产品的不同变体。这将通过v-for声明来实现。然而,现在您对功能更加熟悉,我们可以引入一个新的属性。
使用循环键
在 Vue 中使用循环时,建议您使用额外的属性来标识每个项,即:key。这有助于 Vue 在重新排序、排序或过滤时识别数组的元素。使用:key的示例如下:
<div v-for="item in items" :key="item.id">
{{ item.title }}
</div>
键属性应该是项本身的唯一属性,而不是数组中的项的索引,以帮助 Vue 识别特定的对象。有关使用循环键的更多信息,请参阅官方 Vue 文档。
在显示变体时,我们将使用key属性,但使用barcode属性。
在表格中显示变体
在您的变体容器中添加一个表格元素,并循环遍历items数组。现在,显示title、quantity和price。添加一个包含值为“添加到购物篮”的按钮的额外单元格。我们将在第十一章“构建电子商务商店 - 添加结账”中进行配置。不要忘记在价格前面添加$货币符号,因为它目前只是一个“原始”数字。
注意 - 当在模板文字中使用$符号时,JavaScript 将尝试解释它,以及花括号,作为 JavaScript 变量。为了抵消这一点,用反斜杠在货币前面 - 这告诉 JavaScript 下一个字符是字面的,不应以任何其他方式解释:
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div class="variations">
<table>
<tr v-for="variation in product.variationProducts" :key="variation.barcode">
<td>{{ variation.quantity }}</td>
<td>\${{ variation.price }}</td>
<td><button>Add to basket</button></td>
</tr>
</table>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
尽管我们显示了价格和数量,但我们没有输出变体的实际属性(如颜色)。为了做到这一点,我们需要对变体进行一些处理,使用一个方法。
变体对象包含每个变体类型的子对象,每个类型都有一个名称和一个值。它们还以 slug 转换后的键存储在对象中。有关更多详细信息,请参见以下屏幕截图:
在表格开头添加一个新的单元格,将变体传递给名为variantTitle()的方法:
<div class="variations">
<table>
<tr v-for="variation in product.variationProducts" :key="variation.barcode">
<td>{{ variantTitle(variation) }}</td>
<td>{{ variation.quantity }}</td>
<td>\${{ variation.price }}</td>
<td><button>Add to basket</button></td>
</tr>
</table>
</div>
在methods对象中创建新的方法:
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
}
}
现在,我们需要构建一个包含变体标题的字符串,显示所有可用选项。为此,我们将构建一个包含每个类型的数组,然后将它们连接成一个字符串。
将variants存储为一个变量,并创建一个空数组。现在我们可以循环遍历variants对象中可用的键,并创建一个输出字符串。如果您决定在字符串中添加 HTML,如下面的示例所示,我们需要更新模板以输出 HTML 而不是原始字符串:
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`<b>${variants[a].name}:</b> ${variants[a].value}`);
}
}
我们的输出数组将有一个项目,格式如下:
["<b>Color:</b> Alloy", "<b>Size:</b> 49 cm"]
现在我们可以将它们连接在一起,将输出从数组转换为字符串。您可以选择使用的字符、字符串或 HTML 取决于您。现在,使用两边带有空格的/。或者,您可以使用</td><td>标签创建一个新的表格单元格。添加join()函数并更新模板以使用v-html:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div class="variations">
<table>
<tr v-for="variation in product.variationProducts" :key="variation.barcode">
<td v-html="variantTitle(variation)"></td>
<td>{{ variation.quantity }}</td>
<td>\${{ variation.price }}</td>
<td><button>Add to basket</button></td>
</tr>
</table>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
...
},
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`<b>${variants[a].name}:</b> ${variants[a].value}`);
}
return output.join(' / ');
}
}
};
将点击事件附加到“添加到购物篮”按钮,并在组件上创建一个新的方法。此方法将需要传入variation对象,以便将正确的对象添加到购物篮中。现在,添加一个 JavaScript alert()来确认您是否选择了正确的对象:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
</div>
<div class="variations">
<table>
<tr v-for="variation in product.variationProducts" :key="variation.barcode">
<td v-html="variantTitle(variation)"></td>
<td>{{ variation.quantity }}</td>
<td>\${{ variation.price }}</td>
<td><button @click="addToBasket(variation)">Add to basket</button></td>
</tr>
</table>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`<b>${variants[a].name}:</b> ${variants[a].value}`);
}
return output.join(' / ');
},
addToBasket(variation) {
alert(`Added to basket: ${this.product.title} - ${this.variantTitle(variation)}`);
}
}
};
注意在警告框中使用的模板字面量-这允许我们使用 JavaScript 变量,而无需使用字符串连接技术。现在,点击“添加到购物篮”按钮将生成一个弹出窗口,列出产品的名称和所点击的变体。
在选择框中显示变体
产品页面上更常见的界面模式是在下拉列表或选择框中显示和选择您的变体。
使用选择框时,我们将有一个变体,它要么是默认选择的,要么是用户已经与之交互并专门选择的。因此,当用户更改选择框时,我们可以更改图像,并在产品页面上显示有关变体的其他信息,包括价格和数量。
我们不会依赖于将变体传递给addToBasket方法,因为它将作为产品组件上的一个对象存在。
将您的<table>元素更新为<select>,将<tr>更新为<option>。将按钮移动到此元素之外,并从click事件中删除参数。从variantTitle()方法中删除任何 HTML。因为它现在在选择框内,所以不需要它。
<div class="variations">
<select>
<option
v-for="variation in product.variationProducts"
:key="variation.barcode"
v-html="variantTitle(variation)"
></option>
</select>
<button @click="addToBasket()">Add to basket</button>
</div>
下一步是创建一个新的变量,可供组件使用。与图片类似,这将在variationProducts数组的第一项完成,并在选择框更改时进行更新。
在数据对象中创建一个名为variation的新项。在数据加载到product计算变量中时填充此变量:
const ProductPage = {
name: 'ProductPage',
template: `...`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false,
variation: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
this.variation = product.variationProducts[0];
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
methods: {
...
}
};
更新addToBasket方法,使用ProductPage组件的variation变量,而不是依赖于参数:
addToBasket() {
alert(`Added to basket: ${this.product.title} - ${this.variantTitle(this.variation)}`);
}
尝试点击“添加到购物篮”按钮-它应该添加第一个变体,而不管在下拉列表中选择了什么。为了在更改时更新变量,我们可以将variations变量绑定到选择框上-就像我们在本书开始时对文本框进行过滤一样。
在select元素上添加v-model属性。当选择时,我们还需要告诉 Vue 要绑定到此变量的内容。默认情况下,它将使用<option>的内容,即我们当前的自定义变体标题。但是,我们希望绑定整个variation对象。在<option>元素上添加一个:value属性:
<div class="variations">
<select v-model="variation">
<option
v-for="variation in product.variationProducts"
:key="variation.barcode"
:value="variation"
v-html="variantTitle(variation)"
></option>
</select>
<button @click="addToBasket()">Add to basket</button>
</div>
更改选择框并单击“添加到购物篮”按钮现在将生成正确的变体。这种方法使我们在表格中显示变体更加灵活。
它允许我们在产品的其他位置显示变体数据。尝试在产品标题旁边添加价格,并在meta容器中显示数量:
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }} - \${{ variation.price }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
<span>
Quantity: <strong>{{ variation.quantity }}</strong>
</span>
</div>
<div class="variations">
<select v-model="variation">
<option
v-for="variation in product.variationProducts"
:key="variation.barcode"
:value="variation"
v-html="variantTitle(variation)"
></option>
</select>
<button @click="addToBasket()">Add to basket</button>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
这两个新属性将在更改变体时更新。如果有选择的变体,则还可以更新图像。为此,请在组件中添加一个watch对象,该对象监视变体变量。更新后,我们可以检查变体是否有图像,如果有,则使用此属性更新图像变量:
const ProductPage = {
name: 'ProductPage',
template: `...`,
components: {
...
},
data() {
...
},
computed: {
...
},
watch: {
variation(v) {
if(v.hasOwnProperty('image')) {
this.updateImage(v.image);
}
}
},
methods: {
...
}
};
在使用watch时,函数将新项作为第一个参数传递。我们可以使用此参数来收集图像信息,而不是引用组件上的参数。
我们可以进行的另一个改进是,如果变体缺货,则禁用“添加到购物篮”按钮并在下拉菜单中添加注释。此信息从变体的quantity键中获取。
检查数量,如果小于 1,则在选择框中显示缺货消息,并使用disabled HTML 属性禁用“添加到购物篮”按钮。我们还可以更新按钮的值:
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }} - \${{ variation.price }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
<span>
Quantity: <strong>{{ variation.quantity }}</strong>
</span>
</div>
<div class="variations">
<select v-model="variation">
<option
v-for="variation in product.variationProducts"
:key="variation.barcode"
:value="variation"
v-html="variantTitle(variation) + ((!variation.quantity) ? ' - out of stock' : '')"
></option>
</select>
<button @click="addToBasket()" :disabled="!variation.quantity">
{{ (variation.quantity) ? 'Add to basket' : 'Out of stock' }}
</button>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
如果使用bicycles.csv数据集,则 Keirin Pro Track Frameset 产品(/#/product/keirin-pro-track-frame)包含几种变体,其中一些没有库存。这样可以测试“缺货”功能以及图像更改。
我们可以对产品页面进行的另一项操作是仅在存在多个变体时显示下拉菜单。一个只有一个变体的产品的示例是 15 mm Combo 扳手(#/product/15mm-combo-wrench)。在这种情况下,显示<select>框没有意义。由于我们在加载时在Product组件上设置了variation变量,因此我们不依赖于选择来最初设置变量。因此,当只有一个备选产品时,我们可以使用v-if=""完全删除选择框。
与图像一样,检查数组的长度是否大于 1,这次是variationProducts数组:
<div class="variations">
<select v-model="variation" v-if="product.variationProducts.length > 1">
<option
v-for="variation in product.variationProducts"
:key="variation.barcode"
:value="variation"
v-html="variantTitle(variation) + ((!variation.quantity) ? ' - out of stock' : '')"
></option>
</select>
<button @click="addToBasket()" :disabled="!variation.quantity">
{{ (variation.quantity) ? 'Add to basket' : 'Out of stock' }}
</button>
</div>
通过在不需要时删除元素,我们现在有了一个更简洁的界面。
在切换 URL 时更新产品详细信息
在浏览不同的产品 URL 以检查变体时,您可能已经注意到点击后退和前进不会更新页面上的产品数据。
这是因为Vue-router意识到在页面之间使用相同的组件,所以它不会销毁和创建一个新实例,而是重用组件。这样做的缺点是数据不会更新;我们需要触发一个函数来包含新的产品数据。好处是代码更高效。
为了告诉 Vue 检索新数据,我们需要创建一个watch函数;而不是观察一个变量,我们将观察$route变量。当它更新时,我们可以加载新数据。
在数据实例的slug中创建一个新变量,并将默认值设置为路由参数。更新product计算函数以使用此变量而不是路由:
const ProductPage = {
name: 'ProductPage',
template: `...`,
components: {
PageNotFound
},
data() {
return {
slug: this.$route.params.slug,
productNotFound: false,
image: false,
variation: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.slug];
this.image = (product.images.length) ? product.images[0] : false;
this.variation = product.variationProducts[0];
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
watch: {
...
},
methods: {
...
}
};
现在我们可以创建一个watch函数,监视$route变量。当它改变时,我们可以更新slug变量,从而更新显示的数据。
在观察路由时,函数有两个参数传递给它:to和from。to变量包含有关我们要去的路由的所有内容,包括参数和使用的组件。from变量包含有关当前路由的所有内容。
通过在路由更改时将slug变量更新为新参数,我们强制组件使用来自存储的新数据重新绘制:
const ProductPage = {
name: 'ProductPage',
template: `<div>
<div v-if="product">
<div class="images" v-if="image">
<div class="main">
<img
:src="image.source"
:alt="image.alt || product.title">
</div>
<div class="thumbnails" v-if="product.images.length > 1">
<template v-for="img in product.images">
<img
:src="img.source"
:alt="img.alt || product.title"
width="100"
@click="updateImage(img)">
</template>
</div>
</div>
<h1>{{ product.title }} - \${{ variation.price }}</h1>
<div class="meta">
<span>
Manufacturer: <strong>{{ product.vendor.title }}</strong>
</span>
<span v-if="product.type">
Category: <strong>{{ product.type }}</strong>
</span>
<span>
Quantity: <strong>{{ variation.quantity }}</strong>
</span>
</div>
<div class="variations">
<select v-model="variation" v-if="product.variationProducts.length > 1">
<option
v-for="variation in product.variationProducts"
:key="variation.barcode"
:value="variation"
v-html="variantTitle(variation) + ((!variation.quantity) ? ' - out of stock' : '')"
></option>
</select>
<button @click="addToBasket()" :disabled="!variation.quantity">
{{ (variation.quantity) ? 'Add to basket' : 'Out of stock' }}
</button>
</div>
<div v-html="product.body"></div>
</div>
<page-not-found v-if="productNotFound"></page-not-found>
</div>`,
components: {
PageNotFound
},
data() {
return {
slug: this.$route.params.slug,
productNotFound: false,
image: false,
variation: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.slug];
this.image = (product.images.length) ? product.images[0] : false;
this.variation = product.variationProducts[0];
if(!product) {
this.productNotFound = true;
}
}
return product;
}
},
watch: {
variation(v) {
if(v.hasOwnProperty('image')) {
this.updateImage(v.image);
}
},
'$route'(to) {
this.slug = to.params.slug;
}
},
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`${variants[a].name}: ${variants[a].value}`);
}
return output.join(' / ');
},
addToBasket() {
alert(`Added to basket: ${this.product.title} - ${this.variantTitle(this.variation)}`);
}
}
};
完成产品页面后,我们可以继续创建一个类别列表,包括type和vendor变量。还要删除代码中的任何console.log()调用,以保持代码整洁。
总结
本章涵盖了很多内容。我们将产品的 CSV 文件加载并存储到 Vuex 存储中。从那里,我们创建了一个产品详细页面,该页面使用 URL 中的动态变量加载特定产品。我们创建了一个产品详细视图,允许用户浏览图库并从下拉列表中选择变体。如果变体有关联的图像,主图像会更新。
在第十章《构建电子商务商店-浏览产品》中,
我们将创建一个分类页面,创建过滤和排序功能,帮助用户找到他们想要的产品。