您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
浅谈移动前端的最佳实践
 
作者:叶小钗 来源:博客园 发布于 2015-03-03
   次浏览      
 

这几天,第三轮全站优化结束,测试项目在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,
    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
}

真实的差异还有很多,我这里也没法一一列出,这里要说明的一个问题其实就是:

zepto设计的目的是提供jquery的类似的APIs,不以100%覆盖jquery为目的,一个5-10k的通用库、下载并执行快、有一个熟悉通用的API,所以你能把你主要的精力放到应用开发上。

上图是1.8版本与Zepto完整版的对比,Gzip在2G情况下20K造成的差距在2-5s之间,3G情况会有1s的差距,这也是我们选择Zepto的原因,下面简单介绍下Zepto。

Zepto清单

你真实项目时,完全可以按照需要选取模块即可,下面简单再列几个差异:

其它差异

① 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事件也在没问题的......

这里简单看看细节实现:

clone: function (elem, dataAndEvents, deepDataAndEvents) {
  var i, l, srcElements, destElements,
        clone = elem.cloneNode(true),
        inPage = jQuery.contains(elem.ownerDocument, elem);

  // Fix IE cloning issues
  if (!support.noCloneChecked && (elem.nodeType === 1 || elem.nodeType === 11) &&
            !jQuery.isXMLDoc(elem)) {

    // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
    destElements = getAll(clone);
    srcElements = getAll(elem);

    for (i = 0, l = srcElements.length; i < l; i++) {
      fixInput(srcElements[i], destElements[i]);
    }
  }

  // Copy the events from the original to the clone
  if (dataAndEvents) {
    if (deepDataAndEvents) {
      srcElements = srcElements || getAll(elem);
      destElements = destElements || getAll(clone);

      for (i = 0, l = srcElements.length; i < l; i++) {
        cloneCopyEvent(srcElements[i], destElements[i]);
      }
    } else {
      cloneCopyEvent(elem, clone);
    }
  }

  // Preserve script evaluation history
  destElements = getAll(clone, "script");
  if (destElements.length > 0) {
    setGlobalEval(destElements, !inPage && getAll(elem, "script"));
  }

  // Return the cloned set
  return clone;
},
function cloneCopyEvent(src, dest) {
  var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;

  if (dest.nodeType !== 1) {
    return;
  }

  // 1. Copy private data: events, handlers, etc.
  if (dataPriv.hasData(src)) {
    pdataOld = dataPriv.access(src);
    pdataCur = dataPriv.set(dest, pdataOld);
    events = pdataOld.events;

    if (events) {
      delete pdataCur.handle;
      pdataCur.events = {};

      for (type in events) {
        for (i = 0, l = events[type].length; i < l; i++) {
          jQuery.event.add(dest, type, events[type][i]);
        }
      }
    }
  }

  // 2. Copy user data
  if (dataUser.hasData(src)) {
    udataOld = dataUser.access(src);
    udataCur = jQuery.extend({}, udataOld);

    dataUser.set(dest, udataCur);
  }
}

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四个属性,分别代表该元素上、左、右、下四条边界相对于浏览器窗口左上角(注意,不是文档区域的左上角)的偏移像素值。

offset: function(coordinates){
  if (coordinates) return this.each(function(index){
    var $this = $(this),
        coords = funcArg(this, coordinates, index, $this.offset()),
        parentOffset = $this.offsetParent().offset(),
        props = {
          top:  coords.top  - parentOffset.top,
          left: coords.left - parentOffset.left
        }

    if ($this.css('position') == 'static') props['position'] = 'relative'
    $this.css(props)
  })
  if (this.length==0) return null
  var obj = this[0].getBoundingClientRect()
  return {
    left: obj.left + window.pageXOffset,
    top: obj.top + window.pageYOffset,
    width: Math.round(obj.width),
    height: Math.round(obj.height)
  }
},

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实现是:

var extend = function (protoProps, staticProps) {
  var parent = this;
  var child;

  // The constructor function for the new subclass is either defined by you
  // (the "constructor" property in your `extend` definition), or defaulted
  // by us to simply call the parent's constructor.
  if (protoProps && _.has(protoProps, 'constructor')) {
    child = protoProps.constructor;
  } else {
    child = function () { return parent.apply(this, arguments); };
  }

  // Add static properties to the constructor function, if supplied.
  _.extend(child, parent, staticProps);

  // Set the prototype chain to inherit from `parent`, without calling
  // `parent`'s constructor function.
  var Surrogate = function () { this.constructor = child; };
  Surrogate.prototype = parent.prototype;
  child.prototype = new Surrogate;

  // Add prototype properties (instance properties) to the subclass,
  // if supplied.
  if (protoProps) _.extend(child.prototype, protoProps);

  // Set a convenience property in case the parent's prototype is needed
  // later.
  child.__super__ = parent.prototype;

  return child;
};

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实现,这里举个案例:

这里优劣之分各位自己判断,我反正完全偏向了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代码出错还会闪退呢。问题一,站不住脚,而且完全可以使用这种方式处理:

<header >
<a class="header" href="taobao://wireless">后退</a>
<h1 class="js_title">
标题
</h1>
</header>

就算是js报错,我这里假设一来就报错,处处报错,但以上协议native是一定可以捕捉的,js正确的情况便e.preventDefault(),错误便跳回首页,这个不是不可处理。

问题二其实与问题一一致,最初进入的时候明明可以有个可关闭的native loading,在webview加载好后再系统级别的关闭loading即可,没有什么不能解决的。

之所以我这里会如此激烈的拒绝native提供的头,是因为H5页面是一般是三套共用,H5站点,ios,android,而H5的dom操作千变万化,头部一些奇怪的需求展示,native根本无从支持,这里还会涉及跨团队合作,所以Hybrid开始的时候一定要坚决抵制native 提供的业务类UI,不然后期交流很麻烦。

   
次浏览       
 
相关文章

手机软件测试用例设计实践
手机客户端UI测试分析
iPhone消息推送机制实现与探讨
Android手机开发(一)
 
相关文档

Android_UI官方设计教程
手机开发平台介绍
android拍照及上传功能
Android讲义智能手机开发
相关课程

Android高级移动应用程序
Android系统开发
Android应用开发
手机软件测试
最新活动计划
软件架构设计方法、案例与实践 8-23[特惠]
Linux内核编程及设备驱动 8-15[北京]
Python、数据分析与机器学习 8-23[特惠]
嵌入式软件架构设计 8-22[线上]
QT应用开发 9-5[北京]

android人机界面指南
Android手机开发(一)
Android手机开发(二)
Android手机开发(三)
Android手机开发(四)
iPhone消息推送机制实现探讨
手机软件测试用例设计实践
手机客户端UI测试分析
手机软件自动化测试研究报告
更多...   


Android高级移动应用程序
Android应用开发
Android系统开发
手机软件测试
嵌入式软件测试
Android软、硬、云整合


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...