一、項目介紹
這個項目主要參考了去哪兒網的布局,完成了首頁、城市選擇頁面、詳情頁面的開發。
- 首頁:實現了多區域輪播的功能,以及多區域列表的展示;
- 城市選擇頁面:在這個頁面實現了城市展示、城市搜索、城市右側字母和左側區塊動態聯動的效果,當用戶在城市列表切換了新的城市后,首頁對應的城市也會跟着變化;
- 景點詳情頁面:實現公用的畫廊組件,以及遞歸展示的列表組件。
1.1 技術棧
Vue 2.5: 用於構建用戶界面的漸進式框架
Vuex: 專為 Vue.js 應用程序開發的狀態管理模式。
Vue Router: 是 Vue.js 官方的路由管理器。
keep-alive: Vue提供的一個抽象組件,用來對組件進行緩存,從而節省性能
vue-awesome-swiper: 基於 Swiper、適用於 Vue 的輪播組件,支持服務端渲染和單頁應用。
stylus: css預處理器
Axios: 一個基於 promise 的 HTTP 庫
better-scroll: 是一款重點解決移動端(已支持 PC)各種滾動場景需求的插件。
webpack: 一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。
EsLint: 幫助檢查Javascript編程時語法錯誤,規范代碼風格的工具
iconfont: 阿里巴巴圖標庫
fastclick: 解決移動端點擊延遲300ms的問題
1.2 項目展示
- 景點門票首頁
- 城市列表頁面
- 景點詳情頁面
1.3 項目收獲
1. 理解整個vue項目的開發流程,上手中型vue項目的開發
- Vue Router 來做多頁面的路由
- Vuex 多個組件的數據共享
- 插件swiper實現頁面輪播效果
- Axios 來進行 Ajax 數據的獲取
2. 移動端頁面布局技巧
3. stylus 編寫前端的樣式
4. 公用組件的拆分
5. 規范的代碼編寫
1.4 項目目錄
附上項目目錄和倉庫地址vue仿去哪兒網webapp
F:.
│ .babelrc
│ .editorconfig
│ .eslintignore
│ .eslintrc.js
│ .gitignore
│ .postcssrc.js
│ index.html
│ package-lock.json
│ package.json
│ README.en.md
│ README.md
│
├─build
│ build.js
│ check-versions.js
│ logo.png
│ utils.js
│ vue-loader.conf.js
│ webpack.base.conf.js
│ webpack.dev.conf.js
│ webpack.prod.conf.js
│
├─config
│ dev.env.js
│ index.js
│ prod.env.js
│
├─src
│ │ App.vue
│ │ main.js
│ │
│ ├─assets
│ │ └─styles
│ │ │ border.css
│ │ │ iconfont.css
│ │ │ mixins.styl
│ │ │ reset.css
│ │ │ varibles.styl
│ │ │
│ │ └─iconfont
│ │ iconfont.eot
│ │ iconfont.svg
│ │ iconfont.ttf
│ │ iconfont.woff
│ │
│ ├─common
│ │ ├─fade
│ │ │ FadeAnimation.vue
│ │ │
│ │ └─gallary
│ │ Gallary.vue
│ │
│ ├─pages
│ │ │ testGit.js
│ │ │
│ │ ├─city
│ │ │ │ City.vue
│ │ │ │
│ │ │ └─components
│ │ │ Alphabet.vue
│ │ │ Header.vue
│ │ │ List.vue
│ │ │ Search.vue
│ │ │
│ │ ├─detail
│ │ │ │ Detail.vue
│ │ │ │
│ │ │ └─components
│ │ │ Banner.vue
│ │ │ Header.vue
│ │ │ List.vue
│ │ │
│ │ └─home
│ │ │ Home.vue
│ │ │
│ │ └─components
│ │ Header.vue
│ │ Icons.vue
│ │ Recommend.vue
│ │ Swiper.vue
│ │ Weekend.vue
│ │
│ ├─router
│ │ index.js
│ │
│ └─store
│ index.js
│ mutations.js
│ state.js
│
└─static
.gitkeep
1.5 項目代碼初始化
由於做的是webapp,所以需要針對移動端,做相應的准備。
1. meta標簽相關設置
index.html
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
效果:頁面比例始終是1:1,用戶通過手指操作縮放是無效的
2. 引入reset.css
目的:重置頁面樣式
因為在不同移動端、不同瀏覽器上頁面的初始樣式是不一樣的,引入reset.css為了保證在每個瀏覽器上展示出的初始效果是一樣的
3. 引入border.css
目的:解決移動端1像素邊框問題
4. 項目中安裝fastclick
npm install fastclick --save
目的:解決移動端300ms延遲問題
1.6 頁面組件化
路由
router-index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/pages/home/Home'
import City from '@/pages/city/City'
import Detail from '@/pages/detail/Detail'
Vue.use(Router)
export default new Router({
routes: [{
path: '/',
name: 'Home',
component: Home
}, {
path: '/city',
name: 'City',
component: City
}, {
path: '/detail/:id',
name: 'Detail',
component: Detail
}],
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
頁面相關目錄
pages
├─city
│ │ City.vue
│ │
│ └─components
│ Alphabet.vue
│ Header.vue
│ List.vue
│ Search.vue
│
├─detail
│ │ Detail.vue
│ │
│ └─components
│ Banner.vue
│ Header.vue
│ List.vue
│
└─home
│ Home.vue
│
└─components
Header.vue
Icons.vue
Recommend.vue
Swiper.vue
Weekend.vue
common
├─fade
│ FadeAnimation.vue
│
└─gallary
Gallary.vue
比如,對於景點門票頁面,可以將其拆分成若干個小組件,放到 components 目錄下,通過在 Home.vue 容器組件中引用組件,整合出頁面
Home.vue部分代碼
<template>
<div>
<home-header></home-header>
<home-swiper :list="swiperList"></home-swiper>
<home-icons :list="iconList"></home-icons>
<home-recommend :list="recommendList"></home-recommend>
<home-weekend :list="weekendList"></home-weekend>
</div>
</template>
<script>
import HomeHeader from './components/Header'
import HomeSwiper from './components/Swiper'
import HomeIcons from './components/Icons'
import HomeRecommend from './components/Recommend'
import HomeWeekend from './components/Weekend'
import axios from 'axios'
import { mapState } from 'vuex'
export default {
name: 'Home',
components: {
HomeHeader,
HomeSwiper,
HomeIcons,
HomeRecommend,
HomeWeekend
},
data () {
return {
lastCity: '',
swiperList: [],
iconList: [],
recommendList: [],
weekendList: []
}
}
}
</script>
二、項目插件的使用
2.1 Ajax 獲取 首頁數據
vue推薦使用axios,實現跨平台的數據請求
安裝 axios
npm install axios --save
在 Home.vue 發送 Ajax 請求是最好的選擇,這個組件獲取 Ajax 數據之后,可以把數據傳給每個子組件
把一些靜態的文件放置在static目錄下,通過 http://localhost:8080/static/mock/index.json 可以訪問到
static
│ .gitkeep
│
└─mock
city.json
detail.json
index.json
Home.vue 部分代碼
<template>
<div>
<home-header></home-header>
<home-swiper :list="swiperList"></home-swiper>
<home-icons :list="iconList"></home-icons>
<home-recommend :list="recommendList"></home-recommend>
<home-weekend :list="weekendList"></home-weekend>
</div>
</template>
<script>
import HomeHeader from './components/Header'
import HomeSwiper from './components/Swiper'
import HomeIcons from './components/Icons'
import HomeRecommend from './components/Recommend'
import HomeWeekend from './components/Weekend'
import axios from 'axios'
import { mapState } from 'vuex'
export default {
name: 'Home',
components: {
HomeHeader,
HomeSwiper,
HomeIcons,
HomeRecommend,
HomeWeekend
},
data () {
return {
lastCity: '',
swiperList: [],
iconList: [],
recommendList: [],
weekendList: []
}
},
computed: {
...mapState(['city'])
},
methods: {
getHomeInfo () {
axios.get('/api/index.json?city=' + this.city)
.then(this.getHomeInfoSucc)
},
getHomeInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.swiperList = data.swiperList
this.iconList = data.iconList
this.recommendList = data.recommendList
this.weekendList = data.weekendList
}
}
},
mounted () {
this.lastCity = this.city
this.getHomeInfo()
}
}
</script>
<style>
</style>
父子組件之間進行通訊
父組件通過 props 傳遞數據給子組件,子組件通過 emit 發送事件傳遞數據給父組件
以 List 組件 為例(List.vue 部分代碼)
<template>
<div>
<div class="title">熱銷推薦</div>
<ul>
<router-link
tag="li"
class="item border-bottom"
v-for="item of list"
:key="item.id"
:to="'/detail/' + item.id"
>
<img class="item-img" :src="item.imgUrl" />
<div class="item-info">
<p class="item-title">{{item.title}}</p>
<p class="item-desc">{{item.desc}}</p>
<button class="item-button">查看詳情</button>
</div>
</router-link>
</ul>
</div>
</template>
<script>
export default {
name: 'HomeRecommend',
props: {
list: Array
}
}
</script>
2.2 輪播圖
安裝 vue-awesome-swiper 插件
npm install vue-awesome-swiper@2.6.7 --save
輪播在多個組件中使用
以 home-components-Swiper.vue 為例
<template>
<div class="wrapper">
<swiper :options="swiperOption" v-if="showSwiper">
<swiper-slide v-for="item of list" :key="item.id">
<img class="swiper-img" :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</div>
</template>
<script>
export default {
name: 'HomeSwiper',
props: {
list: Array
},
data () {
return {
swiperOption: {
pagination: '.swiper-pagination',
loop: true
}
}
},
computed: {
showSwiper () {
return this.list.length
}
}
}
</script>
2.3 Better-scroll
安裝
npm install better-scroll --save
使用
<div class="wrapper">
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
<!-- you can put some other DOMs here, it won't affect the scrolling
</div>
import BScroll from '@better-scroll/core'
let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper)
2.4 使用vuex實現數據共享
安裝vuex
npm install vuex --save
希望在 城市列表頁面 點擊城市,首頁右上角城市可以 進行相應的改變。
具體描述為:
項目中是為了實現城市選擇列表頁面和首頁的數據傳遞,並且沒有公用的組件,city/components/List.vue
、home/components/Header.vue、Home.vue組件,都需要獲取到數據。
因為這個項目沒有需要進行異步的操作,也不需要對數據進行額外的處理,所以項目中只用到了 state 和 mutations。在 state 中存儲了 city 數據,然后在 mutation 里定義事件類型和函數 changeCity
store
index.js
mutations.js
state.js
state.js
let defaultCity = '上海'
try {
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (e) {}
export default {
city: defaultCity
}
mutations.js
export default {
changeCity (state, city) {
state.city = city
try {
localStorage.city = city
} catch (e) {}
}
}
index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
Vue.use(Vuex)
export default new Vuex.Store({
state,
mutations
})
Home.vue 組件,在計算屬性中,this.$store.state.xxx
,在這個項目中是 this.$store.state.city
可以獲取到 state 數據。當然,為了使代碼更加簡潔,用 mapState 將 this.xxx
映射為 this.$store.state.xxx
。
在 List.vue 中,通過 commit 來觸發 mutations 里面的方法進行數據的修改。同樣,為了使代碼更加簡潔,引入 mapMutations 將 this.changeCity(city)
映射為 this.$store.commit('changeCity', city)
。
【city/List.vue 具體是】
import { mapState, mapMutations } from 'vuex'
computed: {
...mapState({
currentCity: 'city'
})
},
methods: {
handleCityClick (city) {
// this.$store.commit('changeCity', city)
this.changeCity(city)
this.$router.push('/')
},
...mapMutations(['changeCity'])
}
這樣就實現了這幾個組件的數據共享。
三、項目難點
3.1 兄弟組件間聯動
實現功能:點擊城市列表頁面右側的字母,列表選項會滾動到對應的字母區域。【gif展示】
兄弟組件的傳值,可以通過 bus 總線的形式來傳值。但是因為我們現在這個非父子組件比較簡單,可以讓 Alphabet.vue 組件將值傳遞給父組件 City.vue 組件,然后 City.vue 組件再將值轉發給 List.vue 組件,這樣就實現了兄弟組件的傳值。【子組件給父組件,父組件再轉給另一個子組件】。這樣,在 Alphabet.vue 中點擊右側字母,會獲取到對應的字母。
Alphabet.vue
在循環的元素上加一個點擊事件,例如 handleLetterClick,然后在 methods 中寫這個事件方法:
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick"
>
{{item}}
</li>
</ul>
</template>
<script>
methods: {
handleLetterClick(e) {
this.$emit("change", e.target.innerHTML);
}
}
</script>
接下來,將父組件接收到的這個數據轉發給子組件 List.vue,父組件是通過屬性向子組件傳值的。
首先在父組件 City.vue 里的 data 中定義一個 letter,默認值是空,在 handleLetterClick
方法中,當接受到外部傳來的 letter 的時候,讓 this.letter = letter
。
City.vue
<template>
<div>
<city-header></city-header>
<city-search :cities="cities"></city-search>
<city-list
:cities="cities"
:hot="hotCities"
:letter="letter"
></city-list>
<city-alphabet
:cities="cities"
@change="handleLetterChange"
></city-alphabet>
</div>
</template>
<script>
data() {
return {
hotCities: [],
cities: {},
letter: ""
};
},
methods: {
handleAlpChange(letter) {
this.letter = letter;
}
}
</script>
最后只需要把 letter 傳遞給子組件 List.vue 就可以了,在 City.vue 組件的模板 city-list 中通過 :letter="letter"
向子組件 List 傳值,在 props 中接收這個 letter,並且驗證類型為 String 類型。
List.vue
props: {
hot: Array,
cities: Object,
letter: String
}
這樣就實現了兄弟組件的傳值。
【項目難點】
接下來要做的是,當 List.vue 發現 letter 有改變的時候,就需要讓組件顯示的列表項跟 letter 相同的首字母的列表項要顯示出來,怎么做呢?
這個時候就要借助一個偵聽器,監聽letter的變化;
better-scroll 給提供了這樣一個接口,scroll.scorllToElement
,如果 letter 不為空的時候,就調用 this.scroll.scrollToElement()
這個方法,可以讓滾動區自動滾到某一個元素上,那么怎么傳這個元素呢?在循環城市這一塊中,給循環項加一個 ref 引用來獲取當前 Dom 元素,等於 key,然后回到偵聽器的 letter 中,定義一個 element,它就等於通過 ref 獲取到的元素:
List.vue
watch: {
letter() {
if (this.letter) {
const element = this.$refs[this.letter][0];
this.scroll.scrollToElement(element);
}
}
},
這個時候就可以通過字母獲取到對應的區域,然后把 element 傳入 scrollToElement 里,注意,上邊代碼最后加了一個 [0],這是因為如果不加,通過 ref 或的內容就是一個數組,這個數組里的第一個元素才是真正的 DOM 元素,這個時候,點擊右側字母表,就可以跳到對應的字母下的城市列表了。
點擊跳轉的功能實現啦
接下來再實現一下滑動右側字母表,左側城市列表切換的效果。
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@click="handleLetterClick"
>
{{item}}
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
},
data () {
return {
touchStatus: false,
startY: 0,
timer: null
}
},
updated () {
this.startY = this.$refs['A'][0].offsetTop
},
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove (e) {
if (this.touchStatus) {
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - this.startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}, 16)
}
},
handleTouchEnd () {
this.touchStatus = false
}
}
}
</script>
3.2 search組件
功能:進入到城市選擇頁面的時候,當 focus 到搜索框,輸入城市名或拼音能夠把搜索的結果顯示出來。
<template>
<div>
<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="輸入城市名或拼音" />
</div>
<div
class="search-content"
ref="search"
v-show="keyword"
>
<ul>
<li
class="search-item border-bottom"
v-for="item of list"
:key="item.id"
@click="handleCityClick(item.name)"
>
{{item.name}}
</li>
<li class="search-item border-bottom" v-show="hasNoData">
沒有找到匹配數據
</li>
</ul>
</div>
</div>
</template>
<script>
import Bscroll from 'better-scroll'
import { mapMutations } from 'vuex'
export default {
name: 'CitySearch',
props: {
cities: Object
},
data () {
return {
keyword: '',
list: [],
timer: null
}
},
computed: {
hasNoData () {
return !this.list.length
}
},
watch: {
keyword () {
if (this.timer) {
clearTimeout(this.timer)
}
if (!this.keyword) {
this.list = []
return
}
this.timer = setTimeout(() => {
const result = []
for (let i in this.cities) {
this.cities[i].forEach((value) => {
if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {
result.push(value)
}
})
}
this.list = result
}, 100)
}
},
methods: {
handleCityClick (city) {
this.changeCity(city)
this.$router.push('/')
},
...mapMutations(['changeCity'])
},
mounted () {
this.scroll = new Bscroll(this.$refs.search)
}
}
</script>
【性能優化---防抖】
寫一個偵聽器 watch,在里邊監聽 keyword 的改變,考慮到性能優化,使用防抖的方式來實現,先在 data 中定義一個 timer 定時器,默認值為 null,然后在監聽 keyword 的方法中,判斷,當 timer 為 null 時,清除這個定時器。下面寫這個定時器的方法,當延時 100ms 的時候,箭頭函數會被執行。
3.3 遞歸組件
遞歸組件的意思就是在組件自身調用組件自身。
數據 detail.json
{
"ret": true,
"data": {
"sightName": "大連聖亞海洋世界(AAAA景區)",
"bannerImg": "http://img1.qunarzz.com/sight/p0/201404/23/04b92c99462687fa1ba45c1b5ba4ad77.jpg_600x330_bf9c4904.jpg",
"gallaryImgs": ["http://img1.qunarzz.com/sight/p0/201404/23/04b92c99462687fa1ba45c1b5ba4ad77.jpg_800x800_70debc93.jpg", "http://img1.qunarzz.com/sight/p0/1709/76/7691528bc7d7ad3ca3.img.png_800x800_9ef05ee7.png"],
"categoryList": [{
"title": "成人票",
"children": [{
"title": "成人三館聯票",
"children": [{
"title": "成人三館聯票 - 某一連鎖店銷售"
}]
},{
"title": "成人五館聯票"
}]
}, {
"title": "學生票"
}, {
"title": "兒童票"
}, {
"title": "特惠票"
}]
}
}
list.vue
<template>
<div>
<div
class="item"
v-for="(item, index) of list"
:key="index"
>
<div class="item-title border-bottom">
<span class="item-title-icon"></span>
{{item.title}}
</div>
<div v-if="item.children" class="item-chilren">
<detail-list :list="item.children"></detail-list>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DetailList',
props: {
list: Array
}
}
</script>
<style lang="stylus" scoped>
.item-title-icon
position: relative
left: .06rem
top: .06rem
display: inline-block
width: .36rem
height: .36rem
background: url(http://s.qunarzz.com/piao/image/touch/sight/detail.png) 0 -.45rem no-repeat
margin-right: .1rem
background-size: .4rem 3rem
.item-title
line-height: .8rem
font-size: .32rem
padding: 0 .2rem
.item-chilren
padding: 0 .2rem
</style>
上面代碼中,在
list-children
這個元素下,先做了一個判斷,當item.children
下有值的時候,調用一下自身,也就是 detail-list 這個組件,這個組件也是通過屬性的形式,傳一個 list,因為在 list.vue 中已經通過 props 接收到 list 了,而且外層已經循環過 list 了,現在要獲取 list 下的 children 中的數據,所以直接讓這個 list 屬性等於item.children
就可以了。因為數據存在層級關系,可以通過添加樣式呈現出來,效果如下圖:
四、項目中遇到的問題及解決方案
這部分內容並不是所有在項目中遇到的問題和解決方法,因為上文中也有相應的描述,這部分內容是對上文的補充。
4.1 localStorage
剛開始在實現首頁右上角城市定位顯示的時候,src 目錄下新建了一個 store 目錄,存儲了 Vuex 中的默認數據,city 直接設置成了“北京”,但是其實這樣去寫,是有問題的,點擊城市,會改變這個 city,但是當頁面刷新了,就又變回了北京。
考慮到在真實的項目中,如果你這次選中了一個城市,下次再打開這個網頁的時候,上次選的城市還應該在的,怎么解決這個問題呢?
這時可以借助 HTML5 中提供了一個新的 api,叫做 localStorage localStorage
,它可以實現本地存儲,在這里也就是實現保存城市的功能。
store/index.js中,這樣去寫代碼,當用戶嘗試去改變城市的時候,我不但把 state 中的 city 改了,同時還去存一個 localStorage
,直接寫 localStorage.city = city
就可以了。然后讓 stare 中 city 的默認值是 localStorage.city || "北京"
,就可以了。也就是 city 的值我默認先去 localStorage
中取,如果取不到,才用默認的 “北京”。
store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
city: localStorage.city || "北京"
},
mutations: {
changeCity(state, city) {
state.city = city;
localStorage.city = city;
}
}
})
這個時候打開頁面,當用戶選擇一個城市,然后刷新頁面,可以看到上次選擇的城市還在。但是當使用 localStorage
的時候,建議在外層包裹一個 try{ }catch(e){ }
,因為在某些瀏覽器,如果用戶關閉了本地存儲這樣的功能,或者使用隱身模式,使用 localStorage
可能導致瀏覽器直接拋出異常,代碼就運行不了了,為了避免這種問題,建議在外層加一個 try{ }catch(e){ }
,怎么加呢?
先定義一個默認的 defaultCity
等於“北京”,然后寫一個 try{ }catch(e){ }
,這樣寫:如果有 localStorage.city
,default.city
就等於 localStorage.city
,下邊 state 中的 city 就可以等於 defaultCity
了,同樣在 mutations 的 changeCity 中也要寫一個 try{ }catch(e)
:
store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
let defaultCity = "北京"
try {
if (localStorage.city) {
defaultCity = localStorage.city;
}
} catch (e) { }
export default new Vuex.Store({
state: {
city: defaultCity
},
mutations: {
changeCity(state, city) {
state.city = city;
try {
localStorage.city = city;
} catch (e) { }
}
}
})
現在我們看到 store/index.js 這個文件慢慢的變得復雜起來了,實際上,在真正的項目開發和之中,會做進一步的拆分,也就是把這個文件拆分為 State、Actions、Mutations,在 store 中創建一個文件叫 state.js(只存儲公用數據),然后把設置默認數據的這塊代碼放進去,並通過 export 導出,內容就是在 index.js 中定義的 state 對象里的內容:
let defaultCity = "北京"
try {
if (localStorage.city) {
defaultCity = localStorage.city;
}
} catch (e) { }
export default {
city: defaultCity
}
接下來,只需要在 index.js 中 import state 就可以了:
import Vue from "vue";
import Vuex from "vuex";
import state from "./state";
Vue.use(Vuex);
export default new Vuex.Store({
state: state,
mutations: {
changeCity(state, city) {
state.city = city;
try {
localStorage.city = city;
} catch (e) { }
}
}
})
接着,在 store 目錄下創建一個文件,叫做 mutations.js,然后把 index.js 中的 mutations 對象里的代碼剪切進去:
export default {
changeCity(state, city) {
state.city = city;
try {
localStorage.city = city;
} catch (e) { }
}
}
最終 index.js 就變成了這樣:
import Vue from "vue";
import Vuex from "vuex";
import state from "./state";
import mutations from "./mutations";
Vue.use(Vuex);
export default new Vuex.Store({
state: state,
mutations: mutations
})
這樣,我們就將 vuex 的代碼拆分成了 State、Actions、Mutations 這幾個部分,未來它的維護性也會得到比較大的提高。
4.2 keep-alive
使用 keep-alive 優化網頁性能
當寫完城市列表響應代碼,啟動服務,打開頁面,這樣看不存在什么問題,基本的一些業務邏輯都已經實現了,但是在控制台中打開 Network 網絡這個選項,選擇 XHR,當初次進入首頁的時候,請求了一個 index.json 的文件,然后切換到列表頁,又請求了一個 city.json,然后再回到首頁,index.json 又請求了一次,再次去列表頁,city.json 又請求了一次,也就是,每一次路由發生變化的時候,Ajax 都會重新的被發送。
思考是什么原因導致這樣的問題呢,打開 Home.vue 首頁這個組件,每一次打開這個首頁的時候,都會被重新的渲染,所以 mounted 這個鈎子就會被重新的執行,那么這個 Ajax 數據就會被重新獲取,那么這么能讓它只獲取一次呢?
打開 main.js,可以看到入口組件是 App 這個組件,再打開 App.vue,router-view 顯示的是當前地址所對應的內容,我們可以在外層包裹一個 keep-alive 的一個標簽,他是 Vue 自帶的一個標簽,他的意思就是我的路由的內容被加載一次后,我就把路由中的內容放到內存之中,下一次再進入這個路由的時候,不需要重新渲染這個組件,去重新執行鈎子函數,只要去內存里把以前的內容拿出來就可以。
這個時候,回到頁面上,再打開 Network,進入到列表頁,選擇城市再返回首頁,就不會再去加載 index.json 了,同樣再進入列表頁,也不會再去加載 city.json 了,他直接會從內存中調數據,而不會重新去法 Ajax 請求了。
這樣還是存在邏輯上的問題的,當我在“北京”的時候,首頁顯示的是“北京”的內容,當切換為“上海”時,首頁就應該顯示“上海”的內容,所以城市發生改變的時候,首頁還需要重新發一次 Ajax 請求,來獲取不同城市的數據信息,我們對這一塊做一個調整。
打開 Home.vue 組件,改一下 axios 請求地址這里,在他的后面帶一個參數,讓他等於 Vuex 中存的當前的城市,所以還需要在 Home.vue 組件中引用 Vuex,import { mapState } from "vuex"
,然后再加一個計算屬性:
computed:{
...mapState(['city'])
}
獲取到城市對應的內容,然后就可以在發 Ajax 的時候,把 city 放在請求的參數里面:
axios.get("/api/index.json?city=" + this.city)
.then(this.getHomeInfoSucc);
這個時候,我們打開頁面,可以看到請求參數里已經攜帶了當前的城市:
但是,例如當你切換了城市“桂林”,回到首頁,並沒有重新發 Ajax 請求,雖然上面的城市變成了“桂林”,但是底下的內容還是“北京”的內容,我們希望底下的內容跟着變,該怎么做呢?
當我們在 App.vue 中用了 keep-alive
的時候,這塊的內容已經被緩存起來了,他直接取得是緩存里的數據,那如何去改變緩存里的數據呢?當你使用 keep-alive
的時候,組件中會多出一個生命周期函數 activted
,
可以在 mounted 和 activated 兩個生命周期函數下打印一些內容,到瀏覽器上看一下他倆的執行:
mounted() {
console.log("mounted");
this.getHomeInfo();
},
activated(){
console.log("activted");
}
打開頁面,可以看到,mounted 和 activated 都會執行,當切換了城市,再回到首頁的時候,組件的 mounted 就不會執行了,就只有 activated 會被執行,那么我們借助 activated 這個生命周期函數就可以實現我們想要的功能了。
首先在頁面被掛載的時候,也就是 mounted 中一定會去發一個 Ajax 請求,當頁面重新被顯示的時候,activated 一定會被重新的執行,那么我們就可以在頁面每次重新顯示的時候,可以判斷當前頁面上的城市和上次頁面上顯示的城市是否是相同的,如果不相同的,就再發一次 Ajax 請求。
先在 data 中設置一個數據 lastCity
,默認值是空,接着當頁面被掛載的時候,讓它等於 this.city
,對上一次的城市做一個保存:
mounted() {
this.lastCity = this.city
this.getHomeInfo();
}
當頁面被重新激活的時候,我們在 activted 中這樣寫:
activated() {
if(this.lastCity != this.city){
this.lastCity = this.city
this.getHomeInfo();
}
}
如果上一次的城市 lastCity 不等於當前城市的時候,就重新發一個 Ajax 請求,直接調用上面 getHomeInfo
方法就可以了。當上次的 city 和這次的 city 不一樣時,還需要讓他等於這次的 city。回到頁面上,可以看到當切換的城市和上次的城市一樣時,Ajax 就不會請求 city.json 了,當不一樣時,才會去請求 city.json。
回到代碼里面,通過 activted 這樣一個 keep-alive 新增的生命周期函數,結合 lastCity 這樣一個臨時緩存變量,就實現了首頁代碼性能優化的調整。
五、后續安排
- 頁面功能進一步完善
- 將項目部署到服務器上
- 結合node,對接真實數據
說明,本項目來源於Dell Vue2.5開發去哪兒網App從零基礎入門到實戰項目
本文的寫作靈感與思路一部分來源於神三元的react hooks+redux+immutable.js打造網易雲音樂精美webApp