Vue.js開發去哪兒網WebApp


一、項目介紹

這個項目主要參考了去哪兒網的布局,完成了首頁、城市選擇頁面、詳情頁面的開發。

  • 首頁:實現了多區域輪播的功能,以及多區域列表的展示;
  • 城市選擇頁面:在這個頁面實現了城市展示、城市搜索、城市右側字母和左側區塊動態聯動的效果,當用戶在城市列表切換了新的城市后,首頁對應的城市也會跟着變化;
  • 景點詳情頁面:實現公用的畫廊組件,以及遞歸展示的列表組件。

vue仿去哪兒網webapp

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. 景點門票首頁

2去哪兒網首頁展示

  1. 城市列表頁面

4城市搜索-字母表

  1. 景點詳情頁面

5詳情頁面-畫廊

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

輪播在多個組件中使用

6-swiper

以 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 中點擊右側字母,會獲取到對應的字母。

4城市搜索-字母表

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 獲取到的元素:

scroll.scorllToElement

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 到搜索框,輸入城市名或拼音能夠把搜索的結果顯示出來。

3城市列表-搜索

<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 就可以了。因為數據存在層級關系,可以通過添加樣式呈現出來,效果如下圖:

遞歸.PNG

四、項目中遇到的問題及解決方案

這部分內容並不是所有在項目中遇到的問題和解決方法,因為上文中也有相應的描述,這部分內容是對上文的補充。

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.citydefault.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 都會重新的被發送。

keep-alive-1

思考是什么原因導致這樣的問題呢,打開 Home.vue 首頁這個組件,每一次打開這個首頁的時候,都會被重新的渲染,所以 mounted 這個鈎子就會被重新的執行,那么這個 Ajax 數據就會被重新獲取,那么這么能讓它只獲取一次呢?

打開 main.js,可以看到入口組件是 App 這個組件,再打開 App.vue,router-view 顯示的是當前地址所對應的內容,我們可以在外層包裹一個 keep-alive 的一個標簽,他是 Vue 自帶的一個標簽,他的意思就是我的路由的內容被加載一次后,我就把路由中的內容放到內存之中,下一次再進入這個路由的時候,不需要重新渲染這個組件,去重新執行鈎子函數,只要去內存里把以前的內容拿出來就可以。

keep-alive-4

這個時候,回到頁面上,再打開 Network,進入到列表頁,選擇城市再返回首頁,就不會再去加載 index.json 了,同樣再進入列表頁,也不會再去加載 city.json 了,他直接會從內存中調數據,而不會重新去法 Ajax 請求了。

keep-alive-2

這樣還是存在邏輯上的問題的,當我在“北京”的時候,首頁顯示的是“北京”的內容,當切換為“上海”時,首頁就應該顯示“上海”的內容,所以城市發生改變的時候,首頁還需要重新發一次 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

keep-alive-3

可以在 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM