淺談移動前端的最佳實踐

前言

這幾天,第三輪全站優化結束,測試項目在2G首屏載入速度取得了一些優化成績,對比下來有10s左右的差距:javascript

此次優化工做結束後,已是第三次大規模折騰公司框架了,這裏將一些本身知道的移動端的建議提出來分享下,但願對各位有用css

文中有誤請您提出,以避免誤人自誤html

技術選型

單頁or多頁

spa(single page application)也就是咱們經常說的web應用程序webapp,被認爲是業內的發展趨勢,主要有兩個優勢:前端

① 用戶體驗好java

② 能夠更好的下降服務器壓力node

可是單頁有幾個致命的缺點:react

① SEO支持很差,每每須要單獨寫程序處理SEO問題jquery

② webapp自己的內存管理難,Javascript、Css很是容易互相影響android

固然,這裏不是說多頁便不能有好的用戶體驗,不能下降服務器壓力;多頁也會有變量污染的問題發生,但形成webapp依舊是「發展趨勢」,而沒有大規模應用的主要緣由是:ios

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, cssReset = {} 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' cssReset[transitionProperty = prefix + 'transition-property'] = cssReset[transitionDuration = prefix + 'transition-duration'] = cssReset[transitionDelay = prefix + 'transition-delay'] = cssReset[transitionTiming = prefix + 'transition-timing-function'] = cssReset[animationName = prefix + 'animation-name'] = cssReset[animationDuration = prefix + 'animation-duration'] = cssReset[animationDelay = prefix + 'animation-delay'] = cssReset[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(cssReset) 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 on() & off()

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 $.os and $.browser information

設備判斷,檢測當前設備以及瀏覽器型號

fx  ✔

The animate() method

animate方法,這裏叫fx模塊有點讓人摸不着頭腦

fx_methods  

Animated showhidetoggle, and fade*() methods.

一些jQuery有的方法,Zepto沒有的,這裏作修復,好比fadeIn fadeOut意義不大

assets  

Experimental support for cleaning up iOS memory after removing image elements from the DOM.

沒有實際使用過,具體用處不明

data  

A full-blown data() method, capable of storing arbitrary objects in memory.

數據存儲模塊

deferred  

Provides $.Deferred promises API. Depends on the "callbacks" module.

神奇的deferred模塊,語法糖,爲解決回調嵌套而生

callbacks  

Provides $.Callbacks for use in "deferred" module.

服務於deferred,實際未使用過

selector   ✔

Experimental jQuery CSS extensions support for functionality such as$('div:first') and el.is(':visible').

擴展選擇器,一些語法糖

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 andSelf & end() chaining methods

語法糖,鏈式操做

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/2 11 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 clone 20 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 history 34 destElements = getAll(clone, "script"); 35 if (destElements.length > 0) { 36 setGlobalEval(destElements, !inPage && getAll(elem, "script")); 37  } 38 39 // Return the cloned set 40 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 data 68 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 null 15 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 calling 18 // `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 needed 28 // later. 29 child.__super__ = parent.prototype; 30 31 return child; 32 };
View Code
child.__super__ = parent.prototype;

這是一段極爲糟糕的設計,他是將parent原型的指向給到了類的的屬性上,這裏能夠看作靜態方法,那麼我在實際使用的時候要如何使用呢?

我在內部原型鏈上或者實例方法通常使用this便能指向自己,可是卻不能執行本類的方法,若是要使用指向構造函數我須要這麼作:

this.constructor this.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不被爬蟲識別,不能跨域共享,因此不要用以存儲業務關鍵信息,更加不要存儲安全信息,要作到有,錦上添花;無,毫無影響才行:

複製代碼
① 500萬字符限制
② 通常存儲ajax請求返回數據,而且須要設置過時時間
③ 具備清理機制,將過時數據清理
④ 不存儲敏感信息
⑤ 不存儲SEO依賴數據,至少不能嚴重依賴
⑥ 隱私模式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=hybrid12334 15 var _getHybridUrl = function (params) { 16 var url = ''; 17 //...aa操做paramss生成url 18 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化帶來的頁面切換效果仍是很讚的,正是道不虛行,只在人也。

 

原文地址:http://www.cnblogs.com/yexiaochai/p/4219523.html

相關文章
相關標籤/搜索