8.旅游网站城市列表页面开发
本章将讲解景点详情页面的开发,主要讲解渐隐渐显 Header 组件的制作,公用组件的拆分,路由参数的获取与处理,以及递归组件的使用。在详情页面,我们还会对通用动画效果进行代码封装。
8.1路由配置
router-link用于页面的跳转:
<router-link to='/city'>
<div class="header-right">
城市
<span class="iconfont arrow-icon"></span>
</div>
</router-link>
8.2搜索框布局
注意搜索框中box-sizing:border-box
8.3列表布局
8.4BetterScroll的使用和字母表布局
BetterScroll插件的使用:
<div ref="wrapper">
<div></div>
</div>
<script type="text/javascript">
import Bscroll from 'better-scroll'
export default{
name:'CityList',
//生命周期函数,会在DOM挂载结束后执行
mounted(){
this.scroll = new Bscroll(this.$refs.wrapper)
}
}
</script>
8.5页面的动态数据渲染
主要是数组和对象循环:
//数组
"hotCities": [{
"id": 1,
"spell": "beijing",
"name": "北京"
}, {
"id": 3,
"spell": "shanghai",
"name": "上海"
}, {
"id": 47,
"spell": "xian",
"name": "西安"
}, {
"id": 239,
"spell": "sanya",
"name": "三亚"
}, {
"id": 188,
"spell": "lijiang",
"name": "丽江"
}, {
"id": 125,
"spell": "guilin",
"name": "桂林"
}]
//对象
"cities": {
"A": [{
"id": 56,
"spell": "aba",
"name": "阿坝"
}, {
"id": 57,
"spell": "akesu",
"name": "阿克苏"
}, {
"id": 58,
"spell": "alashanmeng",
"name": "阿拉善盟"
}],
"B": [{
"id": 1,
"spell": "beijing",
"name": "北京"
}, {
"id": 66,
"spell": "baicheng",
"name": "白城"
}, {
"id": 67,
"spell": "baise",
"name": "百色"
}]
}
数组和对象循环:
//数组循环
<div class="area">
<div class="title border-topbottom">
热门城市
</div>
<div class="button-list">
<div
class="button-wrapper"
v-for="item of hot"
:key="item.id">
<div class="button">{{item.name}}</div>
</div>
</div>
</div>
//对象循环
<div class="area"
v-for="(item,key) of cities"
:key="key">
<div class="title border-topbottom">{{key}}</div>
<div class="item-list"
v-for="innerItem of item"
:key="innerItem.id">
<div class="item border-bottom">{{innerItem.name}}</div>
</div>
</div>
8.6兄弟组件数据传递
兄弟组件间传递数据可以通过父组件作为中转站进行传递。
//CityAlphabet.vue
<ul class="list">
<li class="item"
v-for="(item,key) of cities"
@click="handleLetterClick"
>
{{key}}
</li>
</ul>
export default{
props:{
cities:Object
},
name:'CityAlphabet',
methods:{
handleLetterClick(e){
//子组件(兄弟)之间数据传递,可以通过父组件作为中转站进行数据传递
this.$emit('change',e.target.innerText) //向外传递事件和内容
//console.log(e.target.innerText)
}
}
}
//City.vue
<CityAlphabet :cities="cities"
@change="handleLetterChange"></CityAlphabet>
<CityList :cities="cities"
:hot="hotCities"
:letter="letter"></CityList>
export default{
name:'City',
components:{
CityHeader:CityHeader,
CitySearch:CitySearch,
CityList:CityList,
CityAlphabet:CityAlphabet
},
data:function () {
return{
cities:{}, //ABCD对应城市
hotCities:[], //热门城市
letter:''
}
},
methods:{
getCityInfo(){
axios.get('/api/city.json')
.then(this.handleGetCityIndoSucc)
},
handleGetCityIndoSucc(res){
res = res.data
if (res.ret && res.data) {
const data = res.data
this.cities = data.cities
this.hotCities = data.hotCities
}
console.log(res.data)
},
//父组件监听到子组件传递的事件之后触发函数
//父组件通过属性的形式传递数据给子组件
handleLetterChange(letter){
this.letter = letter
console.log(letter)
}
},
mounted(){
this.getCityInfo()
}
}
//CityList.vue
<div class="area"
v-for="(item,key) of cities"
:key="key"
:ref="key">
<div class="title border-topbottom">{{key}}</div>
<div class="item-list"
v-for="innerItem of item"
:key="innerItem.id">
<div class="item border-bottom">{{innerItem.name}}</div>
</div>
</div>
export default{
name:'CityList',
props:{
hot:Array,
cities:Object,
letter:String
},
//生命周期函数,会在DOM挂载结束后执行
mounted(){
this.scroll = new Bscroll(this.$refs.wrapper)
},
watch:{
letter (){
if(this.letter){
//通过字母获取DOM节点
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
console.log(this.letter)
}
}
}
数据传递过程:
子组件CityAlphabet通过绑定点击事件@click="handleLetterClick"
触发函数,子组件通过$emit
向外传递事件和内容,把要传递内容放在参数中this.$emit('change',e.target.innerText)
,然后在父组件中,通过对对应子组件绑定事件进行监听接收数据,<CityAlphabet :cities="cities" @change="handleLetterChange"></CityAlphabet>
,监听到事件然后就触发函数,在函数中把数据保存在data中,
data:function () {
return{
cities:{}, //ABCD对应城市
hotCities:[], //热门城市
letter:''
}
},
methods:{
//父组件监听到子组件传递的事件之后触发函数
//父组件通过属性的形式传递数据给子组件
handleLetterChange(letter){
this.letter = letter
console.log(letter)
}
}
然后在子组件属性中设置:
<CityList :cities="cities"
:hot="hotCities"
:letter="letter"></CityList>
这样在子组件中,通过props就可以接收到值了
export default{
name:'CityList',
props:{
hot:Array,
cities:Object,
letter:String
},
//生命周期函数,会在DOM挂载结束后执行
mounted(){
this.scroll = new Bscroll(this.$refs.wrapper)
},
watch:{
letter (){
if(this.letter){
//通过字母获取DOM节点
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
console.log(this.letter)
}
}
}
别忘记父组件中data存储数据,子组件中props接收数据。
8.7列表性能优化
刷新频率过快,可以使用节流timer。
8.8搜索逻辑实现
8.9Vuex实现数据共享
这个状态自管理应用包含以下几个部分:
1.state,驱动应用的数据源;
2.view,以声明方式将 state 映射到视图;
3.actions,响应在 view 上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的极简示意:
action响应页面操作,导致状态变化,state数据改变,然后通过view以声明的方式将state映射到视图。
代码示例:
//子组件CityList.vue
<div class="button-list">
<div
class="button-wrapper"
v-for="item of hot"
:key="item.id"
@click="handleCityClick(item.name)">
<div class="button">{{item.name}}</div>
</div>
</div>
methods:{
handleCityClick(city){
this.$store.dispatch('changeCity',city)
}
}
//通过Vuex创建的仓库
export default new Vuex.Store({
state: {
city:'北京'
},
actions: {
//子组件通过this.$store.dispatch('changeCity',city),传递到action
//ctx表示上下文
changeCity(ctx,city){
//通过commit提交到mutations
ctx.commit('changeCity',city)
}
},
mutations: {
changeCity(state,city){
//state指公用数据
state.city = city
}
},
});
或者我们直接通过mutations,不经过actions也可以:
methods:{
handleCityClick(city){
this.$store.commit('changeCity',city)
}
}
//通过Vuex创建的仓库
export default new Vuex.Store({
state: {
city:'北京'
},
mutations: {
changeCity(state,city){
//state指公用数据
state.city = city
}
},
});
8.10Vuex的高级使用及localStorage
localStorage:
可以在子组件中直接使用vuex的状态:
import {mapState} from 'vuex'
export default {
name:'HomeHeader',
computed:{
//把vuex里面的数据映射到该组件computed的计算属性里,city映射到mapState
...mapState(['city'])
}
}
<router-link to='/city'>
<div class="header-right">
{{this.city}}
<span class="iconfont arrow-icon"></span>
</div>
</router-link>
甚至关于vuex中state、action、mutations也可以在子组件中使用:
import {mapState, mapMutations} from 'vuex'
export default{
name:'CityList',
computed:{
//把vuex里面的数据映射到该组件computed的计算属性里,city映射到mapState,且到该组件还可以通过对象形式另命名为currentCity
...mapState({
currentCity:'city'
})
},
methods:{
handleCityClick(city){
//this.$store.dispatch('changeCity',city)
//有了下面直接子组件引用mutations,可以直接这样写
this.changeCity(city)
//通过router进行跳转,跳转到首页
this.$router.push('/')
},
//在子组件中直接使用mutations,
...mapMutations(['changeCity'])
}
}
<div class="button-list">
<div class="button-wrapper">
<div class="button">
{{this.currentCity}}
</div>
</div>
</div>
vuex中getter使用:
store.js中的getter类似组件中的computed一样,用于计算store.js中数据
Vue.use(Vuex);
//通过Vuex创建的仓库
export default new Vuex.Store({
state: state,
actions: {
//子组件通过this.$store.dispatch('changeCity',city),传递到action
//ctx表示上下文
changeCity(ctx,city){
//通过commit提交到mutations
ctx.commit('changeCity',city)
}
},
mutations:mutations,
getters:{
doubleCity(state){
return state.city + ' ' + state.city
}
}
});
<router-link to='/city'>
<div class="header-right">
<!-- {{this.doubleCity}} -->
{{this.city}}
<span class="iconfont arrow-icon"></span>
</div>
</router-link>
export default {
name:'HomeHeader',
computed:{
//把vuex里面的数据映射到该组件computed的计算属性里,city映射到mapState
...mapState(['city']),
//vuex中getters映射到computed的计算属性里
//store.js中的getter类似组件中的computed一样,用于计算store.js中数据
...mapGetters(['doubleCity'])
}
}
Vuex中核心概念:
1.state
数据源,存放数据用
2.action
Action 提交的是 mutation,而不是直接变更状态,要commit到mutation。
Action 可以包含任意异步操作。
3.mutation
同步对数据改变
4.getter
类似组件中的computed,对Vuex中数据进行计算
5.module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
8.11使用keep-alive优化网页性能