Vue2项目前台开发:第三章
一、项目开发的步骤
1、书写静态页面
2、拆分组件
3、获取服务器的数据动态展示:写api => Vuex调用api把数据放store => 组件拿到仓库store的数据 => 动态渲染
4、完成相应的动态业务逻辑
这里拆分组件比较简单,把中间这部分拆分出来,其他地方直接在Search组件中保留
二、各种请求数据并展示数据
流程:写api => Vuex调用api请求数据放store => 组件拿到仓库store的数据 => 动态渲染
1.写Search模块的接口
这是向真正的服务器发请求,所以用封装好的axios实例:requests。带参数,且参数params至少有个默认值(空对象{}
)否则会请求失败。
src/api/index.js
//本文件用于:API的统一管理
import requests from './request'; //axios请求后台ajax数据
import mockRequests from './mockRequest'; //axios请求后台mock数据
........
........
//4.Search模块的接口
// /api/list post 需要带参数,且参数params至少有个默认值(空对象{})否则会请求失败
export const reqSearchInfo = function (params) {
return requests({
url: '/list',
method: 'post',
data: params //请求体参数
})
}
2.写Vuex中的search仓库
注意调用接口时要传参,默认值要设置为空对象,不然请求会失败的
src/store/search/index.js
//本文件用于配置search模块的数据
import { reqSearchInfo } from '@/api'
export const search = {
namespaced: true,
state: {
searchInfo: {}
},
actions: {
async getSearchInfo(context, params = {}) {
let result = await reqSearchInfo(params); //这里至少要传一个默认参数:空对象,否则请求不成功
if (result.code === 200) {
context.commit('GETSEARCHINFO', result.data);
}
}
},
mutations: {
GETSEARCHINFO(state, value) {
state.searchInfo = value;
}
},
getters: {}
}
3.组件拿到search仓库的数据
(1)观察数据结构
观察数据,我们要拿到searchInfo
中的attrsList,goodsList,trademarkList
对应的关系是这样滴
(2)配置getters来简化数据
那么这样的话,如果用mapState来接收,实际上是很麻烦的:
所以这里我们选择在仓库中配置getters:
//getters类似计算属性,可以简化仓库中的数据
getters: {
//当前形参是当前仓库的state
goodsList(state) {
//要等searchInfo的数据拿回来了再返回值,要不然可能就直接拿空对象了
return state.searchInfo.goodsList || []; //如果没网返回空数组
},
attrsList(state) {
return state.searchInfo.attrsList || [];
},
trademarkList(state) {
return state.searchInfo.trademarkList || [];
},
}
下面这里由于我开启了命名空间,所以从组件中读取getters数据是这样写的 ...mapGetters('search', ['goodsList', 'attrsList', 'trademarkList'])
图示:
4.渲染商品数据到页面
v-for遍历数组,然后把对应的数据填进去就行了,这块儿没什么好说的
5.search模块根据不同的参数获取数据展示
(1)把派发actions的操作封装为函数
当用户点击搜索或三级联动的时候,需要根据关键字再发一次请求来获取相应的数据,而我们派发action
请求数据的操作是放在Searchmounted
挂载函数里面的,而我们在Search
页再点搜索或三级联动时,mounted不会再执行了.
所以我们应该把派发actions请求的操作封装成函数,在需要时候调用。
(2)设置参数默认值
观察api接口文档,发现向服务器发请求时可以带10个参数,我们将这些参数的默认值
配置在Search组件的data
中,以对象
的形式存储,然后在派发actions请求时把这个对象传过去,就能够作为axios发送ajax请求的请求体参数
,然后后台拿着这些传过来的参数进行一些筛选排序之类的操作(我猜的)
(3)Object.assign合并对象
复习一个api:Object.assign(target, source)
,它有个功能是可以合并具有相同属性的对象,返回修改后的对象
(4)把三级联动或搜索的参数拿过来
用上面那个api,可以把我们点击三级联动或搜索时query和params参数拿过来(注意名字要一致),然后重名的属性相继覆盖。
这里要在mounted
之前(created
或beforeMount
都行),因为要在派发请求之前拿到带三级联动或搜索带的query和params参数的searchParams
,这样就可以将我们从三级联动或搜索获取到的参数覆盖掉初始带给服务器的参数
后面两个对象依次覆盖前面重复的属性值,最后第一个对象原值改变,返回第一个对象,这样就能把相应的参数带给后端,然后后端再一过滤啥的,就能实现搜索功能了
(5)点击三级联动或搜索就再次请求数据
我们观察开发者工具,会发现其实跳转到路由组件后,组件身上的data
中会有$route
这个数据
每次用户点击三级联动或者搜索,都会触发push
路由跳转和传参(如果已经跳了就只传参),那么我们在进入Search
组件后,每次点击三级联动或者搜索都会带来$route
中参数的变化,也就是$route
的变化。
那么这样的话我们就可以去监视$route的变化,每次只要参数发生变化,就重新整理数据并派发请求,然后服务器根据传过去的参数做一些操作(筛选),然后把处理过的数据响应过来,就可以在页面展示了
watch: {
//$route竟然是data里的一个属性诶
//监听路由的信息是否发生变化,如果发生变化就再次请求
$route: {
immediate: true, //其实加上这句话,beforeMount和mounted里的东西都可以去掉
handler() {
//每次请求前,最好把相应的1,2,3级分类的id置空,不然有冗余不太好,本次请求就比较干净
//别的不用重置,因为这三个是只传一个,其他的会直接被新值覆盖,没必要重置
// Object.assign(this.searchParams, { category1Id: '' }, { category2Id: '' }, { category3Id: '' })
this.searchParams.category1Id = '';
this.searchParams.category2Id = '';
this.searchParams.category3Id = '';
//每次参数变化,都要重新整理数据然后派发请求才行
Object.assign(this.searchParams, this.$route.query, this.$route.params);
this.getData();
}
}
}
6.渲染子组件数据到页面
子组件用到的是getters中的attrsList
和 trademarkList
这里和前面Floor是不同的,这里可以直接让子组件从仓库中拿数据,这是因为这个数据的结构是{ [], [], [] ...}
,而且我们已经通过getters把每个数组分出来了,且没有组件复用的情况,所以不用父传子(当然父传子也可以,但是没必要)
拿到数据之后v-for对应生成列表就行了,这块儿比较简单
三、面包屑部分的开发
这块儿挺多细节的,我感觉比较难,他喵的
所谓的面包屑其实就是这个东西:
我们可以根据用户请求的数据来决定是否展示面包屑,且删除某个面包屑时,数据要重新发起请求返回新的后台筛选数据放到页面上。
1.面包屑的展示问题
我们首先要解决面包屑的展示问题,如果携带的参数searchParams.categoryName
为空,就是没有面包屑,有的话,面包屑就是这个参数的值(参数searchParams.categoryName
是否有值是由路由跳转时是否传入categoryName
决定的,是结合Object.assign
)。所以这里可以用v-if
来判断是否展示面包屑
<ul class="fl sui-tag">
<li class="with-x" v-if="searchParams.categoryName">
{{searchParams.categoryName}}<i @click="deleteCategory">×</i>
</li>
<li class="with-x" v-if="searchParams.keyword">
{{searchParams.keyword}}<i @click="deleteKeyword">×</i>
</li>
</ul>
然后在❌的地方分别配置删除三级联动和搜索回调
2.点击❌删除三级联动的相关数据
1、首先名字置空(或undefined
,如果属性值为""
还是会把相应的字段带给服务器。但是你把相应的字段变为undefined
,当前这个字段不会带给服务器-------性能更好,省带宽),这样的话面包屑先取消显示(v-if控制)
2、其次所有Id也要置空,这样删除面包屑后才能返回全部的服务器数据(之前监视$route时id置空却不会返回全部数据是因为categoryName
和keyword
没有置空,只要这几个不是全空,服务器就会做一些筛选并返回相关数据)当然其实一步也可以不写,因为下一步👇
3、自己跳自己,去掉地址栏中的query
参数(三级导航的数据),但是要保留params
参数(搜索框的数据),因为这个回调是点击三级联动的面包屑才触发的,如果有搜索条件还是要保留keyword
带来的数据的。其实仔细想想这段代码的目的就是去掉地址栏中的参数(如果不考虑监视$route
会自动派发的话)
4、派发请求返回数据。当然如果写了第三步,第二步和第四步都可以省略,因为我们之前监视了$route
的变化,一旦变化就id置空=>重新整理数据=>派发请求
5、具体的细节见如下代码,里面注释好好琢磨吧
deleteCategory() {
//删除分类的名字和id,但置空的是本组件data内的数据,而不是$route中的query参数
this.searchParams.categoryName = ""; //点击x后,名字置空,这样v-if就生效把节点弄没
// this.searchParams.category1Id = undefined; //写成undefined就不会带给服务器了,省带宽
// this.searchParams.category2Id = undefined; //id也要置空,这样服务器才会返回全部数据
// this.searchParams.category3Id = '';
this.$router.push({ //去掉地址栏的query参数
name: 'sousuo', //自己跳自己
params: this.$route.params //只传params(华为),query就被拿掉了
});
// this.getData();//发请求返回全部数据,但是由于监视$route,有上边就不用这行了
},
3.点击❌删除搜索关键字的相关数据
和上边是类似的,一样的套路。这里有个需要注意的地方就是当我们去掉关键字对应的面包屑时,搜索框中的文字也要去除,所以这里我们要同步修改Header
中的keyword
使用的是全局事件总线:复习全局事件总线
剩下的就是:
1、首先关键词置空
(或undefined
),这样的话面包屑先取消显示(v-if
控制)
2、自己跳自己,去掉地址栏中的params
参数(搜索关键字的数据),但是要保留query
参数(三级联动的数据),因为这个回调是点击关键字的面包屑才触发的,如果有三级联动条件还是要保留categoryName
带来的数据的。
3、派发请求返回数据。当然如果写了第二步,这一步可以省略,因为我们之前监视了$route
的变化,一旦变化就重新整理数据=>派发请求
4、具体的细节见如下代码,里面注释好好琢磨吧
deleteKeyword() {
//删除关键词,但置空的是本组件data内的数据,而不是$route中的params参数
this.searchParams.keyword = undefined;//点击x后,关键字置空,这样v-if就生效把节点弄没
this.$router.push({ //去掉地址栏的params参数
name: 'sousuo',
query: this.$route.query //只传query(有就筛选,没有就返回全部数据),params(华为)就被拿掉了
});
this.$bus.$emit('deleteKeyword'); //触发全局事件总线,把Header中的输入框关键字置空
// this.getData(); //发请求返回全部数据,但是由于监视$route,有上边就不用这行了
}
4.面包屑处理品牌信息
点击品牌信息出现面包屑,并且更新页面数据
结论:我们应该把子组件SearchSelector
中的数据传给父组件Search
,父组件就可以拿着数据修改data
中传给后台的参数,再发送请求时就会把trademark
参数带给服务器了。
(1)子传父:组件自定义事件
1、父组件中给子组件标签添加自定义事件getTrademark
:
<SearchSelector @getTrademark="getTrademark" />
2、子组件通过点击事件触发自定义事件并把当前trademark传过去
3、这样父组件就拿到了数据
(2)请求后台数据并添加品牌面包屑和❌
拿到数据之后按照api接口的格式调整数据给data中的searchParams.trademark
,然后发送请求传给服务器。
getTrademark(trademark) {
//整理品牌字段参数 按照接口文档格式写"ID:品牌名称"
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
//要记住,只有searchParams里的某属性为空时,才会返回该属性对应所有数据
//所以下面这么写有bug是因为keyword置空了,但this.searchParams.trademark没有置空
// this.$router.push({
// name: 'sousuo',
// params: {keyword: trademark.tmName}
// });
this.getData();
}
这里我本来想直接把品牌名给keyword
,这样触发params
参数的改变,从而$route
监测到就会发请求更新数据,但是这样会有bug,就是关闭面包屑时品牌信息不会重置(只剩下当前一个品牌信息)。这是因为只有searchParams里的某属性为空时,才会返回该属性对应所有数据(相当于重置数据)
,这里把品牌名字放入keyword
,然后关闭面包屑时只把keyword
置空了,但是this.searchParams.trademark
没有置空,这样的话后台还是会根据trademark
去筛选……所以正确的写法应该是和keyword,categoryName差不多的写法,就是点击❌时把trademark
置空:
<!-- 品牌面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
{{searchParams.trademark.split(':')[1]}}<i @click="deleteTrademark">×</i>
</li>
deleteTrademark() {
//删除品牌的面包屑并重新请求
this.searchParams.trademark = undefined;
this.getData(); //这里边不涉及$route里的参数修改,所以不能靠监视来派发请求
},
四、平台售卖属性的操作
点击某个属性就会显示响应的面包屑,并发送相对应的数据给后台发请求,拿到新数据
1.子传父:组件自定义事件
大致的思路和上面的品牌信息面包屑是类似的,只是也有一些不同点。
1、先在父组件的子组件标签处写好自定义事件和它的回调
<SearchSelector @getTrademark="getTrademark" @getAttribute="getAttribute" />
......
<script>
......
getAttribute() {
//回调函数体
}
</script>
2、观察接口文档
3、去子组件传值并触发自定义事件。根据上一步的分析,我们应该把a1(每个大对象)
和a2(每个attrValueList中的值)
都传给父组件,方便它整理数据。并给每个a2绑定点击事件
4、回调中触发自定义事件并传值:
5、父组件自定义事件的回调中接收值并整理数据
getAttribute(attr, attrValue) {
let prop = `${attr.attrId}:${attrValue}:${attr.attrName}`; //整理成api文档规定的格式
......
}
2.请求后台数据并添加面包屑和❌
通过以上分析,我们可以得出:
1、props的参数格式默认为一个数组,需要存储多个属性元素,所以在添加数据时应该使用数组的push
方法。
2、在添加时判断数组内有没有该元素即可,有就不添加,没有就添加,使用indexOf
所以父组件的自定义事件回调应该这么写:
getAttribute(attr, attrValue) {
let prop = `${attr.attrId}:${attrValue}:${attr.attrName}`;
if (this.searchParams.props.indexOf(prop) === -1) {
//加个判断:解决重复点击会重复显示多个面包屑的bug:props数组去重
//如果props里没有该元素,就添加进去并发请求,如果已经有了就不发请求了
this.searchParams.props.push(prop);
this.getData();
}
}
3、每次点击一个属性,都应该生成对应的面包屑且发送相应的请求,由于数组内有多个元素,页面生成面包屑不能再使用v-if
,而使用v-for
,页面展示时通过split
方法把字符串拆开拿到那个a2的值
4、每次删除一个属性,都应该删除对应的面包屑且再次拿着props的剩余数据发送请求,这里要删除数组中被点击的元素,我们用的方法是把index
传过去,并使用数组的splice
方法删除该元素,然后再次发送请求,这样就欧了
deleteProps(index) {
//点击叉号时删除当前数组中的元素
this.searchParams.props.splice(index, 1);//(start,deletecount),改变原数组
this.getData();
},
五、Search模块的排序操作(重点)
需求:默认综合高亮降序,如果点击综合改变排序方式,点击价格则价格高亮,再点击价格改变排序方式:
查阅接口文档,后台默认收到的参数order
格式为:'排序字段 : 排序方式'
,其中1综合,2价格 asc升序 desc降序
,如order: '1:desc'
1.高亮样式的展示
高亮样式是否展示取决于当前order
中的排序字段是1还是2
,如果是1那么综合
有active
红色高亮样式,如果是2那么价格
有active
红色高亮样式,所以我们可以用计算属性来决定样式的展示
//决定排序属性是否高亮的两个计算属性
isOne() {
//如果order中包含1,那么就是当前按综合排序,返回true给avtive样式
return this.searchParams.order.includes('1');
},
isTwo() {
//如果order中包含2,那么就是当前按价格排序,返回true给avtive样式
return this.searchParams.order.includes('2');
},
2.升降序箭头的展示
1、箭头是否展示是和高亮同步的,如果高亮都没有,那么箭头肯定不展示,所以箭头这个标签外边要加上v-show
,值和高亮的那个一致
2、箭头是升序还是降序取决于当前order
中是asc
还是desc
,如果是asc那么上箭头显示,如果是desc就是下箭头展示。所以箭头这里的展示也要通过计算属性,方法是判断当前order
中是否包含asc
或desc
//决定排序箭头显示上箭头还是下箭头
isDown() {
//如果order中包含desc,返回true,否则返回false
return this.searchParams.order.includes('desc');
},
isUp() {
//如果order中包含asc,返回true,否则返回false
return this.searchParams.order.includes('asc');
}
3.实现点击切换高亮和箭头方向
通过以上分析我们发现,不管是高亮还是箭头是否展示,还是箭头的方向,都是由order
中的数据决定的,只要data中的数据searchParams.order
改变,Vue就会重新解析模板,就会影响这些东西的显示效果。所以我们操作数据并再次发送请求
,就能够实现高亮、箭头、商品数据的同步。
1、绑定点击事件并传参用来表示当前点击的是哪个板块儿
2、把传过来的参数结合排序类型取反直接给 this.searchParams.order
重新赋值(通过模板字符串和三元表达式),然后再发请求就行了。这里老师写的复杂了,但其实直接改数据就行了,前面我们说了,数据改变Vue就会重新解析模板的。
changeOrder(orderNumber) {
//orderNumber是一个标记,代表用户点击的是综合(1)还是价格(2),用户点击时传过来
//1.获取当前状态的排序类型
let originOrderType = this.searchParams.order.split(':')[1];
//2.直接改数据,传入当前点击的参数,排序类型取反,然后就能引起Vue重新解析模板
this.searchParams.order = `${orderNumber}:${originOrderType === 'desc' ? 'asc' : 'desc'}`;
//3.再次发送请求
this.getData();
}
六、实现分页器功能(重点)
分页器因为好多地方都在用,所以我们将其封装为全局组件,步骤为创建引入注册使用
src/components/Pagination/index.vue
<template>
<div class="pagination">
<button>上一页</button>
<button>1</button>
<button>···</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button>6</button>
<button>7</button>
<button>···</button>
<button>9</button>
<button>下一页</button>
<button style="margin-left: 30px">共 60 条</button>
</div>
</template>
<script>
export default {
name: 'Pagination',
};
</script>
<style lang="less" scoped>
.pagination {
text-align: center;
button {
margin: 0 5px;
background-color: #f4f4f5;
color: #606266;
outline: none;
border-radius: 2px;
padding: 0 4px;
vertical-align: top;
display: inline-block;
font-size: 13px;
min-width: 35.5px;
height: 28px;
line-height: 28px;
cursor: pointer;
box-sizing: border-box;
text-align: center;
border: 0;
&[disabled] {
color: #c0c4cc;
cursor: not-allowed;
}
&.active {
cursor: not-allowed;
background-color: #409eff;
color: #fff;
}
}
}
</style>
1.分页器所需要的数据(参数)
2.分页器起始与结束数字计算
自定义分页器,在开发的时候先自己传递假的数据进行调试,调试成功后再用服务器数据