本文主要写了第一次使用
Vue Vue-router Vuex Vue-cli
开发一个仿去哪儿移动端网页的项目记录。
项目所用到的插件:
vue-awesome-swiper 图片轮转
better-scroll 页面滚动
fastClick 解决移动端点击延迟300毫秒问题
axios 基于promise的ajax
项目中所用的css预处理器
stylus CSS预处理器为CSS提供了更多的更加灵活的可编程性
安装 stylus
yarn add stylus --save
yarn add stylus-loader --save
项目中所使用的自适应移动端开发配置与布局工具
在vue-cli目录中的index.html首页中配置
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,er-scalable=no">
使网页能够适配屏幕大小以及放大缩小影响体验。
import 'styles/ reset.css ' /解决移动端不同样式问题/
import 'styles/ border.css '/解决移动端像素边框的问题/
解决不同机型的样式,像素边框
项目结构:
去哪儿首页知识点 :
HomeSwiper 组件
使用 vue-awesome-swiper 轮播插件 2.6.7 版本(新版本有些Bug,为了开发的稳定性选择用老版本)
npm install vue-awesome-swiper@2.6.7 --save
轮播图当中的CSS样式有需要关注的一个地方
该样式主要是防止网速过慢时有部分模块未加载而导致页面结构发生抖动的情况,
所以把
wrapper宽度100%,高度由宽度的27%自动撑开成一个独立的div,使之在加载时就替代着轮转图的位置
解决了抖动问题
.wrapper
{
overflow: hidden;
width:100%;height:0;
padding-bottom:27%;
}
或者写成
.wrapper
{
width:100%;height:27vw;
}
显示轮播下标点以及页面循环切换应配置swiperOption
类的pagination
与loop
属性
HomeIcons 组件
小技巧:利用padding-bottom
配置等距div
布局(需要记得height
配置为0)
制作iconList
的图标分页功能(重点)
使用vue
中的计算功能,根据图标数目自动更换页数以实现切换分页功能
computed: {
pages() {
const pages = [];
this.iconList.forEach((item, index) => {
const page = Math.floor(index / 8);
if (!pages[page]) {
pages[page] = [];
}
pages[page].push(item);
});
return pages;
}
}
};
小功能:创建一个mixins.styl
定义css
样式
能够在字符数超出边框长度时隐藏并添加省略号
ellipsis()
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
Recommend.vue以及
Weekends.vue组件中
css`样式配置思想值得学习
Ajax(Axios)获取首页数据
npm install axios --save
yarn add axios --save
使用方法:
methods: {
getHomeInfo(){
axios.get('/api/index.json') //接收到接口数据
.then(this.getHomeInfoSucc)
},
//回调函数
getHomeInfoSucc(res){
console.log(res)
res=res.data
if(res.ret && res.data){
this.city = res.data.city
this.swiperList = res.data.swiperList
this.swiperList = res.data.swiperList
this.iconList = res.data.iconList
this.recommendList = res.data.recommendList
this.weekendList = res.data.weekendList
}
}
}
传入虚拟json
数据到static
的mock
文件夹中,并设置代理路径
进入`config`中的`index.js`设置`api`所指定的代理路径
proxyTable: {
'/api':{
target:'http://localhost:8080',
pathRewrite:{
'^/api':'/static/mock'
}
}
}
这样做
webpack-dev-server
会将此配置自动替换
配置主文件夹中.gitignore
添加 staitc/mock
,防止被推送到仓库
城市选择页面
router-link
组件:
<router-link :to=""> //to 后面跟需要跳转的 path 。
使用路由跳转需要配置对应的vue-router
import Vue from 'vue'
import Router from 'vue-router'
import Homefrom '@/pages/home/Home'
import City from '@/pages/city/City'
Vue.use(Router)
export default newRouter
({
routes: [{
path: '/',
name: 'Home',
component: Home },
{
path: '/city',
name: 'City',
component: City
}]
})
由于引入了border.css
所以可以在元素classs
属性中添加border
所带的移动端像素边框
若想修改像素边框的元素
可以采用,例:
.border-topbottom
&:before
border-color:#ccc
&:after
border-color:#ccc
better-scroll插件的使用:
yarn add better-scroll --save //首先用yarn安装插件
然后在要使用的vue中script引入
import BScroll from "better-scroll";
mounted()
{
this.scroll = new BScroll(this.$refs.wrapper);
}
注意:使用时应该有个外层DOM
结构,可以在要采用滚动部分最外层div
处用ref
添加一个DOM
引用
<div class="list" ref="wrapper">
city-components(实现点击右侧字母表定位首字母城市)
这里需要采用兄弟联动,但不用bus方法(教程)
采用 Alphabet.vue(子组件) $emit传递给 City.vue(父组件) ,然后再通过 City.vue(父组件) 传递给 List.vue(子组件)的方式
Alphabet.vue:
<template>
<ulclass="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick" //绑定一个click
>{{item}}</li>
</ul>
</template>
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
}
}
City.vue:
<city-alphabet :cities="cities" @change="handleLetterClick"></city-alphabet>
//@change里的函数传入了e.target.innerText这个值
methods: {
handleLetterClick (letter) {
this.letter = letter
}
}
然后再创建一个data
用于存储传递的数据:
data () {
return {
cities: {},
hotCities: [],
letter: '' // Alphabet 通过 change 事件传递过来的数据
}
}
再将letter
的数据进行绑定,传给list.vue
<city-list :cities="cities" :hot="hotCities" :letter="letter"></city-list>
并在list.vue
中进行接收
props: {
hot: Array,
cities: Object,
letter: String // 拿到传过来的letter
}
利用监听器来读取数据是否发生变化
并使用Better-scroll
的方法
scrollToElement
来实现跳转
watch: {
letter () {
if (this.letter) {
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
}
}
在使用$refs
前应该先用ref
属性引用DOM
<div class="area" v-for="(item, key) of cities" :key="key" :ref="key">
<div class="title border-topbottom">{{key}}</div>
<div class="item-list">
<div class="item border-bottom" v-for="innerItem of item" :key="innerItem.id">{{innerItem.name}}</div>
</div>
</div>
alphabet 滑动跳转功能
实现的逻辑:
1.先获取A
字母距离顶部边框高度
2.进行右侧字母表滑动时,取当前位置距离顶部边框高度
3.通过computed
计算出,当前手指位置与A
字母高度的差值
4.最后再通过Math.floor
向下取整每个字母高度得出下标,取得当前字母,并触发change
事件实现跳转
<template>
<ul class="list">
<li class="item"
v-for="item of letters"
:key="item"
:ref="item"
@touchstart="handleTouchStart" //触摸开始
@touchmove="handleTouchMove" //开始滑动
@touchend="handleTouchEnd" //触摸结束
@click="handleLetterClick"
>
{{item}}
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
// 计算属性中定义 letters 是一个数组,从 cities 数据中遍历ABC等城市开头的数据
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
},
data () {
return {
touchStatus: false // 用来判断触摸行为
}
},
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove (e) { //e是事件对象
if (this.touchStatus) {
const startY = this.$refs['A'][0].offsetTop // A 距离头部header区域下沿的高度
const touchY = e.touches[0].clientY - 80 // 手指距离 header区域下沿的高度 80是header上沿到下沿的高度大小
const index = Math.floor((touchY - startY) / 20) // 当前移动的字母下标,得到index后可取出对应的字母
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index]) // 通过 $emit 发送事件给City.vue
}
}
},
handleTouchEnd () {
this.touchStatus = false
}
}
}
</script>
最后需要在handleTouchMove
里进行节流函数操作
浅析函数防抖与函数节流
在data中再创建一个timer:null
handleTouchMove (e) {
if (this.touchStatus) {
// 函数节流主要是降低操作频率,提高性能
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
const startY = this.startY
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}, 12)//每隔12毫秒再读取一次
}
}
search模糊查询功能
<template>
<div>
<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音"> //用v-model进行双向绑定
</div>
<div class="search-content">
<ul>
<li v-for="item of list">{{item.name}}</li>
</ul>
</div>
</div>
</template>
使用监听器监听keyword
的变化
<script>
export default {
name: 'CitySearch',
props: {
cities: Object
},
data () {
return {
keyword: '',
list: [],
timer: null
}
},
watch: {
keyword () {
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
const result = []
for (let i in this.cities) { //循环city对象
this.cities[i].forEach((value) => { //取出每个节点的城市的值
if (value.spell.indexOf(this.keyword) > -1 ||
value.name.indexOf(this.keyword) > -1) { /*当双向绑定的KeyWord值发生改变时,
将带有首字符的value值传入*/
result.push(value)
}
})
}
this.list = result //将带有数据的result数组存储到list中
}, 100)
}
}
}
</script>
最后实现一下input
无输入的情况下,
数据冗余bug
的解决方式:
if (!this.keyword) {
this.list = []
return
}
由于搜索层是覆盖在原本页面上的,
以及当模糊查询没找到相匹配的城市时没有提示,
影响体验,所以进行优化
使用v-show
组件:
<li class="search-item border-bottom" v-show="!list.length">没有找到匹配</li>
search-content
的显示隐藏
用v-show
判断当keyword
值为0则为false
,不显示搜索内容框
<div class="search-content" v-show="keyword">
<ul>
<li class="search-item border-bottom" v-for="item of list">{{item.name}}</li>
<li class="search-item border-bottom" v-show="!list.length">没有找到匹配</li>
</ul>
</div>
使用vuex
实现数据共享
用于实现点击城市更改index
首页中数据的更改
重点:下图需熟记
1.先对
vuex
进行安装与配置
yarn add vuex --save
2.在src
中创建一个store
文件夹
在store中创建index.js
用于放置vuex
的功能数据
3.进行vuex
操作
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
city: '珠海'
},
mutations: {
changeCity (state/*这个参数用于放置全局数据*/, city/*这是传入的值*/)
{
state.city = city
}
}
})
在main.js
中引入store
,使得store
可以全局使用
import store from './store' //引入 store
new Vue({
el: '#app',
router,
store, //传递 store 实例到根组件
components: { App },
template: '<App/>'
})
在想使用的组件中定义一个@click
点击事件
并传入相应的值
如 :
@click="handleCityClick(item.name)" //item.name为传入值
然后根据vuex
操作图进行操作
methods: {
handleCityClick (city) {
this.$store.commit('changeCity', city)
this.$router.push('/') //这里是编程式函数跳转到首页
}
}
注意:这样做还不够,因为点击跳转时并未保存到内置存储当中,刷新页面时又会恢复到初始状态
使用内置存储localStorage:
export default new Vuex.Store({
state: {
city: localStorage.city || '珠海'
},
mutations: {
changeCity (state, city) {
state.city = city
localStorage.city = city
}
}
})
再次注意:建议try catch
异常处理优化一下,因为可能某些情况下用户禁用了localStorage
的功能
关于vuex
中组件的使用(详细请看官方文档)
///这里引入vuex的一个组件
用于映射store中state的数据
<script>
import { mapState } from "vuex";
export default {
name: "HomeHeader",
computed:{
...mapState(['city']) //此处采用扩展运算符
}
};
</script>
keep-alive组件对于网页性能的优化
未进行优化的网页在每次切换页面时,
ajax
都会重新请求数据影响到网页的性能
这个时候就应该使用keep-alive了
他会将访问过的页面储存到内存中以便重新访问时加载
大大加强了性能,减少了数据上的冗余
App.vue:
<template>
<div id="app">
<keep-alive> //只需要包裹一层keep-alive
<router-view/>
</keep-alive>
</div>
</template>
包裹了keep-alive
的标签,
将得到keep-alive
特有的生命周期钩子
Home.vue:
<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) {
console.log(res);
res = res.data;
if (res.ret && res.data) {
this.swiperList = res.data.swiperList;
this.swiperList = res.data.swiperList;
this.iconList = res.data.iconList;
this.recommendList = res.data.recommendList;
this.weekendList = res.data.weekendList;
}
}
},
mounted() {
this.lastCity = this.city
this.getHomeInfo();
},
activated(){ //此处使用生命周期钩子函数在发现不同城市时自行修改页面显示信息
//activated会在跳转页面时进行判断
if (this.lastCity !== this.city) {
this.lastCity = this.city
this.getHomeInfo()
}
}
};
</script>
补充:在 keep-alive
添加一个exclude="Detail"
属性,
能将Detail排除于keep-alive
的功能之外
详情页面
关于tag
属性route-link
补充
<router-link
tag="li" //这个标签相当于router-link成为了一个有路由功能的li标签
v-for="item of list"
:key="item.id"
class="item border-bottom"
:to="'/detail/' + item.id"
>
实现图片下方黑色渐变效果
background-image中linear-gradient的运用
.banner-info {
background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8))
}
全局画廊组件(Gallary.vue
)
配置滑动插件
<swiper :options="swiperOption">
<!-- slides -->
<swiper-slide v-for="(item, index) of imgs" :key="index">
<img class="gallary-img" :src="item">
</swiper-slide>
<!-- Optional controls -->
<div class="swiper-pagination" slot="pagination"></div> //图片页下标
</swiper>
对图片页下标样式的修改
具体参照SwiperAPI文档
<script>
export default {
name: "CommonGallary",
props: {
imgs: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
swiperOption: {
pagination: ".swiper-pagination",
paginationType: "fraction",
observeParents: true, ////swiper 插件监听到自身或父级元素DOM变化时,自动自我刷新。解决 swiper 刷新宽度计算 bug 的问题
observer: true,
loop:true
}
};
},
methods: {
handleGallaryClick() {
this.$emit("close");
}
}
};
</script>
解决分页不显示Bug
.container >>> .swiper-container {
overflow: inherit; //轮播插件自带overflow:hidden,需要通过>>>向外修改
}
实现header渐隐渐显的效果
<div>
<router-link to="/" tag="div" class="header-abs" v-show="showAbs">
<div class="iconfont header-abs-back"></div>
</router-link> //返回首页
<div class="header-fixed" v-show="!showAbs" :style="opacityStyle">
<router-link to="/">//下拉显示header头部
<div class="iconfont header-fixed-back"></div>
</router-link>景点详情
</div>
</div>
<script>
export default {
name: "DetailHeader",
data() {
return {
showAbs: true, //用于判断头部的显示
opacityStyle: {
opacity: 0
}
};
},
methods: {
handleScroll() {
const top = document.documentElement.scrollTop; 拿到滚动头部开始到滑动的距离
if (top > 60) {
let opacity = top / 140; //这里实现opacity值的动态变化
opacity = opacity > 1 ? 1 : opacity; //三元判断若opacity>1了,则值不再变化(1则为完全显示)
this.opacityStyle = { opacity };
this.showAbs = false;
} else {
this.showAbs = true;
}
console.log(document.documentElement.scrollTop);
}
},
activated() {//跳转页面到页面关闭期间时则运行
window.addEventListener("scroll", this.handleScroll); //添加滚动监听事件scroll
}
};
</script>
为避免影响其他组件进行全局解绑(修复windows
全局组件绑定监听器而导致切换页面还会继续加载的bug)
deactivated() {//关闭此页面时执行
window.removeEventListener("scroll", this.handleScroll);
}
用递归实现详情页
每次在组件中配置name
属性的重要原因,
以便于在组件自身调用自身的递归方式的时候调用。
效果:
实现:
<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-children">//在 div 标签中做一个 v-if 判断,如果存在 item.children,则为true
<detail-list :list="item.children"></detail-list>// 把item.children作为list传给自身进行递归
</div>
</div>
</div>
关于Detail中ajax
读取不同页面数据
路由器router
中index.js
的配置与其他页面有所不同
需要在配置跳转path
属性中,
加上:id
用于axios
提取不同页面数据
同步加载写法(增加一个异步加载知识点)
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', //此处多加了:id
name: 'Detail',
component: Detail
}
],
scrollBehavior (to, from, savePosition) {
return { x: 0, y: 0 }
}
})
异步加载写法
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: ()=>{@/pages/home/Home}
}, {
path: '/city',
name: 'City',
component: ()=>{@/pages/city/City}
}, {
path: '/detail/:id', //此处多加了:id
name: 'Detail',
component: ()=>{@/pages/detail/Detail}
}
]
关于同步异步介绍
通过$route.params.id
取得点击页面时的id
methods: {
getDetailInfo() {
axios.get(`/api/detail.json?id=${this.$route.params.id}`)//取得不同Id到后台请求不同的页面数据
.then(res => {
res = res.data;
if (res.ret && res.data) {
const data = res.data;
console.log(data);
this.sightName = data.sightName;
this.bannerImg = data.bannerImg;
this.gallaryImgs = data.gallaryImgs;
this.list = data.categoryList;
}
});
}
},
mounted() {
this.getDetailInfo();
}
};
通过id
在获取新页面后,
载入页面会停留在原来的下拉角度
所以应在router
中index.js
添加
scrollBehavior (to, from, savePosition) {
return { x: 0, y: 0 }
}
gallary的动画效果
<template>
<transition>
<slot></slot> //slot这个插槽是用于中间插入gallary组件
</transition>
</template>
<script>
export default {
name: "FadeAnimation"
};
</script>
<style lang="stylus" scoped>
.v-enter, .v-leave-to {
opacity: 0;
}
.v-enter-active, .v-leave-active {
transition: opacity 0.5s;
}
</style>
然后在Banner.vue
中引入,
这时slot
插槽属性就相当于common-gallary
<fade-animation>
<common-gallary
:imgs="bannerImgs"
v-show="showGallary"
@close="handleGallaryClick"
></common-gallary>
</fade-animation>
进行项目的上线
当项目完成之后,准备上线,必须先运行
npm run build
完成打包之后在进入dist
目录
把里面的文件放到后端文件夹中,项目完成上线