[TOC]
## **移動端適配**
相對于PC端來說,移動端設備分辨率百花齊放,千奇百怪,對于每一個開發者來說,移動端適配是我們進行移動端開發第一個需要面對的問題。
在移動端我們經常可以在head標簽中看到這段代碼:
~~~
<meta?name='viewport'?content='width=device-width,initial-scale=1,user-scale=no'?/>
~~~
**通過meta標簽對viewport的設置,定義了頁面的縮放比例;要了解這些參數的意義,我們需要先知道幾個視口寬度的意義。**
* layoutviewport布局寬度,就是網頁的寬度
* visualviewport可是寬度,就是瀏覽器窗口的寬度,這個值決定了我們手機一屏能看到的內容;visualviewport和layoutviewport的
* 大小關系,決定了是否會出現滾動條,當visualviewport更大或者剛好等于layoutviewport時是不會出現滾動條的。
* idealviewport為瀏覽器定義的可完美適配移動端的viewport,固定不變,可以認為是設備視口寬度device-width。
**meta的設置其實就是對layoutviewport和visualviewport進行設置。**
* width=device-width表示頁面寬度layoutviewport與設備視口寬度idealviewport一致
* initial-scale=1表示頁面寬度和網頁寬度與設備視口寬度的初始縮放比例,visualviewport由這個比例決定,但是對于layoutviewport來說,它同時受到兩個屬性的影響,然后取其中較大的那個值。
user-scale=no禁止縮放
所以現在我們知道,這段在移動端常見的代碼的意思,即將visualviewport和layoutviewport設置為idealviewport的值;這樣我們在移動端就不會出現滾動條,網頁內容可以比較好的展示出來,在這個前提下我們再考慮頁面的適配問題。
UI出圖的時候一般是有一個固定的寬度的,而我們實際的移動端設備的寬度卻都不太一樣,但是如果頁面元素的縮放比例和頁面寬度的縮放比例一致,在不同尺寸的設備下我們網頁的效果也將會是一致的。
## **使用相對單位**
**rem**
rem 是相對于根元素 html 的 font-size 來做計算。通常在頁面初始化時加載時通過對document.documentElement.style.fontSize 設置來實現。一般我們將根元素html的font-size設置為寬度的1/10,不同設備的寬度不同,但是同樣數值的rem比例與設備的寬度比例是一致的。
~~~
document.documentElement.style.fontSize?=?document.documentElement.clientWidth?/?10?+?'px';
~~~
在實際項目中我們無須在開發中自己進行轉換,可以使用pxtorem在輸出的時候將px轉換為rem。
## **視口單位**
將視口寬度window.innerWidth和視口高度window.innerHeight(即layoutviewport)等分為 100 份。
vw : 1vw 為視口寬度的 1% vh : 1vh 為視口高度的 1% vmin : vw 和 vh 中的較小值 vmax : 選取 vw 和 vh 中的較大值
和rem相比較,視口單位不需要使用js對根元素進行設置,兼容性稍差,但是大部分設備都已經支持了,同樣的無須再開發時進行單位換算,直接使用相關的插件postcss-px-to-viewport在輸出的時候進行轉換。
## **修改viewport**
之前我們提到了layoutviewport布局寬度實際上不是一個固定值,而是通過meta設置屬性,通過idealviewport計算出來的值,我們可以通過控制meta的屬性來將layoutviewport固定為某一個值。一般設計圖的寬度為750px,現在我們的目標就是將layoutviewport設置為750px;layoutviewport受到兩個屬性的影響,width屬性我們之間設置為750,initial-scale縮放比例應該為idealviewport的寬度/750;當我們未改變meta標簽屬性的時候,layoutviewport的值其實就是idealviewport的值,所以可以通過document.body.clientWidth或者window.innerWidth來獲取。
~~~
;(function?()?{
????const?width?=?document.body.clientWidth?||?window.innerWidth
????const?scale?=?width?/?750
????const?content?=?'width=750,?
initial-scale='?+?scale?+?',
minimum-scale='?+?scale?+?',
maximum-scale='?+?scale?+?',
viewport-fit=cover'
????document.querySelector('meta[name="viewport"]').content?=?content
})()
~~~
設置完成之后,layoutviewport在不同的設備中會始終保持為750px,我們開發時可以直接使用設計稿尺寸。
## **布局樣式**
布局的方式可以是各種各樣的,但是出于兼容性的考慮,有些樣式我們最好避免使用,難以解決的問題,那就不去面對。
## **需要謹慎對待的fixed**
position:fixed在日常的頁面布局中非常常用,在許多布局中起到了關鍵的作用。它的作用是:position:fixed的元素將相對于屏幕視口(viewport)的位置來指定其位置。并且元素的位置在屏幕滾動時不會改變。但是,在許多特定的場合,position:fixed的表現與我們想象的大相徑庭。
1. iOS彈出鍵盤;軟鍵盤喚起后,頁面的 fixed元素將失效(iOS認為用戶更希望的是元素隨著滾動而移動,也就是變成了 absolute定位),既然變成了absolute,所以當頁面超過一屏且滾動時,失效的 fixed 元素就會跟隨滾動了。
2. 當元素祖先的 transform 屬性非 none時,定位容器由視口改為該祖先。說的簡單點,就是position:fixed的元素會相對于最近的并且應用了transform的祖先元素定位,而不是窗口。導致這個現象的原因是使用了transform的元素將創建一個新的堆疊上下文。堆疊上下文(Stacking Context):堆疊上下文是 HTML 元素的三維概念,這些 HTML 元素在一條假想的相對于面向(電腦屏幕的)視窗或者網頁的用戶的z 軸上延伸,HTML元素依據其自身屬性按照優先級順序占用層疊上下文的空間。順序如下圖所示,總之堆疊上下文會對定位關系產生影響。想要進一步可以查看不受控制的position:fixed。
鍵盤彈出與使用transform屬性的情況在移動端是很常見的,所以需要謹慎使用position:fixed。
## **推薦使用flex**
flex,即彈性布局,移動端兼容性較好,能夠滿足大部分布局需求。現在我們使用flex來實現h5中常見的頂部標題欄+中部滾動內容+底部導航欄的布局
## **頁面跳轉**
**轉場動畫**
在vue中我們通過vue-router來管理路由,每個路由跳轉類似與在不同的頁面之間進行切換,從用戶友好的角度來說,每次切換頁面的時候最好添加一個轉場效果。如果轉場動畫不區分路由是打開新頁面、還是返回之前頁面我們只需要在外使用添加一個動畫效果即可;但是一般打開和返回是應用不同的動畫效果的,所以我們需要在切換路由的時候區分路由是前進還是后退。為了區分路由的動作,我們在路由文件中設置meta為數字,meta表示其路由的深度,然后監聽$route,根據to、from meta值的大小設置不同的跳轉動畫。如果應用到多種跳轉動畫,可以根據詳情,具體情況具體應用。
~~~
<template>?
<transition?:name="transitionName">
<router-view></router-view>
</transition>
</template>
<script>
export?default?{
name:?'app',
??data?()?{
????return?{
??????transitionName:?'fade'
????}
??},
??watch:?{
????'$route'?(to,?from)?{
??????let?toDepth?=?to.meta
??????let?fromDepth?=?from.meta
??????if?(fromDepth?>?toDepth)?{
????????this.transitionName?=?'fade-left'
??????}?else?if?(fromDepth?<?toDepth)?{
????????this.transitionName?=?'fade-right'
??????}?else?{
????????this.transitionName?=?'fade'
??????}
????}
??}
}
</script>
~~~
## **登錄跳轉**
雖然這樣能夠實現跳轉效果,但是需要在編寫router時添加設置,比較麻煩;我們可以使用開源項目vue-navigation來實現,更加方便,無須對router進行多余的設置。npm i -S vue-navigation安裝,在main.js中導入:
~~~
import?Navigation?from?'vue-navigation'Vue.use(Navigation,?{router})?//?router為路由文件
~~~
在App.vue中設置:
~~~
this.$navigation.on('forward',?(to,?from)?=>?{
????this.transitionName?=?'fade-right'
})
?this.$navigation.on('back',?(to,?from)?=>?{
????this.transitionName?=?'fade-left'
})
?this.$navigation.on('replace',?(to,?from)?=>?{
????this.transitionName?=?'fade'
})
~~~
vue-navigation插件還有一個重要的功能就是保存頁面狀態,與keep-alive相似,但是keep-alive保存狀態無法識別路由的前進后退,而實際應用中,我們的需求是返回頁面時,希望頁面狀態保存,當進入頁面時希望獲取新的數據,使用vue-navigation可以很好的實現這個效果。具體使用可以查看vue-navigation有詳細使用說明與案例。另外也可以嘗試vue-page-stack,兩個項目都能實現我們需要的效果,vue-page-stack借鑒了vue-navigation,也實現了更多的功能,并且最近也一直在更新。
PS: 這里的動畫效果引用自animate.scss;
## **底部導航欄**
之前我們已經實現了底部導航欄的基本樣式,這里我們再做一些說明。當頁面路由路徑與router-link的路由匹配時,router-link將會被設置為激活狀態,我們可以通過設置active-class來設置路徑激活時應用的類名,默認為router-link-active,而激活的類名還有一個router-link-exact-active,這個類名是由exact-active-class來設置的,同樣是設置路徑激活時應用的類名;active-class與exact-active-class其實是由路由的匹配方式決定的。
一般路由的匹配方式是包含匹配。舉個例子,如果當前的路徑是 /a 開頭的,那么 也會被設置 CSS 類名。按照這個規則,每個路由都會激活 ,而使用exact屬性可以使用“精確匹配模式”。精確匹配只有當路由完全相同的時候才會被激活。
## **路由守衛**
移動端的路由守衛一般不會太復雜,主要是登錄權限的判斷,我們設置一個路由白名單,將所有不需要登錄權限的路由放入其中;對于需要登錄的路由做判斷,沒有登錄就跳轉登錄頁面,要求用戶進行登錄后在訪問,如果登錄后需要返回原有路由就把目標頁面的路由作為參數傳遞給登錄頁面,再在登錄后進行判斷,如果存在目標頁面參數就跳轉目標頁面,沒有就跳轉首頁。
如果你的應用涉及到權限,那需要標注每個路由需要的權限,在meta中設置roles,roles是數組來保存需要的權限;從后臺的接口中獲取用戶擁有的權限和roles進行對比就可以判斷是否具有相關權限了。
~~~
const?whiteList?=?['/login']router.beforeEach((to,?from,?next)?=>?{
??const?hasToken?=?store.getters.auth??if?(hasToken)?{
????if?(to.path?===?'/login')?{
??????next({?path:?'/'?})
????}?else?{
??????const?needRoles?=?to.meta?&&?to.meta.roles?&&?to.meta.roles.length?>?0
??????if?(needRoles)?{
????????const?hasRoles?=?store.state.user.roles.some(role?=>?to.meta.roles.includes(role))
????????if?(hasRoles)?{
??????????next()
????????}?else?{
??????????next('/403')
????????}
??????}?else?{
????????next()
??????}
????}
??}?else?{
????if?(whiteList.includes(to.path))?{
??????next()
????}?else?{
??????next('/login')
????}
??}
})
~~~
## **組件**
**自動加載**
在我們的項目中,往往會使用的許多組件,一般使用頻率比較高的組件為了避免重復導入的繁瑣一般是作為全局組件在項目中使用的。而注冊全局組件我們首先需要引入組件,然后使用Vue.component進行注冊;這是一個重復的工作,我們每次創建組件都會進行,如果我們的項目是使用webpack構建(vue-cli也是使用webpack),我們就可以通過require.context自動將組件注冊到全局。創建components/index.js文件:
~~~
export?default?function?registerComponent?(Vue)?{
??/**???*?參數說明:???*?1.?其組件目錄的相對路徑???*?2.?是否查詢其子目錄???*?3.?匹配基礎組件文件名的正則表達式???**/
??const?modules?=?require.context('./',?false,?/\w+.vue$/)
??modules.keys().forEach(fileName?=>?{
????//?獲取組件配置
????const?component?=?modules(fileName)
????//?獲取組件名稱,去除文件名開頭的?`./`?和結尾的擴展名
????const?name?=?fileName.replace(/^\.\/(.*)\.\w+$/,?'$1')
????//?注冊全局組件
????//?如果這個組件選項是通過?`export?default`?導出的,
????//?那么就會優先使用?`.default`,
????//?否則回退到使用模塊的根。
????Vue.component(name,?component.default?||?component)??})}
~~~
之后在main.js中導入注冊模塊進行注冊,使用require.context我們也可以實現vue插件和全局filter的導入。
~~~
import?registerComponent?from?'./components'registerComponent(Vue)
~~~
## **通過v-model綁定數據**
v-model是語法糖,它的本質是對組件事件進行監聽和數據進行更新,是props和 o n 監 聽 事 件 的 縮 寫 , v ? m o d e l 默 認 傳 遞 v a l u e , 監 聽 i n p u t 事 件 。現 在 我 們 使 用 v ? m o d e l 來 實 現 下 數 字 輸 入 框 , 這 個 輸 入 框 只 能 輸 入 數 字 , 在 組 件 中 我 們 只 需 要 定 義 v a l u e 來 接 受 傳 值 , 然 后 在 輸 入 值 滿 足 我 們 輸 入 條 件 ( 輸 入 為 數 字 ) 的 時 候 使 用 on監聽事件的縮寫,v-model默認傳遞value,監聽input事件。現在我們使用v-model來實現下數字輸入框,這個輸入框只能輸入數字,在組件中我們只需要定義value來接受傳值,然后在輸入值滿足我們輸入條件(輸入為數字)的時候使用 on監聽事件的縮寫,v?model默認傳遞value,監聽input事件。現在我們使用v?model來實現下數字輸入框,這個輸入框只能輸入數字,在組件中我們只需要定義value來接受傳值,然后在輸入值滿足我們輸入條件(輸入為數字)的時候使用emit觸發input事件。
~~~
<template>
??<div>
????<input?type="text"?:value="value"?@input="onInput">
??</div>
</template><script>
export?default?{
??name:?'NumberInput',
??props:?{
????value:?String
??},
??methods:?{
????onInput?(event)?{
??????if?(/^\d+$/.test(event.target.value))?{
????????this.$emit('input',?event.target.value)
??????}?else?{
????????event.target.value?=?this.value
??????}
????}
??}
}
</script>
~~~
使用的時候,我們只需要使用v-model綁定值就可以了。v-model默認會利用名為value的prop和名為input的事件,但是很多時候我們想使用不同的prop和監聽不同的事件,我們可以使用model選項進行修改。
~~~
Vue.component('my-checkbox',?{
??model:?{
????prop:?'checked',
????event:?'change'
??},??props:?{
????//?this?allows?using?the?`value`?prop?for?a?different?purpose
????value:?String,
????//?use?`checked`?as?the?prop?which?take?the?place?of?`value`
????checked:?{
??????type:?Number,
??????default:?0
????}
??},
??//?...
})
~~~
~~~
<my-checkbox?v-model="foo"?value="some?value">
</my-checkbox>
~~~
上述代碼相當于:
~~~
<my-checkbox??:checked="foo"??@change="val?=>?{?foo?=?val?}"??value="some?value">
</my-checkbox>
~~~
## **通過插件的方式來使用組件**
在很多第三方組件庫中,我們經常看到直接使用插件的方式調用組件的方式,比如VantUI的Dialog彈出框組件,我們不但可以使用組件的方式進行使用,也可以通過插件的形式進行調用。
~~~
this.$dialog.alert({??message:?'彈窗內容'});
~~~
將組件作為插件使用的原理其實并不復雜,就是使用手動掛載Vue組件實例。
~~~
import?Vue?from?'vue';export?default?function?create(Component,?props)?{
????//?先創建實例
????const?vm?=?new?Vue({
????????render(h)?{
????????????//?h就是createElement,它返回VNode
????????????return?h(Component,?{props})
????????}
????}).$mount();
????//?手動掛載
????document.body.appendChild(vm.$el);
????//?銷毀方法
????const?comp?=?vm.$children[0];
????comp.remove?=?function()?{
????????document.body.removeChild(vm.$el);
????????vm.$destroy();
????}
????return?comp;
}
~~~
調用create傳入組件和props參數就可以獲取組件的實例,通過組件實例我們就可以調用組件的各種功能了。
~~~
<template>
??<div?class="loading-wrapper"?v-show="visible">
????加載中
??</div>
</template>
<script>
export?default?{
??name:?'Loading',
??data?()?{
????return?{
??????visible:?false
????}
??},
??methods:?{
????show?()?{
??????this.visible?=?true
????},
????hide?()?{
??????this.visible?=?false
????}
??}
}
</script>
<style?lang="css"?scoped>
.loading-wrapper?{
??position:?absolute;
??top:?0;
??bottom:?0;
??width:?100%;
??background-color:?rgba(0,?0,?0,?.4);
??z-index:?999;
}
</style>
<!--使用-->
const?loading?=?create(Loading,?{})
loading.show()?//?顯示loading.hide()?//?關閉
~~~
## **第三方組件**
移動端各種組件、插件已經相對完善,在項目開發中重復造輪子是一件很不明智的事情;開發項目時我們可以借助第三方組件、插件提高我們的開發效率。
常用組件庫
VantUI是有贊開源的一套輕量、可靠的移動端Vue組件庫;支持按需引入、主題定制、SSR,除了常用組件外,針對電商場景還有專門的業務組件,如果是開發電商項目的話,推薦使用。官方文檔關于主題定制是在webpack.config.js中進行設置的:
~~~
//?webpack.config.js
module.exports?=?{
??rules:?[
????{
??????test:?/\.less$/,
??????use:?[
????????//?...其他?loader?配置
????????{
??????????loader:?'less-loader',
??????????options:?{
????????????modifyVars:?{
??????????????//?直接覆蓋變量
??????????????'text-color':?'#111',
??????????????'border-color':?'#eee'
??????????????//?或者可以通過?less?文件覆蓋(文件路徑為絕對路徑)
??????????????'hack':?`true;?@import?"your-less-file-path.less";`
????????????}
??????????}
????????}
??????]
????}
??]};
~~~
但我們的項目可能是使用vue-cli構建,這時我們需要在vue.config.js中進行設置:
~~~
module.exports?=?{
??css:?{
????loaderOptions:?{
??????less:?{
????????modifyVars:?{
??????????'hack':?`true;?@import?"~@/assets/less/vars.less";`
????????}
??????}
????}
??}}
~~~
另外vux、mint-ui也是很好的選擇。
## **常用插件**
better-scroll是一個為移動端各種滾動場景提供絲滑的滾動效果的插件,如果在vue中使用可以參考作者的文章當 better-scroll 遇見 Vue。
swiper是一個輪播圖插件,如果是在vue中使用可以直接使用vue-awesome-swiper,vue-awesome-swiper基于Swiper4,并且支持SSR。