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:放置的是项目的源代码
- main.js是项目的入口文件;
- App.vue是项目最原始的根组件;
- router/index.js放置的该项目所有的路由;
- pages中存放的项目用的一些组件;
- assets放置的是项目用到的一些图片资源、样式等;
- config:存放的是项目的配置文件
- index.js存放的基础配置信息;
- dev.env.js放置的是开发环境的一些配置信息;
- 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"></span>
八、代码优化
(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"></div> <!--iconfont不是自定义样式,只要用了iconfont就要加上它--> 5 </div> 6 <div class="header-right"> 7 {{this.city}} //2,使用接收的数据 8 <span class="iconfont arrow-icon"></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"></span> 5 </div> 6 </router-link>
同理,在城市列表页中的header中点击返回箭头可以回到首页,如下:
1 <router-link to = '/'> //在返回箭头外包裹router-link标签就行
2 <div class="iconfont header-back"></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"></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"></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个渐隐渐现的效果;


浙公网安备 33010602011771号