十、建立电子商务商店——浏览产品

第 9 章中,我们使用 Vue 路由动态路由加载数据,将我们的产品数据加载到 Vuex 存储中,并创建了一个产品详细信息页面,用户可以在其中查看产品及其变体。查看产品详细信息页面时,用户可以更改下拉列表中的变化,价格和其他详细信息将更新。

在本章中,我们将:

  • 创建包含特定产品的主页列表页
  • 使用可重用组件创建类别页面
  • 创建一个排序机制
  • 动态创建过滤器并允许用户过滤产品

列出产品

在创建任何筛选、策划列表、订购组件和功能之前,我们需要创建一个基本产品列表–首先显示所有产品,然后我们可以创建一个分页组件,然后在整个应用中重用。

添加新路线

让我们为routes阵列添加一条新路由。现在,我们将处理具有/路由的HomePage组件。请确保将其添加到routes阵列的顶部,这样它就不会被任何其他组件覆盖:

const router = new VueRouter({
  routes: [
 {
 path: '/',
 name: 'Home',
 component: HomePage
 },
    {
      path: '/product/:slug', 
      component: ProductPage
    },

    {
      path: '/404', 
      alias: '*',
      component: PageNotFound
    }
  ]
});

HomePage组件中,创建一个新的computed属性并收集store中的所有产品。确保在模板中显示任何内容之前已加载产品。用以下代码填充HomePage组件:

const HomePage = {
  name: 'HomePage',

  template: `<div v-if="products"></div>`,

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

通过产品循环

当查看任何商店的类别列表时,显示的数据往往具有重复出现的主题。它通常由图像、标题、价格和制造商组成

将有序列表添加到模板中–由于产品将对其进行排序,因此将其放置在有序列表中具有语义意义。在<ol>中,添加一个v-for循环遍历产品并显示每个产品的标题,如下所示。在继续显示之前,最好确保product变量存在:

template: `<div v-if="products">
  <ol>
 <li v-for="product in products" v-if="product">
 <h3>{{ product.title }}</h3>
 </li>
 </ol>
</div>`,

在浏览器中查看页面时,您可能会注意到产品列表非常长。为这些产品中的每一个加载图像对用户的计算机来说都是一个巨大的负担,同时也会有那么多的产品展示在用户面前。在我们向模板中添加更多信息(如价格和图像)之前,我们将研究如何对产品进行分页,从而允许以更易于管理的块访问数据。

创建分页

最初,创建分页似乎非常简单——因为您只需要返回固定数量的产品。但是,如果我们希望使分页对产品列表具有交互性和反应性,那么它需要更高级一些。我们需要建立我们的分页,以便能够处理不同长度的产品-在我们的产品列表已被过滤成较少的产品的情况下。

计算值

创建分页组件和显示正确产品背后的算法依赖于四个主要变量:

  • 每页项目:通常由用户设置;但是,首先,我们将使用固定的数字 12
  • 合计项目:显示的产品总数
  • 页数:可以用产品数除以每页的项目数来计算
  • 当前页码:结合其他页码,我们可以准确返回所需的产品

根据这些数字,我们可以计算分页所需的一切。这包括要显示的产品、是否显示下一个/上一个链接,以及如果需要,要跳到不同链接的组件。

在我们继续之前,我们将把products对象转换成一个数组。这允许我们对其使用 split 方法,这将允许我们返回特定的产品列表。这也意味着我们可以很容易地计算项目的总数。

更新您的products计算函数以返回array而不是object。这是通过使用map()功能实现的,该功能是 ES2015 对简单for环路的替代。此函数现在返回一个包含产品对象的数组:

products() {
  let products = this.$store.state.products;
 return Object.keys(products).map(key => products[key]);
},

在名为pagination的计算对象中创建新函数。此函数将返回一个对象,其中包含有关分页的各种数字,例如总页数。这将允许我们创建产品列表并更新导航组件。如果products变量有数据,我们只需要返回对象。该函数显示在以下代码段中:

computed: {
  products() {
    let products = this.$store.state.products;
    return Object.keys(products).map(key => products[key]);
  },

  pagination() {
 if(this.products) {

 return {

 }
 }
 }
},

我们现在需要跟踪两个变量–项目perPagecurrentPage。在HomePage组件上创建data函数并存储这两个变量。稍后,我们将为用户提供更新perPage变量的功能。突出显示的代码部分显示了我们的data功能:

const HomePage = {
  name: 'HomePage',

  template: `...`,

 data() {
 return {
 perPage: 12, 
 currentPage: 1
 }
 },

  computed: {
    ...
  }
};

You may be wondering when to use local data on a component and when to store the information in the Vuex store. This all depends on where you are going to be using the data and what is going to manipulating it. As a general rule, if only one component uses the data and manipulate it, then use the local data() function. However, if more than one component is going to be interacting with the variable, save it in the central store.

回到pagination()计算函数,存储一个具有products数组长度的变量。有了这个变量,我们现在可以计算总页数。要做到这一点,我们要做以下等式:

每页产品/项目总数

一旦我们得到这个结果,我们需要将它四舍五入到最接近的整数。这是因为如果有任何遗留问题,我们需要为其创建一个新页面

例如,如果您每页显示 12 个项目,而您有 14 个产品,则会产生 1.1666 页的结果-这不是有效的页码。四舍五入可以确保我们有两个页面来展示我们的产品。为此,请使用Math.ceil()JavaScript 函数。我们还可以将产品总数添加到我们的产量中。检查以下代码以使用Math.ceil()功能:

pagination() {
  if(this.products) {
    let totalProducts = this.products.length;

    return {
 totalProducts: totalProducts,
 totalPages: Math.ceil(totalProducts / this.perPage)
    }
  }
}

我们需要做的下一个计算是计算出当前页面的当前产品范围。这有点复杂,因为我们不仅需要从页码中计算出我们需要什么,而且数组切片是基于项目索引的——这意味着第一个项目是0

要确定从何处获取切片,我们可以使用以下计算:

(当前页码每页项目)–每页项目*

最后的减法可能看起来很奇怪,但这意味着在1页上,结果是0。这使我们能够计算出需要在哪个索引处对products数组进行切片。

再举一个例子,如果我们在第三页,结果将是 24,这是第三页的开始。切片的末尾是这个结果加上每页的项目数。这样做的好处是我们可以更新每页的项目,所有的计算都会更新。

使用这两个结果在pagination结果中创建一个对象–这将允许我们稍后轻松访问它们:

pagination() {
  if(this.products) {
    let totalProducts = this.products.length,
      pageFrom = (this.currentPage * this.perPage) - this.perPage;

    return {
      totalProducts: totalProducts,
      totalPages: Math.ceil(totalProducts / this.perPage),
      range: {
 from: pageFrom,
 to: pageFrom + this.perPage
 }
    }
  }
}

显示分页列表

通过计算分页属性,我们现在可以使用起点和终点操纵products数组。我们将使用一种方法来截断产品列表,而不是使用硬编码值或其他计算函数。这样做的好处是可以传递任何产品列表,同时也意味着 Vue 不会缓存结果。

使用新方法paginate在组件内部创建新方法对象。这应该接受一个参数,该参数将是我们要切片的products数组。在函数中,我们可以使用前面计算的两个变量返回正确数量的产品:

methods: {
  paginate(list) {
    return list.slice(
      this.pagination.range.from, 
      this.pagination.range.to
    );
  }
}

更新模板以在产品中循环时使用此方法:

template: `<div v-if="products">
  <ol>
    <li v-for="product in paginate(products)" v-if="product">
      <h3>{{ product.title }}</h3>
    </li>
  </ol>
</div>`,

现在,我们可以在浏览器中查看它,并注意它将从我们的对象返回前 12 个产品。将data对象中的currentPage变量更新为两个或三个将显示不同的产品列表,具体取决于数量。

为了继续我们的语义方法来列出我们的产品,我们应该在不在第一页时更新我们的订单列表的起始位置。这可以通过使用 HTML 属性start来完成–这允许您指定应该用哪个数字开始一个有序列表。

使用pagination.range.from变量设置我们已排序列表的起始点–记住添加1,因为在第一页它将是0

template: `<div v-if="products">
  <ol :start="pagination.range.from + 1">
    <li v-for="product in paginate(products)" v-if="product">
      <h3>{{ product.title }}</h3>
    </li>
  </ol>
</div>`

现在增加代码中的页码时,您会注意到排序列表从每个页面的适当位置开始。

创建分页按钮

通过代码更新页码对用户不友好–因此我们应该添加一些页面来增加和减少页码变量。为此,我们将创建一个函数,将currentPage变量更改为其值。这允许我们在下一页和上一页按钮中使用它,如果需要,还可以添加一个编号的页面列表。

首先,在pagination容器中创建两个按钮。如果我们处于导航的末端,我们希望禁用这些按钮-例如,返回时您不想进入1下方,前进时不想超过最大页数。我们可以通过在按钮上设置disabled属性来实现这一点——就像我们在产品详细信息页面上所做的那样,并将当前页面与这些限制进行比较。

添加一个disabled属性,在上一页上,按钮检查当前页是否为一页。在下一页按钮上,将其与我们的pagination方法的totalPages值进行比较。实现上述属性的代码如下所示:

<button :disabled="currentPage == 1">Previous page</button>
<button :disabled="currentPage == pagination.totalPages">Next page</button>

currentPage变量设置回1并在浏览器中加载主页。您应该注意到“上一页”按钮已禁用。如果更改currentPage变量,您会注意到按钮会根据需要变为活动和非活动。

我们现在需要为按钮创建一个点击方法来更新currentPage。创建一个名为toPage()的新函数。这应接受单个变量–这将直接更新currentPage变量:

methods: {
 toPage(page) {
 this.currentPage = page;
 },

  paginate(list) {
    return list.slice(this.pagination.range.from, this.pagination.range.to);
  }
}

将点击处理程序添加到按钮中,下一页按钮通过currentPage + 1,上一页按钮通过currentPage - 1

template: `<div v-if="products">
  <button @click="toPage(currentPage - 1)" :disabled="currentPage == 1">Previous page</button>
  <button @click="toPage(currentPage + 1)" :disabled="currentPage == pagination.totalPages">Next page</button>

  <ol :start="pagination.range.from + 1">
    <li v-for="product in paginate(products)" v-if="product">
      <h3>{{ product.title }}</h3>
    </li>
  </ol>
</div>`

我们现在可以在产品中来回导航。作为对用户界面的一个很好的补充,我们可以通过使用此处提到的代码,使用我们可用的变量来指示页码和剩余的页面数:

template: `<div v-if="products">
  <p>
 Page {{ currentPage }} out of {{ pagination.totalPages }}
 </p>
  <button @click="toPage(currentPage - 1)" :disabled="currentPage == 1">Previous page</button>
  <button @click="toPage(currentPage + 1)" :disabled="currentPage == pagination.totalPages">Next page</button>

  <ol :start="pagination.range.from + 1">
    <li v-for="product in paginate(products)" v-if="product">
      <h3>{{ product.title }}</h3>
    </li>
  </ol>
</div>`

在导航时更新 URL

用户体验的另一个改进是在页面导航中更新 URL–这将允许用户共享 URL、将其添加书签并稍后返回。分页时,页面处于临时状态,不应成为 URL 的主要端点。相反,我们可以利用 Vue 路由的查询参数。

更新toPage方法,在页面更改时将参数添加到 URL。这可以通过使用$router.push来实现,但是,我们需要小心不要删除将来可能用于过滤的任何现有参数。这可以通过将路由中的当前查询对象与包含page变量的新查询对象组合来实现:

toPage(page) {
  this.$router.push({
 query: Object.assign({}, this.$route.query, {
 page
 })
 }); 
  this.currentPage = page;
},

当从一个页面导航到另一个页面时,您会注意到 URL 获得了一个新参数?page=,该参数等于当前页面名称。但是,按 refresh 将不会产生正确的页面结果,而是再次显示第 1 页。这是因为我们需要将当前的page查询参数传递给HomePage组件上的currentPage变量。

这可以使用created()函数来完成——更新变量——确保我们首先检查了它的存在。created功能是 Vue 生命周期的一部分,在第 4 章使用 Dropbox API获取文件列表中有介绍:

created() {
 if(this.$route.query.page) {
 this.currentPage = parseInt(this.$route.query.page);
 }
}

我们需要确保currentPage变量是一个整数,以帮助我们完成后面需要做的任何运算,因为string不喜欢计算。

创建分页链接

在查看分页产品时,最好的做法是截短页码列表,允许用户跳转多个页面。我们已经有了在页面之间导航的机制——这可以扩展这种机制。

作为一个简单的入口点,我们可以通过循环创建指向每个页面的链接,直到达到totalPages值。Vue 允许我们在不使用任何 JavaScript 的情况下执行此操作。在组件底部创建一个nav元素,其中包含一个列表。使用一个v-for,为totalPages变量中的每个项创建一个page变量:

<nav>
  <ol>
    <li v-for="page in pagination.totalPages">
      <button @click="toPage(page)">{{ page }}</button>
    </li>
  </ol>
</nav>

这将为每个页面创建一个按钮–例如,如果总共有 24 个页面,这将创建 24 个链接。这不是期望的效果,因为我们希望在当前页面之前和之后有几页。例如,如果当前页面为 15,则页面链接应为 12、13、14、15、16、17 和 18。这意味着链接更少,对用户来说也没有那么沉重。

首先,在data对象中创建一个新变量,该变量将记录显示所选页面任一侧的页面数量–一个好的值是三:

data() {
  return {
    perPage: 12, 
    currentPage: 1,
    pageLinksCount: 3
  }
},

接下来,创建一个名为pageLinks的新计算函数。此函数需要获取当前页面,并计算出哪些页码比当前页面少三个,哪些页码比当前页面多三个。从这里开始,我们需要检查下限不小于 1,上限不大于总页数。继续之前,请检查 products 数组是否包含项:

pageLinks() {
  if(this.products.length) {
    let negativePoint = parseInt(this.currentPage) - this.pageLinksCount,
      positivePoint = parseInt(this.currentPage) + this.pageLinksCount;

    if(negativePoint < 1) {
      negativePoint = 1;
    }

    if(positivePoint > this.pagination.totalPages) {
      positivePoint = this.pagination.totalPages;
    }

    return pages;
  }
}

最后一步是创建一个数组和一个从较低范围到较高范围循环的for循环。这将创建一个数组,其中最多包含七个页码范围内的数字:

pageLinks() {
  if(this.products.length) {
    let negativePoint = parseInt(this.currentPage) - this.pageLinksCount,
      positivePoint = parseInt(this.currentPage) + this.pageLinksCount,
      pages = [];

    if(negativePoint < 1) {
      negativePoint = 1;
    }

    if(positivePoint > this.pagination.totalPages) {
      positivePoint = this.pagination.totalPages;
    }

    for (var i = negativePoint; i <= positivePoint; i++) {
 pages.push(i)
 }

 return pages;
  }
}

我们现在可以用新的pageLinks变量替换导航组件中的pagination.totalPages变量,并将创建正确数量的链接,如下所示:

<nav>
  <ul>
    <li v-for="page in pageLinks">
      <button @click="toPage(page)">{{ page }}</button>
    </li>
  </ul>
</nav>

但是,在浏览器中查看此内容会导致一些奇怪的行为。虽然将生成正确数量的链接,但单击它们或使用“下一个/上一个”按钮将使按钮保持不变–即使您导航到按钮范围之外。这是因为计算值是缓存的。我们可以通过两种方式来解决这个问题——要么将函数移动到method对象中,要么添加watch函数来观察路由并更新当前页面

选择第二个选项意味着我们可以确保没有其他结果和输出被缓存并相应地更新。在组件中添加一个watch对象,并将currentPage变量更新为页面查询变量。确保它存在,否则默认为 1。watch方法如下图所示:

watch: {
  '$route'(to) {
    this.currentPage = parseInt(to.query.page) || 1;
  }
}

这确保了当导航到不同的页面时,所有计算变量都会更新。打开HomePage组件,确保所有分页组件都能相应地工作,并更新列表。

更新每页的项目

我们需要创建的最后一个用户界面是允许用户更新每页产品的数量。首先,我们可以创建一个带有v-model属性的<select>框,直接更新值。这将按预期工作,并相应地更新产品列表,如图所示:

template: `<div v-if="products">
  <p>
    Page {{ currentPage }} out of {{ pagination.totalPages }}
  </p>

 Products per page: 
 <select v-model="perPage">
 <option>12</option>
 <option>24</option>
 <option>48</option>
 <option>60</option>
 </select>

  <button @click="toPage(currentPage - 1)" :disabled="currentPage == 1">Previous page</button>
  <button @click="toPage(currentPage + 1)" :disabled="currentPage == pagination.totalPages">Next page</button>

  <ol :start="pagination.range.from + 1">
    <li v-for="product in paginate(products)" v-if="product">
      <h3>{{ product.title }}</h3>
    </li>
  </ol>

  <nav>
    <ul>
      <li v-for="page in pageLinks">
        <button @click="toPage(page)">{{ page }}</button>
      </li>
    </ul>
  </nav>
</div>

问题在于,如果用户所处的页面高于值更改后可能出现的页面。例如,如果有 30 个产品,每页 12 个产品,这将创建三个页面。如果用户导航到第三页,然后每页选择 24 种产品,则只需要两页,第三页将为空。

这可以通过一个监视功能再次解决。当perPage变量更新时,我们可以检查当前页面是否高于totalPages变量。如果是,我们可以将其重定向到最后一页:

watch: {
  '$route'(to) {
    this.currentPage = parseInt(to.query.page);
  },

  perPage() {
 if(this.currentPage > this.pagination.totalPages) {
 this.$router.push({
 query: Object.assign({}, this.$route.query, {
 page: this.pagination.totalPages
 })
 })
 }
 }
}

创建 ListProducts 组件

在继续创建过滤和排序之前,我们需要提取产品列表逻辑并将其模板化到我们的组件中,这样我们就可以轻松地重用它。该组件应该接受一个products属性,它应该能够列出并分页。

打开ListProducts.js文件,将HomePage.js文件中的代码复制到组件中。移动数据对象并复制paginationpageLinks计算函数。将 watch 和 methods 对象以及created()功能从HomePage移动到ListProducts文件。

更新HomePage模板,使用<list-products>组件和products道具,传入products计算值。相比之下,HomePage成分现在应该小得多:

const HomePage = {
  name: 'HomePage',

  template: `<div>
    <list-products :products="products"></list-products>
  </div>`,

  computed: {
    products() {
      let products = this.$store.state.products;
      return Object.keys(products).map(key => products[key]);
    }
  }
};

ListProducts组件中,我们需要添加一个道具对象,让组件知道需要什么。这一部分现在意义重大。我们还需要向该组件添加一些东西,以使其更加通用。这些措施包括:

  • 如果有多个页面,则显示下一个/上一个链接
  • 如果产品超过 12 个,则显示“每页产品”组件;如果产品超过上一步,则仅显示每个步骤
  • 仅当pageLinks分量大于我们的pageLinksCount变量时才显示该分量

所有这些添加项都已添加到以下组件代码中,如下所示。我们还删除了不必要的products计算值:

Vue.component('list-products', {
  template: `<div v-if="products">
    <p v-if="pagination.totalPages > 1">
      Page {{ currentPage }} out of {{ pagination.totalPages }}
    </p>

    <div v-if="pagination.totalProducts > 12">
      Products per page: 
      <select v-model="perPage">
        <option>12</option>
        <option>24</option>
        <option v-if="pagination.totalProducts > 24">48</option>
        <option v-if="pagination.totalProducts > 48">60</option>
      </select>
    </div>

    <button 
      @click="toPage(currentPage - 1)" 
      :disabled="currentPage == 1" 
      v-if="pagination.totalPages > 1"
    >
      Previous page
    </button>
    <button 
      @click="toPage(currentPage + 1)" 
      :disabled="currentPage == pagination.totalPages" 
      v-if="pagination.totalPages > 1"
    >
      Next page
    </button>

    <ol :start="pagination.range.from + 1">
      <li v-for="product in paginate(products)" v-if="product">
        <h3>{{ product.title }}</h3>
      </li>
    </ol>

    <nav v-if="pagination.totalPages > pageLinksCount">
      <ul>
        <li v-for="page in pageLinks">
          <button @click="toPage(page)">{{ page }}</button>
        </li>
      </ul>
    </nav>
  </div>`,

 props: {
 products: Array
 },

  data() {
    return {
      perPage: 12, 
      currentPage: 1,
      pageLinksCount: 3
    }
  },

  computed: {
    pagination() {
      if(this.products) {
        let totalProducts = this.products.length,
          pageFrom = (this.currentPage * this.perPage) - this.perPage,
          totalPages = Math.ceil(totalProducts / this.perPage);

        return {
          totalProducts: totalProducts,
          totalPages: Math.ceil(totalProducts / this.perPage),
          range: {
            from: pageFrom,
            to: pageFrom + this.perPage
          }
        }
      }
    },

    pageLinks() {
      if(this.products.length) {
        let negativePoint = this.currentPage - this.pageLinksCount,
          positivePoint = this.currentPage + this.pageLinksCount,
          pages = [];

        if(negativePoint < 1) {
          negativePoint = 1;
        }

        if(positivePoint > this.pagination.totalPages) {
          positivePoint = this.pagination.totalPages;
        }

        for (var i = negativePoint; i <= positivePoint; i++) {
          pages.push(i)
        }

        return pages;
      }
    }
  },

  watch: {
    '$route'(to) {
      this.currentPage = parseInt(to.query.page);
    },
    perPage() {
      if(this.currentPage > this.pagination.totalPages) {
        this.$router.push({
          query: Object.assign({}, this.$route.query, {
            page: this.pagination.totalPages
          })
        })
      }
    }
  },

  created() {
    if(this.$route.query.page) {
      this.currentPage = parseInt(this.$route.query.page);
    }
  },

  methods: {
    toPage(page) {
      this.$router.push({
        query: Object.assign({}, this.$route.query, {
          page
        })
      });

      this.currentPage = page;
    },

    paginate(list) {
      return list.slice(this.pagination.range.from, this.pagination.range.to)
    }
  }
});

您可以通过临时截断HomePage模板中的 products 数组来验证您的条件呈现标记是否正常工作–完成后不要忘记将其删除:

products() {
  let products = this.$store.state.products;
  return Object.keys(products).map(key => products[key]).slice(1, 10);
}

为主页创建策划的列表

有了我们的产品列表组件,我们可以继续为我们的主页制作一个精心策划的产品列表,并向产品列表添加更多信息。

在本例中,我们将对主页组件上要显示的产品句柄数组进行硬编码。如果这是在开发中,您可能希望通过内容管理系统或类似系统来控制此列表。

在您的HomePage组件上创建一个data函数,其中包括一个名为selectedProducts的数组:

data() {
  return {
    selectedProducts: []
  }
},

使用产品列表中的几个handles填充数组。试着得到大约 6 个,但如果你超过 12 个,记住它将与我们的组件分页。将您选择的句柄添加到selectedProducts数组:

data() {
  return {
    selectedProducts: [
      'adjustable-stem',
 'colorful-fixie-lima',
 'fizik-saddle-pak',
 'kenda-tube',
 'oury-grip-set',
 'pure-fix-pedals-with-cages'
    ]
  }
},

通过我们选择的手柄,我们现在可以过滤产品列表,使其仅包含selectedProducts阵列中包含的产品列表。最初的直觉可能是在 products 数组上结合使用 JavaScriptfilter()函数和includes()

products() {
  let products = this.$store.state.products;

  products = Object.keys(products).map(key => products[key]);
  products = products.filter(product => this.selectedProducts.includes(product.handle));

  return products;
}

问题是,尽管它似乎有效,但它不尊重所选产品的订购。filter 函数只是删除任何不匹配的项,并按加载顺序保留其余产品

幸运的是,我们的产品保存在一个以句柄为键的键/值对中。使用这个,我们可以利用 products 对象并使用for循环返回一个数组

在计算函数中创建一个空数组output。在selectedProducts数组中循环,找到每个需要的产品并添加到output数组中:

products() {
  let products = this.$store.state.products,
    output = [];

  if(Object.keys(products).length) {
 for(let featured of this.selectedProducts) {
 output.push(products[featured]);
 }
 return output;
 }
}

这将创建相同的产品列表,但这次的顺序正确。尝试重新排序、添加和删除项目,以确保列表做出相应的反应。

显示更多信息

ListProduct我们现在可以在ListProduct中显示更多产品信息。如本章开头所述,我们应展示:

  • 形象
  • 标题
  • 价格
  • 制造商

我们已经显示了标题,可以很容易地从产品信息中提取图像和制造商。不要忘记总是从images数组中检索第一个图像。打开ListProducts.js文件并更新产品以显示此信息–确保在显示之前检查图像是否存在。制造商名称列在产品数据中的vendor对象下:

<ol :start="pagination.range.from + 1">
  <li v-for="product in paginate(products)" v-if="product">
    <img v-if="product.images[0]" :src="product.images[0].source" :alt="product.title" width="120">
    <h3>{{ product.title }}</h3>
    <p>Made by: {{ product.vendor.title }}</p>
  </li>
</ol>

价格的计算会有点复杂。这是因为产品的每种变化都可能有不同的价格,但是,这些价格通常是相同的。如果有不同的价格,我们应该显示最便宜的价格,并在价格前加上中的

我们需要创建一个函数,通过变量循环,计算出最便宜的价格,如果有价格范围,则添加来自的一词。为了实现这一点,我们将循环各种变化,并建立一个独特的价格数组——如果价格在数组中还不存在的话。一旦完成,我们可以检查长度-如果有多个价格,我们可以添加前缀,如果没有,这意味着所有变化都是相同的价格。

在名为productPriceListProducts组件上创建一个新方法。这接受一个参数,即变量。在内部,创建一个空数组,prices

productPrice(variations) {
  let prices = [];
}

循环遍历变体,如果prices数组不存在,则将价格附加到该数组中。创建一个使用includes()函数检查数组中是否存在价格的for循环:

productPrice(variations) {
  let prices = [];

  for(let variation of variations) {
 if(!prices.includes(variation.price)) {
 prices.push(variation.price);
 }
 }
}

通过我们的价格数组,我们现在可以提取最低数量并检查是否有多个项目。

要从数组中提取最小的数字,我们可以使用 JavaScriptMath.min()函数。使用.length属性检查数组的长度。最后,返回price变量:

productPrice(variations) {
  let prices = [];

  for(let variation of variations) {
    if(!prices.includes(variation.price)) {
      prices.push(variation.price);
    }
  }

 let price = '$' + Math.min(...prices);

 if(prices.length > 1) {
 price = 'From: ' + price;
 }

  return price;
}

productPrice方法添加到模板中,记住将product.variationProducts传递到模板中。我们最不需要添加到模板的是产品的链接:

<ol :start="pagination.range.from + 1">
  <li v-for="product in paginate(products)" v-if="product">
    <router-link :to="'/product/' + product.handle">
      <img v-if="product.images[0]" :src="product.images[0].source" :alt="product.title" width="120">
    </router-link> 
    <h3>
      <router-link :to="'/product/' + product.handle">
        {{ product.title }}
      </router-link>
    </h3>

    <p>Made by: {{ product.vendor.title }}</p>
    <p>Price {{ productPrice(product.variationProducts) }}</p>
  </li>
</ol>

理想情况下,产品链接应使用命名路由,而不是硬编码链接,以防路由发生变化。向产品路线添加名称,并更新to属性以使用该名称:

{
  path: '/product/:slug',
  name: 'Product',
  component: ProductPage
}

将模板更新为现在使用路由名称,使用params对象:

<ol :start="pagination.range.from + 1">
  <li v-for="product in paginate(products)" v-if="product">
    <router-link :to="{name: 'Product', params: {slug: product.handle}}">
      <img v-if="product.images[0]" :src="product.images[0].source" :alt="product.title" width="120">
    </router-link>
    <h3>
      <router-link :to="{name: 'Product', params: {slug: product.handle}}">
        {{ product.title }}
      </router-link>
    </h3>
    <p>Made by: {{ product.vendor.title }}</p>
    <p>Price {{ productPrice(product.variationProducts) }}</p>
  </li>
</ol>

创建类别

如果商店没有可导航的类别,那么它就不是真正可用的商店。幸运的是,我们的每一种产品都有一个type键,指示要显示的类别。我们现在可以创建一个类别页面,列出该特定类别的产品。

创建类别列表

在显示特定类别中的产品之前,我们首先需要生成可用类别的列表。为了提高我们应用的性能,我们还将存储每个类别的产品手柄。类别结构如下所示:

categories = {
  tools: {
    name: 'Tools',
    handle: 'tools',
    products: ['product-handle', 'product-handle'...]
  },
  freewheels: {
    name: 'Freewheels',
    handle: 'freewheels',
    products: ['another-product-handle', 'product'...]
  }
};

这样创建类别列表意味着我们可以随时获得类别内的产品列表,同时能够循环浏览类别并输出titlehandle以创建类别链接列表。因为我们已经有了这些信息,检索到产品列表后,我们将创建类别列表。

打开app.js并导航到Vue实例上的created()方法。我们不会在products存储方法下创建第二个$store.commit,而是将利用 Vuex 的不同功能—actions

操作允许您在存储本身中创建函数。动作无法直接改变状态——这仍然取决于突变,但它允许您将多个突变组合在一起,这在本例中非常适合我们。如果您想在改变状态之前运行异步操作,那么操作也是完美的——例如使用setTimeoutJavaScript 函数。

导航到你的Vuex.Store实例,在突变后,添加一个新的actions对象。在内部,创建一个名为initializeShop的新函数:

const store = new Vuex.Store({
  state: {
    products: {}
  },

  mutations: {
    products(state, payload) {
      state.products = payload;
    }
  },

 actions: {
 initializeShop() {

 }
 }
});

对于动作参数,第一个参数是存储本身,我们需要使用它来利用突变。有两种方法,第一种是使用单个变量并在函数中访问其属性。例如:

actions: {
  initializeShop(store) {
    store.commit('products');
  }
}

然而,使用 ES2015,我们能够使用参数分解并利用我们需要的属性。对于这个动作,我们只需要commit函数,如下所示:

actions: {
  initializeShop({commit}) {
    commit('products');
  }
}

如果我们也想从存储中获取状态,我们可以将其添加到花括号中:

actions: {
  initializeShop({state, commit}) {
    commit('products');
    // state.products
  }
}

使用这种访问属性的“分解”方法可以使我们的代码更干净,重复性更少。删除state属性,并在标有products的花括号后添加第二个参数。这将是我们格式化产品的数据。将该变量直接传递给产品的commit函数:

initializeShop({commit}, products) {
  commit('products', products);
}

使用动作与使用mutations一样简单,除了使用$store.commit而不是$store.dispatch之外。更新你的created方法–不要忘记也更改函数名,并检查你的应用是否仍然有效:

created() {
  CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
    this.$store.dispatch('initializeShop', this.$formatProducts(data));
  });
}

下一步是为我们的类别创建一个变异。由于我们可能希望独立于我们的产品更新我们的类别–我们应该在mutations中创建第二个功能。也应该是这个函数循环遍历产品并创建类别列表。

首先,在 state 对象中创建一个名为categories的新属性。默认情况下,这应该是一个对象:

state: {
  products: {},
  categories: {}
}

接下来,创建一个名为categories的新突变。与状态一起,这应该采用第二个参数。为保持一致,请将其命名为payload——因为 Vuex 将其称为:

mutations: {
  products(state, payload) {
    state.products = payload;
  },

 categories(state, payload) {

 }
},

现在来看看功能。这种突变需要在产品中循环。对于每种产品,都需要隔离type。一旦有了标题和 slug,它就需要检查是否存在带有该 slug 的条目;如果有,则将产品句柄附加到products数组中,如果没有,则需要创建一个新数组和详细信息。

创建一个空的categories对象并在有效负载中循环,为产品和类型设置一个变量:

categories(state, payload) {
 let categories = {}; 
 Object.keys(payload).forEach(key => {
 let product = payload[key],
 type = product.type;
 });
}

我们现在需要检查是否存在具有当前type.handle密钥的条目。如果没有,我们需要用它创建一个新条目。条目需要有标题、句柄和空产品数组:

categories(state, payload) {
  let categories = {};

  Object.keys(payload).forEach(key => {
    let product = payload[key],
      type = product.type;

 if(!categories.hasOwnProperty(type.handle)) {
 categories[type.handle] = {
 title: type.title,
 handle: type.handle,
 products: []
 }
 }
  });
}

最后,我们需要将当前产品句柄附加到条目的 products 数组中:

categories(state, payload) {
  let categories = {};

  Object.keys(payload).forEach(key => {
    let product = payload[key],
      type = product.type;

    if(!categories.hasOwnProperty(type.handle)) {
      categories[type.handle] = {
        title: type.title,
        handle: type.handle,
        products: []
      }
    }

    categories[type.handle].products.push(product.handle);
  });
}

您可以通过在函数末尾添加console.log来查看categories输出:

categories(state, payload) {
  let categories = {};

  Object.keys(payload).forEach(key => {
    ...
  });

  console.log(categories);
}

将突变添加到initializeShop动作中:

initializeShop({commit}, products) {
  commit('products', products);
  commit('categories', products);
}

在浏览器中查看应用时,您将面临 JavaScript 错误。这是因为有些产品不包含我们用来对其进行分类的“类型”。即使解决了 JavaScript 错误,仍然有许多类别被列出。

为了帮助确定类别的数量,并对未分类的产品进行分组,我们应该创建一个“杂项”类别。这将使用两种或两种以下的产品对所有类别进行比较,并将产品分组到各自的组中。

创建“杂项”类别

我们需要否定的第一个问题是无名类别。当在我们的产品中循环时,如果没有找到类型,我们应该插入一个类别,这样所有的东西都是分类的

categories方法中创建一个新对象,该对象包含新类别的标题和句柄。对于句柄和变量,将其称为 other。将标题命名为杂项,使其更易于使用

let categories = {},
  other = {
 title: 'Miscellaneous',
 handle: 'other'
 };

在产品中循环时,我们可以检查type键是否存在,如果不存在,则创建一个other类别并附加到它:

Object.keys(payload).forEach(key => {
  let product = payload[key],
    type = product.hasOwnProperty('type') ? product.type : other;

  if(!categories.hasOwnProperty(type.handle)) {
    categories[type.handle] = {
      title: type.title,
      handle: type.handle,
      products: []
    }
  }

  categories[type.handle].products.push(product.handle);
});

现在查看该应用将显示 JavaScript 控制台中的所有类别–允许您查看类别数量的大小。

让我们将任何包含两个或两个以下产品的类别合并到“其他”类别中,不要忘记在之后删除该类别。在产品循环之后,通过类别循环,检查可用产品的数量。如果少于三个,则将其添加到“其他”类别:

Object.keys(categories).forEach(key => {
  let category = categories[key];

  if(category.products.length < 3) {
    categories.other.products = categories.other.products.concat(category.products);
  }
});

然后,我们可以删除刚刚从中窃取产品的类别:

Object.keys(categories).forEach(key => {
  let category = categories[key];

  if(category.products.length < 3) {
    categories.other.products = categories.other.products.concat(category.products);
    delete categories[key];
  }
});

这样,我们就有了一个更易于管理的类别列表。我们可以做的另一个改进是确保类别按字母顺序排列。这有助于用户更快地找到他们想要的类别。在 JavaScript 中,数组比对象更容易排序,所以我们再次需要遍历对象键数组并对它们进行排序。创建一个新对象,并在对其排序时添加类别。之后,将其存储在state对象上,这样我们就有了可用的类别:

categories(state, payload) {
  let categories = {},
    other = {
      title: 'Miscellaneous',
      handle: 'other'
    };

  Object.keys(payload).forEach(key => {
    let product = payload[key],
      type = product.hasOwnProperty('type') ? product.type : other;

    if(!categories.hasOwnProperty(type.handle)) {
      categories[type.handle] = {
        title: type.title,
        handle: type.handle,
        products: []
      }
    }

    categories[type.handle].products.push(product.handle);
  });

  Object.keys(categories).forEach(key => {
    let category = categories[key];

    if(category.products.length < 3) {
      categories.other.products =      categories.other.products.concat(category.products);
      delete categories[key];
    }
  });

  let categoriesSorted = {}
 Object.keys(categories).sort().forEach(key => {
 categoriesSorted[key] = categories[key]
 });
 state.categories = categoriesSorted;
}

有了这些,我们现在可以在HomePage模板中添加一个类别列表。为此,我们将创建命名的router-view组件——允许我们将内容放在商店的侧栏中的选定页面上。

显示类别

存储了类别后,我们现在可以继续创建ListCategories组件。我们希望在主页上的侧边栏以及商店类别页面上显示我们的类别导航。由于我们想在多个地方显示它,我们有几个选项来显示它。

我们可以像使用<list-products>组件一样使用模板中的组件。问题是,如果我们想在侧栏中显示我们的列表,并且侧栏需要在整个站点上保持一致,那么我们必须在视图之间复制和粘贴大量 HTML。

更好的方法是使用命名路由并在index.html中设置一次模板。

更新应用模板以包含<main><aside>元素。在这些元素中,创建一个router-view,将main中的元素保留为未命名,同时将aside元素中的元素命名为sidebar

<div id="app">
  <main>
    <router-view></router-view>
  </main>
 <aside>
 <router-view name="sidebar"></router-view>
 </aside>
</div>

在 routes 对象中,我们现在可以向不同的命名视图添加不同的组件。在Home路线上,将component键更改为components,并添加一个对象-指定每个组件及其视图:

{
  path: '/',
  name: 'Home',
  components: {
 default: HomePage,
 sidebar: ListCategories
 }
}

默认值表示组件将进入未命名的router-view。这允许我们在需要时仍然使用单键component键。为了将组件正确加载到侧栏视图中,我们需要改变ListCategories组件的初始化方式。不要使用Vue.component,而是将其初始化为view组件:

const ListCategories = {
  name: 'ListCategories'

};

我们现在可以继续制作类别列表的模板。由于我们的分类保存在商店中,现在应该已经熟悉如何加载和显示它们了。建议您将状态中的类别加载到计算函数中,以便在需要以任何方式操作它时,更干净的模板代码和更容易的自适应。

在创建模板之前,我们需要为类别创建路由。回到我们在第 9 章中的计划,使用 Vue 路由动态路由加载数据,我们可以看到路由将是/category/:slug——添加此路由和name并启用道具,因为我们将在slug中使用它们。确保已创建CategoryPage文件并初始化组件。

const router = new VueRouter({
  routes: [
    {
      path: '/',
      name: 'Home',
      components: {
        default: HomePage,
        sidebar: ListCategories
      }
    },
    {
 path: '/category/:slug',
 name: 'Category',
 component: CategoryPage,
 props: true
 },
    {
      path: '/product/:slug',
      name: 'Product',
      component: ProductPage
    },

    {
      path: '/404', 
      alias: '*',
      component: PageNotFound
    }
  ]
});

回到我们的ListCategories组件;循环浏览存储的类别,并为每个类别创建链接。在每个名称后的括号中显示产品数量:

const ListCategories = {
  name: 'ListCategories',

 template: `<div v-if="categories">
 <ul>
 <li v-for="category in categories">
 <router-link :to="{name: 'Category', params: {slug: category.handle}}">
 {{ category.title }} ({{ category.products.length }})
 </router-link>
 </li>
 </ul>
 </div>`,

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

我们的分类链接现在显示在主页上,我们可以开始制作分类页面。

在类别中显示产品

点击其中一个类别链接(即/#/category/grips)将导航到一个空白页面——这要感谢我们的路线。我们需要创建一个模板,并设置类别页面来显示产品。作为起点,创建与产品页面类似的CategoryPage组件

创建一个包含空容器和PageNotFound组件的模板。创建一个名为categoryNotFound的数据变量,如果设置为true,则确保PageNotFound组件显示。创建一个props对象,该对象允许传递slug属性,最后创建一个category计算函数。

CategoryPage组件应如下所示:

const CategoryPage = {
  name: 'CategoryPage',

  template: `<div>
    <div v-if="category"></div>
    <page-not-found v-if="categoryNotFound"></page-not-found>
  </div>`,

  components: {
    PageNotFound
  },

  props: {
    slug: String
  },

  data() {
    return {
      categoryNotFound: false,
    }
  },

  computed: {
    category() {
    }
  }
};

category计算函数中,根据 slug 从存储中加载正确的类别。如果不在列表中,请将categoryNotFound变量标记为 true-类似于我们在ProductPage组件中所做的操作:

computed: {
  category() {
    let category;

 if(Object.keys(this.$store.state.categories).length) {

 category = this.$store.state.categories[this.slug];

 if(!category) {
 this.categoryNotFound = true;
 }
 }

 return category;
  }
}

加载类别后,我们可以在模板中输出标题:

template: `<div>
  <div v-if="category">
    <h1>{{ category.title }}</h1>
  </div>
  <page-not-found v-if="categoryNotFound"></page-not-found>
</div>`,

我们现在可以继续在分类页面上显示产品。为此,我们可以使用HomePage组件中的代码,因为我们有完全相同的场景–一系列产品句柄。

创建一个新的computed功能,以获取当前类别的产品,并像我们在主页上所做的那样对其进行处理:

computed: {
  category() {
    ...
  },

  products() {
    if(this.category) {
 let products = this.$store.state.products,
 output = [];

 for(let featured of this.category.products) {
 output.push(products[featured]);
 }

 return output; 
 }
  }
}

我们不需要检查该函数中是否存在产品,因为我们正在检查类别是否存在,并且只有在数据已加载的情况下才会返回 true。将组件添加到 HTML 并传入products变量:

template: `<div>
  <div v-if="category">
    <h1>{{ category.title }}</h1>
    <list-products :products="products"></list-products>
  </div>
  <page-not-found v-if="categoryNotFound"></page-not-found>
</div>`

这样,我们就为每个类别列出了我们的类别产品。

代码优化

随着CategoryPage组件的完成,我们可以看到它与主页之间有很多相似之处——唯一的区别是主页有一个固定的产品数组。为了避免重复,我们可以将这两个部分结合起来——这意味着我们只需要在需要时更新一个。

我们可以通过在主页上标识时显示它来解决固定阵列问题。这样做的方法是检查 slug 道具是否有值。如果没有,我们可以假设我们在主页上。

首先,更新Home路由,指向CategoryPage组件并启用道具。使用命名视图时,必须为每个视图启用道具。将 props 值更新为具有每个命名视图的对象,为每个视图启用 props:

{
  path: '/',
  name: 'Home',
  components: {
    default: CategoryPage,
    sidebar: ListCategories
  },
  props: {
 default: true, 
 sidebar: true
 }
}

接下来,在CategoryPagedata函数中创建一个名为categoryHome的新变量。这将是一个与 category 对象具有相同结构的对象,包含products数组、标题和句柄。尽管不使用手柄,但遵循惯例是一种良好的做法:

data() {
  return {
    categoryNotFound: false,
    categoryHome: {
 title: 'Welcome to the Shop',
 handle: 'home',
 products: [
 'adjustable-stem',
 'fizik-saddle-pak',
 'kenda-tube',
 'colorful-fixie-lima',
 'oury-grip-set',
 'pure-fix-pedals-with-cages'
 ]
 }
  }
}

我们需要做的最后一件事是检查 slug 是否存在。如果没有,请将新对象指定给计算函数中的类别变量:

category() {
  let category;

  if(Object.keys(this.$store.state.categories).length) {
    if(this.slug) {
 category = this.$store.state.categories[this.slug];
 } else {
 category = this.categoryHome;
 }

    if(!category) {
      this.categoryNotFound = true;
    }
  }

  return category;
}

转到主页,验证新组件是否正常工作。如果是,您可以删除HomePage.js并将其从index.html中删除。更新类别路由,将类别列表也包括在侧边栏中,并使用props对象:

{
  path: '/category/:slug',
  name: 'Category',
  components: {
 default: CategoryPage,
 sidebar: ListCategories
 },
  props: {
 default: true, 
 sidebar: true
 }
},

订购某一类别的产品

随着我们的分类页面显示正确的产品,是时候在我们的ListProducts组件中添加一些订购选项了。在线查看店铺时,您通常可以通过以下方式订购产品:

  • 标题:升序(A-Z)
  • 标题:下降(Z-A)
  • 价格:上涨(1-999 美元)
  • 价格:降价($999-$1)

然而,一旦我们有了适当的机制,您就可以添加任何您想要的订购条件。

首先,在ListProducts组件中创建一个包含上述每个值的选择框。添加一个额外的按…排序的第一个产品:

<div class="ordering">
  <select>
    <option>Order products</option>
    <option>Title - ascending (A - Z)</option>
    <option>Title - descending (Z - A)</option>
    <option>Price - ascending ($1 - $999)</option>
    <option>Price - descending ($999 - $1)</option>
  </select>
</div>

我们现在需要为选择框创建一个变量,以便在data函数中进行更新。添加一个名为ordering的新键,并为每个选项添加一个值,这样解释值就更容易了。使用字段和顺序构造值,用连字符分隔。例如,Title - ascending (A-Z)将变成title-asc

<div class="ordering">
  <select v-model="ordering">
    <option value="">Order products</option>
    <option value="title-asc">Title - ascending (A - Z)</option>
    <option value="title-desc">Title - descending (Z - A)</option>
    <option value="price-asc">Price - ascending ($1 - $999)</option>
    <option value="price-desc">Price - descending ($999 - $1)</option>
  </select>
</div>

更新后的data功能变为:

data() {
  return {
    perPage: 12, 
    currentPage: 1,
    pageLinksCount: 3,

    ordering: ''
  }
}

为了更新产品的顺序,我们现在需要操纵产品列表。这需要在列表被拆分分页之前完成,因为用户希望整个列表被排序,而不仅仅是当前页面。

存储产品价格

在我们继续之前,我们需要解决一个问题。要按价格排序,理想情况下,价格需要在产品本身上可用,而不是专门为模板计算,目前是模板。为了解决这个问题,我们将在产品添加到商店之前计算价格。这意味着它将作为产品本身的属性提供,而不是动态创建。

我们需要知道的细节是最便宜的价格,以及该产品是否有多种不同的价格。后者意味着我们知道在列出产品时是否需要显示"From:"。我们将为每个产品创建两个新属性:pricehasManyPrices

导航到商店中的products突变,创建一个新对象和一个产品循环:

products(state, payload) {
 let products = {};

 Object.keys(payload).forEach(key => {
 let product = payload[key];

 products[key] = product;
 });

  state.products = payload;
}

ListProducts组件上的productPrice方法复制代码,并将其放入循环中。更新第二个for循环,使其通过product.variationProducts循环。一旦for循环完成,我们就可以将新属性添加到产品中。最后,使用新产品对象更新状态:

products(state, payload) {
  let products = {};

  Object.keys(payload).forEach(key => {
    let product = payload[key];

    let prices = [];
 for(let variation of product.variationProducts) {
 if(!prices.includes(variation.price)) {
 prices.push(variation.price);
 }
 }

 product.price = Math.min(...prices);
 product.hasManyPrices = prices.length > 1;

    products[key] = product;
  });

  state.products = products;
}

我们现在可以在ListProducts组件上更新productPrice方法。更新函数,使其接受产品,而不是变体。从函数中删除for循环,并更新变量,使其使用产品的pricehasManyPrices属性:

productPrice(product) {
  let price = '$' + product.price;

  if(product.hasManyPrices) {
    price = 'From: ' + price;
  }

  return price;
}

更新模板,以便将产品传递给功能:

<p>Price {{ productPrice(product) }}</p>

把订货单接线好

有了我们的价格,我们就可以开始下订单了。创建一个名为orderProducts的新computed函数,返回this.products。我们希望确保我们总是从源头上进行排序,而不是订购以前订购过的东西。从paginate函数中调用此新函数,并从此方法和模板中删除参数:

computed: {
 ...

  orderProducts() {
 return this.products;
 }, },

methods: {
  paginate() {
    return this.orderProducts.slice(
      this.pagination.range.from,  
      this.pagination.range.to
    );
  },
}

为了确定我们需要如何对产品进行分类,我们可以使用this.ordering值。如果它存在,我们可以在连字符上拆分字符串,这意味着我们有一个包含字段和订单类型的数组。如果它不存在,我们只需返回现有的产品阵列:

orderProducts() {
  let output;

 if(this.ordering.length) {
 let orders = this.ordering.split('-');
 } else {
 output = this.products;
 }
 return output;
}

根据排序数组第一项的值对products数组进行排序。如果是字符串,我们将使用localCompare,在比较时忽略大小写。否则,我们将简单地从另一个值中减去一个值–这是sort函数所期望的:

orderProducts() {
  let output;

  if(this.ordering.length) {
    let orders = this.ordering.split('-');

    output = this.products.sort(function(a, b) {
 if(typeof a[orders[0]] == 'string') {
 return a[orders[0]].localeCompare(b[orders[0]]);
 } else {
 return a[orders[0]] - b[orders[0]];
 }
 });

  } else {
    output = this.products;
  }
  return output;
}

最后,我们需要检查orders数组中的第二项是asc还是desc。默认情况下,当前排序函数将返回按ascending顺序排序的项目,因此如果值为desc,我们可以反转数组:

orderProducts() {
  let output;

  if(this.ordering.length) {
    let orders = this.ordering.split('-');

    output = this.products.sort(function(a, b) {
      if(typeof a[orders[0]] == 'string') {
        return a[orders[0]].localeCompare(b[orders[0]]);
      } else {
        return a[orders[0]] - b[orders[0]];
      }
    });

 if(orders[1] == 'desc') {
 output.reverse();
 }
  } else {
    output = this.products;
  }
  return output;
}

前往您的浏览器,查看产品订购信息!

创建 Vuex getter

我们的分类介绍就像其他任何一个步骤一样。过滤允许您查找具有特定尺寸、颜色、标签或制造商的产品。我们的过滤选项将根据页面上的产品构建。例如,如果所有产品都没有 XL 码或蓝色,则没有必要将其显示为过滤器。

为了实现这一点,我们还需要将当前类别的产品传递给过滤组件。然而,产品在CategoryPage组件上进行加工。我们可以将功能移到 Vuex 存储区getter,而不是重复此处理。getter 允许您从存储中检索数据,并像在组件的函数中一样对其进行操作。然而,由于它是一个中心位置,这意味着几个组件可以从处理中受益。

getter 是计算函数的 Vuex 等价物。它们声明为函数,但称为变量。但是,可以通过返回其中的函数来操纵它们以接受参数。

我们将把categoryproducts函数从CategoryPage组件移到 getter 中。然后,getter函数将返回一个包含类别和产品的对象。

在商店中创建一个名为getters的新对象。在内部,创建一个名为categoryProducts的新函数:

getters: {
  categoryProducts: () => {

  }
}

getter 本身接收两个参数,状态作为第一个参数,任何其他 getter 作为第二个参数。要将参数传递给 getter,必须返回接收参数的 getter 内部的函数。幸运的是,在 ES2015 中,这可以通过双箭头(=>语法实现。由于我们不打算在此函数中使用任何其他 getter,因此不需要调用第二个参数。

当我们将所有逻辑提取出来时,传入slug变量作为第二个函数的参数:

categoryProducts: (state) => (slug) => {

}

当我们将选择和检索类别和产品的逻辑转移到商店中时,将HomePage类别内容存储在state本身是有意义的:

state: {
  products: {},
  categories: {},

  categoryHome: {
 title: 'Welcome to the Shop',
 handle: 'home',
 products: [
 'adjustable-stem',
 'fizik-saddle-pak',
 'kenda-tube',
 'colorful-fixie-lima',
 'oury-grip-set',
 'pure-fix-pedals-with-cages'
 ]
 }
}

将类别选择逻辑从CategoryPage组件中的category计算函数移动到 getter 中。更新slugcategoryHome变量以使用相关位置的内容:

categoryProducts: (state) => (slug) => {
  if(Object.keys(state.categories).length) {
    let category = false;

    if(slug) {
      category = this.$store.state.categories[this.slug];
    } else {
      category = state.categoryHome;
    }
  }
}

分配了类别后,我们现在可以根据类别中存储的句柄加载产品。将代码从products计算函数移到 getter 中。将变量分配组合在一起并删除 store product retrieval 变量,因为我们有现成的状态可用。确保检查类别是否存在的代码仍然存在:

categoryProducts: (state) => (slug) => {
  if(Object.keys(state.categories).length) {
    let category = false,
      products = [];

    if(slug) {
      category = this.$store.state.categories[this.slug];
    } else {
      category = state.categoryHome;
    }

    if(category) {
 for(let featured of category.products) {
 products.push(state.products[featured]);
 }
 }
  }
}

最后,我们可以使用充实的产品数据在category上添加一个新的productDetails数组。返回函数末尾的category。如果slug变量输入作为一个类别存在,我们将返回所有数据。如果没有,它将返回false——我们可以从中显示我们的PageNotFound组件:

categoryProducts: (state) => (slug) => {
  if(Object.keys(state.categories).length) {
    let category = false,
      products = [];

    if(slug) {
      category = state.categories[slug];
    } else {
      category = state.categoryHome;
    }

    if(category) {
      for(let featured of category.products) {
        products.push(state.products[featured]);
      }

      category.productDetails = products;
    }

    return category;
  }
}

在我们的CategoryPage组件中,我们可以删除products()计算函数并更新category()函数。要调用getter函数,请参考this.$store.getters

computed: {
  category() {
    if(Object.keys(this.$store.state.categories).length) {
      let category = this.$store.getters.categoryProducts(this.slug);

      if(!category) {
        this.categoryNotFound = true;
      }
      return category;
    }
  }
}

不幸的是,在继续之前,我们仍然必须检查这些类别是否存在。这样我们就可以知道,没有名称为的类别,而不是未加载的类别。

为了使它更整洁,我们可以将这个检查提取到另一个 getter 中,并在另一个 getter 和组件中使用它。

创建一个名为categoriesExist的新 getter,并返回if语句的内容:

categoriesExist: (state) => {
  return Object.keys(state.categories).length;
},

更新categoryProductsgetter 以接受第一个函数的参数中的 getter,并使用此新 getter 指示其输出:

categoryProducts: (state, getters) => (slug) => {
  if(getters.categoriesExist) {
    ...
  }
}

在我们的CategoryPage组件中,我们现在可以使用this.$store.getters.categoriesExist()调用新的 getter。为了避免在这个函数中重复两次this.$store.getters,我们可以映射要本地访问的 getter。这允许我们调用this.categoriesExist()作为更可读的函数名。

computed对象的开头,添加一个名为...Vuex.mapGetters()的新函数。此函数接受数组或对象作为参数,开头的三个点确保内容扩展为与computed对象合并。

传入包含两个 getter 的名称的数组:

computed: {
 ...Vuex.mapGetters([
 'categoryProducts',
 'categoriesExist'
 ]),

  category() {
    ...
  }
}

这意味着我们有this.categoriesExistthis.categoryProducts供我们使用。更新类别函数以使用这些新函数:

computed: {
  ...Vuex.mapGetters([
    'categoriesExist',
    'categoryProducts'
  ]),

  category() {
    if(this.categoriesExist) {
      let category = this.categoryProducts(this.slug);

      if(!category) {
        this.categoryNotFound = true;
      }
      return category;
    }
  }
}

更新模板以反映计算数据中的更改:

template: `<div>
  <div v-if="category">
    <h1>{{ category.title }}</h1>
    <list-products :products="category.productDetails"></list-products>
  </div>
  <page-not-found v-if="categoryNotFound"></page-not-found>
</div>`,

基于产品构建过滤组件

如前所述,我们的所有过滤器都将从当前类别的产品中创建。这意味着如果没有IceToolz生产的产品,它将不会显示为可用的过滤器。

首先,打开ProductFiltering.js组件文件。我们的产品过滤将进入侧栏,因此将组件定义从Vue.component更改为对象。我们仍然希望在过滤后显示我们的类别,因此在ProductFiltering中添加ListCategories组件作为声明组件。添加模板键并包含<list-categories>组件:

const ProductFiltering = {
  name: 'ProductFiltering',

  template: `<div>
    <list-categories />
  </div>`,

  components: {
    ListCategories
  }
}

更新类别路由,将ProductFiltering组件包含在侧栏中,而不是ListCategories

{
  path: '/category/:slug',
  name: 'Category',
  components: {
    default: CategoryPage,
    sidebar: ProductFiltering
  },
  props: {
    default: true, 
    sidebar: true
  }
}

您现在应该有Home路由,它包括CategoryPageListCategories组件,还有Category路由,它包括ProductFiltering组件。

CategoryPage组件复制道具和计算对象——因为我们将使用大量现有代码。将category计算函数重命名为filters。删除 return 和componentNotFoundif 语句。您的组件现在应该如下所示:

const ProductFiltering = {
  name: 'ProductFiltering',

  template: `<div>
    <list-categories />
  </div>`,

  components: {
    ListCategories
  },

  props: {
 slug: String
 },

 computed: {
 ...Vuex.mapGetters([
 'categoriesExist',
 'categoryProducts'
 ]),
 filters() {
 if(this.categoriesExist) {
 let category = this.categoryProducts(this.slug);

 }
 }
 }
}

我们现在需要根据类别中的产品构建过滤器。我们将通过在产品中循环、从预选值中收集信息并显示它们来实现这一点。

创建一个包含topics键的data对象。这将是一个包含子对象的对象,对于我们要筛选的每个属性,该子对象的模式为'handle': {},现在已经很熟悉了。

每个子对象将包含一个handle,它是要筛选的产品(例如,供应商)的值,title,它是密钥的用户友好版本,以及一个将填充的值数组。

我们将从两个开始,vendortags;但是,在我们处理产品时,将动态添加更多内容:

data() {
  return {
    topics: {
      vendor: {
        title: 'Manufacturer',
        handle: 'vendor',
        values: {}
      },
      tags: {
        title: 'Tags',
        handle: 'tags',
        values: {}
      }
    }
  }
},

我们现在将开始在产品中循环。除了这些价值之外,我们还将跟踪有多少产品具有相同的价值,从而允许我们向用户指示将显示多少产品。

循环通过filters方法中类别的products,首先找到每个产品的vendor。对于遇到的每一个,检查它是否存在于values数组中。

如果没有,则添加一个带有namehandlecount的新对象,这是一个产品句柄数组。我们存储了一个句柄数组,以便验证产品是否已被看到。如果我们保持一个原始的数字计数,我们可能会遇到一个场景,过滤器被触发两次,使计数加倍。通过检查产品句柄是否已经存在,我们可以检查它只出现一次。

如果该名称的筛选器确实存在,请在检查其不存在后将句柄添加到数组:

filters() {
  if(this.categoriesExist) {

    let category = this.categoryProducts(this.slug),
      vendors = this.topics.vendor;

 for(let product of category.productDetails) {

        if(product.hasOwnProperty('vendor')) {
 let vendor = product.vendor; 
 if(vendor.handle) { if(!vendor.handle.count.includes(product.handle)) {
              category.values[item.handle].count.push(product.handle);
            }
          } else {
 vendors.values[vendor.handle] = {
 ...vendor,
 count: [product.handle]
 }
 }
 } 
 }

 }
  }
}

这利用了之前使用的对象扩展省略号(...,这使我们不必写:

vendors.values[product.vendor.handle] = {
  title: vendor.title,
 handle: vendor.handle,
  count: [product.handle]
}

尽管如此,如果您对它更满意,请随意使用它。

复制代码以使用tags,但是由于tags本身是一个数组,我们需要循环遍历每个标记并相应添加:

for(let product of category.productDetails) {

  if(product.hasOwnProperty('vendor')) {
    let vendor = product.vendor;

    if(vendor.handle) {
      if(!vendor.handle.count.includes(product.handle)) {
        category.values[item.handle].count.push(product.handle);
      }
    } else {
      vendors.values[vendor.handle] = {
        ...vendor,
        count: [product.handle]
      }
    }
  }

 if(product.hasOwnProperty('tags')) {
 for(let tag of product.tags) {
 if(tag.handle) {
 if(topicTags.values[tag.handle]) {
 if(!topicTags.values[tag.handle].count.includes(product.handle)) {
            topicTags.values[tag.handle].count.push(product.handle);
          }
 } else {
 topicTags.values[tag.handle] = {
 ...tag,
 count: [product.handle]
 }
 }
 }
 }
 }

}

我们的代码已经变得重复和复杂,让我们通过创建一个方法来处理重复的代码来简化它。

创建一个具有addTopic函数的methods对象。这需要两个参数:要附加到的对象和单数项。例如,其用途是:

if(product.hasOwnProperty('vendor')) {
  this.addTopic(this.topics.vendor, product.vendor, product.handle);
}

创建函数并从hasOwnPropertyif 声明中抽象出逻辑。命名两个参数categoryitem,并相应更新代码:

methods: {
  addTopic(category, item, handle) {
    if(item.handle) {

      if(category.values[item.handle]) {
        if(!category.values[item.handle].count.includes(handle)) {
          category.values[item.handle].count.push(handle);
        }

      } else {

        category.values[item.handle] = {
          ...item,
          count: [handle]
        }
      }
    }
  }
}

更新filters计算函数以使用新的addTopic方法。删除函数顶部的变量声明,因为它们将直接传递到方法中:

filters() {
  if(this.categoriesExist) {

    let category = this.categoryProducts(this.slug);

    for(let product of category.productDetails) {

      if(product.hasOwnProperty('vendor')) {
        this.addTopic(this.topics.vendor, product.vendor, product.handle);
      }

      if(product.hasOwnProperty('tags')) {
        for(let tag of product.tags) {
          this.addTopic(this.topics.tags, tag, product.handle);
        }
      }

    }
  }
}

此函数结束时,返回this.topics。虽然我们可以在模板中直接引用topics,但我们需要确保触发filters计算属性:

filters() {
  if(this.categoriesExist) {
    ...
  }

  return this.topics;
}

在基于各种类型创建动态过滤器之前,让我们先显示当前过滤器。

由于topics对象是如何设置的,我们可以循环每个子对象,然后循环每个子对象的values。我们将使用复选框制作过滤器,输入值是每个过滤器的句柄:

template: `<div>
 <div class="filters">
 <div class="filterGroup" v-for="filter in filters">
 <h3>{{ filter.title }}</h3>

 <label class="filter" v-for="value in filter.values">
 <input type="checkbox" :value="value.handle">
 {{ value.title }} ({{ value.count }})
 </label>
 </div> 
 </div>

  <list-categories />
</div>`,

为了跟踪检查的内容,我们可以使用v-model属性。如果存在具有相同v-model的复选框,Vue 将为每个项目创建一个数组。

向数据对象中的每个topic对象添加一个checked数组:

data() {
  return {
    topics: {
      vendor: {
        title: 'Manufacturer',
        handle: 'vendor',
        checked: [],
        values: {}
      },
      tags: {
        title: 'Tags',
        handle: 'tags',
        checked: [],
        values: {}
      }
    }
  }
}

接下来,向每个复选框添加一个v-model属性,在filter对象上引用该数组,并使用一个点击绑定器,引用一个updateFilters方法:

<div class="filters">
  <div class="filterGroup" v-for="filter in filters">
    <h3>{{ filter.title }}</h3>

    <label class="filter" v-for="value in filter.values">
      <input type="checkbox" :value="value.handle" v-model="filter.checked"  @click="updateFilters">
      {{ value.title }} ({{ value.count }})
    </label>
  </div> 
</div>

现在创建一个空方法-我们稍后将配置它:

methods: {
    addTopic(category, item) {
      ...
    },

 updateFilters() {

 }
}

动态创建过滤器

通过创建和监视固定过滤器,我们可以借此机会创建动态过滤器。这些过滤器将观察产品上的variationTypes(例如,颜色和尺寸),并列出选项–再次列出每个选项的计数。

为了实现这一点,我们需要首先在产品上通过variationTypes循环。在添加任何内容之前,我们需要检查topics对象上是否存在该变体类型,如果不存在,我们需要添加一个骨架对象。这扩展了变体(包含titlehandle,还包括空checkedvalue属性:

filters() {
  if(this.categoriesExist) {

    let category = this.categoryProducts(this.slug);

    for(let product of category.productDetails) {

      if(product.hasOwnProperty('vendor')) {
        this.addTopic(this.topics.vendor, product.vendor);
      }

      if(product.hasOwnProperty('tags')) {
        for(let tag of product.tags) {
          this.addTopic(this.topics.tags, tag);
        }
      }

 Object.keys(product.variationTypes).forEach(vkey => {
 let variation = product.variationTypes[vkey];

 if(!this.topics.hasOwnProperty(variation.handle)) {
 this.topics[variation.handle] = {
 ...variation,
 checked: [],
 values: {}
 }
 }
 });

    }
  }

  return this.topics;
}

创建了空对象后,我们现在可以在 product 对象上循环通过variationProducts。对于每个变量,我们都可以使用当前变量的句柄访问变量。从那里,我们可以使用我们的addTopic方法将值(例如,蓝色或 XL)包含在过滤器中:

Object.keys(product.variationTypes).forEach(vkey => {
  let variation = product.variationTypes[vkey];

  if(!this.topics.hasOwnProperty(variation.handle)) {
    this.topics[variation.handle] = {
      ...variation,
      checked: [],
      values: {}
    }
  }

  Object.keys(product.variationProducts).forEach(pkey => {
 let variationProduct = product.variationProducts[pkey]; 
 this.addTopic(
 this.topics[variation.handle],
 variationProduct.variant[variation.handle],      product.handle
 );
 });

});

然而,我们确实需要更新我们的addTopic方法。这是因为动态属性有一个value,而不是标题。

在您的addTopic方法中添加if语句,以检查value是否存在,如果存在–将其设置为title

addTopic(category, item, handle) {
  if(item.handle) {

    if(category.values[item.handle]) {
      if(!category.values[item.handle].count.includes(handle)) {
        category.values[item.handle].count.push(handle);
      }

    } else {

 if(item.hasOwnProperty('value')) {
 item.title = item.value;
 }

      category.values[item.handle] = {
        ...item,
        count: [handle]
      }
    }
  }
}

在浏览器中查看应用应该会显示您动态添加的过滤器,以及我们添加的原始过滤器。

重置过滤器

在类别之间导航时,您会注意到,当前过滤器不会重置。这是因为我们没有清除每个导航之间的过滤器,并且数组是持久的。这并不理想,因为这意味着它们随着您的浏览而变长,并且不适用于列出的产品。

为了解决这个问题,我们可以创建一个返回默认主题对象的方法,当 slug 更新时,调用该方法重置topics对象。将topics对象移动到一个名为defaultTopics的新方法:

methods: {
 defaultTopics() {
 return {
 vendor: {
 title: 'Manufacturer',
 handle: 'vendor',
 checked: [],
 values: {}
 },
 tags: {
 title: 'Tags',
 handle: 'tags',
 checked: [],
 values: {}
 }
 }
 },

  addTopic(category, item) {
    ...
  }

  updateFilters() {

  }
}

data函数中,将 topics 的值改为this.defaultTopics()调用该方法:

data() {
  return {
    topics: this.defaultTopics()
  }
},

最后,在slug更新时,增加一个 watch 功能重置 topics 键:

watch: {
  slug() {
 this.topics = this.defaultTopics();
 }
}

在复选框筛选器更改时更新 URL

我们的过滤组件在与交互时将更新 URL 查询参数。这允许用户查看有效的过滤器,为它们添加书签,并在需要时共享 URL。我们已经在分页中使用了查询参数,在筛选时将用户放回第一页是有意义的,因为可能只有一页。

为了构造过滤器的查询参数,我们需要循环遍历每个过滤器类型,并为checked数组中的每个过滤器添加一个新参数。然后我们可以调用router.push()来更新 URL,进而更改显示的产品。

updateFilters方法中创建一个空对象。循环浏览主题并用选中的项目填充filters对象。将路由中的query参数设置为filters对象:

updateFilters() {
  let filters = {};

 Object.keys(this.topics).forEach(key => {
 let topic = this.topics[key];
 if(topic.checked.length) {
 filters[key] = topic.checked;
 }
 });

 this.$router.push({query: filters});
}

选中和取消选中右侧的过滤器应使用选中的项目更新 URL

页面加载时预选过滤器

当加载 URL 中已有过滤器的类别时,我们需要确保选中右侧的复选框。这可以通过循环现有查询参数并向 topics 参数添加任何匹配的键和数组来实现。由于query可以是数组,也可以是字符串,因此我们需要确保 checked 属性无论如何都是数组。我们还需要确保查询键实际上是一个过滤器,而不是一个页面参数:

filters() {
  if(this.categoriesExist) {

    let category = this.categoryProducts(this.slug);

    for(let product of category.productDetails) {
      ...
    }

 Object.keys(this.$route.query).forEach(key => {
      if(Object.keys(this.topics).includes(key)) {
        let query = this.$route.query[key];
        this.topics[key].checked = Array.isArray(query) ? query : [query];
      }
    });
  }

  return this.topics;
}

页面加载时,将检查 URL 中的筛选器。

过滤产品

我们的过滤器现在正在创建并动态附加到,激活过滤器会更新 URL 中的查询参数。我们现在可以根据 URL 参数显示和隐藏产品。我们将在产品进入ListProducts组件之前对其进行过滤。这样可以确保分页工作正常。

当我们过滤时,打开ListProducts.js并为每个列表项添加一个:key属性,值为handle

<ol :start="pagination.range.from + 1">
  <li v-for="product in paginate(products)" :key="product.handle">
    ...
  </li>
</ol>

打开CategoryPage视图,在标题为filtering()methods对象中创建一个方法,并添加一个return true作为开始。该方法应接受两个参数,productquery对象:

methods: {
  filtering(product, query) {

 return true;
 }
}

接下来,在category计算函数中,如果有查询参数,我们需要过滤产品。但是,我们需要小心,如果存在页码,我们不会触发过滤器,因为这也是一个查询。

创建一个名为filters的新变量,它是路由中查询对象的副本。接下来,如果页面参数存在,delete将从我们的新对象中删除它。从那里,我们可以检查查询对象是否有任何其他内容,如果有,在我们的产品数组上运行本机 JavaScriptfilter()函数–将产品和新的查询/过滤器对象传递给我们的方法:

category() {
  if(this.categoriesExist) {
    let category = this.categoryProducts(this.slug),
 filters = Object.assign({}, this.$route.query);

 if(Object.keys(filters).length && filters.hasProperty('page')) {
 delete filters.page;
 }

 if(Object.keys(filters).length) {
 category.productDetails = category.productDetails.filter(
 p => this.filtering(p, filters)
 );
 }

    if(!category) {
      this.categoryNotFound = true;
    }
    return category;
  }
}

刷新应用以确保产品仍然显示。

过滤产品涉及一个相当复杂的过程。我们要检查查询参数中是否有属性;如果是,我们将占位符值设置为false。如果产品上的属性与查询参数的属性匹配,我们将占位符设置为true。然后对每个查询参数重复此操作。完成后,我们将只显示具有所有标准的产品。

我们将要构建的方式允许产品在类别中OR,但AND具有不同的部分。例如,如果用户选择多种颜色(红色和绿色)和一个标签(附件),它将显示所有红色或绿色附件的产品。

我们的过滤是用标签、供应商和动态过滤器创建的。由于其中两个属性是固定的,我们必须首先检查它们。动态过滤器将通过重建其构建方式进行验证。

创建一个hasProperty对象,它将是我们的占位符对象,用于跟踪产品的查询参数。我们将从vendor开始,因为这是最简单的属性。

我们首先循环查询属性,以防有多个属性(例如,红色和绿色)。接下来,我们需要确认query中是否存在vendor——如果存在,则将hasProperty对象中的供应商属性设置为false。然后检查供应商句柄是否与查询属性相同。如果匹配,我们将hasProperty.vendor属性更改为true

filtering(product, query) {
  let display = false,
 hasProperty = {};

 Object.keys(query).forEach(key => {
 let filter = Array.isArray(query[key]) ? query[key] : [query[key]];

 for(attribute of filter) {
 if(key == 'vendor') {

 hasProperty.vendor = false;
 if(product.vendor.handle == attribute) {
 hasProperty.vendor = true;
 }

 }      }
 });

 return display;
}

这将更新hasProperty对象,以确定供应商是否匹配所选过滤器。我们可以使用tags复制功能–记住产品上的标签是我们需要过滤的对象。

还需要检查过滤器构造的动态特性。通过检查每个variationProduct上的变量对象,并更新hasProperty对象(如果匹配):

filtering(product, query) {
  let display = false,
    hasProperty = {};

    Object.keys(query).forEach(key => {
      let filter = Array.isArray(query[key]) ? query[key] : [query[key]];

      for(attribute of filter) {
        if(key == 'vendor') {

          hasProperty.vendor = false;
          if(product.vendor.handle == attribute) {
            hasProperty.vendor = true;
          }

        } else if(key == 'tags') {
 hasProperty.tags = false;

 product[key].map(key => {
 if(key.handle == attribute) {
 hasProperty.tags = true;
 }
 });

 } else {
 hasProperty[key] = false;

 let variant = product.variationProducts.map(v => {
 if(v.variant[key] && v.variant[key].handle == attribute) {
 hasProperty[key] = true;
 }
 });
 }
 }
    });

  return display;
}

最后,我们需要检查hasProperty对象的每个属性。如果所有值都设置为true,我们可以将产品的显示设置为true,这意味着它将显示。如果其中一个为false,则产品不会显示,因为它不符合所有标准:

filtering(product, query) {
  let display = false,
    hasProperty = {};

    Object.keys(query).forEach(key => {
      let filter = Array.isArray(query[key]) ? query[key] : [query[key]];

      for(attribute of filter) {
        if(key == 'vendor') {

          hasProperty.vendor = false;
          if(product.vendor.handle == attribute) {
            hasProperty.vendor = true;
          }

        } else if(key == 'tags') {
          hasProperty.tags = false;

          product[key].map(key => {
            if(key.handle == attribute) {
              hasProperty.tags = true;
            }
          });

        } else {
          hasProperty[key] = false;

          let variant = product.variationProducts.map(v => {
            if(v.variant[key] && v.variant[key].handle == attribute) {
              hasProperty[key] = true;
            }
          });
        }
      }

 if(Object.keys(hasProperty).every(key => hasProperty[key])) {
 display = true;
 }

    });

  return display;
}

我们现在有一个成功的过滤产品列表。在浏览器中查看应用并更新过滤器–注意每次单击时产品的显示和隐藏方式。请注意,即使按“刷新”,也仅显示过滤后的产品。

总结

在本章中,我们创建了一个类别列表页面,允许用户查看类别中的所有产品。此列表可以分页,同时更改顺序。我们还创建了一个过滤组件,允许用户缩小结果范围。

我们的产品现在可以浏览、过滤和查看,我们可以继续制作购物车和结账页面。