出處:http://www.cnblogs.com/yexiaochai/p/4219523.html
前言
這幾天,第三輪全站優化結束,測試項目在2G首屏載入速度取得了一些優化成績,對比下來有10s左右的差距:
這次優化工作結束後,已經是第三次大規模折騰公司框架了,這裏將一些自己知道的移動端的建議提出來分享下,希望對各位有用
文中有誤請您提出,以免誤人自誤
技術選型
單頁or多頁
spa(single page application)也就是我們常常說的web應用程序webapp,被認爲是業內的發展趨勢,主要有兩個優點:
① 用戶體驗好
② 可以更好的降低服務器壓力
但是單頁有幾個致命的缺點:
① SEO支持不好,往往需要單獨寫程序處理SEO問題
② webapp本身的內存管理難,Javascript、Css非常容易互相影響
當然,這裏不是說多頁便不能有好的用戶體驗,不能降低服務器壓力;多頁也會有變量污染的問題發生,但造成webapp依舊是“發展趨勢”,而沒有大規模應用的主要原因是:
webapp模式門檻較高,很容易玩壞
其實webapp的最大問題與上述幾點沒有關係,實際上阻礙webapp的是技術門檻與手機性能,硬件方面不必多說,這裏主要說技術門檻。
webapp做的好,可以玩動畫,可以玩真正意義上的預加載,可以玩無縫頁面切換,從某些方面甚至可以媲美原生APP,這也是webapp受到追捧的原因。
但是,以上很容易被玩壞!因爲webapp模式不可避免的需要用到框架,站點需要一個切實可行的控制器來管理History以及頁面view實例化工作,於是大家會選擇諸如:
Backbone、angularJS、canJs之類的MVC框架,於是整個前端的技術要求被平白無故的提升了一個等級,原來操作dom可以做的事情,現在不一定能做了。
很多人對以上框架只停留在使用層面,幾輪培訓後,對底層往往感到一頭霧水,就算開發了幾個項目後,仍然還是隻能瞭解View層面的東西;有對技術感興趣的同事會慢慢了解底層,但多數仍然只關注業務開發,這個時候網站體驗便會受到影響,還讓webapp受到質疑。
所以這裏建議是:
① 精英團隊在公司有錢並且網站週期在兩年以上的話可以選用webapp模式
② 一般團隊還是使用多頁吧,坑不了
③ 更好的建議是參考下改變後的新浪微博,採用僞單頁模式,將網站分爲幾個模塊做到組件化開發,碰到差距較大的頁面便刷新也無不可
PS:事實上webapp模式的網站體驗確實會好一點
框架選擇
移動前端依舊離不開框架,而且框架呈變化狀態,以我廠爲例,我們幾輪框架選型是:
① 多頁應用+jQuery
② jQuery mobile(這個坑誰用誰知道)
③ 開始webapp模式(jQuery+requireJS+Backbone+underscore)
④ 瘦身(zepto+requireJS+Backbone View部分+underscore)
......
移動大潮來臨後,瀏覽器基本的兼容得到了保證,所以完整的jQuery變得不是那麼必須,因爲尺寸原因,所以一般被zepto替換,zepto與jQuery有什麼差異呢?
jQuery VS Zepto
首先,Zepto與jQuery的API大體相似,但是實現細節上差異甚大,我們使用Zepto一般完成兩個操作:
① dom操作
② ajax處理
但是我們知道HTML5提供了一個document.querySelectorAll的接口,可以解決我們90%的需求,於是jQuery的sizzle便意義不大了,後來jQuery也做了一輪優化,讓用戶打包時候選擇,需要sizzle才用。
其次jQuery的一些屬性操作上做足了兼容,比如:
el.css('transform', 'translate(-968px, 0px) translateZ(0px)')//jQuery會自動根據不同瀏覽器內核爲你處理爲:el.css('-webkit-transform', 'translate(-968px, 0px) translateZ(0px)')
又比如說,以下差異比比皆是:
el.hide(1000);//jQuery具有動畫,Zepto不會鳥你
然後,jQuery最初實現animate是採用js循環設置狀態記錄的方式,所以可以有效的記住狀態暫停動畫元素;Zepto的animate完全依賴於css3動畫,暫停需要再想辦法
// Zepto.js// (c) 2010-2014 Thomas Fuchs// Zepto.js may be freely distributed under the MIT license.;(function($, undefined){ var prefix = '', eventPrefix, endEventName, endAnimationName, vendors = { Webkit: 'webkit', Moz: '', O: 'o' }, document = window.document, testEl = document.createElement('div'), supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i, transform, transitionProperty, transitionDuration, transitionTiming, transitionDelay, animationName, animationDuration, animationTiming, animationDelay, c***eset = {} function dasherize(str) { return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase() } function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() } $.each(vendors, function(vendor, event){ if (testEl.style[vendor + 'TransitionProperty'] !== undefined) { prefix = '-' + vendor.toLowerCase() + '-' eventPrefix = event return false } }) transform = prefix + 'transform' c***eset[transitionProperty = prefix + 'transition-property'] = c***eset[transitionDuration = prefix + 'transition-duration'] = c***eset[transitionDelay = prefix + 'transition-delay'] = c***eset[transitionTiming = prefix + 'transition-timing-function'] = c***eset[animationName = prefix + 'animation-name'] = c***eset[animationDuration = prefix + 'animation-duration'] = c***eset[animationDelay = prefix + 'animation-delay'] = c***eset[animationTiming = prefix + 'animation-timing-function'] = '' $.fx = { off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined), speeds: { _default: 400, fast: 200, slow: 600 }, cssPrefix: prefix, transitionEnd: normalizeEvent('TransitionEnd'), animationEnd: normalizeEvent('AnimationEnd') } $.fn.animate = function(properties, duration, ease, callback, delay){ if ($.isFunction(duration)) callback = duration, ease = undefined, duration = undefined if ($.isFunction(ease)) callback = ease, ease = undefined if ($.isPlainObject(duration)) ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration if (duration) duration = (typeof duration == 'number' ? duration : ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000 if (delay) delay = parseFloat(delay) / 1000 return this.anim(properties, duration, ease, callback, delay) } $.fn.anim = function(properties, duration, ease, callback, delay){ var key, cssValues = {}, cssProperties, transforms = '', that = this, wrappedCallback, endEvent = $.fx.transitionEnd, fired = false if (duration === undefined) duration = $.fx.speeds._default / 1000 if (delay === undefined) delay = 0 if ($.fx.off) duration = 0 if (typeof properties == 'string') { // keyframe animation cssValues[animationName] = properties cssValues[animationDuration] = duration + 's' cssValues[animationDelay] = delay + 's' cssValues[animationTiming] = (ease || 'linear') endEvent = $.fx.animationEnd } else { cssProperties = [] // CSS transitions for (key in properties) if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') ' else cssValues[key] = properties[key], cssProperties.push(dasherize(key)) if (transforms) cssValues[transform] = transforms, cssProperties.push(transform) if (duration > 0 && typeof properties === 'object') { cssValues[transitionProperty] = cssProperties.join(', ') cssValues[transitionDuration] = duration + 's' cssValues[transitionDelay] = delay + 's' cssValues[transitionTiming] = (ease || 'linear') } } wrappedCallback = function(event){ if (typeof event !== 'undefined') { if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below" $(event.target).unbind(endEvent, wrappedCallback) } else $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout fired = true $(this).css(c***eset) callback && callback.call(this) } if (duration > 0){ this.bind(endEvent, wrappedCallback) // transitionEnd is not always firing on older Android phones // so make sure it gets fired setTimeout(function(){ if (fired) return wrappedCallback.call(that) }, (duration * 1000) + 25) } // trigger page reflow so new elements can animate this.size() && this.get(0).clientLeft this.css(cssValues) if (duration <= 0) setTimeout(function() { that.each(function(){ wrappedCallback.call(this) }) }, 0) return this } testEl = null})(Zepto)
View Code
其實,我們簡單從實現上就可以看出,Zepto這裏是偷懶了,其實現最初就沒有想考慮IE,所以winphone根本不能愉快的玩耍
zepto.Z = function(dom, selector) { dom = dom || [] dom.__proto__ = $.fn dom.selector = selector || '' return dom }
真實的差異還有很多,我這裏也沒法一一列出,這裏要說明的一個問題其實就是:
jQuery大而全,兼容、性能良好;Zepto針對移動端定製,一些地方缺少兼容,但是尺寸小
zepto設計的目的是提供jquery的類似的APIs,不以100%覆蓋jquery爲目的,一個5-10k的通用庫、下載並執行快、有一個熟悉通用的API,所以你能把你主要的精力放到應用開發上。
上圖是1.8版本與Zepto完整版的對比,Gzip在2G情況下20K造成的差距在2-5s之間,3G情況會有1s的差距,這也是我們選擇Zepto的原因,下面簡單介紹下Zepto。
Zepto清單
模塊 | 建議 | 描述 |
---|---|---|
zepto | Core module; contains most methods 核心模塊,包含初始化Zepto對象的實現,以及dom選擇器、css屬性操作、dom屬性操作 | |
event | Event handling via Zepto事件處理庫,包含整個dom事件的實現 | |
ajax | XMLHttpRequest and JSONP functionality Zepto ajax模塊的實現 | |
form | Serialize & submit web forms form表單相關實現,可以刪去,移動端來說意義不大 | |
ie | Support for Internet Explorer 10+ on the desktop and Windows Phone 8 這個便是爲上面那段實現還賬的,幾行代碼將方法屬性擴展至dom集合上(所以標準瀏覽器返回的是一個實例,ie返回的是一個加工後的數組) | |
detect | Provides 設備判斷,檢測當前設備以及瀏覽器型號 | |
fx | The animate方法,這裏叫fx模塊有點讓人摸不着頭腦 | |
fx_methods | Animated 一些jQuery有的方法,Zepto沒有的,這裏做修復,比如fadeIn fadeOut意義不大 | |
assets | Experimental support for cleaning up iOS memory after removing image elements from the DOM. 沒有實際使用過,具體用處不明 | |
data | A full-blown 數據存儲模塊 | |
deferred | Provides 神奇的deferred模塊,語法糖,爲解決回調嵌套而生 | |
callbacks | Provides 服務於deferred,實際未使用過 | |
selector | Experimental jQuery CSS extensions support for functionality such as 擴展選擇器,一些語法糖 | |
touch | X | Fires tap– and swipe–related events on touch devices. This works with both `touch` (iOS, Android) and `pointer` events (Windows Phone). 提供簡單手勢庫,這個大坑,誰用誰知道!!!幾個有問題的地方: ① 事件直接綁定至document,性能浪費 ② touchend時候使用settimeOut導致event參數無效,所以preventDefault無效,點透等情況也會發生 |
gesture | Fires pinch gesture events on touch devices 對原生手勢操作的封裝 | |
stack | Provides 語法糖,鏈式操作 | |
ios3 | String.prototype.trim and Array.prototype.reduce methods (if they are missing) for compatibility with iOS 3.x. 沒有用過 |
你真實項目時,完全可以按照需要選取模塊即可,下面簡單再列幾個差異:
其它差異
① selector
如上所述,Zepto的選擇器只是jQuery的一個子集,但是這個子集滿足我們90%的使用場景
② clone
Zepto的clone不支持事件clone,這句話的意思是dom clone後需要自己再處理事件,舉個例子來說:
var el = $('.el'); el.on('click', function() { alert(1) })
1 //true的情況jQuery會連帶dom事件拷貝,Zepto沒有做這個處理2 //jQuery庫,點擊clone的節點會打印1,Zepto不會3 4 var el1 = el.clone(true);5 $('#wrap').append(el1);
這個差異還比較好處理,現在都會使用事件代理,所以沒clone事件也在沒問題的......
這裏簡單看看細節實現:
1 clone: function (elem, dataAndEvents, deepDataAndEvents) { 2 var i, l, srcElements, destElements, 3 clone = elem.cloneNode(true), 4 inPage = jQuery.contains(elem.ownerDocument, elem); 5 6 // Fix IE cloning issues 7 if (!support.noCloneChecked && (elem.nodeType === 1 || elem.nodeType === 11) && 8 !jQuery.isXMLDoc(elem)) { 9 10 // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/211 destElements = getAll(clone);12 srcElements = getAll(elem);13 14 for (i = 0, l = srcElements.length; i < l; i++) {15 fixInput(srcElements[i], destElements[i]);16 }17 }18 19 // Copy the events from the original to the clone20 if (dataAndEvents) {21 if (deepDataAndEvents) {22 srcElements = srcElements || getAll(elem);23 destElements = destElements || getAll(clone);24 25 for (i = 0, l = srcElements.length; i < l; i++) {26 cloneCopyEvent(srcElements[i], destElements[i]);27 }28 } else {29 cloneCopyEvent(elem, clone);30 }31 }32 33 // Preserve script evaluation history34 destElements = getAll(clone, "script");35 if (destElements.length > 0) {36 setGlobalEval(destElements, !inPage && getAll(elem, "script"));37 }38 39 // Return the cloned set40 return clone;41 },42 function cloneCopyEvent(src, dest) {43 var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;44 45 if (dest.nodeType !== 1) {46 return;47 }48 49 // 1. Copy private data: events, handlers, etc.50 if (dataPriv.hasData(src)) {51 pdataOld = dataPriv.access(src);52 pdataCur = dataPriv.set(dest, pdataOld);53 events = pdataOld.events;54 55 if (events) {56 delete pdataCur.handle;57 pdataCur.events = {};58 59 for (type in events) {60 for (i = 0, l = events[type].length; i < l; i++) {61 jQuery.event.add(dest, type, events[type][i]);62 }63 }64 }65 }66 67 // 2. Copy user data68 if (dataUser.hasData(src)) {69 udataOld = dataUser.access(src);70 udataCur = jQuery.extend({}, udataOld);71 72 dataUser.set(dest, udataCur);73 }74 }
jQuery clone
1 clone: function(){2 return this.map(function(){ return this.cloneNode(true) })3 },
下面是Zepto的clone實現,我啥也不說了,爲什麼jQuery這麼大呢,是有道理的。
③ data
Zepto的data只能存儲字符串,你想存儲複雜對象的話便把他先轉換爲字符串
④ offset
el.offset()//Zepto返回Object {left: 8, top: 8, width: 485, height: 18}//jQuery返回Object {top: 8, left: 8}
getBoundingClientRect 函數是W3C組織在第一版本的W3C CSSOM View specification草案中確定的一個標準方法,在此之前,只有IE瀏覽器是支持該方法的,W3C在這次草案中把它扶正成爲標準。
getBoundingClientRect 方法返回的是調用該方法的元素的TextRectangle對象,該對象具有top、left、right、bottom四個屬性,分別代表該元素上、左、右、下四條邊界相對於瀏覽器窗口左上角(注意,不是文檔區域的左上角)的偏移像素值。
1 offset: function(coordinates){ 2 if (coordinates) return this.each(function(index){ 3 var $this = $(this), 4 coords = funcArg(this, coordinates, index, $this.offset()), 5 parentOffset = $this.offsetParent().offset(), 6 props = { 7 top: coords.top - parentOffset.top, 8 left: coords.left - parentOffset.left 9 }10 11 if ($this.css('position') == 'static') props['position'] = 'relative'12 $this.css(props)13 })14 if (this.length==0) return null15 var obj = this[0].getBoundingClientRect()16 return {17 left: obj.left + window.pageXOffset,18 top: obj.top + window.pageYOffset,19 width: Math.round(obj.width),20 height: Math.round(obj.height)21 }22 },
Zepto offset
offset: function (options) { if (arguments.length) { return options === undefined ? this : this.each(function (i) { jQuery.offset.setOffset(this, options, i); }); } var docElem, win, elem = this[0], box = { top: 0, left: 0 }, doc = elem && elem.ownerDocument; if (!doc) { return; } docElem = doc.documentElement; // Make sure it's not a disconnected DOM node if (!jQuery.contains(docElem, elem)) { return box; } // Support: BlackBerry 5, iOS 3 (original iPhone) // If we don't have gBCR, just use 0,0 rather than error if (typeof elem.getBoundingClientRect !== strundefined) { box = elem.getBoundingClientRect(); } win = getWindow(doc); return { top: box.top + win.pageYOffset - docElem.clientTop, left: box.left + win.pageXOffset - docElem.clientLeft }; },
jQuery offset
差距不大,jQuery的更加嚴謹,總會做很多兼容,jQuery大是有道理的
MVC框架選擇
MVC框架流行的有Backbone、angularJS、reactJS、canJS等,我個人比較熟悉Backbone與canJS,近期也在整理canJS的一些筆記
首先提一下Backbone,我認爲其最優秀的就是其View一塊的實現,Backbone的View規範化了dom事件的使用,避免了事件濫用,避免了事件“失效”
但是Backbone的路由處理一塊很弱,事實上一點用也沒有,而且就算view一塊的繼承關係也非常難以處理,extend實現是:
1 var extend = function (protoProps, staticProps) { 2 var parent = this; 3 var child; 4 5 // The constructor function for the new subclass is either defined by you 6 // (the "constructor" property in your `extend` definition), or defaulted 7 // by us to simply call the parent's constructor. 8 if (protoProps && _.has(protoProps, 'constructor')) { 9 child = protoProps.constructor;10 } else {11 child = function () { return parent.apply(this, arguments); };12 }13 14 // Add static properties to the constructor function, if supplied.15 _.extend(child, parent, staticProps);16 17 // Set the prototype chain to inherit from `parent`, without calling18 // `parent`'s constructor function.19 var Surrogate = function () { this.constructor = child; };20 Surrogate.prototype = parent.prototype;21 child.prototype = new Surrogate;22 23 // Add prototype properties (instance properties) to the subclass,24 // if supplied.25 if (protoProps) _.extend(child.prototype, protoProps);26 27 // Set a convenience property in case the parent's prototype is needed28 // later.29 child.__super__ = parent.prototype;30 31 return child;32 };
View Code
child.__super__ = parent.prototype;
這是一段極爲糟糕的設計,他是將parent原型的指向給到了類的的屬性上,這裏可以看做靜態方法,那麼我在實際使用的時候要如何使用呢?
我在內部原型鏈上或者實例方法一般使用this便能指向本身,但是卻不能執行本類的方法,如果要使用指向構造函數我需要這麼做:
this.constructorthis.constructor.__super__
如果我這裏想要執行父類的一個方法,還得關注起作用域指向,於是只能這樣寫
this.constructor.__super__.apply(this, arguments)
而我總是認爲javascript的construct未必非常靠譜,於是整個人都不好了,所以在一輪使用後,基本便放棄Backbone了,但是Backbone優秀的一面也不能抹殺,我們可以借鑑Backbone實現一些更加適合項目的基礎架子
Backbone另一個令人詬病的地方是其插件少,其實這裏有點苛刻,移動端才興起不久,webapp的項目又少,這裏沒有是很正常,別人的插件也未必能用的順心。
angularJs我本身沒有實際使用過,不好評價,根據一些朋友的實際使用情況可以得出一個結論:
規定的非常死,業務代碼可保持一致,入門簡單深入難,一旦出現問題,不太好改,對技術要求較高
這裏各位根據實際情況選擇就好,我這裏的建議還是自己讀懂一個MV*的框架,抽取需要的重寫,像angularJS一次升級,之前的項目如何跟着升級,這些問題很頭疼也很實際。
上次抱着解決webappSEO難題時候對reactJS有所接觸,其源碼洋洋灑灑10000行,沒有一定功力與時間還是暫時不碰爲好。
canJS學習成本與Backbone差不多,我這邊準備出系列學習筆記,好不好後面調研再說。
總結一句:不建議直接將業務庫框架直接取來使用,更不建議使用過重的業務框架,最好是能明白框架想要解決的問題,與自己項目的實際需求,自己造輪子知根知底。
框架建議
最好給出一個小小的建議,希望對各位有用:
第三方庫(基礎庫):
requireJS+Zepto+閹割版underscore(將其中不太用到的方法去掉,主要使用模板引擎一塊)+ Fastclick
MVC庫/UI庫:
建議自己寫,不要太臃腫,可以抄襲,可以借鑑,不要完全拿來就用
這樣出來的一套框架比較輕量級,知根知底,不會出現改不動的情況,最後提一句:不經過調研,沒有實際場景在框架中玩模式,玩高級理念死得快,不要爲技術而技術。
網站是如何變慢的?
尺寸——慢的根源
兵無定勢,水無常形,按照之前所說,我們選取了對我們最優的框架,做出來的網站應該很快,但第一輪需求結束後有第二輪,第二輪需求結束後有第三輪,網站版本會從1.1-X.1,業務的增長以及市場份額的角力帶來的是一月一發布,一季一輪替,沒有不變的道理。
框架最大的敵人是需求,代碼最大的敵人是變更,最開始使用的是自己熟悉的技術,突然一天多出了一些莫名其妙的場景:
① webapp模式很不錯,爲了快速業務發展,將接入Hybrid技術,並且使用一套代碼
② 微信入口已經很火了,爲了快速業務發展,將接入微信入口,並且使用一套代碼
③ UI組件已經舊了,換一批ios8風格的組件吧
④ 全站樣式感覺跟不上潮流了,換一套吧
網站變慢的核心原因是尺寸的膨脹,尺寸優化纔是前端優化的最重要命題,①、②場景是不可預知場景,面對這種不可預知場景,會寫很多橋接的代碼,而這類代碼往往最後都會證明是不好的!
框架首次處理未知場景所做的代碼,往往不是最優的,如Hybrid、如微信入口
剩下兩個場景是可預見的改變,但是此類變更會帶來另一個令人頭疼的問題,新老版本交替。業務20多個業務團隊,不可能一個版本便全部改變,便有個逐步推進的過程。
全站樣式替換/對未知場景的代碼優化,很多時候爲了做到透明,會產生冗餘代碼,爲了做兼容,常常有很長一段時間新老代碼共存的現象
於是不可預知造成的尺寸膨脹,經過重構優化,而爲了做兼容,居然會造成尺寸進一步的增加
所謂優化不一定馬上便有效果,開發人員是否扛得住這種壓力,是否有全團隊推動的能力會變得比本身技術能力更加重要
事實上的情況複雜的多,以上只是一廂情願的以“接口統一”、“透明升級”爲前提,但是透明的代價是要在重構代碼中做兼容,而兼容又本身是需要重構掉的東西,當兼容產生的代碼比優化還多的時候,我們可能就會放棄兼容,而提供一套接口完全不統一的東西;更加真實情況是我們根本不會去做這種對比,便直接將老接口廢掉,這個時候造成的影響是“天怒人怨”,但是我們爽了,爽了的代價是單個團隊的推動安撫。
這裏請參考angularJS升級,新浪微博2.0接口與1.1不兼容問題,這裏的微信接口提出,難保一年後不會完全推翻......
所以,尺寸變大的主要原因是因爲冗餘代碼的產生,如何消除冗餘代碼是一個重點,也是一個難點。
版本輪替——哪些能刪的痛點
數月後,20多個團隊悉數切入到最新的框架,另一個令人頭疼的問題馬上又出來了,雖然大家樣式都接入到最新的風格了,但是老的樣式哪些能刪?哪些不能刪又是一個令人頭疼的問題。
幾個月前維護CSS同事嫌工資低了,換了一個同事維護全站基礎css;再過了一段時間,組織架構調整,又換了一個同事維護;再過了一段時間,正在維護css的同事覺得自己級別低了,在公司內部等待晉級確實熬不住,於是也走了。這個基礎css儼然變成了一筆爛賬,誰也不敢刪,誰也不願意動,動一下錯一下。
這個問題表面上看是一個css問題,其實這是一個前端難題,也是過度解耦,拆分機制不正確帶來的麻煩。
CSS是前端不可分割的一部分,HTML模板與Javascript可以用requireJS處理,很大程度上解決了javascript變量污染的問題,css一般被一起分離了出來,單獨存放。一個main.css包含全站重置的樣式,表單、列表、按鈕的基礎樣式,完了就是全站基礎的UI組件。
總有業務團隊在實際做項目時會不自主的使用main.css中的一些功能,如果只是使用了基礎的重置還好,但是一旦真的使用其中通用的表單、列表等便2B了
main.css的初衷當然是將各個業務團隊通用的部分提煉出來,事實上也該這樣做,但理想很豐滿,現實很殘酷,不同的人對SEO、對語義化對命名的理解不太一樣,換一個人就會換一套東西。第一批項目上線後,過了幾個月,開發人員成長非常巨大,對原來的命名結構,完全不削一顧,自己倒騰出一套新的東西,讓各個團隊換上去,其它團隊面對這種要求是及其頭疼的,因爲各個團隊會有自己的CSS團隊,這樣一搞勢必該業務團隊的HTML結構與CSS要被翻新一次,這樣的意義是什麼,便不太明顯了。2個星期過去了,新一批“規範化”的結構終於上線了,2個月後所有的業務團隊全部接了新的結構,似乎皆大歡喜,但是那個同事被另一個團公司挖過去當前端leader了,於是一大羣草泥馬正在向業務團隊的菊花奔騰過去!這裏的建議是:
業務團隊不要依賴於框架的任何dom結構與css樣式,特別不要將UI組件中的dom結構與樣式單獨摳出來使用,否則就準備肥皂吧
CSS冗餘的解決方案
對前端具有實際推動作用的,我覺得有以下技術:
① jQuery,解決IE時代令人頭疼的兼容問題
② 移動浪潮,讓HTML5與CSS3流行起來
③ requireJS,模塊化加載技術讓前端開發能協同作戰,也一定限度的避免了命名污染
④ Hybrid,Hybrid技術將前端推向了一個前所未有的高度,這門技術讓前端肆無忌憚的侵佔着native的份額
如果說接下來會有一門技術會繼續推動前端技術發展,有可能是web components,或者出現了新的設備。
web component是前端幾項技術的融合,裏面有一項功能爲shadow dom,shadow dom是一種瀏覽器行爲,他允許在document文檔中渲染時插入一個獨立的dom子樹,但這個dom樹與主dom樹完全分離的,不會互相影響。以一個組件爲例,是這個樣子的:
一個組件就只有一個div了,這是一件很棒的事情,但實際的支持情況不容樂觀:
然後web components還會有一些附帶的問題:
① css與容器一起出現,而沒有在一個文件中,在很多人看來很“奇怪”,我最初也覺得有點怪
② 大規模使用後,用於裝載HTML的容器組件如何處理,仍然沒有一個很好的方案
③ 對於不支持的情況如何做降級,如何最小化代碼
④ 沒有大規模使用的案例,至少國內沒有很好的驗證過
其中shadow dom思想也是解決css重複的一個辦法,以一個頁面爲例,他在原來的結構是這個樣子的:
main.css view1.js view1.html view2.js view2.css 開發的時候是這個樣子: view1.css view1.js view1.html 最終發佈是這個樣子: view1.js
這一切歸功於requireJS與grunt打包工具,這裏給一個實際的例子:
這裏最終會被打包編譯爲一個文件:
這樣的話版本UI升級只與js有關係,requireJS配置即可,這裏只是UI的應用,很容易便可以擴展到page view級別,使用得當的話媽媽再也不用關心我們的版本升級以及css冗餘了
這裏處理降級時,會給css加前綴,如一個組件id爲ui,其中的css會編譯爲 #ui * {}#ui div {}由於css選擇器是由右至左的,這種代碼產生的搜索消耗是一個缺點,但是與尺寸的降低比起來便不算什麼
網絡請求
請求是前端優化的生命,優化到最後,優化到極致,都會在請求數、請求量上做文章,常用並且實用的手段有:
① CSS Sprites
② lazyload
③ 合併腳本js文件
④ localsorage
......
無論CDN還是Gzip,都是在傳輸上做文章,金無足赤,月無常圓,以上技術手段皆有其缺陷,是需要驗證的,如何正確恰當的使用,我這裏談下我的理解
CSS Sprites
CSS Sprites可以有效的減低請求數,偶爾還可以降低請求量,但是隨着發展,可能會有以下問題:
① 新增難,特別是css維護工作換人的情況下
② 刪除難,這個問題更加明顯,1年後,前端風格已經換了兩批了,這裏要知道哪些圖標還在用,哪些沒用變得非常困難
③ 調整難,一個圖標剛開始是紅色,突然需要變成藍色,這類需求會讓這個工作變得不輕鬆
④ 響應式,這個更會導致指數級的增長,背景圖要隨着寬度縮放這種需求更加討厭
這裏放一張做的很好的圖:
由圖所示,這裏是對尺寸做了一定區分的,但是這裏仍然不是最優,其實以上很多圖標可以直接由CSS3實現,這裏舉兩個案例:
http://iconfont.cn/repositories(svg)
http://codepen.io/saeedalipoor/pen/fgiwK(CSS3)
這裏優劣之分各位自己判斷,我反正完全偏向了CSS3......
爲什麼要降低請求數
請求消耗
每次http請求都會帶上一些額外信息,比如cookie每次都會帶上,上述的CSS Sprites的意義就是,當請求一個gzip後還不到1K的圖標,搞不好請求數據比實際需求數據還大
而一次http還會導致其它開銷,每次都會經歷域名解析、開啓連接、發送請求等操作,以一個圖片請求在正常網速與2G情況來說:
可以看到,在網速正常的情況下,等待消耗的時間可能比傳輸還多,這個時候,CSS Sprites的意義就馬上出來了,這裏再說一個問題並行加載的問題。
瀏覽器併發數
我之前碰到一次圖片加載阻塞js的案例,其出現原因就是瀏覽器併發數限制,這裏以一個圖爲例:
chrome在請求資源下會有所限制,移動端的限制普遍在6個左右,這個時候在併發數被佔滿時,你的ajax便會被擱置,這在webapp中情況更加常見,所以網絡限制的情況下請求數控制是必要的,而且可以降低服務器端的壓力。
離線存儲
工作中實際使用的離線緩存有localstorage與Application cache,這兩個皆是好東西,一個常用於ajax請求緩存,一個常用於靜態資源緩存,這裏簡單說下我的一些理解。
localstorage
首先localsorage有500萬字符的限制,基本來說就是5M左右的限制,瀏覽器各有不同,也會有讀寫的性能損耗,所以不能毫無限制的使用
localstorage不被爬蟲識別,不能跨域共享,所以不要用以存儲業務關鍵信息,更加不要存儲安全信息,要做到有,錦上添花;無,毫無影響才行:
⑦ localstorage讀寫有性能損耗,大數據讀寫要避免
Application cache
Application cache是HTML5新增api,雖然都是存儲,卻與localstorage、cookie不太相同,Application cache存儲的是一般是靜態資源,允許瀏覽器請求這些資源時不必通過網絡,設計得當的情況可以取代Hybrid的存儲靜態資源,使用Application cache主要優點是:
使用Application cache可以提升網站載入速度,主要體現在請求傳輸上,把一些http請求轉爲本地讀取,有效地降低網絡延遲,降低http請求,使用簡單,還節約流量何樂而不爲?
而無論什麼存儲技術都會有空間限制(據說是5M),這裏更新的機制是最爲重要的,這裏是我們使用的結論:
application cache是絕對值得使用的,是可以錦上添花。但怎麼用,用多少是需要考慮的點。由於原理上,application cache是把manifest上的資源一起下載下來,所以manifest裏的內容不宜過多,數據量不宜過大;由於manifest的解析通常以頁面刷新爲觸發點,且更新的緩存不會立即被使用,所以緩存的資源應以靜態資源、更新頻率比較低的資源爲主。另外要做好對manifest文件的管理,由於清單內文件不可訪問或manifest更新不及時造成的一些問題。
快的假象
除了真實手段優化代碼處理尺寸,降低請求數,仍然有一些帶有“欺騙”性質的技術可以做首頁加載的優化,比如lazyload、fake頁
lazyload
我們常說的延遲加載是圖片延遲加載,其實非圖片也可延遲加載,看實際需求即可,這裏點到即可,不再多說。
爲img標籤src設置統一的圖片鏈接,而將真實鏈接地址裝在自定義屬性中。 所以開始時候圖片是不會加載的,我們將滿足條件的圖片的src重置爲自定義屬性便可實現延遲加載功能
fake頁
我們應該避免頁面長時間白頁,所以會出現fake頁的概念,頁面渲染僅僅需要HTML以及CSS,這個便是第一個優化點,js對於顯示不是必須,ajax也不是。
若是任由js、ajax加載完成再渲染頁面,用戶很有可能失去耐心,所以搞一些內嵌的css以及通用的html在首頁似乎是一個不錯的選擇
一個靜態HTML頁面,裝載首屏的基本內容,讓首頁快速顯示,然後js加載結束後會馬上重新渲染整個頁面,這個樣子,用戶就可以很快的看到頁面響應,給用戶一個快的錯覺
預加載
這裏的預加載是在瀏覽器空閒的時候加載後續頁面所需資源,是一種浪費用戶流量的行爲,屬於以空間換時間的做法,但是這個實施難度比較高。
預加載的前提是不影響主程序的情況下偷偷的加載,也就是在瀏覽器空閒的時候加載,但是瀏覽器空閒似乎變得不可控制
瀏覽器空閒不可判斷(如果您知道請留言),我們判斷的標準是當前沒有dom事件操作,沒有ajax
可以看出,由於瀏覽器沒有空閒的回調,所以我們只能自己實現,這類的實現不太靠譜,我們的預加載做的就很粗暴,要做預加載需要注意以下幾點:
① 瀏覽器空閒需要一個判斷機制 ② 每次空閒時需要有一個隊列一點一點的加載資源,否則請求一旦發出很容易影響主邏輯 ③ 做好預加載資源隊列的匹配算法,可以是業務團隊配置
移動革命——Hybrid
Hybrid技術將前端推到了前所未有的高度,但是Hybrid開發中本身也有一些需要注意的地方,這裏如果出現了設計上的失誤會對後期業務團隊開發帶問題,有幾點可以注意
拒絕native UI
最初的app一般是native開發的,Hybrid依然依賴於native開發人員,但是請一定拒絕任何native爲webview提供任何業務類UI,強勢的對native說不!!!
最常見的的情況是,native爲前端提供一個native的頭,下面是一個webview裝載html與css,這個是一件非常坑的事情
Hybrid中使用native的頭,是我覺得最頭疼的事情!!!
爲什麼會使用native的頭呢?當時交涉的結果是:
① javascript容易報錯,一旦出錯,頁面會陷入假死 ② 進入webview時,頁面有一個準備動作,資源由native取很快,由線上取很慢;無論如何會出現一段時間的白頁
其實上述皆是可以解決的,Hybrid中會存在native頭的主要原因還是防止頁面亂寫js出錯,但是一般意義的app不是微信這類容器軟件,裏面的頁面是開發人員經過嚴格測試寫出來的,js出錯會假死,native代碼出錯還會閃退呢。問題一,站不住腳,而且完全可以使用這種方式處理:
1 <header >2 <a class="header" href="taobao://wireless">後退</a>3 <h1 class="js_title">4 標題5 </h1>6 </header>
就算是js報錯,我這裏假設一來就報錯,處處報錯,但以上協議native是一定可以捕捉的,js正確的情況便e.preventDefault(),錯誤便跳回首頁,這個不是不可處理。
問題二其實與問題一一致,最初進入的時候明明可以有個可關閉的native loading,在webview加載好後再系統級別的關閉loading即可,沒有什麼不能解決的。
之所以我這裏會如此激烈的拒絕native提供的頭,是因爲H5頁面是一般是三套共用,H5站點,ios,android,而H5的dom操作千變萬化,頭部一些奇怪的需求展示,native根本無從支持,這裏還會涉及跨團隊合作,所以Hybrid開始的時候一定要堅決抵制native 提供的業務類UI,不然後期交流很麻煩。
交互模型
你永遠不能理解服務器端爲什麼會一次性給你那麼多數據,所以你也不能理解設計一個好的Hybrid交互模型爲什麼這麼難!程序員爲什麼總是互相傷害?
簡單來說,Hybrid的交互非常簡單,與ajax交互模型非常相似,這裏以一張簡單的交互圖做說明:
交互的核心是native可以拿到webview的window對象,native可以攔截webview的http請求,於是native便可以幹任何事情了
因爲Hybrid攔截URL各有不同,IOS、android、winphone要做兼容,以window.location設置,創建iframe發出請求。但是,這段兼容的js代碼一定不能交給native的同事寫,必須自己寫!否則500行代碼可以解決的問題,你會發現半年後可能會洋洋灑灑變成幾千行,因爲他們不關注尺寸,不熟悉js....
我這裏有一個簡單的交互代碼,可以參考:
Hybrid調用H5,直接拿到window對象,拿到對應方法即可,H5調用native方法略有不同,比如要拿手機通訊錄可以這樣做:
1 window.Hybrid = {}; 2 3 //封裝統一的發送url接口,解決ios、android兼容問題,這裏發出的url會被攔截,會獲取其中參數,比如: 4 //這裏會獲取getAdressList參數,調用native接口回去通訊錄數據,形成json data數據,拿到webview的window執行,window.Hybrid['hybrid12334'](data) 5 var bridgePostMessage = function (url) { 6 if (isIOS()) { 7 window.location = url; 8 } if (isAndriond()) { 9 var ifr = $('<iframe src="' + url + '"/>');10 $('body').append(ifr);11 }12 };13 14 //根據參數返回滿足Hybrid條件的url,比如taobao://getAdressList?callback=hybrid1233415 var _getHybridUrl = function (params) {16 var url = '';17 //...aa操作paramss生成url18 return url;19 };20 21 //頁面級用戶調用的方法22 var requestHybrid = function (params) {23 //其它操作......24 25 //生成唯一執行函數,執行後銷燬26 var t = 'hybrid_' + (new Date().getTime());27 //處理有回調的情況28 if (params.callback) {29 window.Hybrid[t] = function (data) {30 params.callback(data);31 delete window.Hybrid[t];32 }33 }34 35 bridgePostMessage(_getHybridUrl(params))36 };37 38 //h5頁面開發,調用Hybrid接口,獲取通訊錄數據39 define([], function () {40 return function () {41 //業務實際調用點42 requestHybrid({43 //native標誌位44 tagname: 'getAdressList',45 //返回後執行回調函數46 callback: function (data) {47 //處理data,生成html結構,裝載頁面48 }49 });50 }51 });
當然這個代碼比較簡單,未做一些兼容一些處理,但是完全滿足Hybrid交互模型,這裏返回的json data再有處理,我們這裏便可以設計success、error等回調。你完全想不到真實的js會到達幾千行之巨,這些都是跨部門交流的讓步與疼痛啊!
其它
Hybrid的調試
其實H5的調試就已經是一個老大難問題,Hybrid讓這種場景變得更加複雜,chrome本身提供了一些移動端的調試方法,但是ios未越獄的話不好處理
而標準的公司中又會對ip有所限制,所以使用ip調試也比較麻煩,設置代理也費時費力,這個時候便需要更高級別的人站出來角力了,這塊老大難問題不同公司還不一樣,事實上我也犯難......
① ip調法,手機使用無線連接公司內網,使用手機瀏覽器打開網頁,改一個代碼,刷新一下,不行就代理,通不過就叫leader去推動安全部門開啓特殊端口 ② ios高端調法,具有Mac機情況下手機連接Safari可調速,我用過幾次,但是由於沒有mac機,實際步奏忘了... ③ android機低端調試,android可以直接開啓root權限,使用chromeF12開發者工具調試
關於移動端調試的文章很多,各位去看看有用的吧......
多webview
事實證明多webview在低端android機上很卡,慎用。高端機多webview乾的頁面切換的活CSS3也能做,多webview意義不大
PS:來百度後,發現多webview卡的原因可能是native方的實現有問題,此段存疑
1 多webview與多iframe很類似,webview是一個很重的native空間,一上來就吃掉4M存儲
2 單webview共享一個window對象,document共享,多webview通信機制有門檻,雖然localstorage共享,但通信依舊不方便
3 webview裝載html依舊會有閃現的問題,跳轉難度高
多webview的意義是:
① 很好的頁面切換效果
② 釋放javascript執行環境,以便降低內存
但是目的一依舊會閃,目的二使內存更加吃緊,費力不討好
不恰當的需求
移動端會有一些不恰當的需求,這類需求看似無關重要,卻會對整個移動框架造成隱患,甚至影響整體驗。
喚醒app
移動端第一個噁心需求就算H5網頁喚醒app操作,這個需求一般會出現在頁面底部的廣告欄,比如這個樣子:
如果僅僅是喚醒app倒是簡單,隨之而來的需求是:
① H5站點檢測是否安裝app(尼瑪js如何判斷?),安裝便打開,沒安裝便跳到下載頁 ② 需求變更,ios去AppStore,android強制下載 ③ bug迴歸,android老是強制下載,希望可以判斷,未安裝才下載 ......
總而言之,需求的核心難點就是,H5站點檢測app是否安裝,這個時候你要站出來大聲的告訴產品:
① 純粹js暫時無法判斷app是否安裝
② 前端只能做喚醒的工作或者跳到下載頁的需求,強制下載什麼類似需求請不予理睬
回退關閉彈出層
這個一般會有兩個需求,點擊瀏覽器回退關閉彈出層(框架提供的alert、toast、loading之類),點擊android回退鍵關閉彈出層
如果碰到這個需求,我建議你還是直接拒絕掉,對於UI來說,這類操作會帶來一個信號,js完成這個功能需要操作History
對於多頁來說,這個功能還好點,對於單頁來說,這個步驟便會破壞webapp耐以生存的History隊列,伴隨着可能是回退錯亂,可能是中間頁循環......
webapp的History本就很脆弱,這樣一搞很容易出BUG,有信心處理好History問題的話去實現,否則還是算了吧......
全站IScroll化
全站IScroll化一般爲了解決:
① fixed問題
② webapp中view獨享“scrollTop”
③ webapp page 切換動畫順暢,因爲scrollTop與長短頁問題
④ 嫌棄原生的scroll不夠平滑
這裏還是不建議全站使用IScroll這類技術,IScroll可能帶來,header消失、文本框消失、可視區域便小等問題,現在還是小範圍彈出層使用就好,某天overflow: scroll兼容問題得到解決,區域滾動便不再難了。
這裏倒不是一味抵制IScroll全站化,如果頁面dom結構簡單,如果頁面文本框比較少,又做過充分調研,IScroll化帶來的頁面切換效果還是很讚的,正是道不虛行,只在人也。
結語
文章淺談了一些自己對移動端從開發到優化的一些建議,沒有什麼高深的知識,也許還有很多錯誤的地方,請各位不吝賜教,多多指點,這裏總結一下幾個比較重要的地方:
一 單頁門檻高,體驗好 二 移動框架,輕爲王道 三 mvc業務框架最好自造 四 模塊化(requireJS)必不可少 五 冗餘是優化的敵人,無論網站速度還是代碼維護 六 css解耦乃長遠之計 七 零請求無流量是優化的最終手段 八 速度優化緩存爲王 九 Hybrid帶來移動革命,與native保持接口調用即可 十 坑大的需求還是拒絕算了......
核心點
我的微博粉絲及其少,如果您覺得這篇博客對您哪怕有一絲絲的幫助,微博求粉!!!