這幾天,第三輪全站優化結束,測試項目在2G首屏載入速度取得了一些優化成績,對比下來有10s左右的差距:javascript
此次優化工做結束後,已是第三次大規模折騰公司框架了,這裏將一些本身知道的移動端的建議提出來分享下,但願對各位有用css
文中有誤請您提出,以避免誤人自誤html
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有什麼差別呢?
首先,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)
其實,咱們簡單從實現上就能夠看出,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 | ✔ | 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/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 }
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 },
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的更加嚴謹,總會作不少兼容,jQuery大是有道理的
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 };
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結構與樣式單獨摳出來使用,不然就準備肥皂吧
對前端具備實際推進做用的,我以爲有如下技術:
① 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維護工做換人的狀況下
② 刪除難,這個問題更加明顯,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請求緩存,一個經常使用於靜態資源緩存,這裏簡單說下個人一些理解。
首先localsorage有500萬字符的限制,基原本說就是5M左右的限制,瀏覽器各有不一樣,也會有讀寫的性能損耗,因此不能毫無限制的使用
localstorage不被爬蟲識別,不能跨域共享,因此不要用以存儲業務關鍵信息,更加不要存儲安全信息,要作到有,錦上添花;無,毫無影響才行:
① 500萬字符限制
② 通常存儲ajax請求返回數據,而且須要設置過時時間
③ 具備清理機制,將過時數據清理
④ 不存儲敏感信息
⑤ 不存儲SEO依賴數據,至少不能嚴重依賴
⑥ 隱私模式localstorage不可讀寫,因此不能用它來作頁面通訊 ⑦ localstorage讀寫有性能損耗,大數據讀寫要避免
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頁
咱們常說的延遲加載是圖片延遲加載,其實非圖片也可延遲加載,看實際需求便可,這裏點到便可,再也不多說。
爲img標籤src設置統一的圖片連接,而將真實連接地址裝在自定義屬性中。
因此開始時候圖片是不會加載的,咱們將知足條件的圖片的src重置爲自定義屬性即可實現延遲加載功能
咱們應該避免頁面長時間白頁,因此會出現fake頁的概念,頁面渲染僅僅須要HTML以及CSS,這個即是第一個優化點,js對於顯示不是必須,ajax也不是。
如果任由js、ajax加載完成再渲染頁面,用戶頗有可能失去耐心,因此搞一些內嵌的css以及通用的html在首頁彷佛是一個不錯的選擇
一個靜態HTML頁面,裝載首屏的基本內容,讓首頁快速顯示,而後js加載結束後會立刻從新渲染整個頁面,這個樣子,用戶就能夠很快的看到頁面響應,給用戶一個快的錯覺
這裏的預加載是在瀏覽器空閒的時候加載後續頁面所需資源,是一種浪費用戶流量的行爲,屬於以空間換時間的作法,可是這個實施難度比較高。
預加載的前提是不影響主程序的狀況下偷偷的加載,也就是在瀏覽器空閒的時候加載,可是瀏覽器空閒彷佛變得不可控制
瀏覽器空閒不可判斷(若是您知道請留言),咱們判斷的標準是當前沒有dom事件操做,沒有ajax
能夠看出,因爲瀏覽器沒有空閒的回調,因此咱們只能本身實現,這類的實現不太靠譜,咱們的預加載作的就很粗暴,要作預加載須要注意如下幾點:
① 瀏覽器空閒須要一個判斷機制
② 每次空閒時須要有一個隊列一點一點的加載資源,不然請求一旦發出很容易影響主邏輯
③ 作好預加載資源隊列的匹配算法,能夠是業務團隊配置
Hybrid技術將前端推到了史無前例的高度,可是Hybrid開發中自己也有一些須要注意的地方,這裏若是出現了設計上的失誤會對後期業務團隊開發帶問題,有幾點能夠注意
最初的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會到達幾千行之巨,這些都是跨部門交流的讓步與疼痛啊!
其實H5的調試就已是一個老大難問題,Hybrid讓這種場景變得更加複雜,chrome自己提供了一些移動端的調試方法,可是ios未越獄的話很差處理
而標準的公司中又會對ip有所限制,因此使用ip調試也比較麻煩,設置代理也費時費力,這個時候便須要更高級別的人站出來角力了,這塊老大難問題不一樣公司還不同,事實上我也犯難......
① ip調法,手機使用無線鏈接公司內網,使用手機瀏覽器打開網頁,改一個代碼,刷新一下,不行就代理,通不過就叫leader去推進安所有門開啓特殊端口
② ios高端調法,具備Mac機狀況下手機鏈接Safari可調速,我用過幾回,可是因爲沒有mac機,實際步奏忘了...
③ android機低端調試,android能夠直接開啓root權限,使用chromeF12開發者工具調試
關於移動端調試的文章不少,各位去看看有用的吧......
事實證實多webview在低端android機上很卡,慎用。高端機多webview乾的頁面切換的活CSS3也能作,多webview意義不大
PS:來百度後,發現多webview卡的緣由多是native方的實現有問題,此段存疑
移動端會有一些不恰當的需求,這類需求看似無關重要,卻會對整個移動框架形成隱患,甚至影響總體驗。
移動端第一個噁心需求就算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化通常爲了解決:
① fixed問題
② webapp中view獨享「scrollTop」
③ webapp page 切換動畫順暢,由於scrollTop與長短頁問題
④ 嫌棄原生的scroll不夠平滑
這裏仍是不建議全站使用IScroll這類技術,IScroll可能帶來,header消失、文本框消失、可視區域便小等問題,如今仍是小範圍彈出層使用就好,某天overflow: scroll兼容問題獲得解決,區域滾動便再也不難了。
這裏倒不是一味抵制IScroll全站化,若是頁面dom結構簡單,若是頁面文本框比較少,又作過充分調研,IScroll化帶來的頁面切換效果仍是很讚的,正是道不虛行,只在人也。