travel项目入门vuejs总结

一、安装项目环境

1 windows用户建议下载LTS中64位msi版本node.js,输入node -v,npm -v来验证是否安装成功;
2 本地安装git;
3 在git bash中执行ssh-keygen -t rsa -C "xxx@xxx.com"生成公钥,然后将公钥复制到码云的SSH公钥中;
4 全局安装vue-cli,即输入npm install --global vue-cli;
5 创建一个基于webpack模板的新项目,即输入vue init webpack xxx,可以选择在任意位置来初始化一个vue项目,然后把项目中的文件全部拷到git仓库下;也可以直接在git仓库的当前目录下进行初始化,然后选择合并,这时候如果git仓库中有和vue项目中相同名字的文件则会会替换;
6 使用npm run start或npm run dev(二者是完全等价的,因为都是另外一个指令的别名)启动采用vue-cli建立的基本webpack的vue.js项目;

二、代码规范

(1)在pages下建立home、city和detail文件夹,将各个页面进行拆分;如下:

  其中,文件夹都使用小写来进行代码规范。同时,在home文件夹下再建立一个components文件夹和Home.vue,其中Home.vue是父组件,而components下的组件是子组件,父组件通过使用子组件来进行页面展示(city、detail页面也是进行相应的划分)。这样做的目的是让复杂的页面拆分为一部分一部分的内容,这就是组件化的思想。如下:

  其中,单文件采用首字母大写,同时,参考项目中Home.vue、Detail.vue、City.vue的写法来进行代码规范。单文件中的组件名字首字母也是大写,父组件一般使用与单位件相同的名字,而子组件一般使用加上父组件名字的名字;如下:

            

  同时,在父组件中使用子组件时采用<xxx-xxx>,都为小写的方式,且用-分隔开,如下:

三、git常用命定及操作介绍

1 git clone xxx,将远程仓库克隆到本地;
2 git status,查看仓库中文件的变化情况;
3 git add .|git commit -m 'xxx',通过这两个操作会提交到本地仓库,当然也可以使用git add xxx,往暂存区加入一个文件;
4 如何远程有本地名字一样的分支,使用git push即可推到远程分支上;若无本地名字一样的分支,则使用git push -u origin xxx;
5 若远程通过软件建立了新分支,而本地没远程分支对应的分支,则使用git pull将远程分支拉下来;
6 git checkout xxx,切换分支,同时vscode中也会切换为对应分支上的代码,可以在编辑器左下方看到当前对应的分支;
7 git merge xxx,在当前分支的基础上合并其它分支,当然,既可以合并本地分支也可以合并远程分支;
8 也可以使用vscode中自带的git管理工具,点击左侧中间的图标,会显示更改的文件,点击更改文件上的加号(相当于执行了git add .),然后在消息框中输入提交的描述信息,点击上方的打钩即可完成提交操作(相当于执行了git commit -m 'xxx'),最后在...中点击提交已暂存文件即可提交到对应的远程库(但远程库若无对应的本地分支我不知道会发生什么);
9 感觉还是在git bash中进行代码提交等操作更为舒爽,同时编译器可能会变,但git bash不变,因此算是一个一劳永逸的办法;

四、项目目录介绍

其中:

  • package.json:里面有很多以来包,放的都是依赖;
  • package-lock.json:是package.json的一个锁文件,它可以帮助我们去确定安装的第三方包的一些具体版本,保持传递编程自统一;
  • index.html:是一个首页的模板文件;
  • .gitignore:当我们使用git时,我们希望把我们的代码上传到线上,但有些特殊的文件我们并不希望上传,就可以把这个特殊的文件配置在.gitignore,例如我不希望把static/mock目录下的json数据上传到本地仓库,方然也不会传到线上仓库。如:

  • .eslintrc.js:配置了一些代码的规范,写代码必须按照这些规范才不会报错误的提示;
  • .eslintignore:这个文件说明里面的文件不会受到eslint的检测,写的不标准也不会被检测到;
  • .editorconfig:配置了编辑器的一些语法,比如说indent_size=2的意思就是tab键是2个空格;
  • .babelrc:我们写代码是单页面组件的写法,需要通过babel这个语法解析器做一些语法上的转换,最终能够转化为被浏览器能够编译执行的代码;
  • static:该目录下放置一些静态资源,例如一些图片、模拟的json等;
  • node_modules:放置的这个项目依赖的第三方包,这个内容不用去管,不用管到底依赖了哪些包,只要知道是依赖包就行;
  • src:放置的是项目的源代码
  1.   main.js是项目的入口文件;
  2.   App.vue是项目最原始的根组件;
  3.   router/index.js放置的该项目所有的路由;
  4.   pages中存放的项目用的一些组件;
  5.   assets放置的是项目用到的一些图片资源、样式等;
  • config:存放的是项目的配置文件
  1.   index.js存放的基础配置信息;
  2.   dev.env.js放置的是开发环境的一些配置信息;
  3.   prod.env.js放置的是线上环境的一些配置信息;
  • build:放置的是项目的一些webpack打包的内容,是vue-cli自动帮我们配置好的集合;

五、一些解决方案

(1)由于不同手机浏览器上默认的样式是不统一的,因此需要引入一个reset.css文件把不同手机的初始化样式做一个统一,项目中在main.css中进行引入,如下:

import '@/assets/styles/reset.css'

(2)由于移动端有一像素边框的问题,因此在main.js中需要引入border.css文件,如下:

import '@/assets/styles/border.css'

  在首页的recommend部分中需要使用到1像素边框解决方案,使用方式如下:

 1 <ul>
 2       <li
 3         class= "item border-bottom"
 4         v-for= "item of recommendList"
 5         :key= "item.id"
 6       >
 7         <img class= "item-img" :src= "item.imgUrl" />
 8         <div class= "item-info">
 9           <p class= "item-title">{{item.title}}</p>
10         </div>
11       </li>
12  </ul>

  只需要在class中引入border-bottom即可。

(3)由于移动端存在300ms点击延迟的问题,引入fastclick库可以解决问题,在main.js中引入该库,同时把它绑定到document.body上,如下:

1 import fastClick from 'fastclick'
2 fastClick.attach(document.body)

(4)为了更加方便编写CSS,本项目中使用了stylus预处理器,为CSS提供更加灵活的可编程性。首先在git bash中全局安装stylus,然后在git bash中安装stylus-loader,最后单文件组件中就是使用它;如下:

1 npm install stylus --save
2 npm install stylus-loader --save
3 
4 <style lang= "stylus" scoped>
5 </style>

(5)在chrome中进行拓展vue-devtools可以更加方便的进行vue代码的调试,由于电脑不能FQ,故自己制作插件,教程保存在chrome收藏夹中,同时百度网盘中存储了制作好的压缩文件,使用时解压缩就能使用。

(6)有时在切换页面时,打开的新页面不是显示在最开始部分,而是受前面页面的影响,会拖动到下方的显示,解决方案是在/router/index.js中只增加拖动行为选项,即:

 1 export default new Router({
 2   routes: [
 3     {
 4       path: '/',
 5       name: 'Home',
 6       component: Home
 7     }
 8   ],
 9   scrollBehavior (to, from, savedPosition) {
10     return { x: 0, y: 0 }
11   }
12 })

意思是每次做路由切换时,都让显示的页面X轴、Y轴都初始位置都为0;

六、轮播插件vue-awesome-swiper的使用

  1.首先由于最新版本存在一点问题,我们安装2.6.7版本,在git bash中输入:

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

  2.在main.js中引入这个vue-awesome-swiper,然后还需要引入这个插件的CSS代码,最后需要使用Vue.use()来使用这个插件。即:

1 import VueAwesomeSwiper from 'vue-awesome-swiper' /* 先引入vue-awesome-swiper--1 */
2 import 'swiper/dist/css/swiper.css' /* 然后引入vue-awesome-swiper的样式--2 */
3 Vue.use(VueAwesomeSwiper) /* 最后通过vue.use()来使用vue-awesome-swiper这个插件--3 */

  3.在github上搜索vue-awesome-swiper单页面组件的使用方法,将里面的代码复制到所需要使用轮播的组件中,由于项目用不到ref和@someSwiperEvent,故先进行删除,同时,computed和mouted也暂时用不上,也进行删除,即:

 1 <template>
 2   <swiper :options="swiperOption" ref="mySwiper" @someSwiperEvent="callback">
 3     
 4     <swiper-slide>I'm Slide 1</swiper-slide>
 5     <swiper-slide>I'm Slide 2</swiper-slide>
 6     
 7     <div class="swiper-pagination"  slot="pagination"></div>
 8     <div class="swiper-button-prev" slot="button-prev"></div>
 9     <div class="swiper-button-next" slot="button-next"></div>
10     <div class="swiper-scrollbar"   slot="scrollbar"></div>
11   </swiper>
12 </template>
13 
14 <script>
15   export default {
16     name: 'carrousel',
17     data() {
18       return {
19         swiperOption: {
20           // some swiper options/callbacks
21           // 所有的参数同 swiper 官方 api 参数
22           // ...
23         }
24       }
25     },
26     computed: {
27       swiper() {
28         return this.$refs.mySwiper.swiper
29       }
30     },
31     mounted() {
32       // current swiper instance
33       // 然后你就可以使用当前上下文内的swiper对象去做你想做的事了
34       console.log('this is current swiper instance object', this.swiper)
35       this.swiper.slideTo(3, 1000, false)
36     }
37   }
38 </script>

4.为了在轮播图上加上点,需要在swiperOption中传入1个配置项pagination;同时,为了能够循环播放,再传入一个配置项loop,即:

swiperOption: {
    pagination: '.swiper-pagination',
    loop: true

    paginationType: 'fraction' //会显示当前页/总页数
    observeParents: true,
    observer: true
}

  第5,6,7行是后面中公用轮播插件的设置项,跟这里无关;paginationType设置为fraction是为了显示当前页/总页数,observeParents和observer都设置为true是指swiper插件只要监听到我这个元素或父级元素发生了DOM结构的变化,会自动的自我刷新一次,通过这次自我刷新就能解决swiper宽度计算的问题。

5.项目中轮播图的代码如下所示,以后可以参考这里做就好;

 1 <template>
 2   <div class="wrapper">
 3     <swiper :options="swiperOption"> //3.用swiper做最后的包裹
 4       <swiper-slide v-for= "item of swiperList" :key= "item.id"> //2.借助swiper-slide且通过v-for将图片循环出来
 5         <img class= "swiper-img" :src= "item.imgUrl" /> //1.先显示一张图
 6       </swiper-slide>
 7       <div class="swiper-pagination"  slot="pagination"></div>
 8     </swiper>
 9   </div>
10 </template>
11 
12 <script>
13 export default {
14   name: 'HomeSwiper',
15   data () {
16     return {
17       swiperOption: {
18         pagination: '.swiper-pagination',
19         loop: true
20       },
21       swiperList: [{
22         id: '0001',
23         imgUrl: 'http://img1.qunarzz.com/piao/fusion/1806/d8/5db3f3c1e777cf02.jpg_750x200_54ea5bde.jpg'
24       }, {
25         id: '0002',
26         imgUrl: 'http://img1.qunarzz.com/piao/fusion/1805/e8/14b75b1c81fbe702.jpg_750x200_e6d4f1f1.jpg'
27       }]
28     }
29   }
30 }
31 </script>
32 
33 <style lang="stylus" scoped>
34   .wrapper >>> .swiper-pagination-bullet-active //由于原来轮播图下方只能显示蓝色的点,而控制相应颜色的css类swiper-pagination-bullet-active是外部样式(我乱说的),因此不能直接进行修改,故需要对此样式进行穿透
35     background: #fff
36   .wrapper
37     overflow: hidden
38     width: 100%
39     height: 0
40     padding-bottom: 31.25%
41     background: #eee
42     .swiper-img
43       width: 100%
44 </style>

七、iconfont的使用入门

(1)进入iconfont的官网 -> 点击图标库 -> 官网图标库 -> 大麦网图标库 -> 选择返回和搜索箭头的图标,加入到购物车车 -> 搜索箭头,选择1个箭头加入到购物车中 -> 在购物车列表中,添加至项目 -> 下载至本地 -> 解压缩 -> 将iconfont.eot、iconfont.svg、iconfont.ttf、iconfont.woff放置在iconfont文件夹中(iconfont文件夹是styles目录下新建的一个文件夹);

(2)再把iconfont.css文件放置在styles目录下;

(3)在iconfont.css文件中的@font-face中将src: url部分的内容进行更改,同时也改一下上述iconfont文件夹中其它3个文件的路径;

src: url('./iconfont.eot?t=1529571826105') //未改前
src: url('./iconfont/iconfont.eot?t=1529571826105') //改动后

(4)在main.js中引入iconfont,如下:

1 import './asserts/styles/iconfont.css'

(5)然后进入iconfont的官网,将各个图标的代码进行拷贝,注意得加上样式,最后就可以使用了,如下:

1 <span class="iconfont">&#xe624;</span>
(6)若iconfont项目中重新加入了图标,则需要在travel项目中的iconfont中的4个字体文件进行替换(重新下载最新的项目文件中的字体文件),同时,由于字体文件的路径没有变化,故src部分内容不用变,但data中的数据内容需要从新下载文件中的iconfont.css中拷贝过来,最后就可以重新正常使用了;

八、代码优化

(1)由于项目中#00bcd4这个颜色会多次使用,故可以在styles文件夹中建立一个varibles.styl文件,在里面定义变量bgColor,即:

1 $bgColor= #00bcd4

  同时,在需要该颜色值的单文件中的<style>中引用这个变量,即:

1 @import '~styles/varibles.styl'

  然后就可以在单文件中替换使用该变量了,即:

color: $bgColor

(2)由于/assets/styles这个目录经常使用,这时在build目录下的webpack.base.cof.js文件中的resolve部分,可以看到

1 alias: {
2    'vue$': 'vue/dist/vue.esm.js',
3    '@': resolve('src'),
4    'styles': resolve('src/assets/styles'),
5    'common': resolve('src/common')
6 }

  其中,@代表src目录。同理,我们增加styles选项,代表的是src/assets/styles目录。注意:更改完必须重启服务器才能生效。

(3)由于overflow: hidden、white-space: nowrap、text-overflow: ellipsis这3个css参数在其它地方也经常被使用,因此可以在styles里面创建1个mixins.styl文件,在文件里面创建1个ellipsis方法,即:

1 ellipsis()
2   overflow: hidden
3   white-space: nowrap
4   text-overflow: ellipsis

  然后在需要一起使用这三个参数的的单文件组件中的<style>部分中先引用后使用,即:

1 <style lang="stylus" scoped>
2   @import '~styles/mixins.styl'
3     .icon-desc
4       ellipsis()
5 </style>

  使用这个方法可以实现如果有很多文字的话,后方会出现...,也就是用于解决过长文字的显示问题,可以提高用户体验度。效果如下:

(4)在十五中城市列表右侧字母拖动带动相应显示中,touchmove事件的处理函数handleTouchMove的性能是比较低的,如下所示;首先,由于字母A的offsetTop是固定的,而我们每次调用这个方法都会去运算一次,所以在data里定义1个变量startY,初始值为0,然后再写一个生命周期钩子函数updated,当页面的数据被重新更新时,同时,页面也完成了自己的渲染后,updated这个钩子函数就会被执行。

1 handleTouchMove (e) {
2     const startY = this.$refs['A'][0].offsetTop  //字母A离顶部的距离
3     const touchY = e.touches[0].clientY - 79   //手指离顶部的距离
4 
5     const index = Math.floor((touchY - this.startY) / 20)  //字母的下标
6      if (index >= 0 && index < this.letters.length) {
7           this.$emit('change', this.letters[index]) //触发一个change事件,并携带字母下标的参数,让父组件去进行监听
8       }
9 }
1 updated () {
2     this.startY = this.$refs['A'][0].offsetTop
3 }

然后使第二个优化,做一个函数节流,由于当鼠标在字母表上来回移动的时候,移动的频率是非常高的,因此,可以通过节流来限制一下handleTouchMove函数执行的频率,先通过在data中定义一个timer,初始值为null,即:

 1 handleTouchMove (e) {
 2     if (this.touchStatus) {
 3         if (this.timer) {
 4             clearTimeout(this.timer)
 5         }
 6         this.timer = setTimeout(() => {
 7         const touchY = e.touches[0].clientY - 79
 8         const index = Math.floor((touchY - this.startY) / 20)
 9         if (index >= 0 && index < this.letters.length) {
10             this.$emit('change', this.letters[index])
11         }
12     }, 16) //延迟16ms执行
13     }
14 }            

九、项目页面设计

(1)home目录下的header部分,分别设置为header-left、header-input和header-right三个部分,然后进行相应的CSS样式的编写。

(2)建立1个common目录,在里面用于存放公用的组件;在该目录下创建1个文件夹叫gallary,在gallary下面再创建1个文件叫Gallary.vue和1个文件夹components,之所以这样写是因为如果Gallary.vue很大时,可以将Gallary.vue拆分成很多的小组件,这样有利于代码的维护和扩展;

十、使用轮播插件进行图标区域布局

(1)先定义个大的div,然后将8个小的div包裹其中,若超过8个,则可以通过计算属性,计算出每个图标应该属于哪1页,然后进行相应的显示;同时,也使用vue-awesome-swiper插件进行轮播,由于不需要图片下方的点以及循环播放,故<swiper>中不用传递参数;代码如下:

 1 <template>
 2   <div class="icons">
 3     <swiper> //3
 4       <swiper-slide v-for="(page, index) of pages" :key="index"> //2
 5         <div
 6           class="icon"
 7           v-for="item of page"
 8           :key="item.id"
 9         >
10           <div class='icon-img'> //1
11             <img class='icon-img-content' :src='item.imgUrl' />
12           </div>
13           <p class="icon-desc">{{item.desc}}</p>
14         </div>
15       </swiper-slide>
16     </swiper>
17   </div>
18 </template>
19 
20 <script>
21 export default {
22   name: 'HomeIcons',
23   data () {
24     return {
25       iconList: [{
26         id: '0001',
27         imgUrl: 'http://img1.qunarzz.com/piao/fusion/1611/54/ace00878a52d9702.png',
28         desc: '景点门票'
29       }, {
30         id: '0002',
31         imgUrl: 'http://img1.qunarzz.com/piao/fusion/1711/df/86cbcfc533330d02.png',
32         desc: '滑雪季'
33       }]
34     }
35   },
36   computed: {
37     pages () {
38       const pages = []
39       this.iconList.forEach((item, index) => {
40         const page = Math.floor(index / 8)
41         if (!pages[page]) {
42           pages[page] = []
43         }
44         pages[page].push(item)
45       })
46       return pages
47     }
48   }
49 }
50 </script>
51 
52 <style lang="stylus" scoped>
53     //省略CSS样式部分
54 </style>

 十一、ajax获取数据入门

(1)安装axios,如下:

npm install axios --save

(2)由于有很多子组件,每个组件都有自己的数据,若每个子组件都发一次ajax请求,则主页也会有对应的一次ajax请求,这样网站的性能是比较低的。合理的做法是在父组件中发送一个ajax请求,然后将获得的请求数据传递给每个子组件。同时,通过生命周期钩子函数mounted()【页面渲染完成会调用此函数】中进行ajax数据获取,也要在父组件中引入axios,具体如下:

 1 <template>
 2   <div>
 3     //7,将获取的数据传递给子组件
 4     <home-header :city = "city"></home-header>
 5     <home-swiper :swiperList = "swiperList"></home-swiper>
 6     <home-icons :iconList = "iconList"></home-icons>
 7     <home-recommend :recommendList = "recommendList"></home-recommend>
 8     <home-weekend :weekendList = "weekendList"></home-weekend>
 9   </div>
10 </template>
11 
12 <script>
13 import HomeHeader from './components/Header'
14 import HomeSwiper from './components/Swiper'
15 import HomeIcons from './components/Icons'
16 import HomeRecommend from './components/Recommend'
17 import HomeWeekend from './components/Weekend'
18 import axios from 'axios' //1,引用axios
19 export default {
20   name: 'Home', /* 组件的名字开头大写 */
21   components: {
22     HomeHeader,
23     HomeSwiper,
24     HomeIcons,
25     HomeRecommend,
26     HomeWeekend
27   },
28   data () {  //2,定义好接收数据
29     return {
30       city: '',
31       swiperList: [],
32       iconList: [],
33       recommendList: [],
34       weekendList: []
35     }
36   },
37   methods: {
38     getHomeInfo () {  //4,发送ajax请求
39       axios.get('/api/index.json')
40         .then(this.getHomeInfoSucc)  //5,成功返回则调用getHomeInfoSucc函数
41     },
42     getHomeInfoSucc (res) { /*,6,获取到的json数据会存储在res中,更具体说是存在res.data中 */
43       res = res.data
44       if (res.ret && res.data) { /* 当res.ret为true且res.data这个对象不为空时 */
45         const data = res.data /* 这样处理更为简便,否则就要写成res.data.id等,这样就可以省略为data.id */
46         this.city = data.city
47         this.swiperList = data.swiperList
48         this.iconList = data.iconList
49         this.recommendList = data.recommendList
50         this.weekendList = data.weekendList
51       }
52     }
53   },
54   mounted () { /* 3,页面加载完成后会调用mounted()函数,发送ajax请求 */
55     this.getHomeInfo()
56   }
57 }
58 </script>
59 
60 <style lang= "stylus" scoped>
61 
62 </style>

  以header子组件进行接收并使用父组件传递来的数据为例,如下:

 1 <template>
 2   <div class="header">
 3     <div class="header-left">
 4       <div class="iconfont back-icon">&#xe624;</div> <!--iconfont不是自定义样式,只要用了iconfont就要加上它-->
 5     </div>
 6       <div class="header-right">
 7         {{this.city}} //2,使用接收的数据
 8         <span class="iconfont arrow-icon">&#xe64a;</span>
 9       </div>
10   </div>
11 </template>
12 
13 <script>
14 export default {
15   name: 'HomeHeader',
16   props: {
17     city: String //1,限制接收数据的类型
18   }
19 }
20 </script>

(3)由于在整个项目中,只有static目录下的内容能够被外部访问到,故将模拟的json数据放到static/mock中,但此时axios.get('/api/index.json')中的就得改成'static/mock/index.json',由于上线之前改动代码是有风险的,不建议这样操作,因此,可以在config/index.js文件中的proxyTable中增加一个配置选项,以后不用的话直接删除配置项就可以了,如下:

1 proxyTable: {
2    '/api': {
3     target: 'http://localhost:8080',
4     pathRewrite: { //增加配置项
5       '^/api': '/static/mock'
6     }
7   }
8 }

  这样就能将地址转发到static/mock下【注意,需要重启服务器才能生效】。

十二、路由配置入门

(1)假设需要在/city下显示City.vue组件,则首先应该在src/router/index.js中引入需要显示的组件,即:

import City from '@/pages/city/City'

(2)然后做相应的路由配置,即:

1 export default new Router({
2   routes: [
3     {
4       path: '/city',  //路径
5       name: 'City',
6       component: City  //路径对应的组件
7     }
8   ]
9 })

(3)我们希望在首页header中点击城市或者向下的箭头能够跳转到/city路径下,继而显示组件City中的内容,即:

1 <router-link to = '/city'> //在城市名字或者向下箭头外包裹router-link标签就行
2     <div class="header-right">
3         {{this.city}}
4         <span class="iconfont arrow-icon">&#xe64a;</span>
5     </div>
6 </router-link>

  同理,在城市列表页中的header中点击返回箭头可以回到首页,如下:

1 <router-link to = '/'> //在返回箭头外包裹router-link标签就行
2 <div class="iconfont header-back">&#xe624;</div> <!--iconfont不是自定义样式,只要用了iconfont就要加上它--> 

3 </router-link>

十三、使用better-scroll使页面能够拖动

(1)better-scroll是iscroll的一个封装,使用起来比iscroll更加友好,可以在github上进行搜索,要使用这个这个包,首先要进行安装,如下:

npm install better-scroll --save

(2)然后在需要使用的页面中引入这个包,同时,在生命周期钩子函数中创建1个scroll实例对象属性,最后通过ref获取到相应的DOM,即:

 1 <!--本项目中better-scroll与github上有些不一样,注意区分-->
 2 <template>
 3   <div class="list" ref="wapper"> <!--3,需要滚动的地方加一个ref属性,并且指向正确,后面就能正常滚动-->
 4     <div>
 5       <div class="area">
 6         <div class="title border-topbottom">当前城市</div>
 7         <div class="button-list">
 8           <div class="button-wrapper">
 9             <div class="button">北京</div>
10           </div>
11         </div>
12   </div>
13 </template>
14 
15 <script>
16 import Bscroll from 'better-scroll' /* 1,引入better-scroll */
17 export default {
18   name: 'CityList',
19   mounted () { /* 2,在DOM加载完毕后,调用mounted()函数,创建一个scroll实例对象属性 */
20     this.scroll = new Bscroll(this.$refs.wapper)
21   }
22 }
23 </script>

十四、实现点击城市列表右侧的字母就能够转到相应的城市项中(兄弟组件间传值

(1)首先,在子组件Alphabet.vue中的<li>标签中绑定一个click事件,对应的函数为handleLitterClick,函数定义如下:

1 handleLitterClick (e) {
2    this.$emit('change', e.target.innerText) //e.target.innerText为点击的字母
3 }

(2)在父组件City.vue中去监听change事件,由于子组件Alphabet向父组件传递了一个参数e.target.innerText,通过handleLetterClick函数将此参数接收,即在父组件data中定义1个变量letter进行接收,然后将子组件传递过来的参数赋值给父组件即:

 1 <city-alphabet :cities = "cities" @change = "handleLetterChange"></city-alphabet> //1,监听到change则调用handleLetterChange函数
 2 
 3   data () { //2,定义letter变量
 4     return {
 5       letter: ''
 6     }
 7   }
 8 
 9 handleLetterChange (letter) { //3,将子组件传递过来的参数赋值给父组件的letter变量
10    this.letter = letter
11 }

(3)然后父组件将接收到的letter变量传递给子组件list.vue,然后子组件list.vue通过prop属性进行接收。

1 <city-list :letter = "letter"></city-list> //1,传递给子组件
2 
3 <script>
4 export default {
5   name: 'CityList',
6   props: { //2,子组件进行接收
7     letter: String
8   }
9 </script>

(4)最后借助侦听器watch属性监听letter的变化,即:

1 watch: {
2   letter () {
3     if (this.letter) {
4       const element = this.$refs[this.letter][0] //1,字母的第一个城市
5       this.scroll.scrollToElement(element) //2,进行跳转
6     }
7   }
8 }

(5)当然也可以采用bus的方式进行传值,但因为我们这里是兄弟组件,且传值非常方便,故不建议使用bus方式。

十五、城市列表右侧字母拖动带动相应显示

(1)给字母<li>标签绑定touchstart、touchmove、touchend三个事件,相应的处理函数为handleTouchStart、handleTouchMove和handleTouchEnd,同时在data中定义一个标志位touchStatus,初始值为flase,当触发touchstart事件时将标志位touchStatus设置为true,此时touchmove事件的处理函数handleTouchMove才会正常调用,而当触发touchend事件时,将标志位touchStatus设置为false,handleTouchMove也不能正常被调用。

(2)首先,我们需要知道手机滑动时,手机在手机屏幕上的位置(按在哪个字母上);其次,先计算出字母A距离顶部的距离;再次,再计算出手指距离屏幕顶端的距离;最后,二者作差除以每个字母的高度即可得到当前是第几个字母;即:

1 handleTouchMove (e) {
2     const startY = this.$refs['A'][0].offsetTop  //字母A离顶部的距离
3     const touchY = e.touches[0].clientY - 79   //手指离顶部的距离
4 
5     const index = Math.floor((touchY - this.startY) / 20)  //字母的下标
6      if (index >= 0 && index < this.letters.length) {
7           this.$emit('change', this.letters[index]) //触发一个change事件,并携带字母下标的参数,让父组件去进行监听
8       }
9 }

(3)取出对应字母,然后出发一个change事件,并携带参数为当前滑动的字母,然后父组件进行监听,并根据传递来的字母转发给list子组件,然后进行相应的显示(如十四)。

(4)由于上述是根据下标来获取所对应的字母,故需要建立一个letters数组来存储所有城市首字母的列表,因此,可以通过计算属性来得到letters数组,即:

1 computed: {
2   letters () { /* 把26个字母通过计算属性的方式存储起来 */
3     const letters = []
4     for (let i in this.cities) {
5       letters.push(i)
6     }
7     return letters
8 }

 十六、通过拼音或名字搜索能把相应的结果显示出来

(1)首先在data里定义一个变量keyword,让input框与keyword进行双向绑定;

<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />

(2)在父组件City.vue中,传递citys给子组件,子组件通过props属性对citys进行接收;

<city-search :cities = "cities"></city-search>
props: {
  cities: Object
}

(3)在data中定义1个数组list,存储根据拼音或者汉字搜索出的城市名。同时,增加侦听器属性,监听keyword的改变;且做1个节流函数,以适当减少侦听的频率;

 1 watch: {
 2   keyword () {
 3     if (this.timer) {
 4       clearTimeout(this.timer)
 5     }
 6     if (!this.keyword) { /* 若输入框中无内容,则清空list,适用于刷新的情况 */
 7       this.list = []
 8       return
 9     }
10     this.timer = setTimeout(() => {
11       const result = []
12       for (let i in this.cities) {
13         this.cities[i].forEach((value) => {
14           if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {
15             result.push(value)
16           }
17         })
18       }
19       this.list = result
20     }, 100) //延迟100ms
21   }
22 }

(4)若搜索匹配的结构特别多,但此时无法通过滚动进行查看,这是可以通过better-scroll来实现;首先是引用better-scroll,其次,是创建生命周期钩子函数mounted,在其中创建1个scroll实例对象属性,最后还需要在需要滚动的区域建立ref属性;

import Bscroll from 'better-scroll'
mounted () {
  this.scroll = new Bscroll(this.$refs.search)
}

(5)由于需要在没有keyword时,将整个页面隐藏,而有keyword则进行显示,因此,需要给内容加一个v-show指令;当没有匹配结果时,可以在下方显示没有找到匹配数据,但如果有匹配结果时,不会显示没有找到匹配数据,因此,下方的<li>标签中应加入v-show = "!this.list.length",但应该尽量把逻辑放到<script>中去处理,模板中尽量保持简洁的语法,所以讲!this.list.length用计算属性hasNoData进行替换;

 1 <div
 2   class="search-content"
 3   ref="search"
 4   v-show="keyword"
 5 >
 6   <ul>
 7     <li
 8       class="search-item border-bottom"
 9       v-for="item of list"
10       :key="item.id"
11     >
12       {{item.name}}
13     </li>
14     <li class="search-item border-bottom" v-show="hasNoData">
15       没有找到匹配数据
16     </li>
17   </ul>
18 </div>
1 computed: {
2   hasNoData () {
3     return !this.list.length
4   }
5 }

 十七、使用vuex实现数据共享

(1)当两个父组件City.vue与Home.vue之间进行数据传递时,这时候不能再通过父组件进行数据的转发了,当然也可以通过bus总线的方法,但使用bus还是比较麻烦,vue官方给我们推荐了一个数据框架,即在vue的大型项目开发中,vue只能承担视图层的内容,而涉及到大量数据间传递的时候,往往需要1个数据框架进行同步,在vue中,这个框架就是vuex;

(2)vuex指的是整个项目虚线部分的内容,虚线部分可以理解为1个仓库,由State、Mutations和Actions三部分组成。公共数据都存放在State中,若组件想使用公共数据,则直接调用State即可;而若想改变公共数据的值,则必须走一个流程,即若有1个异步操作或者比较复杂的同步操作,则需要把操作放到Actions中,然后再去调用Mutations,Mutations中存放的是一个一个同步的对state的修改。

  注意,组件调用Actions的时候是通过Dispatch方法来操作Actions,组件调用Mutations或Actions时是通过Commit方法来操作Mutations;【当操作是同步操作且不复杂的时候,可以直接绕开Actions,直接操作Mutations,也就是组件直接调用Commit方法

(3)输入npm install vuex --save安装vuex,然后通过import vuex from 'vuex'来使用vuex;

(4)由于在main.js中直接引入vuex不太好,故在src目录下创建1个store的文件夹,然后在store中新建了1个文件index.js,在该文件中引入vue和vuex,同时,由于vuex是1个插件,通过vue.use()来使用这个插件;即:

 1 import Vue from 'vue'
 2 import Vuex from 'vuex'
 3 import state from './state'
 4 import mutations from './mutations'
 5 import actions from './actions'
 6 
 7 Vue.use(Vuex)
 8 
 9 export default new Vuex.Store({ /* 创建一个仓库 */
10   state,
11   actions,
12   mutations
13 })
当index.js变得复杂起来的时候,我们需要对该文件进行拆分,在/store下创建states.js、actions.js和mutations.js文件,然后在index.js中引入即可;拆分之后,代码的可维护性会大大提高;
 1 //state.js文件
 2 let defaultCity = '南京'
 3 try {
 4   if (localStorage.city) {
 5     defaultCity = localStorage.city
 6   }
 7 } catch (e) {
 8 }
 9 
10 export default {
11   city: defaultCity
12 }
当不使用localStorage时,我们刷新页面时,城市又回到了南京,而不会保持为上一次设置的城市,HTML5中为我们提供了1个新的api,叫localStorage,它能帮我实现类似cookie的功能,做到本地存储;本来只要在该
文件中增加localStorage.city=city,同时,city = localStorage.city || '南京',state的默认值有限从localStorage.city取,若取不到则为‘南京’,由于某些浏览器用户关闭了本地存储的功能或者使用隐
身模式,故使用localStorage是会抛出异常,建议在localStorge外层加1个保护。
1 //action.js文件
2 export default {
3   changeCity (ctx, city) {
4     ctx.commit('changeCity', city)
5   }
6 }
1 //mutation.js文件
2 export default {
3   changeCity (state, city) {
4     state.city = city
5     try {
6       localStorage.city = city /* localStorage要放到这个位置 */
7     } catch (e) {}
8   }
9 }
使用到localStorage的地方就加一个保护,理由同上;

(5)然后在main.js中引入store,同时在Vue实例中注册store,即:

import store from './store'
1 new Vue({
2   el: '#app', /* 这个时候挂载点好像作用不大了,因为已经显示在App这个组件中 */
3   router,
4   store,
5   components: { App },
6   template: '<App/>' /* 就是将App这个组件渲染出来 */
7 })

(7)以前首页city变量是由外部传入的,现在去掉,现在city是存储在前端的,不需要后端ajax告诉我,city是公共数据;同时,由于main.js中的根实例中创建了store,故每个组件都能使用store,故首页中的Header中的city可以替换成this.$store.state.city;当然由于这样写很长,vuex给我提供了比较高级的api,在使用vuex的页面中的<script>中增加如下:

import { mapState } from 'vuex'

  然后在增加一个计算属性如下:

1 computed: {
2   ...mapState({
3     currentCity: 'city' /* 使city这个公共数据映射到currentCity这个计算属性中 */
4   })
5 }

  然后this.$store.state.city就可以直接写成this.currentCity;

(8)我们希望点击热门城市的时候,公用数据也跟着变化,在List.vue中的点击事件所对应的函数handleCityClick(city)中增加命令如下:

this.$store.dispatch('changeCity',city)

  由于这样书写很长,按照上面类似的方法,我们先引入mapActions,即:

import { mapActions } from 'vuex'

  然后在methods中增加:

...mapActions(['changeCity']) //将this.changeCity(city)映射为this.$store.changeCity('city')

  故this.$store.dispatch('changeCity',city)可以替换为this.changeCity(city);然后在/store/actions.js中增加:

export default {
  changeCity (ctx, city) { //ctx参数可以帮我们拿到commit这个方法
    ctx.commit('changeCity', city)
  }
}

  然后在/store/mutations.js中增加:

1 export default {
2   changeCity (state, city) {
3     state.city = city
4     try {
5       localStorage.city = city /* localStorage要放到这个位置 */
6     } catch (e) {}
7   }
8 }

  这样就完成了公用数据的修改。

最后补充,在vue中,可以采用<router-link>来做页面跳转,同时,也可以采用router.push的方式实现页面的跳转,具体如下:

this.$router.push('/')

  vuex还有Getter和Module核心概念,Getter相当于vue中的计算属性,是根据公共数据属性来形成一个新属性;也可以通过映射的方式使用Getter,即:

import { mapGetter } from 'vuex'

然后在计算属性中增加:

compuetd: {
    ...mapGetters(['doubleCity'])
}

  而Module则是对各个组件所用的资源进行各自管理,让各个模块都有自己是state、actions和mutations,这样使用module可以使我们的代码具有更好的维护性,由于本项目公用数据较少,故没有必要用module进行拆分,拆分了反而让我们的代码看起来别扭。

十八、使用keep-alive优化网络性能

(1)选择Network和其中的XHR,可以看出,当每次回到首页和进行城市列表页时都会相应的向后台发送city.json和index.json等ajax请求,每次路由重新切换到首页这个组件的时候,这个组件都会被重新渲染,它的mounted生命周期钩子函数就会被执行,也就会发送ajax请求。

(2)在App.vue中的<router-view />的外层包裹1个<keep-alive>标签,这是vue知道的1个标签,即:

1 <keep-alive>
2   <router-view />
3 </keep-alive>

  意思是路由的内容加载过1次后就把路由中的内容放到内存中去,下一次再进这个路由的时候,不需要重新渲染这个组件,也就不会调用mounted生命周期钩子函数,这样就不会发送ajax请求了。

(3)但改变城市时,也不会去发送ajax请求,故逻辑存在问题。首先,在Home.vue中引入mapstate,即:

import { mapState } from 'vuex'

  然后在计算属性中增加:

1 computed: {
2 ...mapState({
3     city: 'city'
4   })
5 }

  最后在发ajax请求的时,把city数据放到请求的参数里,即:

1 getHomeInfo () {
2    axios.get('/api/index.json?city=' + this.city)
3       .then(this.getHomeInfoSucc)
4 }

(4)当使用缓存时,会多出1个生命周期钩子函数activated(),当刚进入页面时,mounted()和activated()都会被执行,而切换城市之后只有activated()会执行,当页面重新被显示时,会调用activated()函数,所以可以判断当前城市和上一次城市是否相同,若不相同,则再发一次ajax请求;即:

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

(5)在data里定义lastCity变量 ,当页面被挂被挂载时,在mounted()中增加this.lastCity = this.city,可以避免刚进入页面时2个函数相继调用发送2次ajax请求的请求。

十九、动态路由
(1)在Home.vue中的中的Recommend.vue,我们希望点击热销推荐中的内容会转到相应的页面中,一种方案是给<li>标签外包裹一层<router-link to= '/detail'>标签,但由于<router-link>标签本质上是1个<a>标签,因此样式会改变;另一种方案是将<li>标签改成<router-link>,同时增加tag="li"属性,这时不会渲染成<a>标签,而是渲染成<li>标签;

1 <router-link
2    tag= "li"
3    class= "item border-bottom"
4    v-for= "item of recommendList"
5    :key= "item.id"
6    :to = "'/detail/' + item.id"
7 >

(2)我们希望点击不同的推荐景点时,获得不同的返回页面,这时需要讲<router-link>中的to做1个动态的绑定,即

1 <router-link
2    tag= "li"
3    class= "item border-bottom"
4    v-for= "item of recommendList"
5    :key= "item.id"
6    :to = "'/detail/' + item.id"
7 >

  这就实现了1个参数的传递。

(3)在/router/index.js路由配置中,增加:

1 {
2     path: '/detail/:id',
3     name: 'Detail',
4     component: Detail
5 }

  在vue中,通过detail后加1个:id的形式,就写了1个动态路由,意思说前面的路径必须是/detail/,而后面可以带1个参数,参数放到id变量里;【注:项目中只说了如何传递,但实际上都是用Detail来显示,不知道不同的id对应的页面如何进行处理

二十、实现header渐隐渐现的效果

(1)我们希望刚开始进入的时候在header区域不显示景点详情,而往下滚动到60px的时候在header最上方中间位置显示出景点详情,而且在60px到140px之间有1个渐隐渐现的效果,超过140px就完全显示。因此,需要在data里定义1个变量showAbs,当返回箭头显示的时候景点详情不显示,而到了60px景点详情显示,返回箭头不显示,以控制返回箭头和景点详情的显示与否。即:

 1 <router-link
 2     tag="div"
 3     to="/"
 4     class="header-abs"
 5     v-show="showAbs">
 6        <div class="iconfont header-abs-back">&#xe624;</div>
 7 </router-link>
 8 <div
 9     class="header-fixed"
10     v-show="!showAbs"
11     :style="opacityStyle" //动态样式
12 >
13    <router-link to="/">
14       <div class="iconfont header-fixed-back">&#xe624;</div>
15    </router-link>
16       景点详情
17 </div>

(2)因为要有1个渐隐渐现的效果,因此可以给包裹景点详情的标签绑定1个动态样式,在data中定义opacityStyle,即:

1 opacityStyle: {
2     opacity: 0
3 }

(3)我们需要对windows scroll时间进行监听,因为使用了keep-alive,因此页面被展示的时候activated这个钩子函数就会被执行,故activated()如下所示:

1 activated () {
2    window.addEventListener('scroll', this.handleScroll)
3 }

(4)然后继续对handleScroll的编写,即在methods中完成该方法的定义,即:

 1 methods: {
 2   handleScroll () {
 3     const top = document.documentElement.scrollTop
 4     if (top > 60) {
 5       let opacity = top / 140
 6       opacity = opacity > 1 ? 1 : opacity
 7       this.opacityStyle = { opacity }
 8       this.showAbs = false
 9     } else {
10       this.showAbs = true
11     }
12   }
13 }

(5)由于在activated中对windows做了1个事件的绑定,由于windows是1个全局对象,windows绑定的事件不仅对这个组件有效果,而且对其它组件也会产生影响;同时,还存在deactivated()生命周期钩子函数,当页面即将改变或替换成新的页面时,该函数会被执行,通过该函数实现全局事件的解绑;即:

1 deactivated () {
2     window.removeEventListener('scroll', this.handleScroll)
3 }

 二十一、递归组件入门

(1)首先,在Detail.vue中给List.vue中传递list数据,如下:

<detail-list :list="list"></detail-list>
 1 list: [
 2   {
 3     title: '成人票',
 4     children: [
 5     {  title: '成人三馆联票',
 6         children: [
 7         {
 8             title: '成人三馆联票-某一连锁店销售'
 9         }]
10     }, 
11     {
12       title: '成人五馆联票'
13     }
14   ]
15   }, 
16   {
17     title: '学生票'
18   }, 
19   {
20     title: '儿童票'
21   }, 
22   {
23     title: '特惠票'
24   }
25 ]

(2)在List.vue组件中进行接收,然后对这个list进行循环;

(3)这时候就可以巧妙的使用递归组件了,递归组件就是组件调用自身的行为,以前我们都会给组件定义1个名字,定义这个名字很大程度上是为了使用递归组件,代码如下:

 1 <div
 2       class="item"
 3       v-for="(item, index) of list"
 4       :key="index"
 5 >
 6   <div class="item-title border-bottom">
 7      <span class="item-title-icon"></span>
 8      {{item.title}}
 9   </div>
10   <div v-if="item.children" class="item-chilren">
11      <detail-list :list="item.children"></detail-list> //通过组件名字调用自身,每次都要传值给list,然后组件自身也会进行接收
12   </div>
13 </div>

 二十二、使用ajax获取动态数据

(1)当访问id为003这个景点的时候,我们需要获取003这个景点对应的数据,而访问001景点时,同样获取001对应的景点的数据;所以每一次请求,希望把id这个参数带给后端,这个id是动态路由的1个参数。

(2)在/router/index.js中定义了这个动态路由,会把id存在这个变量里,即:

{
   path: '/detail/:id',
   name: 'Detail',
   component: Detail
}

(3)然后在Detail.vue中发送ajax请求,即:

axios.get('/api/detail.json?id='+this.$route.params.id)

由于采用这种办法拼接后面的参数是比较麻烦的,因此可以换1种写法,二者是完全等效的;即:

1 axios.get('/api/detail.json', {
2     params: {
3       id: this.$route.params.id
4     }
5 }

(4)由于使用了<keep-alive>,mounted只会执行1次,因此替换好了之后,当访问002的数据时,并不会重新获取数据;一种方案是使用activated生命周期钩子函数;另一种方案是<keep-alive>中增加exclude="Detail"选项,Detail是组件的名字,这样Detail就不会被缓存了,那么每一次进入页面,mounted生命周期函数就会被执行,也就会重新发送ajax请求。

二十三、组件中定义名字的作用

(1)递归组件中的使用;

(2)取消缓存时的作用;

(3)在vue Devtool调试时可以看到组件的名字,方便调试;

 二十四、在项目中加入基础动画

(1)点击图片进入图片轮播时,需要增加1个渐隐渐现的效果;

(2)在common目录下再创建1个文件夹fade,里面再创建1个文件Fade.vue;

对于单个组件的动画,写1个<transition>标签,在里面有1个<slot>,slot是外部组件插入进来的1个插槽;transition如果想让它有动画效果,vue内部会自动在一些时间节点上向这些标签插入一些class名字,借助这些class,我们可以实现动画,比较经典的如下:

1 <template>
2   <transition>
3     <slot></slot>
4   </transition>
5 </template>
1 <style lang="stylus" scoped>
2   .v-enter, .v-leave-to
3     opacity: 0
4   .v-enter-active, .v-leave-active
5     transition: opacity .5s
6 </style>

(3)这样动画组件就写完了,然后就是如何使用;在Banner.vue中引用和注册刚刚实现的组件,用<fade-animation>对<common-gallary>作1个包裹,这里<common-gallary>就会作1个插槽的形式插入FadeAnimation这个组件里面,<slot>就代表<common-gallary>,当<common-gallary>进行隐藏或展示的时候,就会有1个渐隐渐现的效果;

posted @ 2018-06-21 11:26  追风筝的蜗牛  阅读(423)  评论(0)    收藏  举报