Fork me on GitHub
余鸢

饿了么45个页面重构(四):vuex的使用和food页面开发一

Vuex

在进入项目之前先学习一下Vuex。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。这是官网的解释。个人理解为在data中的属性需要共享给其他vue组件使用的部分叫做状态。简单的说就是data中需要共用的属性。

vuex有几个核心概念,下面是我对它们的理解,简而言之:

  • state:也就是共享的数据
  • mutation:对数据进行修改操作,也就是修改state(Mutation除了接收
    state 作为第一个参数外,还可以接收其他的参数)。
  • getters:取数据
  • action:对取出的数据做处理

这里不过多的解释,感兴趣的同学可以去网上搜一些相关文章!

使用vuex

1、安装vuex

1
npm install vuex --save

2、新建一个store文件夹,并在文件夹下新建index.js文件,文件中引入vue和vuex。

1
2
import Vue from 'vue'
import Vuex from 'vuex'

3、使用vuex

1
Vue.use(Vuex)

同时创建state.js、mutation-type.js、mutations.js、getters.js、actions.js这几个文件,引入到index文件中,实例化vuex时加入这几个对象

1
2
3
4
5
6
7
8
9
10
11
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'
export default new Vuex.Store({
actions,
getters,
state,
mutations
})

4、在main.js 中引入新建的vuex文件,在实例化 Vue对象时加入 store 对象

1
2
3
4
5
6
7
8
import store from './store'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

food页面

点击foodType(例如:点击美食)进入相应的food页面。如图操作:

food页面跳转

页面跳转

修改msite页面foodType的router-link配置。

msite.vue

1
2
3
4
5
6
7
8
<div class="swiper-slide food_types_container" v-for="(item, index) in foodTypes" :key="index">
<router-link :to="{path: '/food',
query: {geohash, title: foodItem.name, restaurant_category_id: getCategoryId(foodItem.link)}}"
v-for="foodItem in item"
:key="foodItem.id"
class="link_to_food">
</router-link>
</div>

设置跳转带的参数geohashtitlerestaurant_category_id。点击图标后进入food页面。

food页面

food页面分为三个部分:对应头部名称、选项排序、餐厅列表。

头部名称

首先设置它的头部名称,我们可以直接根据router-link配置中的查询参数获取相应参数。代码如下:

food.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="food_container">
<head-top :head-title="headTitle" goBack="true"></head-top>
</div>
</template>
export default{
import EHeader from 'components/e-header/e-header'
data () {
return {
headTitle: '',
goBack: true
}
},
created() {
this.headTitle = this.$route.query.title
},
components: {
EHeader
}
}

选项排序

选项排序又分为三部分:分类、排序、筛选。

1
2
3
4
5
<section class="sort_container">
<div class="sort_item" :class="{choose_type:sortBy == 'food'}"></div>
<div class="sort_item" :class="{choose_type:sortBy == 'sort'}"></div>
<div class="sort_item" :class="{choose_type:sortBy == 'activity'}"></div>
</section>

:class表示被绑定的DOM 将与数据保持同步,每当数据有改动,相应的DOM视图也会更新。也就是说当choose_type发生改变时sortBy会更新对应的数据。sortBy表示为筛选条件。

分类

想要展示分类下的内容,需要添加个点击事件chooseType()

1
2
3
4
5
6
7
8
9
10
<div class="sort_item" :class="{choose_type:sortBy == 'food'}">
<div class="sort_item_container" @click="chooseType('food')">
<div class="sort_item_border">
<span>{{foodTitle}}</span>
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg" version="1.1" class="sort_icon">
<polygon points="0,3 10,3 5,8"/>
</svg>
</div>
</div>
</div>

当点击顶部三个选项展示不同的列表,选中当前选项进行展示同时收回其他选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
methods: {
chooseType (type) {
if (this.sortBy !== type) {
this.sortBy = type
// food选项中头部标题发生改变,需要特殊处理
if (type === 'food') {
this.foodTitle = '分类'
} else {
// 将foodTitle 和 headTitle 进行同步
this.foodTitle = this.headTitle
}
} else {
// 再次点击相同选项时收回列表
this.sortBy = ''
if (type === 'food') {
this.foodTitle = this.headTitle
}
}
}
}

截图:

分类

获取展开headTitle左侧的食品分类

点击headTitle后变成分类。通过使用axios请求获取分类数据。同理,先在webpack.dev.conf.js文件中添加后台数据。

webpack.dev.conf.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/api/foodCategory', (req, res) => {
const url = 'https://h5.ele.me/restapi/shopping/v2/restaurant/category'
axios.get(url, {
headers: {
Host: 'h5.ele.me',
Referer: 'https://h5.ele.me/msite/food/'
},
params: req.query
}).then((response) => {
res.json(response.data)
}).catch((e) => {
console.log(e)
})
}),

接着设置访问参数

food.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function getFoodCategory (latitude, longitude) {
const url = '/api/foodCategory'
const data = Object.assign({}, {
latitude,
longitude
})
return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
}

请求中需要得到latitudelongitude的值,想要得到这两个参数值先要获取当前地址,使用vuex管理特性对这两个参数进行状态记录和设置。

state.js

1
2
3
4
5
6
const state = {
latitude: '', // 当前位置纬度
longitude: '' // 当前位置经度
}
export default state

mutation-types.js

1
2
3
export const SET_LATITUDE = 'SET_LATITUDE'
export const SET_LONGITUDE = 'SET_LONGITUDE'

mutations.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as types from './mutation-types'
const matutaions = {
// 记录当前经度
[types.SET_LATITUDE] (state, latitude) {
state.latitude = latitude
},
// 记录当前纬度
[types.SET_LONGITUDE] (state, longitude) {
state.longitude = longitude
}
}
export default matutaions

getters.js

1
2
3
export const latitude = state => state.latitude
export const longitude = state => state.longitude

在food.vue组件中引入getFoodCategory()和vuex管理特性。data中定义category表示分类左侧数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { getFoodCategory } from 'api/food'
import { mapState, mapMutations } from 'vuex'
data () {
return {
sortBy: '',
category: null
}
},
computed: {
...mapState([
'latitude', 'longitude'
])
},
created () {
msiteAdress().then(res => {
// 记录当前经度纬度
this.SET_LATITUDE(res.latitude)
this.SET_LONGITUDE(res.longitude)
// 获取category 种类列表
getFoodCategory(this.latitude, this.longitude).then(resq => {
this.category = resq
console.log(resq)
})
},
methods: {
...mapMutations([
'SET_LATITUDE',
'SET_LONGITUDE'
])
}

dom渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<section class="category_left">
<ul>
<li class="category_left_li"
v-for="(item, index) in category" :key="index"
:class="{category_active:restaurant_category_id == item.id}">
<section>
<span>{{item.name}}</span>
</section>
<section>
<span class="category_count">{{item.count}}</span>
<svg v-if="index" width="8" height="8" xmlns="http://www.w3.org/2000/svg" version="1.1"
class="category_arrow">
<path d="M0 0 L6 4 L0 8" stroke="#666" stroke-width="1.5" fill="none"/>
</svg>
</section>
</li>
</ul>
</section>

遍历category,填充每个item需要的相应数据。绑定当前点击的category,表示为:class="{category_active:restaurant_category_id == item.id}"

得到category数据,dom渲染运行如图:

e11

获取category右侧的数据

获取每个category所对应的数据,data中定义属性categoryDetail表示分类右侧的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
data () {
return {
categoryDetail: null
}
}
created () {
msiteAdress().then(res => {
// 记录当前经度纬度
this.SET_LATITUDE(res.latitude)
this.SET_LONGITUDE(res.longitude)
// 获取category 种类列表
getFoodCategory(this.latitude, this.longitude).then(resq => {
this.category = resq
this.category.forEach(item => {
if (this.restaurant_category_id === item.id) {
this.categoryDetail = item.sub_categories
}
})
})
}

在dom中渲染。当点击category左侧列表的某个选项时,右侧渲染相应的sub_categories列表,创建selectCategoryName(item.id, index)点击事件获取右侧数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<div name="showlist" v-if="category">
<section v-show="sortBy == 'food'" class="category_container sort_detail_type">
<section class="category_left">
<ul>
<li class="category_left_li"
v-for="(item, index) in category" :key="index"
:class="{category_active:restaurant_category_id == item.id}"
@click="selectCategoryName(item.id, index)">
...
</li>
</ul>
</section>
<section class="category_right">
<ul>
<li v-for="(detailItem, index) in categoryDetail" :key="detailItem.id"
v-if="index"
class="category_right_li"
@click="getCategoryIds(detailItem.id, detailItem.name)"
:class="{category_right_choosed: restaurant_category_ids == detailItem.id || (!restaurant_category_ids)&&index == 0}"
>
<img :src="imgBaseUrl + detailItem.image_url + '.png'" v-if="index" class="category_icon">
<span>{{detailItem.name}}</span>
<span>{{detailItem.count}}</span>
</li>
</ul>
</section>
</section>
</div>
methods: {
selectCategoryName (id, index) {
// 第一个选项 -- 全部商家 因为没有自己的列表,所以点击则默认获取选所有数据
if (index === 0) {
this.restaurant_category_ids = null
this.sortBy = ''
} else {
// 不是第一个选项时,右侧展示其子级sub_categories的列表
this.restaurant_category_id = id
this.categoryDetail = this.category[index].sub_categories
}
},
}

遍历categoryDetail,填充每个detailItem需要的相应数据。绑定当前点击的categoryDetail,表示为:class="{category_right_choosed: restaurant_category_ids == detailItem.id || (!restaurant_category_ids)&&index == 0}"

页面渲染如图:

categoryDetail

左侧数据为category,右侧则是categoryDetail。渲染成功!

获取当前餐厅列表

msite.vue组件中,设置router-link时query参数需要带restaurant_category_id,这是就需要在shop-list.vue组件props中添加restaurantCategoryId属性。

shop-list.vue

1
props: ['restaurantCategoryId', 'geohash', 'shopListArr']

继续回到msite.vue组件中,对restaurant_category_id的值做些处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<router-link :to="{path: '/food',
query: {geohash, title: foodItem.name, restaurant_category_id: getCategoryId(foodItem.link)}}"
v-for="foodItem in item" :key="foodItem.id" class="link_to_food">
</router-link>
methods: {
getCategoryId (url) {
let urlData = decodeURIComponent(url).split('=')[3].replace('&navType', '')
if (/restaurant_category_id/gi.test(urlData)) {
return JSON.parse(urlData).restaurant_category_id
} else {
return ''
}
},
}

getCategoryId(url)作用是解码url地址,求restaurant_category_id值。首先对传入的url进行拆分处理,通过正则表达式去匹配,看有没有和urlData相同的字符串,如果匹配成功返回restaurant_category_id的值,如果匹配失败则返回空。

前面我们已经写了个餐厅列表shop-list.vue基础组件,直接拿来使用就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="food_container">
<section class="shop_list_container">
<shop-list></shop-list>
</div>
</template>
import ShopList from 'base/shop-list/shop-list'
data () {
return {
geohash: '',
restaurant_category_id: '',
shopListArr: []
}
},
methods: {
components: {
ShopList
}
}

webpack.dev.conf.js文件中添加后台数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/api/restaurants', (req, res) => {
const url = 'https://h5.ele.me/restapi/shopping/v3/restaurants'
axios.get(url, {
headers: {
Host: 'h5.ele.me',
Referer: 'https://h5.ele.me/msite/food/'
},
params: req.query
}).then((response) => {
res.json(response.data)
}).catch((e) => {
console.log(e)
})
})

food.js文件设置请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function getFoodRestaurants (latitude, longitude, extras, restaurantCategoryIds) {
const url = '/api/restaurants'
const data = Object.assign({}, {
latitude,
longitude,
keyword: '',
offset: 0,
extras,
terminal: 'h5',
brand_ids: '',
restaurant_category_ids: restaurantCategoryIds
})
return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
}

food.vue中引入getFoodRestaurants(),获取商铺列表数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
created () {
this.restaurant_category_id = this.$route.query.restaurant_category_id
if (!this.latitude) {
// 获取位置信息
msiteAdress().then(res => {
// 记录当前经度纬度
this.SET_LATITUDE(res.latitude)
this.SET_LONGITUDE(res.longitude)
// 获取商铺列表
getFoodRestaurants(this.latitude, this.longitude, this.extras, this.restaurant_category_id).then(res2 => {
this.shopListArr = Array.from(Object.keys(res2.items).map(key => res2.items[key].restaurant))
})
})
}
},
}

通过路由里面传递的参数获取restaurant_category_id

页面渲染如图:

e12