前言:
兩個月前開始全身心投入到公司的一個移動端項目,架構選型是vue,這篇文章也是在花費兩個月的時間,項目一期完成之後得空進行的一片總結性文章,其中包括通用的移動端開發的坑以及vue在移動端開發特有的一些坑,本博文目的也是為了讓小夥伴們以後在開發移動端的時候可以盡量避免掉這些坑,進而提高自己的開發效率。
本博文總結順序大概如下
- 移動端開發通用坑
- vue移動開發特有坑以及小技巧分享
- 移動端開發性能優化
一、移動端開發通用坑
1、click300ms延遲?
講道理,現在開發移動端基本是不會有這麼一個問題的。但作為移動端以前的經典坑,我這裡也拿出來說上一說吧。
移動裝置上的web網頁是有300ms延遲的,玩玩會造成按鈕點選延遲甚至是點選失效。這是由于區分單擊事件和輕按兩下螢幕縮放的曆史原因造成的。但在2014年的Chrome 32版本已經把這個延遲去掉了,so you know。但如果你還是出現了300ms的延遲問題,也是有路子搞定的。
解決方案如下:
- fastclick可以解決在手機上點選事件的300ms延遲
- zepto的touch子產品,tap事件也是為了解決在click的延遲問題
- 觸摸事件的響應順序為 touchstart --> touchmove --> touchend --> click,也可以通過綁定ontouchstart事件,加快對事件的響應,解決300ms延遲問題
- 若移動裝置相容性正常的話(IE/Firefox/Safari(IOS 9.3)及以上),隻需加上一個meta标簽
即把viewport設定成裝置的實際像素,那麼就不會有這300ms的延遲。<meta name="viewport" content="width=device-width">
2、移動端樣式相容處理
當今的手機端,各式各樣的手機,螢幕分辨率也是各有不同,為了讓頁面可以可以相容各大手機,解決方案如下
- 設定meta标簽viewport屬性,使其無視裝置的真實分辨率,直接通過dpi,在實體尺寸和浏覽器之間重設分辨率,進而達到能有統一的分辨率的效果。并且禁止掉使用者縮放
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
- 使用rem進行螢幕适配,設定好root元素的font-size大小,然後在開發的時候,所有與像素有關的布局統一換成rem機關。針對不同的手機,使用媒體查詢對root元素font-size進行調整
3、阻止旋轉螢幕時自動調整字型大小
移動端開發時,螢幕有豎屏和橫屏模式,當螢幕進行旋轉時,字型大小則有可能會發生變化,進而影響頁面布局的整體樣式,為避免此類情況發生,隻需設定如下樣式即可
* {
-webkit-text-size-adjust: none;
}
4、修改移動端難看的點選的高亮效果,iOS和安卓下都有效
* {
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
不過這個方法在現在的安卓浏覽器下,隻能去掉那個橙色的背景色,點選産生的高亮邊框還是沒有去掉,有待解決!
一個CSS3的屬性,加上後,所關聯的元素的事件監聽都會失效,等于讓元素變得“看得見,點不着”。IE到11才開始支援,其他浏覽器的目前版本基本都支援。詳細介紹見這裡:
https://developer.mozilla.org/zh-CN/docs/Web/CSS/pointer-eventspointer-events: none;
5、iOS下取消input在輸入的時候英文首字母的預設大寫
<input type="text" autocapitalize="none">
6、禁止 iOS 識别長串數字為電話
<meta name="format-detection" content="telephone=no" />
7、禁止 iOS 彈出各種操作視窗
-webkit-touch-callout: none;
8、禁止ios和android使用者選中文字
-webkit-user-select: none;
9、calc的相容處理
CSS3中的calc變量在iOS6浏覽器中必須加-webkit-字首,目前的FF浏覽器已經無需-moz-字首。 Android浏覽器目前仍然不支援calc,是以要在之前增加一個保守尺寸:
div {
width: 95%;
width: -webkit-calc(100% - 50px);
width: calc(100% - 50px);
}
10、fixed定位缺陷
iOS下fixed元素容易定位出錯,軟鍵盤彈出時,影響fixed元素定位,android下fixed表現要比iOS更好,軟鍵盤彈出時,不會影響fixed元素定位 。iOS4下不支援position:fixed
解決方案: 可用iScroll插件解決這個問題
11、一些情況下對非可點選元素如(label,span)監聽click事件,ios下不會觸發
針對此種情況隻需要對不觸發click事件的那些元素添加一行css代碼即可
cursor: pointer;
12、消除transition閃屏問題
/*設定内嵌的元素在 3D 空間如何呈現:保留 3D*/
-webkit-transform-style: preserve-3d;
/*(設定進行轉換的元素的背面在面對使用者時是否可見:隐藏)*/
-webkit-backface-visibility: hidden;
13、CSS動畫頁面閃白,動畫卡頓
解決方法:
1.盡可能地使用合成屬性transform和opacity來設計CSS3動畫,不使用position的left和top來定位
2.開啟硬體加速
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
14、iOS系統中文輸入法輸入英文時,字母之間可能會出現一個六分之一的空格
解決方法:通過正則去除
this.value = this.value.replace(/\u2006/g, '');
15、input的placeholder會出現文本位置偏上的情況
input 的placeholder會出現文本位置偏上的情況:PC端設定line-height等于height能夠對齊,而移動端仍然是偏上,解決方案時是設定css
line-height:normal;
16、浮動子元素撐開父元素盒子高度
解決方法如下:
- 父元素設定為 overflow: hidden;
- 父元素設定為 display: inline-block; 等
這裡兩種方法都是通過設定css屬性将浮動元素的父元素變成間接變成BFC元素,然後使得子元素高度可以撐開父元素。這裡需要注意的時,最好使用方法1, 因為inline-block元素本身會自帶一些寬高度撐開其本身。
17、往返緩存問題
點選浏覽器的回退,有時候不會自動執行js,特别是在mobilesafari中。這與往返緩存(bfcache)有關系。 解決方法 :
window.onunload = function () {};
18、overflow-x: auto在iOS有相容問題
解決方法:
-webkit-overflow-scrolling: touch;
二、vue移動開發特有坑以及小技巧分享
1、iOS原始輸入法問題
iOS原始輸入法,中文輸入時,無法觸發keyup事件,且keyup.enter事件無論中英文,都無法觸發
- 改用input事件進行監聽
- 将keyup監聽替換成值的watch
- 讓使用者安裝三方輸入法,比如搜狗輸入法(不太現實)
2、input元素失焦問題
業務場景重制: 項目中需要寫一個搜尋元件,相關代碼如下
<template>
<div class="y-search" :style="styles" :clear="clear">
<form action="#" onsubmit="return false;">
<input type="search"
class="y-search-input"
ref="search"
v-model='model'
:placeholder="placeholder"
@input="searchInputFn"
@keyup.enter="searchEnterFn"
@foucs="searchFocusFn"
@blur="searchBlurFn"
/>
<y-icons class="search-icon" name="search" size="14"></y-icons>
</form>
<div v-if="showClose" @click="closeFn">
<y-icons class="close-icon" name='close' size='12'></y-icons>
</div>
</div>
</template>
其中我需要在enter的時候進行對應的搜尋操作并實作失焦,解決方法其實很簡單,在enter時進行DOM操作即可
searchEnterFn (e) {
document.getElementsByClassName('y-search-input')[0].blur()
// dosomething yourself
}
對了,這裡還有一個坑,就是在移動端使用input類型為search的時候,必須使用form标簽包裹起來,這樣在移動端呼出鍵盤的enter才會是搜尋按鈕,否則隻是預設的enter按鈕。
3、vue元件開發
這個點不能算坑,隻能算是小技巧分享吧。
業務場景重制:很多時候,在開發項目的時候是需要抽離公共元件和業務元件的。而有些公共元件在全局注冊的同時可能還需要拓展成vue的執行個體方法,通過把它們添加到 Vue.prototype 上實作,友善直接使用js全局調用。拿一個Message元件做例子吧,代碼比較簡單,就直接上代碼了。
1.首先開發好Message.vue檔案
<template>
<div class='y-mask-white-dialog' v-show='show'>
<div class='y-message animated zoomIn' >
<span v-html="msg"></span>
</div>
</div>
</template>
<script>
export default {
name: 'yMessage',
props: {
msg: String,
timeout: Number,
callback: Function,
icon: String,
},
data() {
return {
show: true,
};
}
};
</script>
<style lang="stylus" scoped>
.y-mask-white-dialog {
background-color: rgba(0, 0, 0, .4);
position: fixed;
z-index: 999;
bottom: 0;
right: 0;
left: 0;
top: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
}
.y-message {
min-width: 2.9rem;
max-width: 5.5rem;
width:100%;
padding: 0.32rem;
font-size: 14px;
text-align: center;
border-radius: 4px;
background :rgba(0,0,0,0.8);
color: #fff;
animation: zoomIn .15s ease forwards;
}
</style>
2.建構Message的Constructor
import Vue from 'vue';
const MsgConstructor = Vue.extend(require('./Message.vue'));
const instance = new MsgConstructor({
// el: document.createElement('div'),
}).$mount(document.createElement('div'));
MsgConstructor.prototype.closeMsg = function () {
const el = instance.$el;
el.parentNode && el.parentNode.removeChild(el);
typeof this.callback === 'function' && this.callback();
};
const Message = (options = {}) => {
instance.msg = options.msg;
instance.timeout = options.timeout || 2000;
instance.icon = options.icon;
instance.callback = options.callback;
document.body.appendChild(instance.$el);
const timer = setTimeout(() => {
clearTimeout(timer);
instance.closeMsg();
}, instance.timeout + 100);
};
export default Message;
3.在main.js裡面進行元件注冊
import Message from './components/Message';
Vue.component(Message.name, Message)
Vue.prototype.$message = Message
然後你就可以盡情使用Message元件了.
// <y-message msg="test message"><y-message>
// or
this.$message({
msg: 'test message'
// ...
})
4、巧用flex布局讓圖檔等比縮放
這也是一個小技巧!項目中需要開發swiper輪播圖,那麼你懂的,圖檔肯定是需要保證等比縮放展示。
<div class="parent">
<img :src="imgSrc" alt="">
</div>
<style lang="stylus" scoped>
.parent {
width: 100px;
height: 100px;
display: flex;
align-items: center;
img {
width :100%;
height: auto;
}
}
</style>
是不是賊簡單,是的,賊簡單

這個樣式同時适應手機全屏預覽豎屏的情況,當手機橫屏的時候,加一個媒體查詢即可搞定
@media (orientation: landscape) {
img {
width auto
height 100%
margin auto
}
}
這裡我就不上輪播圖的代碼了,有點小多。有需要的小夥伴可以私聊我,我後期直接傳到github上去,代碼可以自行查閱。效果圖如下
5、枚舉值過濾處理
業務重制:考慮到項目後期會做國際化,前端需要對項目中幾乎所有的枚舉值進行過濾處理,進而進行展示
接下來就直接講講這塊吧。既然要過濾,那麼首選肯定是vue提供的filter指令。這裡我舉一個支付方式的枚舉值處理的例子。首先配置代碼如下
// 配置檔案
export default {
env: (process.env.NODE_ENV === 'development' ? require('./env/dev') : require('./env/pro')),
headShow: false,
lng: 'zh',
};
枚舉代碼如下
// 擷取語言環境
import config from '../config/index';
const {lng} = config;
// 賬戶類型
const type = {
zh: {
1: '銀行',
2: '支付寶',
3: '微信支付',
},
en: {
1: 'bank_type',
2: 'alipay_type',
3: 'wxpay_type',
}
}
export default type[lng];
枚舉注冊代碼分别如下
import accountType from './accountType'; // 賬戶類型
const factory = {
accountType(value) {
if (value === -1) {
return '請選擇賬戶類型';
}
return accountType[value] ? accountType[value] : '請選擇賬戶類型';
}
}
const filter = [
{
name: 'formatEnum', // 過濾類型
filter: function(value, type, status) {
return factory[type](value, status);
}
}
];
export default {
filter
};
// filter
import baseFilter from './filter/index';
const filters = [
...baseFilter.filter
];
filters.map(f => {
Vue.filter(f.name, f.filter);
return '';
});
接下來就可以輕松使用啦
<li>
<label支付類型</label>
<span>
{{info.account_type | formatEnum('accountType')}}
</span>
</li>
6、時間過濾處理
這點還是屬于過濾處理的一個part,但是手機端有個相容問題,如果是時間戳轉的話,那麼可以轉化成任意我們想要的形式,但是String類型的時間轉化的話,他隻能相容 "yyyy/MM/dd" 形式的時間值,因為我們DateTime元件預設的形式是"yyyy-MM-dd",那麼隻需要在DateTime元件正則替換一下即可,代碼如下
currentValue = _this.currentValue = _this.value ? _this.value.replace(/-/g, '/') : '';
時間過濾代碼如下
const filter = [
{
name: 'formatEnum', // 過濾類型
filter: function(value, type, status) {
return factory[type](value, status);
}
},
{
name: 'formatDate', // 日期
filter: function(value, format = 'yyyy-MM-dd') {
if (!value || value === '') return '';
let v = value;
if (!isNaN(value)) {
v = +value * 1000;
}
const newDate = new Date(v);
return newDate.Format(format);
},
}
];
7、路由權限判定
業務重制:由于不同的使用者,可能擁有不同權限,而目前我們的項目是基于微信公衆号進行開發的,頁面權限這邊也是交給了我們前端處理。既然要前端配置權限,那麼我們能想到的比較好的方式就是通過配置路由檔案,完成權限判定。下面我會列舉一小部分代碼(以我們的工單清單)進行示範,路由配置代碼如下
const getWorkOrder = pageName => resolve => require(['../pages/WorkOrder'], pages => resolve(pages[pageName]))
let routers = [];
routers = [
{
path: '/workorder',
name: 'workorder',
component: room,
children: [
{
path: 'list', // 管家端工單清單
name: 'list',
rule: 3,
component: getWorkOrder('WorkOrderList') //WorkOrder,
},
{
path: 'managerList', // 店長端工單清單
name: 'managerList',
rule: 6,
component: getWorkOrder('ManagerWorkOrder') // WorkOrder,
},
]
}
]
然後進行路由統一過濾處理
import Vue from 'vue';
import Store from 'store';
import Router from 'vue-router';
import routers from './router.config';
Vue.use(Router);
// 周遊路由名稱以及權限
let arr = {};
const routeName = function (router) {
if (!router) return false;
router.forEach((v) => {
arr[v.name] = v.rule;
routeName(v.children);
})
}
routeName(routers);
const RouterModel = new Router({
mode: 'history',
routes: routers,
});
// 路由鈎子,進入路由之前判斷
RouterModel.beforeEach((to, from, next) => {
// 處理query 參數,傳入到 jumpUrl,便于登入後跳轉到對應頁面
let qu = Object.entries(to.query);
if (qu.length > 0) {
qu = qu.map((item, index) => {
return item.join('=');
})
}
if (arr[to.name]) {
// 如果有權限需要
const userInfo = Store.get('yu_info');
const cookie = Vue.prototype.$util.getCookie('X-Auth-Token');
const userId = Vue.prototype.$util.getCookie('userid');
if (userInfo && cookie && +userId > 0) {
next();
} else {
// 未登入,跳轉登入
let param = `jumpUrl=${to.path}`;
if (qu.length > 0) {
param += `&${qu.join("&")}`;
}
if (arr[to.name] && !to.query.rule) {
param += `&rule=${arr[to.name]}`;
}
window.location.href = `/login?${param}`;
}
} else {
// 如果不需要權限放行
next();
}
})
export default RouterModel;
然後在登陸界面定位到微信授權
getCode() {
// 定位到微信授權,若是不需要授權可以在此處處理
let query = this.$route.query;
let param = `jumpUrl=${query.jumpUrl || '/home'}`;
let path = window.location.origin;
// 登入角色處理
let cfg = api[query.rule ? query.rule : 1];
if (query.rule) {
param += `&rule=${query.rule}`;
}
let redirect_url = encodeURIComponent(`${path}/login?${param}`);
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${cfg.tempApp}&redirect_uri=${redirect_url}&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect`;
if (query.rule === 3) {
url += '&agentid=1000004';
}
location.replace = url;
}
如果不是微信端,通路到到rule規則的界面時,則會如下
而當微信授權通過的時候,rule權限不足的情況則會如下
8、使用vue-cli proxyTable進行反向代理,解決跨域問題
開發項目,在前後端聯調的時候肯定是會遇上跨域的問題的。很簡單,做個反向代理呗,對于想了解正向代理和反向代理的,
請點選這裡vue-cli腳手架搭建的工程中,在config/index.js檔案中可以利用預留的proxyTable一項,設定位址映射表,如下
proxyTable: {
'/api': {
target: 'http://www.example.com', // your target host
changeOrigin: true, // needed for virtual hosted sites
pathRewrite: {
'^/api': '' // rewrite path
}
}
}
然後使用http-proxy-middleware插件對api請求位址進行代理
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
http-proxy-middleware位址 三、移動端開發性能優化
對于這點,有一篇文章建議大家可以先看看
《移動前端H5性能優化指南》。接下來我會結合實際項目抽幾個點配合代碼進行較為詳細的講解。
1、首屏渲染優化
決定使用者體驗最重要的一個點之一,這個點的重要性,相信不用我說了。下面直接談實戰。
- 減少資源請求次數
- 加載時使用過渡樣式,防止使用者網絡太差影響對首頁的體驗
- 圖檔使用懶加載,這一part,我們目前項目中使用的vue的三方插件 vue-lazyload ,大緻使用方法如下
// 全局注冊 import VueLazyload from 'vue-lazyload'; Vue.use(VueLazyload, { error: require('./assets/close.svg'), loading: require('./assets/loading.svg'), attempt: 1, }); // 使用 <img v-lazy="room.img" :alt="room.community_name" width="100%">
- HTML使用Viewport,Viewport可以加速頁面的渲染。
除此之外,還有很多點可做優化,進而提升首屏加載速度。<meta name=”viewport” content=”width=device-width, initial-scale=1″>
2、雪碧圖
這個老生常談了,為了減少圖檔請求次數,加快頁面加載,當然會考慮使用雪碧圖。大家這個應該沒啥疑問吧。來個小例子,一天,設計小姐姐給了我一張設計稿,稿子如圖
然後給了我6張圖檔,一看,每張圖都有6K左右的大小。好嘛,隻能自己線上合張雪碧圖,不然,太影響頁面加載,合完雪碧圖順帶線上壓縮優化下,然後總大小隻有6K。還有一個點,就是盡量使用::before或::after僞類,Sprites中的圖檔排版可以更緊 ,圖檔體積更小, HTML更簡潔。部分代碼如下
<style lang="stylus" scoped>
.chose-house {
height: 86px;
width: 64px;
margin 0 auto
position relative
&::before {
content: '\20';
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
background: url('../../assets/sprite-min.png') 0px 0px no-repeat;
}
}
</style>
3、路由懶加載
關于路由懶加載這一部分,尤大在vue-router文檔中也有所提及,
連結點選這裡。
其實在vue項目中使用路由懶加載非常簡單,我們要做的就是把路由對應的元件定義成異步元件,代碼如下
//在router/index.js中引入元件時可使用如下異步方式引入
const Foo = resolve => {
// require.ensure 是 Webpack 的特殊文法,用來設定 code-split point
// (代碼分塊)
require.ensure(['./Foo.vue'], () => {
resolve(require('./Foo.vue'))
})
}
// or
const Foo = resolve => require(['./Foo.vue'], resolve)
再将元件按組分塊,如
const Foo = r => require.ensure([], () => r(require('./Foo.vue')), 'group-foo')
實際項目中的代碼則如同我在章節《路由權限判定》提及到的一樣
const getWorkOrder = pageName => resolve => require(['../pages/WorkOrder'], pages => resolve(pages[pageName]))
let routers = [];
routers = [
{
path: '/workorder',
name: 'workorder',
component: room,
children: [
{
path: 'list', // 管家端工單清單
name: 'list',
rule: 3,
component: getWorkOrder('WorkOrderList') //WorkOrder,
},
{
path: 'managerList', // 店長端工單清單
name: 'managerList',
rule: 6,
component: getWorkOrder('ManagerWorkOrder') // WorkOrder,
},
]
}
]
如上将元件通過傳遞pageName參數分别打包到了各個chunk中,這樣每個元件加載時都隻會加載自己對應的代碼,進而加快渲染速度!
4、全局元件按需注冊
當時我們為了優化首屏渲染速度,也是考慮到這一點,項目的src/main.js檔案主要負責注冊全局元件,插件,路由,以及執行個體化Vue等。在webpack的配置裡面也是當成entry入口進行了配置,如果我在main.js裡面講每個元件都進行import的話,那麼它将會全部一起注冊打包,頁面加載也會将每個元件檔案都加載下來,這樣對渲染速度還是有一定影響的。
解決方法就是:按需注冊,這樣在打包的時候,會按需加載首頁(其他界面也同樣适用)使用到的全局元件。基本步驟如下:
将需要注冊的元件寫進components/base.js檔案中,然後exports出來
exports.Foo = require('./Foo.vue');
exports.Bar = require('./Bar.vue');
exports.Baz = require('./Baz.vue');
在main.js中進行注冊
const components = [
require('./components/base').Foo,
require('./components/base').Bar,
require('./components/base').Baz,
];
components.map(component => {
Vue.component(component.name, component);
});
OK,大功告成!
以上便是我在最近的移動端項目實戰中的一些經驗總結,希望對各位小夥伴或多或少有些幫助吧!如果有幫助,别吝惜你手上的贊哦~
本文作者:qiangdada
本文釋出時間:2017/10/24
本文來自雲栖社群合作夥伴
開源中國,了解相關資訊可以關注oschina.net網站。