转载

浅谈移动前端的最佳实践

前言

这几天,第三轮全站优化结束,测试项目在2G首屏载入速度取得了一些优化成绩,对比下来有10s左右的差距:

浅谈移动前端的最佳实践

这次优化工作结束后,已经是第三次大规模折腾公司框架了,这里将一些自己知道的移动端的建议提出来分享下,希望对各位有用

文中有误请您提出,以免误人自误

技术选型

单页or多页

sap(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)
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 andSelfend() 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%的使用场景

② cloneZepto的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意义不大

......

不恰当的需求

移动端会有一些不恰当的需求,这类需求看似无关重要,却会对整个移动框架造成隐患,甚至影响整体验。

唤醒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保持接口调用即可 十 坑大的需求还是拒绝算了......

核心点

我的微博粉丝及其少,如果您觉得这篇博客对您哪怕有一丝丝的帮助,微博求粉!!!

浅谈移动前端的最佳实践

招聘信息

招聘要求:

不要提钱,那个团队不差钱,就是非常任性!!!

① 前端高手

② 可培养、吃苦耐劳型优质新手资源,由于是创业型团队,有一定门槛,抱歉

③ 有开源框架甚好

④ 能读流行源码甚好

⑤ 工作3年以上甚好

⑥ 有大项目经验,大项目优化建议甚好

浅谈移动前端的最佳实践

浅谈移动前端的最佳实践

正文到此结束
Loading...