淺談移動前端的最佳實踐

出處: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 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/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保持接口調用即可
十 坑大的需求還是拒絕算了......

複製代碼

核心點

我的微博粉絲及其少,如果您覺得這篇博客對您哪怕有一絲絲的幫助,微博求粉!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章