一、项目开发的步骤
1、书写静态页面
2、拆分组件
3、获取服务器的数据动态展示:写api => Vuex调用api请求数据存入store => 组件拿到仓库store的数据 => 动态渲染
4、完成相应的动态业务逻辑
二、数据动态展示
1、search模块API接口的书写
axios已经提前封装好了,向服务器发送post请求(带参数)
ps.发送post请求时,params不能为undefined,至少有个默认值(空对象{ })否则会请求失败。
src/api/index.js
import ajax from './ajax' //封装好的axios实例对象
import ajaxmock from './ajaxmock'
...
const reqProductList = (params)=>ajax({
method:'post',
url:'/list',
data:params
})
export {reqCategoryList,reqBanners,reqFloors,reqProductList}
2、写Vuex中的search仓库
src\store\modules\search\index.js
import { reqProductList } from "@/api"
const state = {
// product信息 查看api可知返回的是一个对象
productList:{}
}
const actions = {
async getProductList({commit},params){
// 去除params中key为空的元素
Object.keys(params).forEach(e=>{
if(!e) delete params[e]
})
const result = await reqProductList(params)
if(result.code == 200) commit('GET_PRODUCT_LIST',result.data)
}
}
const mutations = {
GET_PRODUCT_LIST(state,data){
state.productList = data
}
}
const getters = {
}
export default{
namespaced: true,
state,
actions,
mutations,
getters
}
point:
用到的方法
Object.keys方法:获取对象的key值,返回数组
delete方法:删除对象的属性
3、完善search仓库的getters
观看后端提供的接口解释文档,可以观察到返回的数据结构为数组
getters类似计算属性,可以简化仓库中的数据
const getters = {
// 品牌列表
trademarkList(state){
// 异步请求之前 productList是空对象 考虑网速慢情况 为防止trademarkList返回undefined 导致界面v-for遍历报错
return state.productList.trademarkList || []
},
// 属性列表
attrsList(state){
return state.productList.attrsList || []
},
// 商品列表
goodsList(state){
return state.productList.goodsList || []
},
}
point:
在书写getters时,和state同理,要注意取相应的默认值(空数组或空对象),以防在仓库state为空时,getters取undefined导致页面渲染v-for时报错
开启命名空间(namespaced: true)后,在组件中使用mapGetters函数来绑定带命名空间的模块时,写为
...mapGetters('search', ['goodsList', 'attrsList', 'trademarkList'])
请参考vuex官网 https://vuex.vuejs.org/zh/guide/modules.html #带命名空间的绑定函数
4、渲染商品数据到页面
通过v-for遍历 展示商品列表页面 (简单)
5、search模块根据不同的参数获取数据展示
在search组件的data中写一个option对象,对应发请求的params参数对象,通过各种事件修改option对象的属性值;
封装一个getProductList请求函数,在每次修改option对象后,执行该函数,修改仓库数据,并在页面渲染最新请求返回数据;
封装updateOption函数,作用是通过路由来修改option对象。首次跳转至search路由时,是通过$route携带的params和query来进行请求的发送的。
option对象配置
注意默认值的问题,根据数据类型定义 String的定义空串 Array定义空数组 Number定义默认的数字
src\pages\Search\index.vue
data() {
return {
options:{
// 注意默认的初始值 根据数据类型定义 String的定义空串 Array定义空数组 Number定义默认的数字
'category1Id':'',
'category2Id':'',
'category3Id':'',
'categoryName':'',
'keyword':'',
'props':[],
'trademark':'',
'order':'1:desc',
'pageNo':1,
'pageSize':10,
}
}
},
getProductList函数封装
methods:{
// 根据当前option发送post请求
getProductList(pageNo=1){
// 默认pageNo是1 第一页内容
this.options.pageNo = pageNo
this.$store.dispatch('search/getProductList',this.options)
},
...
}
updateOption函数封装
point:对象的解构赋值
// 根据query和params来更新options数据
updateOption(){
// 解构赋值时,如果解构不成功,默认值会为undefined
let {category1Id,category2Id,category3Id,categoryName} = this.$route.query
let {keyword} = this.$route.params
// 在对象中配合...扩展运算符 后面的(不是undefined的)会覆盖前面的
this.options = {
...this.options,
category1Id,
category2Id,
category3Id,
categoryName,
keyword
}
},
生命周期钩子 beforeMount
mounted(){
this.updateOption()
this.getProductList()
},
beforeMount(){
this.updateOption()
},
mounted(){
this.getProductList()
},
教程说要保证在获取产品数据之前,根据路由params和query修改当前options,所以要在挂载之前beforeMount就修改option
但我认为没有必要,写在mounted里是一样的,反正是单线程顺序执行,在请求之前肯定已经修改好option了,而且getProductList还是异步请求。
(在getProductList里输出了一下option,测试了一下都写在mounted里的写法,也只请求了一次是修改option之后)
三、完成动态业务逻辑:对请求参数进行增删改
1、SearchSelector子组件的书写
两大功能:
获取仓库数据,展示目前商品列表的品牌及属性信息
使用v-for遍历即可
完成增删改功能,修改父组件Search的option,再次发送请求,修改仓库数据,展示最新信息
(子组件给父组件传数据,在子组件中触发事件,并修改父组件的数据)
=>将数据和回调函数通过props传给子组件
=>或在父组件中给子组件绑定自定义事件,在子组件中触发后传值给父组件
父组件
src\pages\Search\index.vue
<SearchSelector
:setTrademark="setTrademark"
:addProp="addProp"/>
// setTrademark、addProp 传给searchselector子组件的方法 修改option里的trademark和prop
setTrademark(trademark){
this.options.trademark = trademark
this.getProductList()
},
addProp(prop){
// 先查找是否已添加过该属性
if(this.options.props.indexOf(prop)!=-1) return
this.options.props.push(prop)
this.getProductList()
},
子组件
src\pages\Search\SearchSelector\index.vue
子组件中声明接受props,并注明类型
props:{
setTrademark:Function,
addProp:Function
},
2、Bread子组件面包屑导航编写
两大功能:
展示当前option中不为空的属性
删除option对象中的指定属性,重发请求
point:
在删除categoryName和keyword属性时,应该对当前路由$route包含的params和query进行相应的修改
方法和数据以props传给子组件
methods里
// 删除相关option
removeCategoryName(){
this.options.category1Id = ''
this.options.category2Id = ''
this.options.category3Id = ''
this.options.categoryName = ''
// 重新跳转到当前路由,当前路由中的query参数也该删去
// $route.path不带query参数, 但带params参数(如果有)
// this.$router.replace({name:'search',params:this.$route.params})
this.$router.replace(this.$route.path)
},
removeKeyword(){
this.options.keyword = ''
// 当前路由中的params参数也该删去
this.$router.replace({name:'search',query:this.$route.query})
},
removeTrademark(){
this.options.trademark = ''
this.getProductList()
},
removeProp(idx){
// 删除下标为idx的prop
this.options.props.splice(idx,1)
this.getProductList()
},
watch:{
// 监听当前路由 发生变化时 更改options 重新发请求
$route(){
this.updateOption()
this.getProductList()
}
},
3、商品展示栏里的排序
通过点击综合/价格切换按哪个排序,默认降序
如果综合已经高亮,再次点击综合,切换降序/升序
computed:{
// 判断当前active的为升序/降序
activeIcon(){
return this.options.order.split(':')[1] === 'asc' ? 'icon-up' : 'icon-down'
},
// 判断当前active的是综合/价格
isPriceActive(){
return this.options.order.indexOf('2') === 0
},
...
}
setOrder(order1){
// order2默认降序
let order2 = 'desc'
// 如果点击的当前order1项已经是active了 就改当前option的第二项即升降序
if(this.options.order.indexOf(order1) === 0){
order2 = (this.options.order.split(':')[1] == 'asc' ? 'desc' : 'asc')
}
let order = `${order1}:${order2}`
this.options.order = order
this.getProductList()
},
point:动态绑定样式的方法:
字符串写法
:style="{fontSize:xxx}" 其中xxx是动态值
适用于样式的类名不确定,需要动态指定
数组写法
:style="[a,b]" 其中a,b是样式对象
适用于要绑定的个数不确定,名字也不确定
对象写法
:style="styleObj"
适用于要绑定样式个数确定,名字确定,但是要动态决定用不用
使用阿里的iconfont
在线地址: https://www.iconfont.cn/
注册并登陆
创建一个可以包含需要的所有图标的项目
搜索图标并添加到购物车
将购物车中的图标添加到指定项目
修改图标的名称
选择Font class的使用方式, 并点击生成在线样式url
在index页面中引入此图标的在线样式链接:
<link rel="stylesheet"href="" target="_blank">http://at.alicdn.com/t/font_1766283_dobjk7xxze7.css">
在组件中使用
<i class=”iconfont icondown”>
可以通过color改变颜色, 通过font-size改变大小
4、pagination子组件分页器
分页器所需要的数据(参数)
1、需要知道当前是第几页:pageNo 字段代表当前页数
2、需要知道每一页需要展示多少条数据:pageSize字段进行代表
3、需要知道整个分页器一共有多少条数据:total字段进行代表----【通过和每页放几个能计算获取另外一条数据:一共有几页】
4、需要知道分页器连续页码个数:showPageNo字段进行代表 5 | 7 【为什么是奇数?因为对称好看】
连续页的起始与结束数字计算
分页器在开发的时候可以先自己传递假的数据进行调试,调试成功后再用服务器数据
<Pagination
:currentPage="options.pageNo"
:pageSize="options.pageSize"
:total="productList.total"
:showPageNo="5"
v-on:changeCurrentPage="getProductList"
/>
name:'Pagination',
props:{
// 当前页码
currentPage:{
type:Number,
default:1
},
// 每页数量
pageSize:{
type:Number,
default:5
},
// 总商品数
total:{
type:Number,
default:0
},
// 连续页码数
showPageNo:{
type:Number,
default:5
},
},
computed:{
// 总共需要的页数
totalPage(){
return Math.ceil(this.total/this.pageSize)
},
// 省略号中间显示的页码
startEnd(){
const {currentPage,showPageNo,totalPage} = this
let start = currentPage - Math.floor(showPageNo/2)
// start最小为2
if(start < 2) start = 2
let end = start + showPageNo
// end最大为totalPage-1
if(end > totalPage - 1) end = totalPage - 1
// 返回一个对象 具有start和end属性
return {start,end}
},
},
向父组件传递数据(当前页码currentPage)
在父组件中绑定自定义事件, v-on:changeCurrentPage="getProductList"
在子组件中触发该自定义事件,并将数据传给父组件,父组件执行回调
getProductList后不要带括号,子组件中触发自定义事件后传的参数会直接在此处使用
methods:{
// 生成一个从 start 到 end 的连续数组
generateArray (start, end) {
return Array.from(new Array(end + 1).keys()).slice(start)
},
changeCurrentPage(currentPage){
this.$emit('changeCurrentPage',currentPage)
}
}