一、项目介绍

这个项目主要参考了去哪儿网的布局,完成了首页、城市选择页面、详情页面的开发。

  • 首页:实现了多区域轮播的功能,以及多区域列表的展示;
  • 城市选择页面:在这个页面实现了城市展示、城市搜索、城市右侧字母和左侧区块动态联动的效果,当用户在城市列表切换了新的城市后,首页对应的城市也会跟着变化;
  • 景点详情页面:实现公用的画廊组件,以及递归展示的列表组件。

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. 景点门票首页

  1. 城市列表页面

  1. 景点详情页面

1.3 项目收获

1. 理解整个vue项目的开发流程,上手中型vue项目的开发

  • Vue Router 来做多页面的路由
  • Vuex 多个组件的数据共享
  • 插件swiper实现页面轮播效果
  • Axios 来进行 Ajax 数据的获取

2. 移动端页面布局技巧

3. stylus 编写前端的样式

4. 公用组件的拆分

5. 规范的代码编写

1.4 项目目录

附上项目目录和仓库地址vue仿去哪儿网webapp

  1. F:.
  2. .babelrc
  3. .editorconfig
  4. .eslintignore
  5. .eslintrc.js
  6. .gitignore
  7. .postcssrc.js
  8. index.html
  9. package-lock.json
  10. package.json
  11. README.en.md
  12. README.md

  13. ├─build
  14. build.js
  15. check-versions.js
  16. logo.png
  17. utils.js
  18. vue-loader.conf.js
  19. webpack.base.conf.js
  20. webpack.dev.conf.js
  21. webpack.prod.conf.js

  22. ├─config
  23. dev.env.js
  24. index.js
  25. prod.env.js

  26. ├─src
  27. App.vue
  28. main.js

  29. ├─assets
  30. └─styles
  31. border.css
  32. iconfont.css
  33. mixins.styl
  34. reset.css
  35. varibles.styl

  36. └─iconfont
  37. iconfont.eot
  38. iconfont.svg
  39. iconfont.ttf
  40. iconfont.woff

  41. ├─common
  42. ├─fade
  43. FadeAnimation.vue

  44. └─gallary
  45. Gallary.vue

  46. ├─pages
  47. testGit.js

  48. ├─city
  49. City.vue

  50. └─components
  51. Alphabet.vue
  52. Header.vue
  53. List.vue
  54. Search.vue

  55. ├─detail
  56. Detail.vue

  57. └─components
  58. Banner.vue
  59. Header.vue
  60. List.vue

  61. └─home
  62. Home.vue

  63. └─components
  64. Header.vue
  65. Icons.vue
  66. Recommend.vue
  67. Swiper.vue
  68. Weekend.vue

  69. ├─router
  70. index.js

  71. └─store
  72. index.js
  73. mutations.js
  74. state.js

  75. └─static
  76. .gitkeep

1.5 项目代码初始化

由于做的是webapp,所以需要针对移动端,做相应的准备。

1. meta标签相关设置

index.html

  1. <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

  1. import Vue from 'vue'
  2. import Router from 'vue-router'
  3. import Home from '@/pages/home/Home'
  4. import City from '@/pages/city/City'
  5. import Detail from '@/pages/detail/Detail'
  6. Vue.use(Router)
  7. export default new Router({
  8. routes: [{
  9. path: '/',
  10. name: 'Home',
  11. component: Home
  12. }, {
  13. path: '/city',
  14. name: 'City',
  15. component: City
  16. }, {
  17. path: '/detail/:id',
  18. name: 'Detail',
  19. component: Detail
  20. }],
  21. scrollBehavior (to, from, savedPosition) {
  22. return { x: 0, y: 0 }
  23. }
  24. })

页面相关目录

  1. pages
  2. ├─city
  3. City.vue

  4. └─components
  5. Alphabet.vue
  6. Header.vue
  7. List.vue
  8. Search.vue

  9. ├─detail
  10. Detail.vue

  11. └─components
  12. Banner.vue
  13. Header.vue
  14. List.vue

  15. └─home
  16. Home.vue

  17. └─components
  18. Header.vue
  19. Icons.vue
  20. Recommend.vue
  21. Swiper.vue
  22. Weekend.vue
  23. common
  24. ├─fade
  25. FadeAnimation.vue

  26. └─gallary
  27. Gallary.vue

比如,对于景点门票页面,可以将其拆分成若干个小组件,放到 components 目录下,通过在 Home.vue 容器组件中引用组件,整合出页面

Home.vue部分代码

  1. <template>
  2. <div>
  3. <home-header></home-header>
  4. <home-swiper :list="swiperList"></home-swiper>
  5. <home-icons :list="iconList"></home-icons>
  6. <home-recommend :list="recommendList"></home-recommend>
  7. <home-weekend :list="weekendList"></home-weekend>
  8. </div>
  9. </template>
  10. <script>
  11. import HomeHeader from './components/Header'
  12. import HomeSwiper from './components/Swiper'
  13. import HomeIcons from './components/Icons'
  14. import HomeRecommend from './components/Recommend'
  15. import HomeWeekend from './components/Weekend'
  16. import axios from 'axios'
  17. import { mapState } from 'vuex'
  18. export default {
  19. name: 'Home',
  20. components: {
  21. HomeHeader,
  22. HomeSwiper,
  23. HomeIcons,
  24. HomeRecommend,
  25. HomeWeekend
  26. },
  27. data () {
  28. return {
  29. lastCity: '',
  30. swiperList: [],
  31. iconList: [],
  32. recommendList: [],
  33. weekendList: []
  34. }
  35. }
  36. }
  37. </script>

二、项目插件的使用

2.1 Ajax 获取 首页数据

vue推荐使用axios,实现跨平台的数据请求

安装 axios

npm install axios --save

在 Home.vue 发送 Ajax 请求是最好的选择,这个组件获取 Ajax 数据之后,可以把数据传给每个子组件

把一些静态的文件放置在static目录下,通过 http://localhost:8080/static/mock/index.json 可以访问到

  1. static
  2. .gitkeep

  3. └─mock
  4. city.json
  5. detail.json
  6. index.json

Home.vue 部分代码

  1. <template>
  2. <div>
  3. <home-header></home-header>
  4. <home-swiper :list="swiperList"></home-swiper>
  5. <home-icons :list="iconList"></home-icons>
  6. <home-recommend :list="recommendList"></home-recommend>
  7. <home-weekend :list="weekendList"></home-weekend>
  8. </div>
  9. </template>
  10. <script>
  11. import HomeHeader from './components/Header'
  12. import HomeSwiper from './components/Swiper'
  13. import HomeIcons from './components/Icons'
  14. import HomeRecommend from './components/Recommend'
  15. import HomeWeekend from './components/Weekend'
  16. import axios from 'axios'
  17. import { mapState } from 'vuex'
  18. export default {
  19. name: 'Home',
  20. components: {
  21. HomeHeader,
  22. HomeSwiper,
  23. HomeIcons,
  24. HomeRecommend,
  25. HomeWeekend
  26. },
  27. data () {
  28. return {
  29. lastCity: '',
  30. swiperList: [],
  31. iconList: [],
  32. recommendList: [],
  33. weekendList: []
  34. }
  35. },
  36. computed: {
  37. ...mapState(['city'])
  38. },
  39. methods: {
  40. getHomeInfo () {
  41. axios.get('/api/index.json?city=' + this.city)
  42. .then(this.getHomeInfoSucc)
  43. },
  44. getHomeInfoSucc (res) {
  45. res = res.data
  46. if (res.ret && res.data) {
  47. const data = res.data
  48. this.swiperList = data.swiperList
  49. this.iconList = data.iconList
  50. this.recommendList = data.recommendList
  51. this.weekendList = data.weekendList
  52. }
  53. }
  54. },
  55. mounted () {
  56. this.lastCity = this.city
  57. this.getHomeInfo()
  58. }
  59. }
  60. </script>
  61. <style>
  62. </style>

父子组件之间进行通讯

父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件

以 List 组件 为例(List.vue 部分代码)

  1. <template>
  2. <div>
  3. <div class="title">热销推荐</div>
  4. <ul>
  5. <router-link
  6. tag="li"
  7. class="item border-bottom"
  8. v-for="item of list"
  9. :key="item.id"
  10. :to="'/detail/' + item.id"
  11. >
  12. <img class="item-img" :src="item.imgUrl" />
  13. <div class="item-info">
  14. <p class="item-title">{{item.title}}</p>
  15. <p class="item-desc">{{item.desc}}</p>
  16. <button class="item-button">查看详情</button>
  17. </div>
  18. </router-link>
  19. </ul>
  20. </div>
  21. </template>
  22. <script>
  23. export default {
  24. name: 'HomeRecommend',
  25. props: {
  26. list: Array
  27. }
  28. }
  29. </script>

2.2 轮播图

安装 vue-awesome-swiper 插件

npm install vue-awesome-swiper@2.6.7 --save

轮播在多个组件中使用

以 home-components-Swiper.vue 为例

  1. <template>
  2. <div class="wrapper">
  3. <swiper :options="swiperOption" v-if="showSwiper">
  4. <swiper-slide v-for="item of list" :key="item.id">
  5. <img class="swiper-img" :src="item.imgUrl" />
  6. </swiper-slide>
  7. <div class="swiper-pagination" slot="pagination"></div>
  8. </swiper>
  9. </div>
  10. </template>
  11. <script>
  12. export default {
  13. name: 'HomeSwiper',
  14. props: {
  15. list: Array
  16. },
  17. data () {
  18. return {
  19. swiperOption: {
  20. pagination: '.swiper-pagination',
  21. loop: true
  22. }
  23. }
  24. },
  25. computed: {
  26. showSwiper () {
  27. return this.list.length
  28. }
  29. }
  30. }
  31. </script>

2.3 Better-scroll

安装

npm install better-scroll --save

使用

  1. <div class="wrapper">
  2. <ul class="content">
  3. <li>...</li>
  4. <li>...</li>
  5. ...
  6. </ul>
  7. <!-- you can put some other DOMs here, it won't affect the scrolling
  8. </div>
  1. import BScroll from '@better-scroll/core'
  2. let wrapper = document.querySelector('.wrapper')
  3. 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

  1. store
  2. index.js
  3. mutations.js
  4. state.js

state.js

  1. let defaultCity = '上海'
  2. try {
  3. if (localStorage.city) {
  4. defaultCity = localStorage.city
  5. }
  6. } catch (e) {}
  7. export default {
  8. city: defaultCity
  9. }

mutations.js

  1. export default {
  2. changeCity (state, city) {
  3. state.city = city
  4. try {
  5. localStorage.city = city
  6. } catch (e) {}
  7. }
  8. }

index.js

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. import state from './state'
  4. import mutations from './mutations'
  5. Vue.use(Vuex)
  6. export default new Vuex.Store({
  7. state,
  8. mutations
  9. })

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 具体是】

  1. import { mapState, mapMutations } from 'vuex'
  2. computed: {
  3. ...mapState({
  4. currentCity: 'city'
  5. })
  6. },
  7. methods: {
  8. handleCityClick (city) {
  9. // this.$store.commit('changeCity', city)
  10. this.changeCity(city)
  11. this.$router.push('/')
  12. },
  13. ...mapMutations(['changeCity'])
  14. }

这样就实现了这几个组件的数据共享。

三、项目难点

3.1 兄弟组件间联动

实现功能:点击城市列表页面右侧的字母,列表选项会滚动到对应的字母区域。【gif展示】

兄弟组件的传值,可以通过 bus 总线的形式来传值。但是因为我们现在这个非父子组件比较简单,可以让 Alphabet.vue 组件将值传递给父组件 City.vue 组件,然后 City.vue 组件再将值转发给 List.vue 组件,这样就实现了兄弟组件的传值。【子组件给父组件,父组件再转给另一个子组件】。这样,在 Alphabet.vue 中点击右侧字母,会获取到对应的字母。

Alphabet.vue

在循环的元素上加一个点击事件,例如 handleLetterClick,然后在 methods 中写这个事件方法:

  1. <template>
  2. <ul class="list">
  3. <li
  4. class="item"
  5. v-for="item of letters"
  6. :key="item"
  7. :ref="item"
  8. @click="handleLetterClick"
  9. >
  10. {{item}}
  11. </li>
  12. </ul>
  13. </template>
  14. <script>
  15. methods: {
  16. handleLetterClick(e) {
  17. this.$emit("change", e.target.innerHTML);
  18. }
  19. }
  20. </script>

接下来,将父组件接收到的这个数据转发给子组件 List.vue,父组件是通过属性向子组件传值的。

首先在父组件 City.vue 里的 data 中定义一个 letter,默认值是空,在 handleLetterClick 方法中,当接受到外部传来的 letter 的时候,让 this.letter = letter

City.vue

  1. <template>
  2. <div>
  3. <city-header></city-header>
  4. <city-search :cities="cities"></city-search>
  5. <city-list
  6. :cities="cities"
  7. :hot="hotCities"
  8. :letter="letter"
  9. ></city-list>
  10. <city-alphabet
  11. :cities="cities"
  12. @change="handleLetterChange"
  13. ></city-alphabet>
  14. </div>
  15. </template>
  16. <script>
  17. data() {
  18. return {
  19. hotCities: [],
  20. cities: {},
  21. letter: ""
  22. };
  23. },
  24. methods: {
  25. handleAlpChange(letter) {
  26. this.letter = letter;
  27. }
  28. }
  29. </script>

最后只需要把 letter 传递给子组件 List.vue 就可以了,在 City.vue 组件的模板 city-list 中通过 :letter="letter" 向子组件 List 传值,在 props 中接收这个 letter,并且验证类型为 String 类型。

List.vue

  1. props: {
  2. hot: Array,
  3. cities: Object,
  4. letter: String
  5. }

这样就实现了兄弟组件的传值。

【项目难点】

接下来要做的是,当 List.vue 发现 letter 有改变的时候,就需要让组件显示的列表项跟 letter 相同的首字母的列表项要显示出来,怎么做呢?

这个时候就要借助一个侦听器,监听letter的变化;

better-scroll 给提供了这样一个接口,scroll.scorllToElement,如果 letter 不为空的时候,就调用 this.scroll.scrollToElement() 这个方法,可以让滚动区自动滚到某一个元素上,那么怎么传这个元素呢?在循环城市这一块中,给循环项加一个 ref 引用来获取当前 Dom 元素,等于 key,然后回到侦听器的 letter 中,定义一个 element,它就等于通过 ref 获取到的元素:

List.vue

  1. watch: {
  2. letter() {
  3. if (this.letter) {
  4. const element = this.$refs[this.letter][0];
  5. this.scroll.scrollToElement(element);
  6. }
  7. }
  8. },

这个时候就可以通过字母获取到对应的区域,然后把 element 传入 scrollToElement 里,注意,上边代码最后加了一个 [0],这是因为如果不加,通过 ref 或的内容就是一个数组,这个数组里的第一个元素才是真正的 DOM 元素,这个时候,点击右侧字母表,就可以跳到对应的字母下的城市列表了。

点击跳转的功能实现啦

接下来再实现一下滑动右侧字母表,左侧城市列表切换的效果。

  1. <template>
  2. <ul class="list">
  3. <li
  4. class="item"
  5. v-for="item of letters"
  6. :key="item"
  7. :ref="item"
  8. @touchstart.prevent="handleTouchStart"
  9. @touchmove="handleTouchMove"
  10. @touchend="handleTouchEnd"
  11. @click="handleLetterClick"
  12. >
  13. {{item}}
  14. </li>
  15. </ul>
  16. </template>
  17. <script>
  18. export default {
  19. name: 'CityAlphabet',
  20. props: {
  21. cities: Object
  22. },
  23. computed: {
  24. letters () {
  25. const letters = []
  26. for (let i in this.cities) {
  27. letters.push(i)
  28. }
  29. return letters
  30. }
  31. },
  32. data () {
  33. return {
  34. touchStatus: false,
  35. startY: 0,
  36. timer: null
  37. }
  38. },
  39. updated () {
  40. this.startY = this.$refs['A'][0].offsetTop
  41. },
  42. methods: {
  43. handleLetterClick (e) {
  44. this.$emit('change', e.target.innerText)
  45. },
  46. handleTouchStart () {
  47. this.touchStatus = true
  48. },
  49. handleTouchMove (e) {
  50. if (this.touchStatus) {
  51. if (this.timer) {
  52. clearTimeout(this.timer)
  53. }
  54. this.timer = setTimeout(() => {
  55. const touchY = e.touches[0].clientY - 79
  56. const index = Math.floor((touchY - this.startY) / 20)
  57. if (index >= 0 && index < this.letters.length) {
  58. this.$emit('change', this.letters[index])
  59. }
  60. }, 16)
  61. }
  62. },
  63. handleTouchEnd () {
  64. this.touchStatus = false
  65. }
  66. }
  67. }
  68. </script>

3.2 search组件

功能:进入到城市选择页面的时候,当 focus 到搜索框,输入城市名或拼音能够把搜索的结果显示出来。

  1. <template>
  2. <div>
  3. <div class="search">
  4. <input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />
  5. </div>
  6. <div
  7. class="search-content"
  8. ref="search"
  9. v-show="keyword"
  10. >
  11. <ul>
  12. <li
  13. class="search-item border-bottom"
  14. v-for="item of list"
  15. :key="item.id"
  16. @click="handleCityClick(item.name)"
  17. >
  18. {{item.name}}
  19. </li>
  20. <li class="search-item border-bottom" v-show="hasNoData">
  21. 没有找到匹配数据
  22. </li>
  23. </ul>
  24. </div>
  25. </div>
  26. </template>
  27. <script>
  28. import Bscroll from 'better-scroll'
  29. import { mapMutations } from 'vuex'
  30. export default {
  31. name: 'CitySearch',
  32. props: {
  33. cities: Object
  34. },
  35. data () {
  36. return {
  37. keyword: '',
  38. list: [],
  39. timer: null
  40. }
  41. },
  42. computed: {
  43. hasNoData () {
  44. return !this.list.length
  45. }
  46. },
  47. watch: {
  48. keyword () {
  49. if (this.timer) {
  50. clearTimeout(this.timer)
  51. }
  52. if (!this.keyword) {
  53. this.list = []
  54. return
  55. }
  56. this.timer = setTimeout(() => {
  57. const result = []
  58. for (let i in this.cities) {
  59. this.cities[i].forEach((value) => {
  60. if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {
  61. result.push(value)
  62. }
  63. })
  64. }
  65. this.list = result
  66. }, 100)
  67. }
  68. },
  69. methods: {
  70. handleCityClick (city) {
  71. this.changeCity(city)
  72. this.$router.push('/')
  73. },
  74. ...mapMutations(['changeCity'])
  75. },
  76. mounted () {
  77. this.scroll = new Bscroll(this.$refs.search)
  78. }
  79. }
  80. </script>

【性能优化---防抖】

写一个侦听器 watch,在里边监听 keyword 的改变,考虑到性能优化,使用防抖的方式来实现,先在 data 中定义一个 timer 定时器,默认值为 null,然后在监听 keyword 的方法中,判断,当 timer 为 null 时,清除这个定时器。下面写这个定时器的方法,当延时 100ms 的时候,箭头函数会被执行。

3.3 递归组件

递归组件的意思就是在组件自身调用组件自身。

数据 detail.json

  1. {
  2. "ret": true,
  3. "data": {
  4. "sightName": "大连圣亚海洋世界(AAAA景区)",
  5. "bannerImg": "http://img1.qunarzz.com/sight/p0/201404/23/04b92c99462687fa1ba45c1b5ba4ad77.jpg_600x330_bf9c4904.jpg",
  6. "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"],
  7. "categoryList": [{
  8. "title": "成人票",
  9. "children": [{
  10. "title": "成人三馆联票",
  11. "children": [{
  12. "title": "成人三馆联票 - 某一连锁店销售"
  13. }]
  14. },{
  15. "title": "成人五馆联票"
  16. }]
  17. }, {
  18. "title": "学生票"
  19. }, {
  20. "title": "儿童票"
  21. }, {
  22. "title": "特惠票"
  23. }]
  24. }
  25. }

list.vue

  1. <template>
  2. <div>
  3. <div
  4. class="item"
  5. v-for="(item, index) of list"
  6. :key="index"
  7. >
  8. <div class="item-title border-bottom">
  9. <span class="item-title-icon"></span>
  10. {{item.title}}
  11. </div>
  12. <div v-if="item.children" class="item-chilren">
  13. <detail-list :list="item.children"></detail-list>
  14. </div>
  15. </div>
  16. </div>
  17. </template>
  18. <script>
  19. export default {
  20. name: 'DetailList',
  21. props: {
  22. list: Array
  23. }
  24. }
  25. </script>
  26. <style lang="stylus" scoped>
  27. .item-title-icon
  28. position: relative
  29. left: .06rem
  30. top: .06rem
  31. display: inline-block
  32. width: .36rem
  33. height: .36rem
  34. background: url(http://s.qunarzz.com/piao/image/touch/sight/detail.png) 0 -.45rem no-repeat
  35. margin-right: .1rem
  36. background-size: .4rem 3rem
  37. .item-title
  38. line-height: .8rem
  39. font-size: .32rem
  40. padding: 0 .2rem
  41. .item-chilren
  42. padding: 0 .2rem
  43. </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

  1. import Vue from "vue";
  2. import Vuex from "vuex";
  3. Vue.use(Vuex);
  4. export default new Vuex.Store({
  5. state: {
  6. city: localStorage.city || "北京"
  7. },
  8. mutations: {
  9. changeCity(state, city) {
  10. state.city = city;
  11. localStorage.city = city;
  12. }
  13. }
  14. })

这个时候打开页面,当用户选择一个城市,然后刷新页面,可以看到上次选择的城市还在。但是当使用 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

  1. import Vue from "vue";
  2. import Vuex from "vuex";
  3. Vue.use(Vuex);
  4. let defaultCity = "北京"
  5. try {
  6. if (localStorage.city) {
  7. defaultCity = localStorage.city;
  8. }
  9. } catch (e) { }
  10. export default new Vuex.Store({
  11. state: {
  12. city: defaultCity
  13. },
  14. mutations: {
  15. changeCity(state, city) {
  16. state.city = city;
  17. try {
  18. localStorage.city = city;
  19. } catch (e) { }
  20. }
  21. }
  22. })

现在我们看到 store/index.js 这个文件慢慢的变得复杂起来了,实际上,在真正的项目开发和之中,会做进一步的拆分,也就是把这个文件拆分为 State、Actions、Mutations,在 store 中创建一个文件叫 state.js(只存储公用数据),然后把设置默认数据的这块代码放进去,并通过 export 导出,内容就是在 index.js 中定义的 state 对象里的内容:

  1. let defaultCity = "北京"
  2. try {
  3. if (localStorage.city) {
  4. defaultCity = localStorage.city;
  5. }
  6. } catch (e) { }
  7. export default {
  8. city: defaultCity
  9. }

接下来,只需要在 index.js 中 import state 就可以了:

  1. import Vue from "vue";
  2. import Vuex from "vuex";
  3. import state from "./state";
  4. Vue.use(Vuex);
  5. export default new Vuex.Store({
  6. state: state,
  7. mutations: {
  8. changeCity(state, city) {
  9. state.city = city;
  10. try {
  11. localStorage.city = city;
  12. } catch (e) { }
  13. }
  14. }
  15. })

接着,在 store 目录下创建一个文件,叫做 mutations.js,然后把 index.js 中的 mutations 对象里的代码剪切进去:

  1. export default {
  2. changeCity(state, city) {
  3. state.city = city;
  4. try {
  5. localStorage.city = city;
  6. } catch (e) { }
  7. }
  8. }
  9. 最终 index.js 就变成了这样:
  10. import Vue from "vue";
  11. import Vuex from "vuex";
  12. import state from "./state";
  13. import mutations from "./mutations";
  14. Vue.use(Vuex);
  15. export default new Vuex.Store({
  16. state: state,
  17. mutations: mutations
  18. })

这样,我们就将 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",然后再加一个计算属性:

  1. computed:{
  2. ...mapState(['city'])
  3. }

获取到城市对应的内容,然后就可以在发 Ajax 的时候,把 city 放在请求的参数里面:

  1. axios.get("/api/index.json?city=" + this.city)
  2. .then(this.getHomeInfoSucc);

这个时候,我们打开页面,可以看到请求参数里已经携带了当前的城市:

但是,例如当你切换了城市“桂林”,回到首页,并没有重新发 Ajax 请求,虽然上面的城市变成了“桂林”,但是底下的内容还是“北京”的内容,我们希望底下的内容跟着变,该怎么做呢?

当我们在 App.vue 中用了 keep-alive 的时候,这块的内容已经被缓存起来了,他直接取得是缓存里的数据,那如何去改变缓存里的数据呢?当你使用 keep-alive 的时候,组件中会多出一个生命周期函数 activted

可以在 mounted 和 activated 两个生命周期函数下打印一些内容,到浏览器上看一下他俩的执行:

  1. mounted() {
  2. console.log("mounted");
  3. this.getHomeInfo();
  4. },
  5. activated(){
  6. console.log("activted");
  7. }

打开页面,可以看到,mounted 和 activated 都会执行,当切换了城市,再回到首页的时候,组件的 mounted 就不会执行了,就只有 activated 会被执行,那么我们借助 activated 这个生命周期函数就可以实现我们想要的功能了。

首先在页面被挂载的时候,也就是 mounted 中一定会去发一个 Ajax 请求,当页面重新被显示的时候,activated 一定会被重新的执行,那么我们就可以在页面每次重新显示的时候,可以判断当前页面上的城市和上次页面上显示的城市是否是相同的,如果不相同的,就再发一次 Ajax 请求。

先在 data 中设置一个数据 lastCity,默认值是空,接着当页面被挂载的时候,让它等于 this.city,对上一次的城市做一个保存:

  1. mounted() {
  2. this.lastCity = this.city
  3. this.getHomeInfo();
  4. }

当页面被重新激活的时候,我们在 activted 中这样写:

  1. activated() {
  2. if(this.lastCity != this.city){
  3. this.lastCity = this.city
  4. this.getHomeInfo();
  5. }
  6. }

如果上一次的城市 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

Vue.js开发去哪儿网WebApp的更多相关文章

  1. Vue.js随笔一(Webpack + Vue.js开发准备,含VNM、NPM、Node、Webpack等相关工具)

    想入门工具是必须的,这一章将向大家带来vue.js相关的程序安装步骤. ①首先你需要有一个NVM(一个非常好用的Node版本管理器): 1.NVM下载地址:https://github.com/cor ...

  2. windows下vue.js开发环境搭建教程

    这篇文章主要为大家详细介绍了windows下vue.js开发环境搭建教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 最近,vue.js越来越火.在这样的大浪潮下,我也开始进入vue的学习行列中 ...

  3. 用Vue.js开发微信小程序:开源框架mpvue解析

    前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小程序提供了代码复用的能力.如果想将 H5 项目改造为小程 ...

  4. 基于 Laravel、Vue.js开发的全新社交系统----ThinkSNS+

    什么是ThinkSNS+ ThinkSNS(简称TS)始于2008年,一款全平台综合性社交系统,为国内外大中小企业和创业者提供社会化软件研发及技术解决方案,目前最新版本为ThinkSNS+.新的产品名 ...

  5. electron-vue:Vue.js 开发 Electron 桌面应用

    相信很多同学都知道 Electron 可以帮助开发人员使用前端技术开发桌面客户端应用,今天介绍的 electron-vue 框架是一套基于 Vue.js 开发 Electron 桌面应用的脚手架,该项 ...

  6. Vue2.5 开发去哪儿网App

    Vue2.5开发去哪儿网App 技术栈和主要框架

  7. MPVUE - 使用vue.js开发微信小程序

    MPVUE - 使用vue.js开发微信小程序 什么是mpvue? mpvue 是美团点评前端团队开源的一款使用 Vue.js 开发微信小程序的前端框架.框架提供了完整的 Vue.js 开发体验,开发 ...

  8. Mac安装vue.js开发环境

    Mac安装vue.js开发环境 DannyHooDanny的专栏订阅 一.vue.js开发环境 二.初始化一个vue.js项目 三.vue.js项目打包部署 本来以为在Mac上搭建vue.js的环境挺 ...

  9. Vue.js 开发实践:实现精巧的无限加载与分页功能

    本篇文章是一篇Vue.js的教程,目标在于用一种常见的业务场景--分页/无限加载,帮助读者更好的理解Vue.js中的一些设计思想.与许多Todo List类的入门教程相比,更全面的展示使用Vue.js ...

随机推荐

  1. c#中convert.toInt32和int.parse()和强制类型转换的区别

    string a="123"; int i=(int)a; 这是会出现错误因为:强制类型转换只能转换值类型不能转换引用类型 string属于引用类型    强制类型转换时如果值类型 ...

  2. 使用vue实现复选框单选多选

    界面样式: <div class="right_con" v-if="isClickApply" style="border:none" ...

  3. 【C语言】利用二维数组输出成绩

    目的:用二维数组输出下面成绩 希望你可以成为第五名童鞋! 代码: #include<stdio.h> int main() { /* 创建一个带有 4行 5 列的数组 */ ][] = { ...

  4. 每天进步一点点------MicroBlaze

             有了前面两个实例的铺垫,下面这个工程就要带大家尝试搭建一个基于MicroBlaze的应用.特权同学也是第一次接插Xilinx的嵌入式开发平台,跑了一个流程下来,正如所料,和Alter ...

  5. qt5.9.0 msvc2015优雅的崩溃:dumpfile

    交给客户的软件奔溃了怎么办? 我们不能再客户电脑上安装vs,也不想傻傻的用log来猜测出错的地方. 利用Dbghelp可以解决这一问题. 首先是vs生成release版本的时候需要同时生成pdb文件, ...

  6. zookeeper linux分布式部署

    安装包下载地址:http://mirror.bit.edu.cn/apache/zookeeper,记住要下载那个bin的不要下tar.gz包不然即使你安装了也会报错误: 找不到或无法加载主类org. ...

  7. Spring 事务归纳

    Spring transaction 什么是事务 A用户向B用户转帐100,第一步要从A帐户扣出100,第二步要将B帐户加上100.其中无论是第一步失败,还是第二步失败.都应该将A.B帐户的余额保持和 ...

  8. opencv:图像边缘发现

    拉普拉斯算子 Mat dst; // ksize 3 ,必须是奇数,不同大小会有不同的效果 Laplacian(src, dst, -1, 3, 1.0, 0, BORDER_DEFAULT); im ...

  9. Swagger与OAuth 手动搭建WebApi 操作笔记

    1.创建一个空的Web应用程序 2.通过nuget 安装以下插件清单,有部分会在安装其他插件时候自动安装: 3.安装完Swagger 会生成一个目录App_Start,在这个目录中增加文件ApiCon ...

  10. 吴裕雄 python 机器学习——集成学习随机森林RandomForestClassifier分类模型

    import numpy as np import matplotlib.pyplot as plt from sklearn import datasets,ensemble from sklear ...