本博文總結順序大概如下
- 移動端開發通用坑
- 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標簽
<meta name="viewport" content="width=device-width">
即把viewport設置成設備的實際像素,那么就不會有這300ms的延遲。
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、阻止旋轉屏幕時自動調整字體大小
移動端開發時,屏幕有豎屏和橫屏模式,當屏幕進行旋轉時,字體大小則有可能會發生變化,從而影響頁面布局的整體樣式,為避免此類情況發生,只需設置如下樣式即可
1 * { 2 -webkit-text-size-adjust: none; 3 }
4、修改移動端難看的點擊的高亮效果,iOS和安卓下都有效
1 * { 2 -webkit-tap-highlight-color: rgba(0,0,0,0); 3 }
不過這個方法在現在的安卓瀏覽器下,只能去掉那個橙色的背景色,點擊產生的高亮邊框還是沒有去掉,有待解決!
一個CSS3的屬性,加上后,所關聯的元素的事件監聽都會失效,等於讓元素變得“看得見,點不着”。IE到11才開始支持,其他瀏覽器的當前版本基本都支持。詳細介紹見這里:https://developer.mozilla.org/zh-CN/docs/Web/CSS/pointer-events
pointer-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,所以要在之前增加一個保守尺寸:
1 div { 2 width: 95%; 3 width: -webkit-calc(100% - 50px); 4 width: calc(100% - 50px); 5 }
10、fixed定位缺陷
iOS下fixed元素容易定位出錯,軟鍵盤彈出時,影響fixed元素定位,android下fixed表現要比iOS更好,軟鍵盤彈出時,不會影響fixed元素定位 。iOS4下不支持position:fixed
解決方案: 可用iScroll插件解決這個問題
11、一些情況下對非可點擊元素如(label,span)監聽click事件,ios下不會觸發
針對此種情況只需要對不觸發click事件的那些元素添加一行css代碼即可
cursor: pointer;
12、消除transition閃屏問題
1 /*設置內嵌的元素在 3D 空間如何呈現:保留 3D*/ 2 -webkit-transform-style: preserve-3d; 3 /*(設置進行轉換的元素的背面在面對用戶時是否可見:隱藏)*/ 4 -webkit-backface-visibility: hidden;
13、CSS動畫頁面閃白,動畫卡頓
解決方法:
1.盡可能地使用合成屬性transform和opacity來設計CSS3動畫,不使用position的left和top來定位
2.開啟硬件加速
1 -webkit-transform: translate3d(0, 0, 0); 2 -moz-transform: translate3d(0, 0, 0); 3 -ms-transform: translate3d(0, 0, 0); 4 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、浮動子元素撐開父元素盒子高度
解決方法如下:
1 父元素設置為 overflow: hidden; 2 父元素設置為 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元素失焦問題
業務場景重現: 項目中需要寫一個搜索組件,相關代碼如下
1 <template> 2 <div class="y-search" :style="styles" :clear="clear"> 3 <form action="#" onsubmit="return false;"> 4 <input type="search" 5 class="y-search-input" 6 ref="search" 7 v-model='model' 8 :placeholder="placeholder" 9 @input="searchInputFn" 10 @keyup.enter="searchEnterFn" 11 @foucs="searchFocusFn" 12 @blur="searchBlurFn" 13 /> 14 <y-icons class="search-icon" name="search" size="14"></y-icons> 15 </form> 16 <div v-if="showClose" @click="closeFn"> 17 <y-icons class="close-icon" name='close' size='12'></y-icons> 18 </div> 19 </div> 20 </template>
其中我需要在enter的時候進行對應的搜索操作並實現失焦,解決方法其實很簡單,在enter時進行DOM操作即可
1 searchEnterFn (e) { 2 document.getElementsByClassName('y-search-input')[0].blur() 3 // dosomething yourself 4 }
對了,這里還有一個坑,就是在移動端使用input類型為search的時候,必須使用form標簽包裹起來,這樣在移動端呼出鍵盤的enter才會是搜索按鈕,否則只是默認的enter按鈕。
3、vue組件開發
這個點不能算坑,只能算是小技巧分享吧。
業務場景重現:很多時候,在開發項目的時候是需要抽離公共組件和業務組件的。而有些公共組件在全局注冊的同時可能還需要拓展成vue的實例方法,通過把它們添加到 Vue.prototype 上實現,方便直接使用js全局調用。拿一個Message組件做例子吧,代碼比較簡單,就直接上代碼了。
1.首先開發好Message.vue文件
1 <template> 2 <div class='y-mask-white-dialog' v-show='show'> 3 <div class='y-message animated zoomIn' > 4 <span v-html="msg"></span> 5 </div> 6 </div> 7 </template> 8 9 <script> 10 export default { 11 name: 'yMessage', 12 props: { 13 msg: String, 14 timeout: Number, 15 callback: Function, 16 icon: String, 17 }, 18 data() { 19 return { 20 show: true, 21 }; 22 } 23 }; 24 </script> 25 26 <style lang="stylus" scoped> 27 .y-mask-white-dialog { 28 background-color: rgba(0, 0, 0, .4); 29 position: fixed; 30 z-index: 999; 31 bottom: 0; 32 right: 0; 33 left: 0; 34 top: 0; 35 display: -webkit-box; 36 display: -webkit-flex; 37 display: -ms-flexbox; 38 display: flex; 39 -webkit-box-pack: center; 40 -webkit-justify-content: center; 41 -ms-flex-pack: center; 42 justify-content: center; 43 -webkit-box-align: center; 44 -webkit-align-items: center; 45 -ms-flex-align: center; 46 align-items: center; 47 } 48 .y-message { 49 min-width: 2.9rem; 50 max-width: 5.5rem; 51 width:100%; 52 padding: 0.32rem; 53 font-size: 14px; 54 text-align: center; 55 border-radius: 4px; 56 background :rgba(0,0,0,0.8); 57 color: #fff; 58 animation: zoomIn .15s ease forwards; 59 } 60 </style>
2.構建Message的Constructor
1 import Vue from 'vue'; 2 3 const MsgConstructor = Vue.extend(require('./Message.vue')); 4 5 const instance = new MsgConstructor({ 6 // el: document.createElement('div'), 7 }).$mount(document.createElement('div')); 8 9 MsgConstructor.prototype.closeMsg = function () { 10 const el = instance.$el; 11 el.parentNode && el.parentNode.removeChild(el); 12 typeof this.callback === 'function' && this.callback(); 13 }; 14 15 const Message = (options = {}) => { 16 instance.msg = options.msg; 17 instance.timeout = options.timeout || 2000; 18 instance.icon = options.icon; 19 instance.callback = options.callback; 20 document.body.appendChild(instance.$el); 21 22 const timer = setTimeout(() => { 23 clearTimeout(timer); 24 instance.closeMsg(); 25 }, instance.timeout + 100); 26 }; 27 28 export default Message;
3.在main.js里面進行組件注冊
1 import Message from './components/Message'; 2 3 Vue.component(Message.name, Message) 4 Vue.prototype.$message = Message 5 然后你就可以盡情使用Message組件了. 6 7 // <y-message msg="test message"><y-message> 8 9 // or 10 11 this.$message({ 12 msg: 'test message' 13 // ... 14 })
4、巧用flex布局讓圖片等比縮放
這也是一個小技巧!項目中需要開發swiper輪播圖,那么你懂的,圖片肯定是需要保證等比縮放展示。
1 <div class="parent"> 2 <img :src="imgSrc" alt=""> 3 </div> 4 5 <style lang="stylus" scoped> 6 .parent { 7 width: 100px; 8 height: 100px; 9 display: flex; 10 align-items: center; 11 12 img { 13 width :100%; 14 height: auto; 15 } 16 } 17 </style>
是不是賊簡單,是的,賊簡單
這個樣式同時適應手機全屏預覽豎屏的情況,當手機橫屏的時候,加一個媒體查詢即可搞定
1 @media (orientation: landscape) { 2 img { 3 width auto 4 height 100% 5 margin auto 6 } 7 }
這里我就不上輪播圖的代碼了,有點小多。有需要的小伙伴可以私聊我,我后期直接傳到github上去,代碼可以自行查閱。效果圖如下

5、枚舉值過濾處理
業務重現:考慮到項目后期會做國際化,前端需要對項目中幾乎所有的枚舉值進行過濾處理,從而進行展示
接下來就直接講講這塊吧。既然要過濾,那么首選肯定是vue提供的filter指令。這里我舉一個支付方式的枚舉值處理的例子。首先配置代碼如下
1 // 配置文件 2 export default { 3 env: (process.env.NODE_ENV === 'development' ? require('./env/dev') : require('./env/pro')), 4 headShow: false, 5 lng: 'zh', 6 }; 7 枚舉代碼如下 8 9 // 獲取語言環境 10 import config from '../config/index'; 11 12 const {lng} = config; 13 // 賬戶類型 14 const type = { 15 zh: { 16 1: '銀行', 17 2: '支付寶', 18 3: '微信支付', 19 }, 20 en: { 21 1: 'bank_type', 22 2: 'alipay_type', 23 3: 'wxpay_type', 24 } 25 } 26 export default type[lng];
枚舉注冊代碼分別如下
1 import accountType from './accountType'; // 賬戶類型 2 3 const factory = { 4 accountType(value) { 5 if (value === -1) { 6 return '請選擇賬戶類型'; 7 } 8 return accountType[value] ? accountType[value] : '請選擇賬戶類型'; 9 } 10 } 11 const filter = [ 12 { 13 name: 'formatEnum', // 過濾類型 14 filter: function(value, type, status) { 15 return factory[type](value, status); 16 } 17 } 18 ]; 19 export default { 20 filter 21 }; 22 // filter 23 import baseFilter from './filter/index'; 24 25 const filters = [ 26 ...baseFilter.filter 27 ]; 28 filters.map(f => { 29 Vue.filter(f.name, f.filter); 30 return ''; 31 });
接下來就可以輕松使用啦
1 <li> 2 <label支付類型</label> 3 <span> 4 {{info.account_type | formatEnum('accountType')}} 5 </span> 6 </li>
6、時間過濾處理
這點還是屬於過濾處理的一個part,但是手機端有個兼容問題,如果是時間戳轉的話,那么可以轉化成任意我們想要的形式,但是String類型的時間轉化的話,他只能兼容 "yyyy/MM/dd" 形式的時間值,因為我們DateTime組件默認的形式是"yyyy-MM-dd",那么只需要在DateTime組件正則替換一下即可,代碼如下
currentValue = _this.currentValue = _this.value ? _this.value.replace(/-/g, '/') : '';
時間過濾代碼如下
1 const filter = [ 2 { 3 name: 'formatEnum', // 過濾類型 4 filter: function(value, type, status) { 5 return factory[type](value, status); 6 } 7 }, 8 { 9 name: 'formatDate', // 日期 10 filter: function(value, format = 'yyyy-MM-dd') { 11 if (!value || value === '') return ''; 12 let v = value; 13 if (!isNaN(value)) { 14 v = +value * 1000; 15 } 16 const newDate = new Date(v); 17 return newDate.Format(format); 18 }, 19 } 20 ];
7、路由權限判定
業務重現:由於不同的用戶,可能擁有不同權限,而目前我們的項目是基於微信公眾號進行開發的,頁面權限這邊也是交給了我們前端處理。既然要前端配置權限,那么我們能想到的比較好的方式就是通過配置路由文件,完成權限判定。下面我會列舉一小部分代碼(以我們的工單列表)進行演示,路由配置代碼如下
1 const getWorkOrder = pageName => resolve => require(['../pages/WorkOrder'], pages => resolve(pages[pageName])) 2 3 let routers = []; 4 routers = [ 5 { 6 path: '/workorder', 7 name: 'workorder', 8 component: room, 9 children: [ 10 { 11 path: 'list', // 管家端工單列表 12 name: 'list', 13 rule: 3, 14 component: getWorkOrder('WorkOrderList') //WorkOrder, 15 }, 16 { 17 path: 'managerList', // 店長端工單列表 18 name: 'managerList', 19 rule: 6, 20 component: getWorkOrder('ManagerWorkOrder') // WorkOrder, 21 }, 22 ] 23 } 24 ]
然后進行路由統一過濾處理
1 import Vue from 'vue'; 2 import Store from 'store'; 3 import Router from 'vue-router'; 4 5 import routers from './router.config'; 6 Vue.use(Router); 7 8 // 遍歷路由名稱以及權限 9 let arr = {}; 10 const routeName = function (router) { 11 if (!router) return false; 12 router.forEach((v) => { 13 arr[v.name] = v.rule; 14 routeName(v.children); 15 }) 16 } 17 routeName(routers); 18 19 const RouterModel = new Router({ 20 mode: 'history', 21 routes: routers, 22 }); 23 24 // 路由鈎子,進入路由之前判斷 25 RouterModel.beforeEach((to, from, next) => { 26 // 處理query 參數,傳入到 jumpUrl,便於登錄后跳轉到對應頁面 27 let qu = Object.entries(to.query); 28 if (qu.length > 0) { 29 qu = qu.map((item, index) => { 30 return item.join('='); 31 }) 32 } 33 if (arr[to.name]) { 34 // 如果有權限需要 35 const userInfo = Store.get('yu_info'); 36 const cookie = Vue.prototype.$util.getCookie('X-Auth-Token'); 37 const userId = Vue.prototype.$util.getCookie('userid'); 38 if (userInfo && cookie && +userId > 0) { 39 next(); 40 } else { 41 // 未登錄,跳轉登錄 42 let param = `jumpUrl=${to.path}`; 43 if (qu.length > 0) { 44 param += `&${qu.join("&")}`; 45 } 46 if (arr[to.name] && !to.query.rule) { 47 param += `&rule=${arr[to.name]}`; 48 } 49 window.location.href = `/login?${param}`; 50 } 51 } else { 52 // 如果不需要權限放行 53 next(); 54 } 55 }) 56 57 export default RouterModel;
然后在登陸界面定位到微信授權
1 getCode() { 2 // 定位到微信授權,若是不需要授權可以在此處處理 3 let query = this.$route.query; 4 let param = `jumpUrl=${query.jumpUrl || '/home'}`; 5 let path = window.location.origin; 6 // 登錄角色處理 7 let cfg = api[query.rule ? query.rule : 1]; 8 if (query.rule) { 9 param += `&rule=${query.rule}`; 10 } 11 12 let redirect_url = encodeURIComponent(`${path}/login?${param}`); 13 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`; 14 if (query.rule === 3) { 15 url += '&agentid=1000004'; 16 } 17 location.replace = url; 18 }
如果不是微信端,訪問到到rule規則的界面時,則會如下
而當微信授權通過的時候,rule權限不足的情況則會如下
8、使用vue-cli proxyTable進行反向代理,解決跨域問題
開發項目,在前后端聯調的時候肯定是會遇上跨域的問題的。很簡單,做個反向代理唄,對於想了解正向代理和反向代理的,請點擊這里
vue-cli腳手架搭建的工程中,在config/index.js文件中可以利用預留的proxyTable一項,設置地址映射表,如下
1 proxyTable: { 2 '/api': { 3 target: 'http://www.example.com', // your target host 4 changeOrigin: true, // needed for virtual hosted sites 5 pathRewrite: { 6 '^/api': '' // rewrite path 7 } 8 } 9 }
然后使用http-proxy-middleware插件對api請求地址進行代理
1 // proxy api requests 2 Object.keys(proxyTable).forEach(function (context) { 3 var options = proxyTable[context] 4 if (typeof options === 'string') { 5 options = { target: options } 6 } 7 app.use(proxyMiddleware(options.filter || context, options)) 8 })
三、移動端開發性能優化

對於這點,有一篇文章建議大家可以先看看《移動前端H5性能優化指南》。接下來我會結合實際項目抽幾個點配合代碼進行較為詳細的講解。
1、首屏渲染優化
決定用戶體驗最重要的一個點之一,這個點的重要性,相信不用我說了。下面直接談實戰。
-
減少資源請求次數
-
加載時使用過渡樣式,防止用戶網絡太差影響對首頁的體驗
-
圖片使用懶加載,這一part,我們目前項目中使用的vue的三方插件vue-lazyload,大致使用方法如下
1 // 全局注冊 2 import VueLazyload from 'vue-lazyload'; 3 4 Vue.use(VueLazyload, { 5 error: require('./assets/close.svg'), 6 loading: require('./assets/loading.svg'), 7 attempt: 1, 8 }); 9 10 // 使用 11 <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更簡潔。部分代碼如下
1 <style lang="stylus" scoped> 2 .chose-house { 3 height: 86px; 4 width: 64px; 5 margin 0 auto 6 position relative 7 &::before { 8 content: '\20'; 9 height: 100%; 10 width: 100%; 11 position: absolute; 12 left: 0; 13 top: 0; 14 background: url('../../assets/sprite-min.png') 0px 0px no-repeat; 15 } 16 } 17 </style>
3、路由懶加載
關於路由懶加載這一部分,尤大在vue-router文檔中也有所提及,鏈接點擊這里。
其實在vue項目中使用路由懶加載非常簡單,我們要做的就是把路由對應的組件定義成異步組件,代碼如下
/
1 /在router/index.js中引入組件時可使用如下異步方式引入 2 const Foo = resolve => { 3 // require.ensure 是 Webpack 的特殊語法,用來設置 code-split point 4 // (代碼分塊) 5 require.ensure(['./Foo.vue'], () => { 6 resolve(require('./Foo.vue')) 7 }) 8 } 9 // or 10 const Foo = resolve => require(['./Foo.vue'], resolve)
再將組件按組分塊,如
const Foo = r => require.ensure([], () => r(require('./Foo.vue')), 'group-foo')
實際項目中的代碼則如同我在章節《路由權限判定》提及到的一樣
1 const getWorkOrder = pageName => resolve => require(['../pages/WorkOrder'], pages => resolve(pages[pageName])) 2 3 let routers = []; 4 routers = [ 5 { 6 path: '/workorder', 7 name: 'workorder', 8 component: room, 9 children: [ 10 { 11 path: 'list', // 管家端工單列表 12 name: 'list', 13 rule: 3, 14 component: getWorkOrder('WorkOrderList') //WorkOrder, 15 }, 16 { 17 path: 'managerList', // 店長端工單列表 18 name: 'managerList', 19 rule: 6, 20 component: getWorkOrder('ManagerWorkOrder') // WorkOrder, 21 }, 22 ] 23 } 24 ]
如上將組件通過傳遞pageName參數分別打包到了各個chunk中,這樣每個組件加載時都只會加載自己對應的代碼,從而加快渲染速度!
4、全局組件按需注冊
當時我們為了優化首屏渲染速度,也是考慮到這一點,項目的src/main.js文件主要負責注冊全局組件,插件,路由,以及實例化Vue等。在webpack的配置里面也是當成entry入口進行了配置,如果我在main.js里面講每個組件都進行import的話,那么它將會全部一起注冊打包,頁面加載也會將每個組件文件都加載下來,這樣對渲染速度還是有一定影響的。
解決方法就是:按需注冊,這樣在打包的時候,會按需加載首頁(其他界面也同樣適用)使用到的全局組件。基本步驟如下:
將需要注冊的組件寫進components/base.js文件中,然后exports出來
1 exports.Foo = require('./Foo.vue'); 2 exports.Bar = require('./Bar.vue'); 3 exports.Baz = require('./Baz.vue');
在main.js中進行注冊
c
1 onst components = [ 2 require('./components/base').Foo, 3 require('./components/base').Bar, 4 require('./components/base').Baz, 5 ]; 6 7 components.map(component => { 8 Vue.component(component.name, component); 9 });
OK,大功告成!
以上便是我在最近的移動端項目實戰中的一些經驗總結,希望對各位小伙伴或多或少有些幫助吧!如果有幫助,別吝惜你手上的贊哦~
