本文主要寫了第一次使用
Vue Vue-router Vuex Vue-cli
開發(fā)一個仿去哪兒移動端網頁的項目記錄。
項目所用到的插件:
vue-awesome-swiper 圖片輪轉
better-scroll 頁面滾動
fastClick 解決移動端點擊延遲300毫秒問題
axios 基于promise的ajax
項目中所用的css預處理器
stylus CSS預處理器為CSS提供了更多的更加靈活的可編程性
安裝 stylus
yarn add stylus --save
yarn add stylus-loader --save
項目中所使用的自適應移動端開發(fā)配置與布局工具
在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,為了開發(fā)的穩(wěn)定性選擇用老版本)
npm install vue-awesome-swiper@2.6.7 --save
輪播圖當中的CSS樣式有需要關注的一個地方
該樣式主要是防止網速過慢時有部分模塊未加載而導致頁面結構發(fā)生抖動的情況才顿,
所以把
wrapper寬度100%,高度由寬度的27%自動撐開成一個獨立的div尤蒿,使之在加載時就替代著輪轉圖的位置
解決了抖動問題
.wrapper
{
overflow: hidden;
width:100%;height:0;
padding-bottom:27%;
}
或者寫成
.wrapper
{
width:100%;height:27vw;
}
顯示輪播下標點以及頁面循環(huán)切換應配置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;
}
}
};
小功能:創(chuàng)建一個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
}
}
然后再創(chuàng)建一個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
}
利用監(jiān)聽器來讀取數據是否發(fā)生變化
并使用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
向下取整每個字母高度得出下標囱皿,取得當前字母勇婴,并觸發(fā)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區(qū)域下沿的高度
const touchY = e.touches[0].clientY - 80 // 手指距離 header區(qū)域下沿的高度 80是header上沿到下沿的高度大小
const index = Math.floor((touchY - startY) / 20) // 當前移動的字母下標嘱腥,得到index后可取出對應的字母
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index]) // 通過 $emit 發(fā)送事件給City.vue
}
}
},
handleTouchEnd () {
this.touchStatus = false
}
}
}
</script>
最后需要在handleTouchMove
里進行節(jié)流函數操作
淺析函數防抖與函數節(jié)流
在data中再創(chuàng)建一個timer:null
handleTouchMove (e) {
if (this.touchStatus) {
// 函數節(jié)流主要是降低操作頻率耕渴,提高性能
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>
使用監(jiān)聽器監(jiān)聽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) { //循環(huán)city對象
this.cities[i].forEach((value) => { //取出每個節(jié)點的城市的值
if (value.spell.indexOf(this.keyword) > -1 ||
value.name.indexOf(this.keyword) > -1) { /*當雙向綁定的KeyWord值發(fā)生改變時,
將帶有首字符的value值傳入*/
result.push(value)
}
})
}
this.list = result //將帶有數據的result數組存儲到list中
}, 100)
}
}
}
</script>
最后實現一下input
無輸入的情況下齿兔,
數據冗余bug
的解決方式:
if (!this.keyword) {
this.list = []
return
}
由于搜索層是覆蓋在原本頁面上的橱脸,
以及當模糊查詢沒找到相匹配的城市時沒有提示窄做,
影響體驗,所以進行優(yōu)化
使用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
中創(chuàng)建一個store
文件夾
在store中創(chuàng)建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('/') //這里是編程式函數跳轉到首頁
}
}
注意:這樣做還不夠组砚,因為點擊跳轉時并未保存到內置存儲當中吻商,刷新頁面時又會恢復到初始狀態(tài)
使用內置存儲localStorage:
export default new Vuex.Store({
state: {
city: localStorage.city || '珠海'
},
mutations: {
changeCity (state, city) {
state.city = city
localStorage.city = city
}
}
})
再次注意:建議try catch
異常處理優(yōu)化一下,因為可能某些情況下用戶禁用了localStorage
的功能
關于vuex
中組件的使用(詳細請看官方文檔)
///這里引入vuex的一個組件
用于映射store中state的數據
<script>
import { mapState } from "vuex";
export default {
name: "HomeHeader",
computed:{
...mapState(['city']) //此處采用擴展運算符
}
};
</script>
keep-alive組件對于網頁性能的優(yōu)化
未進行優(yōu)化的網頁在每次切換頁面時糟红,
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(){ //此處使用生命周期鉤子函數在發(fā)現不同城市時自行修改頁面顯示信息
//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中l(wèi)inear-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 插件監(jiān)聽到自身或父級元素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值的動態(tài)變化
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); //添加滾動監(jiān)聽事件scroll
}
};
</script>
為避免影響其他組件進行全局解綁(修復windows
全局組件綁定監(jiān)聽器而導致切換頁面還會繼續(xù)加載的bug)
deactivated() {//關閉此頁面時執(zhí)行
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
目錄
把里面的文件放到后端文件夾中,項目完成上線