商品列表页面以列表形式显示所有商品,将商品列表和商品列表项分别定义为单独的组件,商品列表组件作为父组件在其内部循环渲染商品列表项子组件。
在 components 目录下新建 BookLisItem.vue。如下:
BookLisItem.vue
{{ item.title }}
{{ currency(factPrice(item.price, item.discount)) }}
定价:{{ currency(item.price) }}
{{ item.author }}
{{ item.publishDate }}
{{ item.bookConcern }}
{{ item.brief }}
to 属性使用了表达式,因此要用 v-bind 指令(这里使用的是简写语法)进行绑定。params 和 path 字段不能同时存在,如果使用了 path 字段,那么 params 将被忽略,所以这里使用命名路由。当然,也可以采用前面例子中拼接路径字符串的方式。
router-link 默认渲染为 a 标签,所有路由的跳转都是在当前浏览器窗口中完成的,但有时希望在新的浏览器窗口中打开目标页面,那么可以使用 target=“_blank”。但要注意,如果使用 v-slot API 定制 router-link ,将其渲染为其它标签,那么就不能使用 a 标签的target 属性,只能编写单击事件响应代码,然后通过 window.open() 方法打开一个新的浏览器窗口。
BookListItem 组件需要的商品数据是由父组件通过 prop 传进来的,所以这里定义了一个 item prop。
单击“加入购物车”按钮时,会调用 addCartItem() 方法将该商品加入购物车中,由于购物车中的商品不需要商品的定价,所以这里先计算出商品的实际价格。
购物车中保存的每种商品都有一个数量,通过 quantity 字段表示,在商品列表项页面中的“加入购物车”功能是一种便捷方式,商品的数量默认为 1,后面会看到商品详情页面中加入任意数量商品功能的实现。
在添加商品到购物车中后,路由跳转到购物车页面,这也是电商网站通常采用的方式,可以刺激用户的冲动消费。
商品列表组件作为商品列表项组件的父组件,负责为商品列表项组件提供商品数据,并通过 v-for 指令循环渲染商品列表项组件。
在 components 目录下新建 BookList.vue 。如下:
BookList.vue
BookList 组件的代码比较简单,主要就是通过 v-for 命令循环渲染 BookListItem 子组件。某些项目的实现是在列表组件中向服务端请求数据渲染列表项,但在本项目中,BookList 组件会被多个页面复用,并且请求的数据接口是不同的,因此 BookList 组件仅仅是定义了一个 list prop 用来接收父组件传递进来的商品列表数据。
单击某个分类链接,将跳转到分类商品页面,在该页面下,将以列表形式列出该分类下的所有商品信息;当搜索框中输入某个关键字,单击“搜索”按钮后,将跳转到搜索结果页面,在该页面下,也是以列表形式列出匹配关键字的所有商品信息。
既然这两个页面都是以列表形式显示商品信息,那么可以将他们合并为一个页面组件来实现,在该页面中无非就是根据路由的路径来动态切换页面标题,以及向服务端请求不同的数据接口。
先给出这两个页面的路由配置,编辑 router 目录下的 index.js 文件。如下:
router/index.js
...
const routes = [
{
path: '/',
redirect: {
name: 'home'
}
},
{
path: '/home',
name: 'home',
meta: {
title: '首页'
},
component: Home
},
{
path: '/category/:id',
name: 'category',
meta: {
title: '图书分类'
},
component: () => import('../views/Books.vue')
},
{
path: '/search',
name: 'search',
meta: {
title: '搜索结果'
},
component: () => import('../views/Books.vue')
}
]
routes.afterEach((to) => {
document.title = to.meta.title;
})
...
在路由配置中,采用的是延迟加载路由的方式,只有在路由到该组件时才加载。关于延时加载路由,可以参看 14.14 节。
将分类图书(/category/:id)和搜索结果(/search)的导航链接对应到同一个目标路由组件 Books 上,同时根据 14.10.1 小节介绍的知识,利用全局后置钩子来为路由跳转后的页面设置标题。
考虑到图书列表的数据是从服务端去请求数据及网络状况的原因,图书列表的显示可能会有延迟,为此,决定编写一个 Loading 组件,在图书列表数据还没有渲染时,给用户一个提示,让用户稍安勿躁。
在 10.9 节中,已经给出了一个使用 loading 图片实现加载提示的示例,也可以沿用该示例实现加载提示。在这里换一种实现方式,考虑到图片本身加载也需要时间(虽然 loading 图片一般都很小),采用 CSS 实现 loading 加载的动画效果,这种实现在网上有很多,本项目从中找了一个实现,并将其封装为组件。
在 components 目录下新建 Loading.vue 。如下:
Loading.vue
主要代码就是 CSS 的样式规则,没必要去深究具体的实现细节,当然想研究 CSS 如何实现该种动画效果另当别论。
有了 Loading 组件,接下来就可以开始编写 Books 组件了。在 views 目录下新建 Books.vue 。如下:
views/Books.vue
{{ title }}
{{ message }}
为了控制 Loading 组件的显示与删除,定义一个数据属性 loading ,其默认值为 true,然后使用 v-if 指令进行条件判断。当成功接收到服务端发回的数据时,将数据属性 loading 设置为 false,这样 v-if 指令就会删除 Loading 组件。
因为分类商品和搜索结果使用的是同一个组件,但是向服务端请求的数据接口是不同的,分类商品请求的接口是 /book/category/6,而搜索请求的接口是 /search?wd=keyword,为此定义了 setRequestUrl 方法动态设置请求的接口 URL。
判断目标路由有多种方式,可以在导航守卫中通过 to.path 或 to.fullPath 判断,也可以使用 this.route.path 和 this.$route.fullPath 判断,如果在路由配置中使用了命名路由,还可以使用 this.route.name 判断,如本例所示。
在组件内导航守卫 beforeRouteEnter() 中请求初次渲染的数据,当然也可以利用 created 生命周期钩子完成相同的功能。
由于搜索框是独立的,用户可能会多次进行搜索行为,所以使用组件内守卫 beforeRouteUpdate() ,在组件被复用的时候再次请求数据。
BookList 组件所需要的数据是通过 list prop 传进去的,由于父子组件生命周期的调用时机问题,可能会出现子组件已经 mounted ,而父组件的数据才传过去,导致子组件不能正常渲染,为此可以添加一个 v-if 指令,使用列表数据的长度作为条件判断,确保子组件能正常接收到数据并渲染。在本项目使用的 Vue.js 版本和采用的实现方式下,不添加 v-if 指令也能正常工作,如果以后遇到子组件的列表数据不能正常渲染,可以试试这种解决方案。
Books 组件的渲染效果与 BookList 组件渲染的效果是类似的,只是多了一个标题,以及在没有请求到数据时给出的一个提示信息。