Giter Club home page Giter Club logo

article's People

Contributors

airen avatar hongru avatar lvscar avatar tancy avatar zerohsu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

article's Issues

阿里无线前端性能优化指南 (Pt.1 加载期优化)

阿里无线前端性能优化指南 (Pt.1 加载优化)

前言

阿里无线前端团队在过去一年对所负责业务进行了全面的性能优化。以下是我们根据实际经验总结的优化指南,希望对大家有所帮助。

第一部分仅包括数据加载期优化。

图片控制

对于网页特别是电商类页面来说,图片通常会占据了大量的视觉空间,是页面中最为重要的展现内容,并且占据网页传输字节的大部分。因此,对图片的优化是我们性能优化的重点.

启用WebP

WebP是一种支持有损压缩和无损压缩的图片文件格式,派生自视频编码格式 VP8。根据 Google 官方的数据,无损压缩后的 WebP 比 PNG 文件少了 26% 的文件大小,有损压缩在具有同等SSIM索引的情况下WebP 比 JPEG 文件少25-34%的文件大小。WebP支持无损透明度(也叫做alpha通道),支持动画格式Animated WebP 。

虽然目前仅Android系统原生支持WebP格式, 但由于其对页面性能优化的巨大意义, 我们会在页面加载时进行环境探测, 如页面渲染环境支持WebP我们会替换页面中的图片链接为WebP格式的版本。 如果页面专门用于可控的客户端内(我们能在客户端中放置专门的WebP decoder),就算是iOS环境我们也会启用WebP.

优化高分屏和弱网适配

从苹果的Retina开始,手机厂商开始越来越多的使用高像素密度显示屏。在浏览器里我们的一个CSS像素往往能对应两个或更多个设备像素,在这种环境下为了追求最好的显示效果,我们会采用数倍于浏览器CSS像素标识的图片尺寸. 这里需要注意的是,如果你采用了2x (两倍CSS像素分辨率) 的图片,由于水平和垂直像素都进行了加倍,最终图片体积会增加4倍(内存占用也会增加4倍). 同理,如果你采用了3x的图片,最终增加的传输体积会增至9倍.

用户喜欢清晰绚丽的图片, 但用户更加痛恨打不开的页面. 在我们的实践中,我们最多使用2x(两倍CSS像素分辨率)的图片。 如果页面专门用于可控的客户端内,我们会根据从客户端获取的网络情况替换页面所使用的图片资源. 在最糟糕的网络环境(2G移动网络),我们甚至会采用按30%质量进行压缩的图片以替换原始图片链接。

单张图片大小控制

有时,如果不设限无论你如何优化糟糕的情况总会出现。在我们的实践中, 针对图片我们设置了单张图片大小不超过50Kb的限制。在每次发布时,我们会对页面图片进行检查, 如果图片超过这个限制仍需要发布,就得走特别的流程了.

合理使用CSS/SVG/ICON Font替代图片

传统桌面Web中,针对页面内的图标,我们往往采用Sprite(雪碧图)技术把多个图标合并到一张大图片中,针对不同的图标显示大图中不同的部分。但在移动互联网环境下, 由于设备内存有限,每使用Sprite技术展示一个图标,都需要浏览器把整个大图解码到内存中一次,考虑到前文提到的多倍CSS像素分辨率情况, 占用的内存和解码时间往往是可观的。不合理的使用Sprite技术会造成移动页面性能不升反降。

更合适的选择是CSS3,SVG和ICON Font技术。如果你的图标能使用这些技术绘制,在任何分辨率和缩放设置都可以提供清晰的效果并减小传输和内存的开销.

对图片进行按需加载

电商类型网站多用多图列表页面展现商品内容, 我们会在非WIFI网络环境时对图片资源进行按需加载,仅仅当图片资源出现在首屏或随着用户滑动屏幕进入可见区域时,我们才进行加载. 进行这项优化的关键在于对全局的图片呈现进行一层抽象,以便统一控制.

网络请求

今天我们谈论的无线页面,很多时候已不再是传统的"页面",而是一个个"单页应用"。随着应用复杂度的逐渐增加,所需加载的除图片等静态数据外,动态数据也会越来越多。如果想追求高质量的单页应用,对这些请求的优化势在必行.

域名收敛

如果你页面中引入的各种资源来自不同的域名,注意每增加一个域名,都会增加一次域名解析开销。 在复杂的国内移动互联网网络环境下,不同域名的解析速度可能会相差数十倍。 所以你需要有意识的收敛页面资源所需解析的域名数, 特别是会阻塞页面渲染的CSS,JS,Font资源。 很多性能糟糕页面究其原因或许会是引入的资源域名解析速度很慢或完全不能正确解析。在我们的实践中, 一个页面所产生的域名解析数不能超过5个。

减少请求数

在优化了需要解析的域名数后,你需要关注页面资源请求数. 如果是长期维护的产品型页面 ,页面中引入的静态资源除最通用的基础库外需要按依赖顺序进行合并压缩. 一般是JS和CSS请求各一。 针对电商厂商多见的营销活动页面,我们甚至开发了工具把全部的CSS和JS资源内联入页面,从而实现除图片外的其余资源One Request就能获得.

另外,资源请求重定向也应该尽量避免,少一次重定向,少一个请求数.

文本数据的优化与压缩

文本数据(HTML,CSS,JavaScript)的优化与压缩分为三个阶段,分别是发布准备(去除注释,合并CSS声明,去除不会被执行的JS代码块), 编译期压缩(合并文件,去除空格,混淆) 和 传输阶段压缩( GZip ) . 这项优化的关键在于梳理流程确保压缩的自动化和服务器GZip指令被正确配置。

数据接口优化与监控

随着近两年Web前后端分离思潮的流行与前端模板技术的发展 , 我们越来越倾向在页面加载完成后再通过接口获取JSON数据并在前端进行页面渲染。这种方式带来了页面第一次加载速度的提升,但常常把原有的性能问题隐藏了起来。 需要花功夫优化的数据获取并最终呈现时间往往被隐藏在空页面快速呈现的表象之下。更严重的情况发生在需要从多个不同接口获取数据,并且这些接口调用还存在互相依赖的情况下,一旦出现这样的情况,页面性能往往是不升反降的.

在我们的实践中, 我们要求数据在后端组装完成后再交由前端渲染,一屏中完整渲染所需数据不能来自多过一个接口。 所有数据源统一收敛到单一的接口服务层,以便进行统计和监控。

用flexible设置Meta,页面会闪动

因为JS动态加载Meta属性,所以初始设置的字体会很大或者很小,然后JS加载后,才会缩放到正常的比例,请问这个是怎么处理的呢?还是文章里有,我没有看到?

手淘Promise实践

之前较早的时候,在我们团队中已经陆续分享过几次Promise的实践,主要分享了Promise的常用特性,包括then/catch,链式调用等。而本次借双11技术巡演的机会,主要结合手淘前端的一些日常业务,来阐述Promise的编程模式。

为什么选择Promise

笔者对Promise的态度是极其推崇的,不仅仅因为它能被完美的Polyfill和解决异步调用的问题,更从ES6/7的发展来看,Promise有更大的用武之地(ES6的generator以及ES7的async/wait)。

从数据到渲染

手淘H5首页在优化数据请求时,跳出原有的框架,尝试多种策略来保证用户体验和数据的稳定,下面将要讲到的几种模式的例子都是和这些优化密切相关的:

并行模式

var dataPromise = requestData('data_api_path');
var templatePromise = requestTemplate('template_path');
var domReadyPromise = new Promise(function(resolve, rejcet) {
    if (document.readyState === 'complete') {
        resolve();
    } else {
        document.addEventListener('DOMContentLoaded', resolve);
    }
});

Promise.all([dataPromise, templatePromise, domReadyPromise])
    .then(function([data, tpl]) { // 为了代码简洁,请允许笔者使用下解构语法,解构语法可参考阮一峰老师的《ES6入门》
        document.body.appendChild(tpl.render(data));
    });

假设已存在requestData/requestTemplate方法的情况下,上述代码在任何时间点运行都可以正常工作。对比用callback的方式,就会让代码很拘谨:

document.addEventListener('DOMContentLoaded', function() {
    requestData('data_api_path', function(data) {
        requestTemplate('template_path', function(tpl){
            document.body.appendChild(tpl.render(data));
        });
    });
});

流程是顺序的,且想要调整会非常的麻烦。即使费尽周折,也显得不值得:

var dataReady;
var templateReady;
var domReady;
function ifDone() {
   if (!!dataReady && !!templateReady && !!domReady) {
       document.body.appendChild(templateReady.render(dataReady));
   }
}

requestData('data_api_path', function(data) {
    dataReady = data;
    ifDone();
})

requestTemplate('template_path', function(tpl) {
    tplReady = tpl;
    ifDone();     
});

if (document.readyState === 'complete') {
    domReady = true;
    ifDone();
} else {
    document.addEventLisener('DOMContentLoaded', function() {
        domReady = true;
        ifDone();
    });
}

有人会说,Promise其实就是屏蔽了callback的一些细节而已,而且一些异步任务的库也能解决这个问题。我认同一些异步的任务库(比如windjs)可以如同Promise一样工作,但不认同只是屏蔽了callback的一些细节。事实证明,当Promise/A+成为标准后,windjs也完成了它光荣的使命。一个(将来)原生的多任务异步管理模式没有理由不取代一个js库。

异常流

手淘双11期间的产品特别是大型运营活动的页面,一些细微样式上的不兼容或适配问题往往是可以容忍的(这些要产生P1故障真的很难),但是万万不能让数据出问题。但实际上,数据出问题的场景实在太多了,比如字段类型不对,js没做容错,又比如,网络故障或为了防止突然间的大流量而智能限流等等。前端很有可能拿到一份期望之外的东西,这个时候,就需要一些降级或容错处理。

Promise的异常流跟try/catch很像,例如:

promise.then(function() {
    // process 1
}).then(function() {
    // process 2
}).catch(function() {
    // error
})

那么在数据这个层面的操作,就好比Promise的字面意思一样,给使用方一个承诺,即提供的数据是可用的。

优雅降级

手淘在前端数据的保障上做的特别多,例如有本地存储,APP网关缓存,服务端的打底数据等。当真实的业务接口无法承受压力时,上述数据保障就会发挥作用了。但,面对各种数据保障,怎样能在代码层面上优雅的实现它呢?请看下面的例子:

function getDataFromWebStorage() {
    if (window.localStorage && window.localStorage['DATA']) {
        return Promise.resolve(window.localStorage['DATA']);
    } else {
        return Promise.reject();
    }
}

function getDataFromAppCache() {
    return requestHybridAPI('get_catch_data', 'data_api_path');
}

function getDataFromBackup() {
    return requestBackup('data_api_path');
}

funtion parseData(dataStr) {
    return JSON.parse(dataStr);
}

function getData() {
    var dataPromise = requestData('data_api_path')
        .then(parseData) // will throw a parse error
        .catch(function() {
            // catch request & parse error
            return getDataFromWebStorage().then(parseData);
        })
        .catch(function() {
            // catch webstorage & parse error
            return getDataFromAppCache().then(parseData);
        })
        .catch(function() {
            // catch appcache & parse error
            return getDataFromBackup().then(parseData);
        })
        .then(function(data) {
            if (window.localStorage) {
                window.localStroage['DATA'] = JSON.stringify(data);
            }
            return data;
        })
        .catch(function(err) {
            // catch kinds of error
            handleKindsOfError(err);
        });
}

上述的异常处理,是希望在获取原有数据失败或解析失败时,尝试从其它渠道来重新获取一份可能有点过时但没那么糟糕的数据。最坏情况,所有渠道的数据都没能复合期望,还可以根据不同的错误分类给出不同的友好提示。

不熟悉Promise异常流的读者,可能会对上述的异常流处理有些疑问:

_catch可以捕获到的范围?_

从当前catch沿着链式调用往前找,它可以一直捕获到前一个catch或者链式调用最开始为止。也就是在调用最开始到第一个catch之间,或者两个catch之间(包括前一个catch)中的所有错误,都会被后面的catch捕获到。

_为什么有些Promise调用链没有catch?_

因为Promise的抛出异常的堆栈和函数调用的堆栈是相反的(这个和原生的抛出异常堆栈是相同的),当getDataFromWebStorage这个方法获取数据失败,抑或是解析数据失败的异常都会抛到上一个调用堆栈上(即上一层的Promise调用链)并寻找下一个最近的catch。如果找不到catch,则会一直往上抛直到有能够处理的catch或者达到调用堆栈的顶端(达到顶端后就会在控制台提示错误了)。

如图:
异常流

高效切换策略

上面在处理异常流的代码中,细心的读者可能已经发现,Promise的代码流,不仅仅给异常处理带来了方便,还可以在适当的场景下,运用不同的策略来展示数据。

例如,在保证用户体验的情况下,希望更快的把页面内容展示给用户。那么请求接口数据的耗时是一个优化点。如果,通过优先展示本地储存数据,先让用户看到页面(可能数据是1分钟前的),如果缓存没有数据,再请求业务数据接口。这样的策略下,上述代码稍作修改就可以轻而易举的胜任:

function getData() {
    var dataPromise = getDataFromWebStorage()
        .then(parseData)
        .catch(function() {
            return requestData('data_api_path').then(parseData);
        })
        .then(function(data) {
            if (window.localStorage) {
                window.localStroage['DATA'] = JSON.stringify(data);
            }
            return data;
        })
        .catch(function(err) {
            // catch kinds of error
            handleKindsOfError(err);
        });
}

没错,仅仅调换了getDataFromWebStoragerequestData的调用顺序,给用户的体验就大不同了!

竞争模式

笔者觉得,在实际业务中往往会忽略竞争模式,下面用一个比较典型例子来加深读者对竞争模式的印象:

element.style.transition = 'opacity 0.4s ease 0s';
element.style.opacity = '0';

var eventPromise = new Promise(function(resolve, reject) {
    element.addEventListener('transitionend', function handler() {
        element.removeEventListener('transitionend', handler);
        resolve();
    });
});

var timeoutPromise = new Promise(function(resolve, reject) {
    setTimeout(resolve, 400);
});

Promise.race([eventPromise, timeoutPromise])
    .then(function() {
        element.style.transition = '';
        element.style.display = 'none';
    });

之前在处理手淘H5首页的跑马灯动画时,预期动画能顺利结束并触发transitionend事件,但适配的结果不尽如人意。事实上在复杂的DOM环境加上浏览器实现的bug,甚至业务代码的一些疏漏,会导致transitionend无法被触发。这种情况下,用一个超时来保证流程能正常执行下去就显得非常必要了。

分工合作,各司其职

特别强调Promise的字面意思是承诺,而实际使用起来它就是一个承诺。而当承诺不可拆分时,即保证了它的原子性。我们在业务代码中,要尽量让每个独立的事务保证自身的原子性,这样在不同的业务场景下,才能随心所欲的串联这些事务。这里笔者所说的事务,其实并不一定要拘泥于是异步事务。手淘H5首页的模板是和iOS模板同构的,模板渲染后需要对模板生成的模块绑定事件或交互。在这种场景下,笔者设计了一种分工模式,来协调每个不同层次间的合作。这使得,代码流看起来跟传统的调用API方式完全不同。

请允许笔者在以下的代码中,使用ES6的export/import语法,更详细的语法介绍可参阅阮一峰老师的《ES6入门》

分工模式

A攻城师负责获取数据(data.js):

export var button = requestData('button_data_api_path');

B攻城师负责获取模板(template.js):

export var button = requestTemplate('button_template_path');

C攻城师负责渲染数据(render.js):

import dataPromise from './data.js';
import templatePromise from './template.js';
import {domReadyPromise} from './util.js';

var deferred = {};
deferred.promise = new Promise(function(resolve, reject) {
    deferred.resolve = resolve;
    deferred.reject = reject;
});

export var renderCompletePromise = deferred.promise;

Promise.all([dataPromise.button, templatePromise.button, domReadyPromise])
    .then(function([data, tpl]) {
        var buttonElement = tpl.render(data);
        document.body.appendChild(buttonElement);
        deferred.resolve(buttonElement);
    });

D攻城师负责赋予交互行为(ctrl.js):

import {renderCompletePromise} from './render.js';

renderCompletePromise.then(function(element) {
    element.addEventListener('click', function() {
        location.href = '//m.taobao.com';    
    });
});

代码中用到了domReadyPromise这个变量,它的实现在最早的示例代码中已经有体现,这里不再赘述。

几个攻城师之间只要互相给出一个承诺就可以,而不用操心对方的API什么时候又更新然后被坑到了等等,这样的合作方式是不是很赞!!当然为了例子生动,笔者用不同攻城师开发一个项目当中的不同层次代码来阐述Promise分工模式,实际情况其实不需要那么多攻城师,读者一个人完全可以在项目中也完成这样的Promise分工模式。

推迟兑现

上述例子中,用到了一个名词,即deferred,其原型defer字面意思是推迟。如果屏蔽掉一些代码细节,希望是这样的:

var deferred = defer([promise]);

传统Promise的方式,创建承诺(new Promise)和兑现承诺(resolve)是同一维度下进行的。而,defer先是创建了一个承诺,其后可以在任何维度下兑现它。例如,下面一个例子:

deferData.js:

export var deferred;

export function getDeferData() {
    var dataPromise = requestData('data_api_path');
    deferred = defer(dataPromise);
    return deferred.promise;
}

deferRender.js:

import {getDeferData} from './deferData.js';

getDeferData().then(function(data) {
    // TODO render
});

deferCtrl.js:

import {deferred} from './deferData.js';
import {domReadyPromise} from './util.js';

domReadyPromise.then(function() {
    var button = document.querySelector('button');

    button.addEventListener('click', function handler() {
        button.removeEventListener('click', handler)
        deferred.resolve();
    });
});

上述三个分工模式下的分层代码,完成了在用户点击某按钮后再渲染页面的流程,

深挖,注意有坑

并行模式也好,分工模式也罢,刚接触时兴奋的任何代码都想Promise下,但各位读者应该避免过度使用Promise。例如笔者曾经这样使用过Promise:

function wait(element, eventName) {
    return new Promise(function(resolve, reject) {
        element.addEventListener(eventName, function handler() {
            element.removeEventListener(eventName, handler);
            resolve();
        })
    });
}

function sleep(resolve) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, time);
    });
}

void function circle() {
    wait(element, 'click')
        .then(function() {
            return sleep(300);
        })
        .then(function() {
            alert('clicked');
        })
        .then(circle);
}();

DOM事件是一个可被连续触发的事务,所以它本身并不是一个承诺。上述例子中的循环模式在一些场景下还是挺有用的,但是,毕竟它打破了DOM事件的原有特性,这里并不推荐为了Promise而用Promise。

小结

Promise的实践远远不止这么一些,一两篇篇幅恐怕很难涵盖所有可以为开发者所用的模式,之后的一篇实践会涉及generator/co甚至async/wait,在引入这些未来的ES特性后,Promise的使命就有了微妙的变化。

使用Flexible实现手淘H5页面的终端适配

更新(2019-01-16)

由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方案。vw的兼容方案可以参阅《如何在Vue项目中使用vw实现移动端适配》一文。

曾几何时为了兼容IE低版本浏览器而头痛,以为到Mobile时代可以跟这些麻烦说拜拜。可没想到到了移动时代,为了处理各终端的适配而乱了手脚。对于混迹各社区的偶,时常发现大家拿手机淘宝的H5页面做讨论——手淘的H5页面是如何实现多终端的适配

那么趁此Amfe阿里无线前端团队双11技术连载之际,用一个实战案例来告诉大家,手淘的H5页面是如何实现多终端适配的,希望这篇文章对大家在Mobile的世界中能过得更轻松。

目标

拿一个双11的Mobile页面来做案例,比如你实现一个类似下图的一个H5页面:

Flexible实现手淘H5页面的终端适配

目标很清晰,就是做一个这样的H5页面。

DEMO

请用手机扫下面的二维码

DEMO

痛点

虽然H5的页面与PC的Web页面相比简单了不少,但让我们头痛的事情是要想尽办法让页面能适配众多不同的终端设备。看看下图你就会知道,这是多么痛苦的一件事情:

Flexible实现手淘H5页面的终端适配

点击这里查看更多终端设备的参数。

再来看看手淘H5要适配的终端设备数据:

Flexible实现手淘H5页面的终端适配

看到这些数据,是否死的心都有了,或者说为此捏了一把汗出来。

手淘团队适配协作模式

早期移动端开发,对于终端设备适配问题只属于Android系列,只不过很多设计师常常忽略Android适配问题,只出一套iOS平台设计稿。但随着iPhone6,iPhone6+的出现,从此终端适配问题不再是Android系列了,也从这个时候让移动端适配全面进入到“杂屏”时代。

Flexible实现手淘H5页面的终端适配

上图来自于paintcodeapp.com

为了应对这多么的终端设备,设计师和前端开发之间又应该采用什么协作模式?或许大家对此也非常感兴趣。

而整个手淘设计师和前端开发的适配协作基本思路是:

  • 选择一种尺寸作为设计和开发基准
  • 定义一套适配规则,自动适配剩下的两种尺寸(其实不仅这两种,你懂的)
  • 特殊适配效果给出设计效果

还是上一张图吧,因为一图胜过千言万语:

Flexible实现手淘H5页面的终端适配

在此也不做更多的阐述。在手淘的设计师和前端开发协作过程中:手淘设计师常选择iPhone6作为基准设计尺寸,交付给前端的设计尺寸是按750px * 1334px为准(高度会随着内容多少而改变)。前端开发人员通过一套适配规则自动适配到其他的尺寸。

根据上面所说的,设计师给我们的设计图是一个750px * 1600px的页面:

Flexible实现手淘H5页面的终端适配

前端开发完成终端适配方案

拿到设计师给的设计图之后,剩下的事情是前端开发人员的事了。而手淘经过多年的摸索和实战,总结了一套移动端适配的方案——flexible方案

这种方案具体在实际开发中如何使用,暂时先卖个关子,在继续详细的开发实施之前,我们要先了解一些基本概念。

一些基本概念

在进行具体实战之前,首先得了解下面这些基本概念(术语):

视窗 viewport

简单的理解,viewport是严格等于浏览器的窗口。在桌面浏览器中,viewport就是浏览器窗口的宽度高度。但在移动端设备上就有点复杂。

移动端的viewport太窄,为了能更好为CSS布局服务,所以提供了两个viewport:虚拟的viewportvisualviewport和布局的viewportlayoutviewport。

George Cummins在Stack Overflow上对这两个基本概念做了详细的解释

而事实上viewport是一个很复杂的知识点,上面的简单描述可能无法帮助你更好的理解viewport,而你又想对此做更深的了解,可以阅读PPK写的相关教程

物理像素(physical pixel)

物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。正是这些设备像素的微小距离欺骗了我们肉眼看到的图像效果。

Flexible实现手淘H5页面的终端适配

设备独立像素(density-independent pixel)

设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。

CSS像素

CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。

屏幕密度

屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。

设备像素比(device pixel ratio)

设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按下面的公式计算得到:

设备像素比 = 物理像素 / 设备独立像素

在JavaScript中,可以通过window.devicePixelRatio获取到当前设备的dpr。而在CSS中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。

dip或dp,(device independent pixels,设备独立像素)与屏幕密度有关。dip可以用来辅助区分视网膜设备还是非视网膜设备。

缩合上述的几个概念,用一张图来解释:

Flexible实现手淘H5页面的终端适配

众所周知,iPhone6的设备宽度和高度为375pt * 667pt,可以理解为设备的独立像素;而其dpr为2,根据上面公式,我们可以很轻松得知其物理像素为750pt * 1334pt

如下图所示,某元素的CSS样式:

width: 2px;
height: 2px;

在不同的屏幕上,CSS像素所呈现的物理尺寸是一致的,而不同的是CSS像素所对应的物理像素具数是不一致的。在普通屏幕下1个CSS像素对应1个物理像素,而在Retina屏幕下,1个CSS像素对应的却是4个物理像素。

有关于更多的介绍可以点击这里详细了解。

看到这里,你能感觉到,在移动端时代屏幕适配除了Layout之外,还要考虑到图片的适配,因为其直接影响到页面显示质量,对于如何实现图片适配,再此不做过多详细阐述。这里盗用了@南宮瑞揚根据mir.aculo.us翻译的一张信息图:

Flexible实现手淘H5页面的终端适配

meta标签

<meta>标签有很多种,而这里要着重说的是viewport的meta标签,其主要用来告诉浏览器如何规范的渲染Web页面,而你则需要告诉它视窗有多大。在开发移动端页面,我们需要设置meta标签如下:

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

代码以显示网页的屏幕宽度定义了视窗宽度。网页的比例和最大比例被设置为100%。

留个悬念,因为后面的解决方案中需要重度依赖meta标签。

CSS单位rem

W3C规范中是这样描述rem的:

font size of the root element.

简单的理解,rem就是相对于根元素<html>font-size来做计算。而我们的方案中使用rem单位,是能轻易的根据<html>font-size计算出元素的盒模型大小。而这个特色对我们来说是特别的有益处。

前端实现方案

了解了前面一些相关概念之后,接下来我们来看实际解决方案。在整个手淘团队,我们有一个名叫lib-flexible的库,而这个库就是用来解决H5页面终端适配的。

lib-flexible是什么?

lib-flexible是一个制作H5适配的开源库,可以点击这里下载相关文件,获取需要的JavaScript和CSS文件。

当然你可以直接使用阿里CDN:

<script src="http://g.tbcdn.cn/mtb/lib-flexible/{{version}}/??flexible_css.js,flexible.js"></script>

将代码中的{{version}}换成对应的版本号0.3.4

使用方法

lib-flexible库的使用方法非常的简单,只需要在Web页面的<head></head>中添加对应的flexible_css.js,flexible.js文件:

第一种方法是将文件下载到你的项目中,然后通过相对路径添加:

<script src="build/flexible_css.debug.js"></script>
<script src="build/flexible.debug.js"></script>

或者直接加载阿里CDN的文件:

<script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js"></script>

另外强烈建议对JS做内联处理,在所有资源加载之前执行这个JS。执行这个JS后,会在<html>元素上增加一个data-dpr属性,以及一个font-size样式。JS会根据不同的设备添加不同的data-dpr值,比如说2或者3,同时会给html加上对应的font-size的值,比如说75px

如此一来,页面中的元素,都可以通过rem单位来设置。他们会根据html元素的font-size值做相应的计算,从而实现屏幕的适配效果。

除此之外,在引入lib-flexible需要执行的JS之前,可以手动设置meta来控制dpr值,如:

<meta name="flexible" content="initial-dpr=2" />

其中initial-dpr会把dpr强制设置为给定的值。如果手动设置了dpr之后,不管设备是多少的dpr,都会强制认为其dpr是你设置的值。在此不建议手动强制设置dpr,因为在Flexible中,只对iOS设备进行dpr的判断,对于Android系列,始终认为其dpr1

if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
        // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
        if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
            dpr = 3;
        } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
            dpr = 2;
        } else {
            dpr = 1;
        }
    } else {
        // 其他设备下,仍旧使用1倍的方案
        dpr = 1;
    }
    scale = 1 / dpr;
}

flexible的实质

flexible实际上就是能过JS来动态改写meta标签,代码类似这样:

var metaEl = doc.createElement('meta');
var scale = isRetina ? 0.5:1;
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
    document.documentElement.firstElementChild.appendChild(metaEl);
} else {
    var wrap = doc.createElement('div');
    wrap.appendChild(metaEl);
    documen.write(wrap.innerHTML);
}

事实上他做了这几样事情:

  • 动态改写<meta>标签
  • <html>元素添加data-dpr属性,并且动态改写data-dpr的值
  • <html>元素添加font-size属性,并且动态改写font-size的值

案例实战

了解Flexible相关的知识之后,咱们回到文章开头。我们的目标是制作一个适配各终端的H5页面。别的不多说,动手才能丰衣足食。

创建HTML模板

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta content="yes" name="apple-mobile-web-app-capable">
        <meta content="yes" name="apple-touch-fullscreen">
        <meta content="telephone=no,email=no" name="format-detection">
        <script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js"></script>
        <link rel="apple-touch-icon" href="favicon.png">
        <link rel="Shortcut Icon" href="favicon.png" type="image/x-icon">
        <title>再来一波</title>
    </head>
    <body>
        <!-- 页面结构写在这里 -->
    </body>
</html>

正如前面所介绍的一样,首先加载了Flexible所需的配置:

<script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js"></script>

这个时候可以根据设计的图需求,在HTML文档的<body></body>中添加对应的HTML结构,比如:

<div class="item-section" data-repeat="sections">
    <div class="item-section_header">
        <h2><img src="{brannerImag}" alt=""></h2>
    </div>
    <ul>
        <li data-repeat="items" class="flag" role="link" href="{itemLink}">
            <a class="figure flag-item" href="{itemLink}">
                <img src="{imgSrc}" alt="">
            </a>
            <div class="figcaption flag-item">
                <div class="flag-title"><a href="{itemLink}" title="">{poductName}</a></div>
                <div class="flag-price"><span>双11价</span><strong>¥{price}</strong><small>({preferential})</small></div>
                <div class="flag-type">{activityType}</div>
                <a class="flag-btn" href="{shopLink}">{activeName}</a>
            </div>
        </li>
    </ul>
</div>

这仅是一个示例文档,大家可以根据自己风格写模板

为了能更好的测试页面,给其配置一点假数据:

//define data
var pageData = {
    sections:[{
        "brannerImag":"http://xxx.cdn.com/B1PNLZKXXXXXaTXXXXXXXXXXXX-750-481.jpg",
        items:[{
            "itemLink": "##",
            "imgSrc": "https://placeimg.com/350/350/people/grayscale",
            "poductName":"Carter's1年式灰色长袖连体衣包脚爬服全棉鲸鱼男婴儿童装115G093",
            "price": "299.06",
            "preferential": "满400减100",
            "activityType": "1小时内热卖5885件",
            "shopLink":"##",
            "activeName": "马上抢!"
        }
            ....
        }]
    }]
}

接下来的工作就是美化工作了。在写具体样式之前,有几个点需要先了解一下。

把视觉稿中的px转换成rem

读到这里,大家应该都知道,我们接下来要做的事情,就是如何把视觉稿中的px转换成rem。在此花点时间解释一下。

首先,目前日常工作当中,视觉设计师给到前端开发人员手中的视觉稿尺寸一般是基于640px750px以及1125px宽度为准。甚至为什么?大家应该懂的(考虑Retina屏)。

正如文章开头显示的示例设计稿,他就是一张以750px为基础设计的。那么问题来了,我们如何将设计稿中的各元素的px转换成rem

Flexible实现手淘H5页面的终端适配

我厂的视觉设计师想得还是很周到的,会帮你把相关的信息在视觉稿上标注出来

目前Flexible会将视觉稿分成**100份**(主要为了以后能更好的兼容vhvw),而每一份被称为一个单位a。同时1rem单位被认定为10a。针对我们这份视觉稿可以计算出:

1a   = 7.5px
1rem = 75px 

那么我们这个示例的稿子就分成了10a,也就是整个宽度为10rem<html>对应的font-size75px

Flexible实现手淘H5页面的终端适配

这样一来,对于视觉稿上的元素尺寸换算,只需要原始的px值除以rem基准值即可。例如此例视觉稿中的图片,其尺寸是176px * 176px,转换成为2.346667rem * 2.346667rem

如何快速计算

在实际生产当中,如果每一次计算px转换rem,或许会觉得非常麻烦,或许直接影响大家平时的开发效率。为了能让大家更快进行转换,我们团队内的同学各施所长,为px转换rem写了各式各样的小工具。

CSSREM

CSSREM是一个CSS的px值转rem值的Sublime Text3自动完成插件。这个插件是由@正霖编写。先来看看插件的效果:

Flexible实现手淘H5页面的终端适配

有关于CSSREM如何安装、配置教程可以点击这里查阅

CSS处理器

除了使用编辑器的插件之外,还可以使用CSS的处理器来帮助大家处理。比如说Sass、LESS以及PostCSS这样的处理器。我们简单来看两个示例。

Sass

使用Sass的同学,可以使用Sass的函数、混合宏这些功能来实现:

@function px2em($px, $base-font-size: 16px) {
    @if (unitless($px)) {
        @warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
        @return px2em($px + 0px); // That may fail.
    } @else if (unit($px) == em) {
        @return $px;
    }
    @return ($px / $base-font-size) * 1em;
}

除了使用Sass函数外,还可以使用Sass的混合宏:

@mixin px2rem($property,$px-values,$baseline-px:16px,$support-for-ie:false){
    //Conver the baseline into rems
    $baseline-rem: $baseline-px / 1rem * 1;
    //Print the first line in pixel values
    @if $support-for-ie {
        #{$property}: $px-values;
    }
    //if there is only one (numeric) value, return the property/value line for it.
    @if type-of($px-values) == "number"{
        #{$property}: $px-values / $baseline-rem;
    }
    @else {
        //Create an empty list that we can dump values into
        $rem-values:();
        @each $value in $px-values{
            // If the value is zero or not a number, return it
            @if $value == 0 or type-of($value) != "number"{
                $rem-values: append($rem-values, $value / $baseline-rem);
            }
        }
        // Return the property and its list of converted values
        #{$property}: $rem-values;
    }
}

有关于更多的介绍,可以点击这里进行了解。

PostCSS(px2rem)

除了Sass这样的CSS处理器这外,我们团队的@颂奇同学还开发了一款npm的工具px2rem。安装好px2rem之后,可以在项目中直接使用。也可以使用PostCSS。使用PostCSS插件postcss-px2rem

var gulp = require('gulp');
var postcss = require('gulp-postcss');
var px2rem = require('postcss-px2rem');

gulp.task('default', function() {
    var processors = [px2rem({remUnit: 75})];
    return gulp.src('./src/*.css')
        .pipe(postcss(processors))
        .pipe(gulp.dest('./dest'));
});

除了在Gulp中配置外,还可以使用其他的配置方式,详细的介绍可以点击这里进行了解。

配置完成之后,在实际使用时,你只要像下面这样使用:

.selector {
    width: 150px;
    height: 64px; /*px*/
    font-size: 28px; /*px*/
    border: 1px solid #ddd; /*no*/
}

px2rem处理之后将会变成:

.selector {
    width: 2rem;
    border: 1px solid #ddd;
}
[data-dpr="1"] .selector {
    height: 32px;
    font-size: 14px;
}
[data-dpr="2"] .selector {
    height: 64px;
    font-size: 28px;
}
[data-dpr="3"] .selector {
    height: 96px;
    font-size: 42px;
}

在整个开发中有了这些工具之后,完全不用担心px值转rem值影响开发效率。

文本字号不建议使用rem

前面大家都见证了如何使用rem来完成H5适配。那么文本又将如何处理适配。是不是也通过rem来做自动适配。

显然,我们在iPhone3G和iPhone4的Retina屏下面,希望看到的文本字号是相同的。也就是说,我们不希望文本在Retina屏幕下变小,另外,我们希望在大屏手机上看到更多文本,以及,现在绝大多数的字体文件都自带一些点阵尺寸,通常是16px24px,所以我们不希望出现13px15px这样的奇葩尺寸

如此一来,就决定了在制作H5的页面中,rem并不适合用到段落文本上。所以在Flexible整个适配方案中,考虑文本还是使用px作为单位。只不过使用[data-dpr]属性来区分不同dpr下的文本字号大小。

div {
    width: 1rem; 
    height: 0.4rem;
    font-size: 12px; // 默认写上dpr为1的fontSize
}
[data-dpr="2"] div {
    font-size: 24px;
}
[data-dpr="3"] div {
    font-size: 36px;
}

为了能更好的利于开发,在实际开发中,我们可以定制一个font-dpr()这样的Sass混合宏:

@mixin font-dpr($font-size){
    font-size: $font-size;

    [data-dpr="2"] & {
        font-size: $font-size * 2;
    }

    [data-dpr="3"] & {
        font-size: $font-size * 3;
    }
}

有了这样的混合宏之后,在开发中可以直接这样使用:

@include font-dpr(16px);

当然这只是针对于描述性的文本,比如说段落文本。但有的时候文本的字号也需要分场景的,比如在项目中有一个slogan,业务方希望这个slogan能根据不同的终端适配。针对这样的场景,完全可以使用rem给slogan做计量单位。

CSS

本来想把这个页面的用到的CSS(或SCSS)贴出来,但考虑篇幅过长,而且这么简单的页面,我想大家也能轻而易举搞定。所以就省略了。权当是给大家留的一个作业吧,感兴趣的可以试试Flexible能否帮你快速完成H5页面终端适配。

效果

最后来看看真机上显示的效果吧。我截了两种设备下的效果:

iPhone4

Flexible实现手淘H5页面的终端适配

iPhone6+

Flexible实现手淘H5页面的终端适配

总结

其实H5适配的方案有很多种,网上有关于这方面的教程也非常的多。不管哪种方法,都有其自己的优势和劣势。而本文主要介绍的是如何使用Flexible这样的一库来完成H5页面的终端适配。为什么推荐使用Flexible库来做H5页面的终端设备适配呢?主要因为这个库在手淘已经使用了近一年,而且已达到了较为稳定的状态。除此之外,你不需要考虑如何对元素进行折算,可以根据对应的视觉稿,直接切入。

当然,如果您有更好的H5页面终端适配方案,欢迎在下面的评论中与我们一起分享。如果您在使用这个库时,碰到任何问题,都可以在Github给我们提Issue。我们团队会努力解决相关需Issues。

Update

同学反馈说需要一个在线的DEMO,那么随便写了一个DEMO,希望对大家有所帮助。没有测试所有设备,可能不同的设备有细节上的差异。

DEMO

请用手机扫下面的二维码

DEMO

Update【2016年01月13日】

首先,由衷的感谢@完颜(@SMbey0nd) 帮忙踩了这个坑,回想起iOS从78,从89,都踩过只至少一个坑,真的也是醉了。

手淘这边的flexible方案临时升级如下:

  • 针对OS 9_3的UA,做临时处理,强制dpr1,即scale也为1,虽然牺牲了这些版本上的高清方案,但是也只能这么处理了
  • 这个版本不打算发布到CDN(也不发不到tnpm),所以大家更新的方式,最好手动复制代码内联到html中,具体代码可以点击这里下载

@airen

使用了flexible库之后,还需要一个px就要去计算一下等于多少rem吗?虽然插件可以自动换算,但是怎样才可以根据设计稿的px来换算呢?也就是说比如设计稿是20px,在安装插件的情况下,我能直接输入20px来等插件自动换算吗?插件有自己的基准值,默认是40,所以每次使用的时候都是自己去随便估计一个px等插件自动换算,因为按照设计稿的px来直接换算的话,设觉比率不对!
我可能说的有点乱了!
反正就是我用了flexible库,并且也用插件自动换算,但是不能直接根据设计稿的px来换算成rem,换算出来的比率和视觉不一样!所以我是不是少了什么步骤???

高级CSS filters

在iOS系统上常常能看到高斯模糊(Gaussian Blur)效果,而这种效果早期使用CSS来实现是较为痛苦的一件事情。其实,早上2011年,浏览器就开始对CSS filters规范有所支持。也就是说在2011年浏览器就可以实现Filters效果。但这种效果基本上都只运用在SVG上(只有SVG支持Filter效果),而且只有Firefox浏览器支持,并且只能运用在HTML上。这也造就CSS要实现Filters效果是非常蛋疼的一件事情。比如说下图的效果:

高级CSS filters
高级CSS filters

对于做原生开发的同学来说,这样的效果并不是难事(可以点击这里这里查看相关讨论),而我们是一名CSSer,我们坚持要用CSS来完成。

可能你首先想到的是CSS的filter中的blur(),或者SVG中的filter。但这些都不是新技术,而且到处也常见:

这些都不是CSS的新特性,那什么是新特性呢?不知道你是否有留意到@Johan在《Safari 9中有哪些新特性》一文中首先介绍的就是一个**Backdrop filters**,而且在@iamvdo的《高级CSS filters》对backdrop-filter属性做了详细的介绍。那么今天在这篇文章我们一起来看看通过哪些CSS的新特性可以实现类似iOS系统中那种高斯模糊效果。

backdrop-filter

backdrop-filter是在Filter Level2提出来的。其取值和filter Level1filter属性的属性值一样,包括:

其效果如下:

CSS filter

既然backdrop-filter整出的效果和filter没有差别,那还整个新属性出来,这不蛋疼?其实否也,你是否有发现过,最早在SVG上得到的filter效果,只能使用在SVG元素上;而filter使用在元素上,会直接影响其后代所有元素。那么问题来了,如果只需要对元素的背景做filter效果,怎么破。这个时候就突显了backdrop-filter的重要性。首先来看一个@iamvdo录制的效果视频:

在线DEMO:

<iframe id="XbGeej" src="http://codepen.io/airen/embed/XbGeej?XbGeej=300&theme-id=0&slug-hash=XbGeej&default-tab=result&user=airen" scrolling="no" frameborder="0" height="300" allowtransparency="true" allowfullscreen="true" class="cp_embed_iframe undefined" style="width: 100%; overflow: hidden;"></iframe>

注:在你的浏览器中你是看不到有任何效果,因为到目前为止仅有Safari 9浏览器支持,如果您想在浏览器中看到相应的效果,可以下载Webkit Nightly浏览器。

回过头来,其主要在#elem中使用了:

background: rgba(0,0,0,.2);
backdrop-filter: blur(2px) hue-rotate(180deg);

那么使用backdrop-filter可以实现我们一直无法实现的高斯模糊效果。

.header {
    background-color: rgba(255,255,255,.6);
    backdrop-filter: blur(5px)
}

每一个元素到达顶部header下面都会有一个blur(5px)的高斯模糊效果。

<iframe id="gpEGjZ" src="http://codepen.io/airen/embed/gpEGjZ?gpEGjZ=400&theme-id=0&slug-hash=gpEGjZ&default-tab=result&user=airen" scrolling="no" frameborder="0" height="400" allowtransparency="true" allowfullscreen="true" class="cp_embed_iframe undefined" style="width: 100%; overflow: hidden;"></iframe>

在上面的示例中,使用了@supports属性来做一个条件判断,当浏览器支持backdrop-filter属性,就有效果:

@supports (-webkit-backdrop-filter: none) {
  .Box-header {
      background: rgba(255,255,255,.6);
      -webkit-backdrop-filter: brightness(1.5) blur(4px);
  }
}

下面的GIF图展示了案例的真实效果

CSS filters

backdrop-filter除了可以实现类似iOS系统上的高斯模糊效果之外,还可以实现一些其它效果,比如说提高图像上的文本的可读性效果,而且还可以结合多个backdrop-filter属性值,可以实现类似于CSS混合模式的图片合层效果。

CSS filters

使用backdrop-filter事项

在使用backdrop-filter时,有一些小细节需要注意:

  • 运用backdrop-filter元素的背景应该使用半透明,不然永远看不到效果
  • backdrop-filter属性和裁剪属性(如border-radiusmaskclip-path等)结全在一起使用时,会有Bug产生
  • backdrop-filter可以创建一个堆栈文本(Stacking Context),类似于opacity属性一样
  • 可以配合动画属性animation一起使用
  • 到目前为止,仅有Safari浏览器支持,而且还需要添加前缀:-webkit-backdrop-filter,如果你使用autoprefixer这样的插件,无需考虑前缀相关事项

浏览器兼容性

可以通过CanIUse查看各浏览器厂商对backdrop-filter的兼容:

<iframe src="http://caniuse.com/css-backdrop-filter/embed" scrolling="no" frameborder="0" height="300" allowtransparency="true" allowfullscreen="true" style="width: 100%; overflow: hidden;"></iframe>

filter()

在2012年的一篇文章《CSS3 Filter的十种特效》介绍过filter属性。如果你使用过这个属性制作一些效果,一定有发现过,他会直接影响其后代元素,有点类似于opacity。但很多时候,只是希望元素的背景做效果调整,又不希望他会影响其他元素。而且又没有backdrop-filter属性的情形之下,filter()就显得格外的重要。

在继续往下阅读之前,你要注意一点,filter()并不等于以前介绍过的filter属性。简单的理解,一个是函数,一个是属性。那么我们今天要说的是filter()函数。为了能更好的与filter属性区分,filter()函数接受两个参数:

filter(<url>, <filter-function-list>)

其中<url>是指一个图像,<filter-function-list>是一个过滤器。这两者结合在一起将会返回一个处理过的新图像。如:

.element {
    background: filter(url(path/to/img.jpg), blur(5px));
}

因此,你可以给图片使用过滤效果,然后填充到元素中,比如background-filterbackground-opacitybackground-blur等等。如下图所示:

CSS Filters

上图展示了,给背景图使用了不同过滤属性得到的不同效果。效果是很完美,但是浏览器对其支持力度相当的差,到目前为止,也仅有Safari 9支持。

注意事项

值得一提的是,backdrop-filterfilter()可以使用CSS3的transitionanimation实现一些圆滑的过度效果或动画,甚至还可以使用JavaScript。

比如下图所示的一个效果:

CSS Filters

总结

虽然使用SVG或者CSS3的filter属性能实现类似的效果,但我还是非常的期待浏览器能支持backdrop-filterfilter()函数的特性。这样可以通过一行简单的CSS代码就能实现很多特殊(以前依赖于图片)效果。当然,这些特性能让我们快速实现需要的一些特殊效果,但在性能上会有很大的影响,根据以前使用filter属性的经验来看,这两个属性对性能的影响也将不小。

除此之外,虽然浏览器支持力度很小,但在iOS系统上我们即将可以使用这样的特性。最常见的一个,可以拿这两个特性实现高斯模糊的效果。

特别声明:本文图片、视频与案例都源自于@iamvdo的《高级CSS filters》一文。

dpr=2或者等于3的时候,border设置为1px的时候异常

请问下,为什么在dpr=2和3的 iphone手机上面 border 设置为1 的时候显示异常啊? 比如有个场景是:添加到购物车,加减号的border设置为1px点击的时候会有异常的情况。 是否和 meta设置有关? content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no 都为0.5 和0.3333333的时候 边框会有异常,请问淘宝是怎么解决的呀?

我理解的阿里无线前端“架构”(半鸡汤,少干货)

写在前面的话:

对于文中涉及“脱敏”的内容,链接经常一环套一环,无法输出,很抱歉...
怀着一个酝酿了蛮长时间的念头,打开电脑,想对一些思考做一点记录。关于标题,关于“前端”,关于“架构” 。
其实是有蛮多话想说的,但是前面这几个字却打上去了又删掉。想为下面的内容找一个合适的开始。但是似乎总是不那么如意。
回到这个话题,或许应该从以前的认知慢慢说起。

过往的认知

不可否认的说,曾经很长的一段时间,当我大部分时间还集中在业务上的时候,对“架构”这个词有点嗤之以鼻。尤其是“前端架构”。觉得说“前端”这个软件工程化都相对薄弱,还在拼死拼活的往成熟的语言领域,往已有的软件工程慢慢靠近的的方向,真的需要所谓的“架构”么。
dbf_contempt500
总觉得在这个阶段,

  • 所谓的前端架构更多的是在纸上谈兵,拿以往的软件工程的**和概念硬往自己身上套。
  • 所谓的做架构的同学更多的是在炫耀自己的技术深度和视野,看到前沿的技术不管合不合适就拿来做方案,硬往业务上推,来成全自己的KPI
  • 总觉的所谓的“技术架构”无非就是一些个人主观化的思路和概念,形成一些看起来成体系的代码组织方式,以便完成业务后可以说:看,基于我的架构有多好的通用性,扩展性,代码看起来多优雅...

回到我现在的立场,我看到的当前团队中不同的同学对于“架构”这个词的认识和看法,无非有两种:

  • 要么和之前的我一样,嗤之以鼻,做架构的有什么了不起,无非是老板给你安排个“轻松点的活”,做做方案,不用受业务的压迫。还得逼着一线每天认真辛苦的码代码的同学接受你一时想出来的Idea。
  • 要么是另一个极端,觉得做架构的同学就NB,觉得比做业务的同学从概念上就高一个等级。然后死命的想要往“架构”这个方向靠。

可是当有一天我自己需要站在团队的角度去思考基础建设的缺失,思考怎么才能帮助到团队的时候。才发现“架构”这个词既没有想象中那么“不堪”,当然也没有想象中那么“容易”。同时,也没有别人眼里那么NB,高人一等。
反而是越来越多的谦卑甚至恐慌,当沉下心来想想,确实,我们可能误会“架构”本身了。我们自己往他身上加了很多的主管臆想。

我理解的架构:是团队的,不是架构师的

第一条我就想说这个,因为TA的确应该是大前提,如果做架构的同学不是站在团队的角度来思考问题,思考解决方案,而是以自己过往的经验,自我的判断说应该怎么怎么样。那必然是会沦落到被人嗤之以鼻,甚至拖业务和效率的后腿。

  • 架构一定不应该成为只是你想要的样子,也不能只是老板想要的样子,一定应该是团队想要的样子。

在我正式接下为团队基础建设方向做规划和思考这件事情之前。去年在团队内做了一段时间的“SWAT”,也就是真正意义的上的“灵活资源”,做团队任意方向的支持。在做“团队支持”这个期间,参与了不同形态的业务项目,产品化的,运营化的,长线的,短线的,消费者端的,商家端的,前台重视内容和体验的,后台重视效率和结构化的,等等一系列不同的项目,包括一些不直接透明到业务的专项。当前参与程度深浅不一,但总体这个过程让我感受到了一件事情:

  • 不能凭想象和自我经验判断说团队需要什么?你要的答案一定要去和团队对话,和团队成员对话,或者参与到不同形态的“他们”当中去,去发掘他们想要什么。

收集信息和问题是做决策和方案的第一步,这个观点说出来大家都知道,但是实际做的过程中可能不一定能想得到了。举个例子:

  • 你要做工具,做给新人的,就要站在新人的角度来使用它,发现它是不是真的有用。做给流程规范的就必须站在项目实践的角度去实践TA,而且不是你自己觉得好用就乐呵呵,因为你自己并不是“架构”这个方向的真正用户
  • 更典型的例子,前端的自动化测试。做之前第一件事情是弄清楚在当前时间节点下,当前团队状况下,团队是否需要TA。进而才是怎么在团队的层面上去落地这件事情。而不是自己想当然的做一套方案。当前的团队并不需要这个东西,那么方案再完善又有什么用?

“架构是属于团队的”,这个观点一个方面是上面所说的,TA的需求和解决方案应该来源于当前团队。另一个方面是架构的进展和设计一定也是对团队透明和公开的。
如果进展和方案不能随时保持对于团队的公开和透明,也很难保证当到了最终产出的时候,还能保持最开始的方向一致性。

今年上半年开始,我们的周会内容有了小小的变动,把以往的单纯的团队内分享的例会转变为一个始终围绕团队基础建设,团队发展,和个人发展的交流会。植入了一个每周固定的环节,就是“基础建设进展和问题一周汇总”。
保持公开和透明,也可以随时就问题进行讨论。给自己和团队一个面对面的机会。
确保是大家想要的,同时也希望能潜移默化的形成大家对于团队建设的方向感和全局观。

我理解的架构:是横向全局的

这应该是做“架构”最基础的要求。也就是需要对整个团队,结合整个行业的发展保持全局的观望和了解。并且可以在此基础上基于团队现状做出对未来的基本判断。
“做出判断”这件事情,说简单也简单,说难也难。简单的是无非就是做几个选择题,选出今年,或者近期内要做的事情。难得是怎么来保证你的选择在当前的团队来看,是正确的。什么阶段做什么事情。

我记得今年上半年开始,我开始尝试担起前端团队的基础建设收敛相关的事情的时候。结合去年和今年的现状,整理过一个简单的框架图。在 "手淘前端在工程化道路上的“匍匐”" 文章里面有Po过。后来有过一些更新和小调整。大致如下:
_
归结起来是

  • 两个中心 (端和效率)
  • 八个方向
    • 基础库+功能组件+UI框架 (对应“效率”)
    • “端”的延伸 (对应“端”)
    • 规范和工程流程
    • 工具链路
    • 数据和性能
    • 自动化测试+持续集成
    • 前端安全
    • 服务和周边

八个方向中,落实到两个中心的必然是今年的重点。工具和性能是去年的重点,今年在已有基础上升级。其他的子方向在今年会开始探索。
这其中由于团队历史和现状的原因,其实有一些点是大家都在火热在抓的,但在我们团队中并没有放到今年的重点。比如

  • 前后端分离

也有在当前团队现状还不到时候做的(并不紧迫)的事情,比如

  • 前端基础服务(包括构建和工程的服务化,新人系统,内部项目域名和服务资源申请和部署自动化.. 等等)

以上的信息可以理解为“架构是横向全局” 这个观点的一个表现。
个人觉得做出判断的前提确实是需要了解别的优秀的团队在做什么,行业在做什么。再结合团队的现状才有可能知道我们需要做什么。
然而,了解别人的过程,其实反而也是让人“谦卑”的过程。

有时候知道的越多,会让人觉得越渺小。

你觉得自己在某方面做的还不错了,但是一定有人有团队有更优秀的方案和实践。

所以,“全局”,不仅是对于自己团队现状的全局认知和判断,也是在其他团队放到一起的“全局”评估。

  • 全局意味着 - 清楚的知道团队在当前阶段应该做什么事情
  • 全局意味着 - 清楚的知道团队的现状,优势和问题
    • 不至于高傲的迷失了方向
    • 也不至于卑微的找不到出路

我理解的架构:也是垂直深入的

在我的理解里,所谓“做架构”的同学们,不应该只是单纯的有“全局观”。同时也需要对每个垂直的领域保持一定的“绝对深度”。

就拿上面关于“全局”的几个子方向来说,我希望在当前定下的细分领域,想要做“架构”的同学在任何一个细分领域上都能保持一定的绝对深度。可能对于一个人的精力和资源会有一些挑战,但是我觉得在一定程度上是应该的。

在精力允许的范围内,每一个子领域里应该都需要尽可能的参与方案的探讨,制定,代码的实现,团队的落地整个过程。

拿我们自己团队的情况来说,至少应该知道:

  • 基础库和组件库,UI框架
    • 未来形态的发展应该是什么样?
    • CommonJS模块范式的迁移的自动化实现方案是什么?代码实现思路是什么?
    • 模块依赖关系弱关系到强关系的包装需要做哪些事情?
    • 控件的规范是否需要迁移到WebComponents?
    • 如果迁移,规范是什么,怎么定最小Feature的polyfill集合?
    • polyfill代码应该怎么来实现?
    • UI部分的组件复用应该怎么来做?可视化还是命令化?
    • UI库的mixin部分的style-lib和组件层面的view-lib怎么更好的管理?
    • ...
  • 端的部分
    • ReactNative的现状和痛点是什么?解决方案是什么?代码实现的难点在哪里?
    • RN的组件库怎么来组织构建?一个RN的组件应该怎么来写?
    • RN在性能和稳定性上的解法有哪些?现状是什么?
    • 业务层面的数据上报方案是什么?代码上的思路该怎么做?
    • 是否能明晰的判断未来,知道什么时候该坚持?什么时候该寻找别的出路?
    • GDOS的目标和意义是什么?为什么要做GDOS?
    • 对接通用算法和选品的难点是什么?怎么样定商品化的json schema?
    • 甚至java的部分,hsf的对接是否也能够参与?
    • ...

以上举例,提出每个子方向细化的问题,在心里对重要的细节有认知,有答案,也是我认为做“架构”的同学所必须要明白的事情。

同理,
工具层面,规范层面,工程流程,性能,单元测试,前端安全等等,期望尽可能深的参与到具体的实践和落地上去。(包括代码的具体实现...)
tool1 tool2 vue img2 img3

做架构不是只有idea,然后全部推动别人去做,更重要的是自己需要深度的参与,才能保持清醒的认知。

这是我个人的认知,不一定对,当然

  • “在保持广度的情况下还要保持一定的深度”

也会对于个人的时间精力有一定的挑战。

反过来说,如果“架构”已经大到需要5个人以上的团队才能支撑,那时再做合理的分工也不迟。

我理解的架构:是海纳百川,是透明开放的

在之前的表述中,提到“架构”至少是需要对团队透明的,来源于团队,尊重团队,也服务于团队。而这里说的海纳百川,开放透明更是侧指我们以公司单位,那么理应在公司内也是透明和开放的。

  • 对外不用多说,公司自有公司的壁垒,但至少对内,我们应该共享一片蓝天。

shot_1296383136
当你不关注,不闻不问的时候,或许还不觉得,但是当有心想去了解一些事情的时候,却发现似乎并没有想象中那么透明。

我在 上周的周报:不聊技术,聊感受 中其实提到了一些关于“技术栈”和“技术栈”之前的壁垒问题,也包含“前端”本身团队壁垒的问题。

我的观点是:

  • 团队技术壁垒不是问题,毕竟每个团队的业务形态,抓的方向并不一致。但是不透明是问题,想发掘其他团队的好东西却要费点功夫。

其实回过头来想想,集团内其实有不少的方式似乎想解决这个问题,比如淘宝的“懒懒”,支付宝的“芝士会” 等等,从定期主题分享的方式尝试抹平BU间不透明的问题。也有属于集团层面的技术博 ATA, 包括前端也有自己的 委员会,本质上也是希望打通BU间的信息。

我们看起来有这些途径,理应可以解决不少壁垒不透明的问题才对,可是到我真实的感受却是还有好多有价值的信息,方案,项目等,我从上面的渠道获取不到的。

可能是“粒度”的问题,可能是“传达”的问题。咋们暂时先不去细究。说实话,我个人觉得比较直接打破我觉得有壁垒的苦恼的事情是 @拔赤 公开的周报。

我近期了解到很多航旅有价值的信息,他们近期着重发力的方向,不可置否的说,基本都是从 @拔赤 每周的周报中觅得的。当然,这和他向来高质量的周报内容有直接关系。

所以,我做的第一件事也是把无线前端从今年上半年开始的每周基础建设,架构的方向和进展以周报的形式公开来。一方面从我们自己开始做到“透明化”,同时也愿意以谦卑的心态和大家进行讨论和交流。

阿里内外的周报系统我觉得是个好的开始。既然有选择“公开”的选项,我觉得也应该加上“周报关注”的功能,只要我关注的人某一周的周报内容是“公开”的,不管他的周报有没有直接抄送我,我都可以收到。

话题有点扯远了,我要表达的意思是,我期望寻得一种途径,可以让我短平快,高效的知道优秀的大家们都在做什么事情。

最近在团队内开始推动一个叫做 “取经之路” 的计划,其实也就是希望团队的同学都能保持有心思去发掘其他团队的优秀的东西,以取经的形式主动去了解,再带回来传道授业解惑。
希望团队本身能从中开拓视野和思路,同时对于做“唐僧”的同学来说,本身也是一种成长。

我理解的架构:关键词不是“高精尖”,而是“合适”

最近越发的觉得“合适”这个词的精妙与深意。站在外人的角度,去评判一件事情的好坏,一个技术方案的优劣,不应该从你的角度去看,连行业的普适标准甚至都不一定受用。因为可能在你看来有失偏颇的方案在他的团队的当下就是“合适”的。

换句话说:

  • 在我看来,技术方案优和劣或许没有绝对之分,只是因为团队的历史原因,团队现状,发展出了不同的样子。只要它对于当前的团队是合适的,我就认为它是好的。

说到这里,我不免又想到了“恋爱”这件事。如果这么说来的话,不觉得和“恋爱”的情况略像么。通俗点说:

  • 爱美之心,人皆有之;漂亮的女孩子,谁都喜欢,你费劲心思去追一个大家公认的女神,这件事情能不能成,最终是变成“金童玉女”的千古流传段子还是变成“癞蛤蟆想吃天鹅肉”的恶俗剧情,前提是要认清自己。当前的自己如果如果就是配不上女神,那何必自讨苦吃,还不如努力锤炼自己,到有一天走上人生巅峰再去赢取白富美也不迟不是么 ;)

比方不一定恰当,但是道理是通的。我想说的是,技术的方案和设计是不是好的,对的,不是看你用的技术,选的方案是不是够高精尖,够前沿。而是看TA是不是适合你当前的团队现状。

举个例子:

  • ES6 当下被好多团队在实践,吵得火热。可以理解为ES6的产品化,包括周边polyfill的完善,以及一整套方案的打通,在当下看起来是靠前沿的,面向未来的,高精尖的。 如果我们的团队就那么几个人,如果团队负责的业务就那么两三个,形态也相对单一,那么我觉得快速的拥抱ES6,尝鲜,玩新技术没有任何问题。而反过来,如果当前团队的体量,现状,团队组成不允许一个步子迈这么大,那么这件事如果硬按“拔苗助长”的方式推进,有可能会产生很大的副作用,开发效率,质量保障可能都会收到影响。

所以,架构和方向不应该朝着“高精尖”的方向走,那不应该是目标,“合适”的,才是最好的。

在适当的时候,用适当的方案去做对应适当的事情,
就好比,
在适当的时候,遇上对的人。

flexible 布局有疑问

我引入了flexiable.js发现会动态更改html font-size的值,如果我要布局的时候,怎么将px转换为rem呢,由于font-size一直在变,我不知道该如何换算

对无线电商动态化方案的思考(二)

上一篇谈到了我对无线电商动态化的理解,并简单提到了我们自己提出的技术方案:Weex,今天就来详细介绍一下 Weex

一句话介绍

Weex 是一款轻量级的移动端跨平台动态性技术解决方案!

几个特点

轻量

体积小巧,语法简单,方便上手

可扩展

业务方可自行横向定制 native 组件和 API

高性能

快速加载,快速渲染,体验流畅

其它

  • 拥抱标准:基于 Web 标准设计语法
  • 响应式界面: 通过简单的模板数据绑定轻松解决数据和视图的同步关联问题
  • 多端统一:iOS、Android、HTML5 多端效果一致,撰写一次就可以轻松达到跨平台的一致性,无需针对多套平台单独开发,省时省力
  • 复杂逻辑描述:动态性不只体现在展示效果的动态性上,更体现在可以实时调整复杂的数据处理方式和逻辑控制方式
  • 组件化:组件之间通过 webcomponents 的设计完美的隔离,并可以通过特定的方式进行数据和事件的传递
  • 生态&链路:我们为 Weex 的开发者和使用者在不同维度上提供了各式各样的工具和平台,包括代码打包工具、开发者调试工具、部署平台、Playground、经典案例、入门指南和详尽的文档等。你不是从零开始,你也不是一个人在战斗!

如何工作

1. 本地组件开发

首先,我们像开发 webcomponents 一样,把一个组件分成 <template><style><script> 三部分,刚好对应一个组件的界面结构、界面样式、数据&逻辑。

components

图:组件化开发思维和书写方式

细节1:顺便说一句,这也是我们认为描述界面的最佳实践。

代码示例:

<template>
  <container style="flex-direction: column;">
    <container repeat="{{itemList}}" onclick="gotoDetail">
      <image class="thumb" src="{{pictureUrl}}"></image>
      <text class="title">{{title}}</text>
    </container>
  </container>
</template>

<style>
  .thumb {width: 200; height: 200;}
  .title {flex: 1; color: #ff0000; font-size: 48; font-weight: bold; background-color: #eeeeee;}
</style>

<script>
  module.exports = {
    data: {
      itemList: [
        {itemId: '520421163634', title: '宝贝标题1', pictureUrl: 'https://gd2.alicdn.com/bao/uploaded/i2/T14H1LFwBcXXXXXXXX_!!0-item_pic.jpg'},
        {itemId: '522076777462', title: '宝贝标题2', pictureUrl: 'https://gd1.alicdn.com/bao/uploaded/i1/TB1PXJCJFXXXXciXFXXXXXXXXXX_!!0-item_pic.jpg'}
      ]
    },
    methods: {
      gotoDetail: function () {
        this.$openURL('https://item.taobao.com/item.htm?id=' + this.itemId)
      }
    }
  }
</script>

显然这些代码是不会被 native app 识别的,我们要想办法让这些代码可运行。所以我们同时做了三件事:

  1. 在本地用一个叫做 transformer 的工具把这套代码转成纯 JavaScript 代码
  2. 在客户端运行一个 JavaScript 引擎,随时接收 JavaScript 代码
  3. 在客户端设计一套 JS Bridge,让 native 代码可以和 JavaScript 引擎相互通信

所以紧接着第二步,就是用 transformer 对代码进行转换,变成客户端可运行的 JavaScript 代码

transformer

图:本地开发时的 Weex Transformer 工作原理

其实本地开发还有一点很重要,就是把复杂的界面通过组件化的方式进行分解,并合理的建立组件之间的组合和调用关系。

最终,我们把简单组件组合成复杂的界面,并通过 transformer 打包成一个完整的程序包 (主体是一段 JavaScript 代码)

细节2:由于 Weex 组件的开发和 Web 组件的开发非常接近,但是对标准的支持范围和一些细节是有不同之处的,我们会贴心的在 transformer 里加入了一些友情提醒,帮助大家回避常犯的书写错误。

2. 客户端渲染

上一节已经提到了,我们在客户端会运行一个 JavaScript 引擎并且有 JS Bridge 通信机制。这里再介绍具体一些:

native 渲染和 JavaScript 引擎之间,主要进行三类通信:

  1. 界面渲染,单向 (JS -> native):这毫无疑问,JavaScript 引擎需要把界面的结构和样式告诉 native 端,这样我们才能得到 native 级别的终极界面效果
  2. 事件绑定与触发,双向:在我们的客户端技术方案中,native 端只负责界面渲染和非常薄的事件触发层,事件的逻辑处理都会放在 JavaScript,这样我们就具备了复杂数据处理和逻辑控制的动态性可能。JS 告诉 native 需要监听的交互行为,而当用户产生对应的交互行为时,native 端会把交互信息回传给 JS
  3. 对外的数据/信息请求与响应,双向:JS 引擎在处理特殊逻辑时,难免需要向服务器请求数据、或请求本地的系统信息和用户信息、或调用 native 的某个功能,这个时候也会通过 JS Bridge 进行请求,native 收到这些请求之后,也会在必要的情况下通过 JS Bridge 把信息回传给 JS 引擎

runtime

图:客户端运行时的 JS 引擎和 native 之间的通信

再加上外层对 Weex 实例的管理,整套机制就可以顺畅的工作起来了

细节3:native 端渲染的时候,我们以图片和文字的形态为主,并大量依赖了标准的 CSS 样式进行细节的渲染

细节4:我们把框架层面的 JS 代码全部提前放在了客户端本地,并提前运行做好准备。这样本地生产的 JavaScript 是非常小的,网络传输的代价也非常低,而在客户端运行的初始化成本也非常低。整条链路都和界面打开速度息息相关

细节5:我们在 JS 处理界面逻辑的过程中采取了数据监听+依赖收集的策略,既没有通过脏检查,也没有通过全量 diff Virtual DOM 树的方式,因为通常在移动端,数据变更都是非常小量的,经过我们的实践,这套方案完全可以应付移动端日常的动态性界面需求

virtualdom

图:Virtual DOM 的数据绑定和更新机制

细节6:我们对业务上通用常用的组件进行了封装,并且暴露规范化的类型 (标签名)、特性、样式、事件、上下级约束等维度的定义。这样所有的业务界面都可以用这些基础的组件搭建而成

细节7:我们对业务上通用常用的 API 进行了封装,并且暴露规范化的 JS API

3. 服务端部署

我们在服务端提供了基础的程序包发布,给每个程序包一个特定的 page id,然后为客户端提供通用的服务,通过 page id 获取程序包,这样本地开发、动态实时部署、客户端动态化渲染和逻辑处理就完美的串联在一起了

细节8:实际上,除了界面本身可以动态化之外,客户端的 JS 引擎的代码、还有部分 native 的实现,我们也准备了相应的动态化机制,也就是说客户端的动态能力本身也是具有动态性的

4. 浏览器端渲染

我们还会面对这样的场景,就是一个客户端的业务,会通过微博之类的渠道进行转播和推广,当用户手机里刚好安装了手机淘宝客户端,那么会直接“拉起”客户端进行相应的界面展示,如果没有装手机淘宝,则需要在浏览器里展示一个相同或接近的界面。自然 Weex 技术方案支持的业务也有这样的需求。所以我们同时提供了 HTML5 版本的技术方案,同一份 JavaScript 程序包,可以同时通过客户端的 JS Bridge 渲染成为 native 界面,也可以通过浏览器渲染成为 web 界面。我们的做法也非常简单,就是把 JS Bridge 背后的 native 处理逻辑同构成了 HTML5 版本。然后发布这样的一个页面。

html5

图:HTML5 浏览器端的架构实现

细节9:我们能够同构 HTML5 版本和 native 版本,主要归功于我们在 JS Bridge、组件定义、API 定义方面的高度抽象——当然 HTML5 的版本在性能和体验上确实有一定的劣势,并不是最理想化的效果,所以核心的主战场还是客户端,这也和目前的移动互联网的形态相吻合

5. 周边

为了方便 native 界面的调试,我们还提供了配套的开发者工具,稍后会有更多介绍,同时我们的上层配套可视化编辑器和装修工具,也在紧张的研发当中。

综上所述,整个 Weex 的工作原理大致可以用一张图来表述:

workflow

## 回看 Weex 的几个特点 ### 轻量

我们致力于把开发体验、网络传输的大小、运行时的开销控制做到极致,并且尽可能的降低多端适配和优雅降级的成本

横向可扩展

我们对组件的定义和业务功能预留了很好的横向可扩展能力,这也业务方可以自由定制属于自己的 native 组件和 API,从而在后期可以通过实时发布不同的程序包来进行动态化控制。同时也因为它的横向可扩展性,Weex 的核心可以非常小,非常易于融入现有的无线技术体系

高性能

我们在网络传输、实例初始化、JS 运算、native 渲染能力等方面做了非常针对性且深入的优化,尤其是针对中低端安卓机,不论是加载时间还是运行时的流畅度,都比之前的方案有质的飞跃。我们对 CPU、内存、帧率、首屏渲染时间等核心性能指标也一直保持高度的关注,也建立了相应的线上监控和数据统计机制。而更多可优化的空间和方案我们还在不断的进行优化尝试。

下图是今天凌晨旧金山 QCon 上 Weex 技术方案的首次公开分享中的一页性能表现对比图,大家可以感受一下:

总结

这就是 Weex,如上一篇文章所介绍的:

  • 致力于移动端
  • 能够充分调度 native 的能力
  • 能够充分解决和回避性能瓶颈
  • 能够灵活扩展
  • 能够多端统一
  • 能够优雅“降级”到 HTML5
  • 能够保持较低的开发成本
  • 能够快速迭代
  • 能够轻量实时发布
  • 能够融入现有的 native 技术体系
  • 能够工程化管理和监控

目前 Weex 还在努力达到更高的性能、更高的扩展性、更低的开发成本、更完整的生态和工具链,也同时尝试接入更多的业务,体现出它的更大的价值。已经有很多业务方迫不及待的在和我们主动取得联系了,未来我们希望 Weex 能够逐步在集团内开放试用,并最终走向开源。

另外整个技术方案还有很多值得分享的东西,比如 transformer 的实现、组件和 API 的设计思路等,我们会再做针对性的分享和介绍

(马上更新第三篇ing)

继续阅读:

小白双十一之旅

从以前的双十一作为用户买买买到今年终于加入到了双十一中,觉得还是需要记录一下所感所想,今年双十一我主要负责行业市场的氛围相关的需求,淘友新增的双十一的页面,以及need项目的相关页面,下面我就只针对行业市场来做一些总结吧。

需求中的坑

大约从9月份开始,我所在的行业市场团队就已经开始把双十一的需求提上日程了。我所负责的双十一的需求其实并不难,只是在现有的一些场景下透出双十一的氛围,不过由于行业市场所涉及的页面比较多,所以开发过程中很琐碎,下面是在整个开发过程中经过的几个历程:

发布

行业市场9大基础频道,平均一个频道3-4个页面,这么多页面,然而一直以来的发布方式是纯手动把新构建好的js,css压缩文件复制粘贴到awp再发布,久而久之导致awp上的页面和本地的页面已经不同步了,就出现了不敢随意乱动的尴尬局面,但是确实不仅大大降低了工作效率,当遇到双十一这种需要同时把所有频道都做改动的情况时,更是分分钟就凌乱的节奏QAQ,最后在第一任师兄@晓田的鼓励下,在把需求完成后集中花了一周时间把线上和本地的html文件同步了,并结合了@岑安的htmlone,改成了awp命令行发布,终于不用再复制粘贴了!~

改改改

做业务改需求是难免的,双十一也不例外,前后经历了两次大大大...老板下发的意见,于是默默的把已经开发完成的需求又回滚回去...也从这两次的改动感受到了,要做一个这么大型且重要的活动,不仅需要从用户,技术等角度去考虑,还需要考虑诸如影响的好坏之类的因素,当然影响方面肯定更多的是由大大大...老板把控啦~

windvane OR 服务端?

开发过程中另一个反复了多次的就是顶部导航条的氛围由哪边来定开关的问题,最开始windvane给出的方案是提供一个借口,由h5根据服务端返回时间判断再调用,听起来是个符合逻辑的方案,但是后来发现一个体验不好的地方,因为接口返回需要时间,就会导致导航条不是一进页面就会透出氛围,而是等了一段时间再变成红色氛围,而且刷新的时候又会重复这个过程,体验很不好,又会觉得不专业。中间跟windvane那边的@炼玉同学沟通了这个问题,看是否能改一个方案例如维护白名单的方法把双十一需要透氛围的h5页面统一控制,最后得到的解决方法是由windvane来控制时间上的开关,h5只管一进来就调用,这个过程虽然反复了多次,也‘*扰’了@炼玉同学好几次,但是终于能解决这个问题啦,很棒!

零点惊魂

终于来到了期待已久了11月10号的0点那一刻,然而却在0点前的2个小时突然被各种呼叫,说行业市场所有页面都打不开,一看发现接口全部是超时了...时了...了...服务端同学第一时间联系了相关同学,了解到是交易链路上排查问题时把tair缓存删了,导致请求都走到了db,扛不住所以挂了,虽然问题原因找到了,但是解决的过程还是不顺利,因为流量实在太大不好控制,最后在大老板的拍板下先把行业市场区块在首页下掉,于是0点那一刻我终于可以放心的买买买了...本来还遗憾做了这么久的双十一居然要夭折了,不过最后在各个同学的努力排查解决下,终于还是在早上之前重新上上去了,也算是看着自己完成的东西出现在了双十一内容里,赞!

全民狂欢

双十一期间不仅仅是所有的技术同学在努力加班加点,其他的行政同学等等也都提供了超级贴心的帮助朋友圈已经转疯了!什么帐篷,按摩椅,健身器材,还有各种零食,水果补充能量,这些都是组成双十一的必不可少的部分,还有大家一起看湖南卫视,一起摇一摇,一起守着0点的珍贵时刻,更有双十一结束后别具特色的‘答蟹宴’,为我们的成绩欢呼的激动一刻,最后上几张‘买家秀’

IMG_4775

IMG_4796

IMG_4770

flow——A static type checker for javascript

介绍

众所周知,javascript是一个动态类型语言,变量类型不需要声明,运算过程中会根据需要自动转换类型,这个是js的优点,够灵活,编码简单。但是同时也是软肋,相信很多人都会遇到这个问题,明明1+2我希望得到3,但是很多时候得到的却是13,什么时候number转成了string都不知道。相比一步一步查出哪里转的,这样写parseInt(1)+parseInt(2)或许更省力更有保障。
今天给大家介绍的flow则可以避免以上的问题。flow 是facebook团队出的一个js静态类型检测器,目的是通过最小的代码修改检测出代码中的类型问题。

安装

官网提供了众多安装方法,起初我觉得使用npm应该会是最便捷的。

     npm install flow-bin —global

真的很慢,请耐心等待。
很慢之后还不一定会成功。
最后我这边实验出比较简单的方法,首先下载文件,然后在终端执行$PATH,找出环境变量路径,把下载的flow文件复制过去就行。

使用

在项目目录下执行 flow init,会生成一个.flowconfig文件
在需要检测的文件顶部增加注释

/* @flow */

执行flow check,检测你代码中是否有类型错误。
示例:

function simple(string){
    return string.length;
}
simple(1);

示例中,想取传入参数的length属性,flow检测到传入值是一个number,不存在length,则会报错。如图
img

flow server

flow server是为了提高检测效率的后台程序,支持在后台运行,并且只监测有修改的文件。
flow:项目第一次执行或者.flowconfig有修改第一次执行的时候会比较慢,请耐心等下
flow:后面有修改之后执行能快速显示错误
flow stop:项目开发完成停止服务

flowconfig

  • [ignore]忽略的文件,路径的匹配规则是正则表达式
  • [include]需要检测的其他目录的文件
  • [libs]当文件中有第三方库引用或者require全局的module时,需要在一个单独的文件里declare这个对象,这个的值是所有declare路径的集合
  • [options]包含若干key-value的配置,详情见官方文档

libs示例

var React = require('react');
var com = React.create({
    render: function(){
        return (
            <div className="page"></div>
        )
    }
})

示例中引用了react模块,没有带相对路径,flow找不到引用的地方。直接执行flow会报错,如图
img
这个时候需要我们在libs对应的目录下增加一个declare的文件,用了告诉flow我们这个引用的对象有哪些方法和熟悉

declare module react {
    declare function create(): any;
}

变量赋值类型

除了通过变量的属性和方法来推断变量类型是否正确外,flow还支持通过在代码里增加类型声明,来达到更精准的错误提示。变量声明类型示例如下

class People {
    name: string;
    constructor(name:string){
        this.name = name
    }
    getAge():number{
        return 18;
    }
}

function getLength(param?:string):number {
    /*param?:string传参类型什么,?表示此处可不传值;*/
    /*\:number为函数返回值类型,如果没有return可不写或写void*/
    var s:string = 'string';/*字符*/
    var ss:String = new String('string');/*字符对象*/

    /* s = ss*///类型不同,flow会报错

    var n:number = 12;/*数字*/
    var nn:Number = new Number(12);/*数字对象*/

    var b:boolean = true;/*bool值,仅能为true/false*/
    var bb:Boolean = new Boolean(true);/*bool值对象*/

    var o:{prop1:number,prop2:string} = {
        /*对象熟悉声明*/
        prop1: 12,
        prop2: '21123'
    }

    var v:void = undefined;/*undefined*/
    var a:any = 'aa';/*任意类型,除undefined*/
    var m:mixed = '1';/*任意类型+undefined*/
    var mm:mixed = undefined;/*任意类型+undefined*/

    var aa:Array<number> = [1,2,3,4];/*数组内值类型声明*/

    var P:Class<People> = People;/*自定义类型声明*/
    var p:People = new People('pt');/*自定义People类型*/

    if (param) {
        return param.length;
    } else {
        return 0;
    }
}

flow的两种模式

/@flow/ 只要带有这个注释,都会进行类型检测
/@flow weak/ 只对有加类型声明的变量进行类型检测

代码编译

增加类型声明的flow语法不是js的标准语法,这里需要使用babel来进行转换。目前可以选择使用的是babel的preset为react,或者babel的plugins为transform-flow-strip-types。不过在使用过程中发现class语法里react和flow同时解析会出现错误,所以为了保险起见最好先执行flow语法转换再做其他的语法转换。示例如下:

gulp.task('babel',['clean',],function(cb){
  return gulp.src('src/*.js')
         .pipe(
            babel({
              plugins: ["transform-flow-strip-types"]
            })
          )
         .pipe(babel({
            presets: ['react','es2015']
         }))
         .pipe(gulp.dest('build/'));
})

更多第一手资料,参见flow的官方文档
本文中提到的代码示例https://github.com/qingying/flow-demo

CSS Animation性能优化

CSS Animation是实现Web Animation方法之一,其主要通过@keyframesanimation-*或者transition来实现一些Web动效。不过今天我们聊的不是怎么制作Web动画,咱们来聊聊CSS Animation性能相关的话题。

浏览器渲染原理

关于浏览器工作原理之前有一篇非常出名的文章《浏览器的工作原理:新式网络浏览器幕后揭秘》。文章详细阐述了浏览器工作原理,下面用两张图来分别描述Firefox和Chrome浏览器对Web页面的渲染过程:

Chrome渲染过程

Chrome渲染过程

有关于Chrome浏览器渲染的详细内容,可以参考《图解浏览器渲染过程 - 基于Webkit/Blink内核Chrome浏览器》一文。

Firefox渲染过程

Firefox渲染过程

特别声明:接下来的内容都是针对于Chrome浏览器进行讨论。

Chrome渲染部分的实际含义

从上面的流程图中不难看出,Chrome渲染主要包括Parse Html、Recalculate Style、Layout、Rasterizer、Paint、Image Decode、Image Resize和Composite Layers等。简单了解一下其含义,以便后续内容的更好理解。

Parse Html

发送一个http请求,获取请求的内容,然后解析HTML的过程。

有一个经典的前端面试题:当你在浏览器中输入google.com并且按下回车之后发生了什么? 这个面试题或许能帮助大家更好的理解Parse Html,甚至是浏览器渲染的其他几个部分。

Recalculate Style

重新计算样式,它计算的是Style,和Layout做的事情完全不同。Layout计算的是一个元素绝对的位置和尺寸,或者说是“Compute Layout”。

Recalculate被触发的时候做的事情就是处理JavaScript给元素设置的样式而已。Recalculate Style会计算Render树(渲染树),然后从根节点开始进行页面渲染,将CSS附加到DOM上的过程。

任何企图改变元素样式的操作都会触发Recalculate。同Layout一样,它也是在JavaScript执行完成后才触发的。

Layout

计算页面上的布局,即元素在文档中的位置及大小。正如前面所述,Layout计算的是布局位置信息。任何有可能改变元素位置或大小的样式都会触发这个Layout事件。

触发Layout的属性非常的多,如果想了解什么属性会触发Layout事件,可以在**CSS Triggers**网站查阅。下图截了一部分:

Laout

Rasterizer

光栅化,一般的安卓手机都会进行光栅化,光栅主要是针对图形的一个栅格化过程。低端手机在这部分耗时还是蛮多的。

Paint

页面上显示东西有任何变动都会触发Paint。包括拖动滚动条,鼠标选择中文字等这些完全不改变样式,只改变显示结果的动作都会触发Paint。

Paint的工作就是把文档中用户可见的那一部分展现给用户。Paint是把Layout和Recalculate的计算的结果直接在浏览器视窗上绘制出来,它并不实现具体的元素计算。

Image Decode

图片解码,将图片解析到浏览器上显示的过程。

Image Resize

图片的大小设置,图片加载解析后,若发现图片大小并不是实际的大小(CSS改变了宽度),则需要Resize。Resize越大,耗时越久,所以尽量以图片的原始大小输出。

Composite Layers

最后合并图层,输出页面到屏幕。浏览器在渲染过程中会将一些含有特殊样式的DOM结构绘制于其他图层,有点类似于PhotoShop的图层概念。一张图片在PotoShop是由多个图层组合而成,而浏览器最终显示的页面实际也是有多个图层构成的。

下面这些因素都会导致新图层的创建:

  • 进行3D或者透视变换的CSS属性
  • 使用硬件加速视频解码的<video>元素
  • 具有3D(WebGL)上下文或者硬件加速的2D上下文的<canvas>元素
  • 组合型插件(即Flash)
  • 具有有CSS透明度动画或者使用动画式Webkit变换的元素
  • 具有硬件加速的CSS滤镜的元素

有关于Composite方面的深入剖析,可以阅读《无线性能优化:Composite》一文。

像素渲染流水线

通过前面的介绍,在屏幕上最终呈现的页面,是类似于图层一样合并输出到屏幕上的。其实所写的Web页面最终以像素的形式在浏览器屏幕上呈现。这样一来,我们需要理解所写的页面代码是如何被转换成屏幕上显示的像素。这个转换过程可以归纳为这样的一个流水线,主要包含五个关键步骤:

像素渲染流水线

  • JavaScript:一般来说,我们会使用JavaScript来实现一些视觉变化的效果。比如CSS Animation、Transition和Web Animation API。
  • Style:计算样式。这个过程是根据CSS选择器,对每个DOM元素匹配对应的CSS样式。这一步结束之后,就确定了每个DOM元素上应该应用什么CSS样式规则。
  • Layout:布局。上一步确定了每个DOM元素的样式规则,这一步就是具体计算每个DOM元素最终在屏幕上显示的大小和位置。Web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,<body>元素的width变化会影响其后代元素的宽度。因此,对于浏览器而言,布局过程是经常发生的
  • Paint:绘制。本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
  • Composite:渲染层合并。前面也说过,对于页面中DOM元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后在屏幕上呈现。对于有位置重叠的元素的页面,这个过程尤其重要,因为一量图层的合并顺序出错,将会导致元素显示异常。

上述过程的每一步都有可能会发生,因此一定要弄清楚自己的代码将会运行在哪一步。

虽然在理论上,而面的每一帧都是结过上述的流水线处理之后渲染出来的,但并不意味着页面每一帧的渲染都需要经过上述五个步骤的处理。实际上,对视觉变化效果的一个帧的渲染,有三种常用的流水线。

JavaScript/CSS =>计算样式=>布局=>绘制=>渲染层合并

像素渲染流水线

如果你修改一个DOM元素的“Layout”属性,也就是改变了元素的样式(比如widthheight或者position等),那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个reflow(重排)过程完成重新布局。被reflow(重排)的元素,接下来也会激发绘制过程,最后激发渲染层合并过程,生成最后的画面。

reflow又叫重排,是指浏览器计算页面的全部或部分布局所做的处理。reflow必定会引发重绘,这对于Web的性能影响是极大的。

JavaScript/CSS => 计算样式 =>绘制 =>渲染层合并

像素渲染流水线

如果你修改一个DOM元素的“Paint Only”属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只会绘制和渲染层合并过程。

JavaScript/CSS => 计算样式 =>渲染层合并

像素渲染流水线

如果你修改一个非样式且非绘制的CSS属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接做渲染层合并。这种方式在性能上是最理想的,对于动画和滚动这种负荷很重的渲染,我们要争取使用第三种渲染过程。

**通过前面这么多的内容介绍,我们可以得知,影响Web性能主要过程包括Layout、Paint和Composite。那么对于CSS Animation而言,我们的所有操作都是通过CSS的样式控制动画,言外之意,只要是会触发Layout、Paint和Composite的CSS属性都会直接影响动画的性能。**在CSS中所有影响Layout、Paint和Composite的属性都可以通过CSS Triggers**网站查阅。那么如何避免达到前面所述的,整个动画尽量避开重排和重绘,只做渲染层合并呢?暂且先不讨论,把这部分放到最后面来讨论。接下来接着先看看其他相关的知识点。

渲染性能

在理解渲染性能之前,我们有必要先了解前面提到的两个概念重排(也就是回流)和重绘。因为这两者与前面介绍的像素渲染流水线中的LayoutPaint都有关系,而且Layout和Paint对性能的渲染又有莫大的关系。

Reflow(重排)

Reflow(重排)指的是计算页面布局(Layout)。某个节点Reflow时会重新计算节点的尺寸和位置,而且还有可能触其后代节点Reflow。在这之后再次触发一次Repaint(重绘)

当Render Tree中的一部分(或全部)因为元素的尺寸、布局、隐藏等改变而需要重新构建。这就称为回流,每个页面至少需要一次回流,就是页面第一次加载的时候。

在Web页面中,很多状况下会导致回流:

  • 调整窗口大小
  • 改变字体
  • 增加或者移除样式表
  • 内容变化
  • 激活CSS伪类
  • 操作CSS属性
  • JavaScript操作DOM
  • 计算offsetWidthoffsetHeight
  • 设置style属性的值
  • CSS3 Animation或Transition

Repaint(重绘)

Repaint(重绘)或者Redraw遍历所有节点,检测节点的可见性、颜色、轮廓等可见的样式属性,然后根据检测的结果更新页面的响应部分。

当Render Tree中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格、而不会影响布局的。就是重绘。

将重排和重绘的介绍结合起来,不难发现:重绘(Repaint)不一定会引起回流(Reflow重排),但回流必将引起重绘(Repaint)

既然如此,那么什么情况之下会触发浏览器的Repaint和Reflow呢?

  • 页面首次加载
  • DOM元素添加、修改(内容)和删除(Reflow + Repaint)
  • 仅修改DOM元素的颜色(只有Repaint,因为不需要调整布局)
  • 应用新的样式或修改任何影响元素外观的属性
  • Resize浏览器窗口和滚动页面
  • 读取元素的某些属性(offsetLeftoffsetTopoffsetHeightoffsetWidthgetComputedStyle()等)

可以说Reflow和Repaint都很容易触发,而它们的触发对性能的影响都非常大,但非常不幸的是,我们无法完全避免,只能尽量不去触发浏览器的Reflow和Repaint。

从前面的内容可以了解到,Reflow和Repaint对性能影响很大,那么具体哪些点会影响到渲染性能呢?

影响Layout的属性

当你改变页面上某个元素的时候,浏览器需要做一次重新布局的操作,这次操作会包括计算受操作影响所有元素的几何数,比如每个元素的位置和尺寸。如果你修改了html这个元素的width属性,那么整个页面都会被重绘。

由于元素相覆盖,相互影响,稍有不慎的操作就有可能导致一次自上而下的布局计算。所以我们在进行元素操作的时候要一再小心尽量避免修改这些重新布局的属性。

具体有关于会影响Layout的CSS属性可以在CSS Triggers网站中查阅。

影响Repaint的属性

有些属性的修改不会触发重排,但会触Repaint(重绘),现代浏览器中主要的绘制工作主要用光栅化软件来完成。所以重新会制的元素是否会很大程度影响你的性能,是由这个元素和绘制层级的关系来决定的,如果这个元素盖住的元素都被重新绘制,那么代价自然就相当地大。

具体有关于会影响Layout的CSS属性可以在CSS Triggers网站中查阅。

如果你在动画里面使用了上述某些属性,导致重绘,这个元素所属的图层会被重新上传到GPU。在移动设备上这是一个很昂贵耗资源的操作,因为移动设备的CPU明显不如你的电脑,这也意味着绘制的工作会需要更长的时间;而上传线CPU和GPU的带宽并非没有限制,所以重绘的纹理上传就自然需要更长的时间。

CSS Triggers网站中可以得知哪些属性会触发重排、哪些属性会触发重绘以及哪些属性会触合成。但并不是CSS中所有的属性都可以用于CSS Animation和Transition中的。在W3C官方规范中明确定了哪些CSS属性可以用于AnimationTransition中。@Rodney Rehm还对这些属性做过一个兼容测试。如果你想深入的了解这方面的知识,建议您阅读下面两篇文章:

如此一来,我们知道可用于CSS Animation或者Transition的CSS属性之后,再配合CSS Triggers网站,可以轻易掌握哪些CSS属性会触发重排、重绘和合成等。虽然无法避免,但我们可以尽量控制

性能优化

如果我们知道浏览器是如何渲染一个页面的,并且去优化渲染过程中的关键步骤,不是是就能事半功倍呢?

有关于这部分的介绍,建议大家阅读《渲染性能》。

像素渲染流水线

在像素渲染流水线中,得知,如果我们能幸运的避免Layout和Paint,那么性能是最好的,言外之意,动画性能也将变得最佳。那么在CSS中可能通过不同的方式来创建新图层。其实这也就是大家常说的,通过CSS的属性来触发GPU加速。浏览器会为此元素单独创建一个“层”。当有单独的层之后,此元素的Repaint操作将只需要更新自己,不用影响到别人。你可以将其理解为局部更新。所以开启了硬件加速的动画会变得流畅很多。

为什么开启硬件加速动画就会变得流畅,那是因为每个页面元素都有一个独立的Render进程。Render进程中包含了主线程和合成线程,主线程负责:

  • JavaScript的执行
  • CSS样式计算
  • 计算Layout
  • 将页面元素绘制成位图(Paint)
  • 发送位图给合成线程

合成线程则主要负责:

  • 将位图发送给GPU
  • 计算页面的可见部分和即将可见部分(滚动)
  • 通知GPU绘制位图到屏幕上(Draw)

我们可以得到一个大概的浏览器线程模型:

线程

我们可以将页面绘制的过程分为三个部分:Layout、Paint和合成。Layout负责计算DOM元素的布局关系,Paint负责将DOM元素绘制成位图,合成则负责将位图发送给GPU绘制到屏幕上(如果有transformopacity等属性则通知GPU做处理)。

GPU加速其实是一直存在的,而如同translate3D这种hack只是为了让这个元素生成独立的 GraphicsLayer , 占用一部分内存,但同时也会在动画或者Repaint的时候不会影响到其他任何元素,对高刷新频率的东西,就应该分离出单独的一个 GraphicsLayer。

GPU对于动画图形的渲染处理比CPU要快。

RenderLayer 树,满足以下任意一点的就会生成独立一个 RenderLayer。

  • 页面的根节点的RenderObject
  • 有明确的CSS定位属性(relativeabsolute或者transform
  • 是透明的
  • 有CSS overflow、CSS alpha遮罩(alpha mash)或者CSS reflection
  • 有CSS 滤镜(fliter)
  • 3D环境或者2D加速环境的canvas元素对应的RenderObject
  • video元素对应的RenderObject

每个RenderLayer 有多个 GraphicsLayer 存在

  • 有3D或者perspective transform的CSS属性的层
  • 使用加速视频解码的video元素的层
  • 3D或者加速2D环境下的canvas元素的层
  • 插件,比如flash(Layer is used for a composited plugin)
  • opacitytransform应用了CSS动画的层
  • 使用了加速CSS滤镜(filters)的层
  • 有合成层后代的层
  • 同合成层重叠,且在该合成层上面(z-index)渲染的层

每个GraphicsLayer 生成一个 GraphicsContext, 就是一个位图,传送给GPU,由GPU合成放出。

那么就是说,GraphicsLayer过少则每次repaint大整体的工作量巨大,而过多则repaint小碎块的次数过多。这种次数过多就称为 层数爆炸 ,为了防止这个爆炸 Blink 引擎做了一个特殊处理。

有关于这部分内容的详细介绍,可以阅读《无线性能优化:Composite》一文。

扯了这么多,我们可以稍微总结一下下:

不是所有属性动画消耗的性能都一样,其中消耗最低的是transformopacity两个属性(当然还有会触发Composite的其他CSS属性),其次是Paint相关属性。所以在制作动画时,建议使用transformtranslate替代marginposition中的toprightbottomleft,同时使用transform中的scaleX或者scaleY来替代widthheight

为了确保页面的流程,必须保证60fps内不发生两次渲染树更新,比如下图,16ms内只发生如下几个操作则是正常及正确的:

线程

页面滚动时,需要避免不必要的渲染及长时间渲染。其中不必要的渲染包括:

  • position:fixed;fixed定位在滚动时会不停的进行渲染,特别是页面顶部有一个fixed,页面底部有个类似返回顶部的fixed,则在滚动时会对整个页面进行渲染,效率非常低。可以通过transform: translateZ(0)或者transform: translate3d(0,0,0)来解决
  • overflow:scroll。前面说了,而在滚动也会触发Repaint和Reflow。在调试过程中注意到一个有趣的现象,有时打开了页面并不会导致crash,但快速滑动的时候却会。由于crash是页面本身内存占比过高,只要优化了页面的内存占用,滑动自然也不会是很大的问题。无论你在什么时候滑动页面,页面滚动都是一个不断重新组合重新绘制的过程。所以减少渲染区域在滚动里就显得非常重要
  • CSS伪类触发。有些CSS伪类在页面滚动时会不小心触发到。比如:hover效果有box-shadowborder-radius等比较耗时的CSS属性时,建议页面滚动时,先取消:hover效果,滚动停止后再加上:hover效果。这个可以通过在外层添加类名进行控制。但添加类名、删除类名也会改变元素时,浏览器就会要重新做一次计算和布局。所以千万要小心这种无意触发重新布局的操作,有的时候可能不是动画,但去付出的代价要比做一个动画更加昂贵。也就是说classname变化了,就一定会出现一次rendering计算,如果一定需要这么做,那可以使用 classlist 的方法。
  • touch事件的监听

长时间渲染包括:

  • 复杂的CSS
  • Image Decodes:特别是图片的Image Decodes及Image Resize这两个过程在移动端是非常耗时的
  • Large Empty Layers: 大的空图层

在CSS中除了开启3D加速能明显的让动画变得流畅之外,在CSS中提供了一个新的CSS特性:will-change。其主要作用就是提前告诉浏览器我这里将会进行一些变动,请分配资源(告诉浏览器要分配资源给我)

will-change属性,允许作者提前告知浏览器的默认样式,那他们可能会做出一个元素。它允许对浏览器默认样式的优化如何提前处理因素,在动画实际开始之前,为准备动画执行潜在昂贵的工作。有关于will-change更详细的介绍可以点击这里

话说回来,will-change并不是万能的,不是说使用了will-change就对动画的性能有提高,而是要正确使用,才会有所改为。在使用will-change时应该注意:

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。
  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。
  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。
  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

在使用will-change一定要注意方式方法,比如常见的错误方法是直接在:hover是使用,并没有告诉浏览器分配资源:

.element:hover {
    will-change: transform;
    transition: transform 2s;
    transform: rotate(30deg) scale(1.5);
}

其正确使用的方法是,在进入父元素的时候就告诉浏览器,你该分配一定的资源:

.element {
    transition: opacity .3s linear;
}
/* declare changes on the element when the mouse enters / hovers its ancestor */
.ancestor:hover .element {
    will-change: opacity;
}
/* apply change when element is hovered */
.element:hover {
    opacity: .5;
}

另外在应用变化之后,取消will-change的资源分配:

var el = document.getElementById('demo');
el.addEventListener('animationEnd', removeHint);

function removeHint() {
    this.style.willChange = 'auto';
}

除了will-change能让我们在制作动画变得更为流畅之外,在CSS层面上,还有别的方案吗?这个答案是肯定的。前面通过大幅的篇幅了解到,影响性能主要是因为重绘和重排。针对于这方面,CSS提供了一个新的属性contain

contain

有关于这方面的详细介绍,可以阅读《Chrome 52 中的 CSS Containment 属性》一文。

总结

本文主要介绍了浏览器渲染一个Web页面的原理,从中了解到影响Web的性能因素,从底层找到影响Web渲染性能的主要因素是CSS的属性会触发浏览器的重绘、重排。而CSS的动画,也主要是控制CSS的属性,从这一方面说明,影响动画的性能也是造成重绘和重排的CSS。为了让一个动画能更佳的流畅,我们就要从技术的手段避免CSS的属性造成浏览器的重绘和重排。以及利用一些CSS的新属性,让动画的性能更好,也就是让动画更为流畅。

文章整理的感觉有些零乱,上述内容如果有不对之处,还希望大婶们多多指点。

参考资料

Weex调试神器——Weex Devtools使用手册

伴随着weex的正式开源,对一款针对weex框架的简单易用的调试工具的呼声也日趋强烈。weex devtools就是为weex前端和native开发工程师服务的一款调试工具,可同时检查weex里DOM属性和Javascript 代码断点调试,支持IOS和Android两个平台。

Chrome devtools对于前端开发者来说最熟悉不过,有广泛的用户基础.weex devtools实现了Chrome Debugging Protocol,其使用体验和普通的web开发一致,对于前端开发者是零学习成本,其主要功能分为两大部分——Debugger和Inspector,第一个版本已经随weex0.6.1 发布, 手淘也已接入。

以下是Devtools的使用介绍,欢迎大家试用。有任何问题建议,请提交这里,会很快得到解答。

Devtools 主要功能一览

连接设备

devtools可以动态检测客户端的连接绑定请求,同时连接/调试多个device(或者模拟器)是很容易的事情。连接的客户端显示在“Device List"界面,如下图所示。点击对应device的Debugger按钮即开始调试流程,并弹出JS断点调试的页面;随后点击Inspector按钮可弹出调试DOM的页面。
devtools-main

Debugger

这个是用来调试weex中的js前端代码的页面,“Sources” tab能够显示所有JS源码,包括jsFramework和bundle代码。可以设置断点、查看调用栈,功能和普通的chrome浏览器调试一样。"Console" 控制台显示前端的Log信息,并能根据级别(info/warn/error等)和关键字过滤。

devtools-debugger

Breakpoint and CallStack

debugger-breakpoint

Inspector

Inspector 功能丰富,能够用来查看 Element \ Network \ Console log \ ScreenCast \ BoxModel \ Native View 等。

devtools-inspector

Element

这里展示的是在Android/iOS上的native DOM树,及其style属性,和layout图。鼠标在DOM 树移动时,在device(或模拟器)上对应节点会高亮显示,有助于native开发者定位和发现节点。screencast只是对屏幕图像拷贝,在远程调试时能看到远程设备界面,数据网络下screencast也将有较大流量花销,,如果设备就在手头儿则建议关掉。
inspector-element

Network

这里展示的是bundle资源加载网络访问的性能。所以如果bundle资源在device本地,Network是没有数据的。

查看网络请求的总耗时和延时

inspector-network

查看网络请求的header和response

inspector-network

控制台

这里显示的是Android/iOS上的native log,并不是前端log(显示在Debugger页面)。同样native log也有对应级别--warn/error等,和关键字过滤,native开发查询很方便。
inspector-console

资源

这里和Network一样,远端访问的资源文件会显示在这里,没有实际作用。因为在Debugger页面,"Sources"里已经有源码并可以断点调试。不过假如你的应用里有数据库文件,在这里可以方便的查看而无需root,这是非常有用的。
inspector-resource

如何安装和启动devtools

无论是跑在IOS或者Android端,weex-devtool都是必需的,用来启动服务器和chrome页面。

安装

$ npm install  -g  weex-toolkit

用法

weex debug [options] [we_file|bundles_dir]

选项:

-h, --help           显示帮助
-V, --verbose        显示debug服务器运行时的各种log
-v, --version        显示版本
-p, --port [port]    设置debug服务器端口号 默认为8088
-e, --entry [entry]  debug一个目录时,这个参数指定整个目录的入口bundle文件,这个bundle文件的地址会显示在debug主页上(作为二维码)
-m, --mode [mode]    设置构建we文件的方式,transformer 最基础的风格适合单文件,loader:wepack风格 适合模块化的多文件.默认为transformer

如何在设备或者模拟器上调试

weex调试初体验之playground

如果你是一名weex调试的新手,那么推荐你先下载playground体验一下devtools调试js bundle的基础流程.点击这里观看视频演示

  • 前提:
    • 安装weex-toolkit, 内含调试命令weex debug
    • android/iOS设备及pc已联网,若位于不同网段请确保防火墙可访问性

  • 调试步骤:
    • 启动debug server.
      执行命令weex debug将启动 debug server.如果启动成功将会在chrome打开一个welcome页面,在网页下方有一个二维码.
    • 启动playground并扫码.
      点击启首页左上角的扫码按钮扫码上一步中网页下方的二维码.此时welcome页面将会出现你的设备信息.playground进入loading页面,等待你的下一步操作.
    • 点击网页上的Debugger按钮.
      app结束loading进入debugging状态.同时chrome将会打开debugger页面.按照页面提示打开该页的JavaScript控制台并进入source tab.
    • 设置断点刷新当前页.
      点击playground首页list中的任意项将打开一个weex bundle,此时在Sources里会出现相应的js 文件,设置断点并刷新playground 即可调试.
    • 点击网页上的Inspector 按钮.
      chrome会打开inspector页面,可以查看Element, Console, Network状态.

weex调试初体验之应用

如果是接入weex的应用想调试自己的bundle代码,有以下几个方式:

  1. 借助playground扫码调试we文件
  • 先确定playground已经是可调试状态
  • 执行weex-toolkit 命令,"weex debug xxx.we",会自动编译打包we文件,并在chrome的device 列表页面最底下新生成一个二维码。
  • 用playground扫描新二维码,手机上即显示xxx.we的结果。相应在"Debugger"和"Inspector"页面调试。
  1. 借助playground扫码调试js bundle文件
  • 先确定playground已经是可调试状态
  • 用二维码生成器为xxx.js 生成一个二维码。
  • 用playground扫描新二维码,手机上即显示xxx.js的结果。相应在"Debugger"和"Inspector"页面调试。
  1. 直接修改应用,接入devtools接口
// host 表示debug server的ip或域名
WXEnvironment.sRemoteDebugMode = enable;
WXEnvironment.sRemoteDebugProxyUrl = "ws://" + host + ":8088/debugProxy/native";
#import "WXDevTool.h"
[WXDevTool setDebug:YES];
[WXDevTool launchDevToolDebugWithUrl:@"ws://host:8088/debugProxy/native"];

手淘年货节舞龙揭幕动画实战

手淘用户这几天应该看到了年货节版本,不知道刚打开首页有没有被一阵锣鼓声、鞭炮声给吓倒。为了营造一种过年的气氛出来。PD们给年货节上了一个舞龙的揭幕动画,而这个任务就落在了小生的头上,为了将.gif动效在称动端上实现,着实费劲。那么今天就来介绍这个动画效果是如何实现的?

动画效果

Web动画在PC上已不是难事,而且客户端自己带的动画特效也是非常的流畅,那么要将下面这种.gif动画效果在移动端上实现,我还是第二次经历(前一次是圣诞节的揭幕动画)。

揭幕动画

一开始看到这个效果,有点心虚也有点醉了。其实最开始打算直接上.gif动效图,但使用.gif动效图存在两个问题:

  • 文件过大(帧数越多,文件越大),可有可能造成应用卡死
  • 动效与音乐的匹配

那要怎么做呢?带着尝试的心情,开始了这个动效之旅。

动效分析

整个动画分为两个场景。那么先简单剖析这两个场景:

动画首屏

揭幕动画一进来是一个静态的蒙层:

动画首屏

在这个屏有以下几个动作:

  • 默认静音按钮不选择(这个是可配置时间段),用户点击之后可以处于选中静音状态
  • 点击整个云彩开始转入动画第二场,在这个过程中第一场渐渐隐去,到达第二场
  • 点击关闭按钮,不进入动画第二场,并且整个动画蒙层关闭

动画第二场

动画第二场

动画进入到第二场时整个动画会有以下几个动作:

  • 龙会有十个舞动动作,而且它会不断重复
  • 鞭炮扭动并且逐渐消失
  • 云彩飘扬
  • 如果静音按钮没选中,在第二场中会有音乐播放,反之不会有音乐播放

动画实现原理

整个动画使用CSS Animation中的animation属性完成。在这里主要使用了animation中的steps()animation-timing-function。其实就是一个多步动画,而多步动画中最主要使用到的是雪碧图,因为雪碧图和animation中的steps()配合能让我们轻松实现下面这样的动画效果:

<iframe id="JdMYdz" src="http://codepen.io/airen/embed/JdMYdz?height=500&theme-id=0&slug-hash=JdMYdz&default-tab=result&user=airen" scrolling="no" frameborder="0" height="500" allowtransparency="true" allowfullscreen="true" class="cp_embed_iframe undefined" style="width: 100%; overflow: hidden;"></iframe>

我样可以看到整个动画人特一直在运动,而且动作与动作之间的变动是非常的协调。

动画制作

了解了整个动画场景以及其实现原理,接下来我们看看具体制作过程又是怎么样的,并且在制作过程中碰到什么样的坑。

动画DEMO

别的先不说,先把整个动画的效果向大家展示一下,用你的手机猛扫下面的二维码:

动画DEMO

(^_^)可别被锣鼓声给吓坏了。

创建模板

把整个动画放在一个场景中,就把它称之为“舞台”吧,并且把这个舞台命名为dragon-poplayer:

<div class="dragon-poplayer"></div>

动画有两个场景,把这个场景称之为“容器”:

<div class="dragon-poplayer" id="dragon-poplayer">
    <div class="dragon-section dragon-ready-play" id="dragon-ready-play">
        <div class="dragon-play">
            <!-- 第一场景 -->
        </div>
    </div>
    <div class="dragon-section dragon-playing" id="dragon-playing">
        <!-- 第二场景 -->
    </div>
</div>

为了能让用户更好的控制整个动画,毕竟不是所有用户都喜欢,在舞台的同级,添加了一个关闭按钮:

<div id="close"></div>

前面也说过了,第一场景中主要有一个静音按钮和触发到第二场景的动作按钮(暂且把它称为播放按钮吧)。另外就是把音乐<audio>也丢在这个容器中。

为了让静音按钮更能个性化,这里采用了模拟checkbox(具体制作方法,可以参考《CSS3制作iPhone的Checkbox》)。

<div class="dragon-poplayer" id="dragon-poplayer">
    <div class="dragon-section dragon-ready-play" id="dragon-ready-play">
        <div class="dragon-play">
            <div class="music">
                <input type="checkbox" name="music" id="music-control">
                <label for="music-control">声音</label>
            </div>
            <div id="music">
                <audio src="//gw.alicdn.com/tfscom/TB1Ydd2LpXXXXaUXFXXsKFbFXXX.mp3" loop="loop" preload="load"></audio>
            </div>
        </div>
    </div>
    <div class="dragon-section dragon-playing" id="dragon-playing">
        <!-- 第二场景 -->
    </div>
    <div id="close"></div>
</div>

第二场景先来看舞动的龙,整条龙有五个部分,分别有五个小朋友举着,为了更好的控制龙更好舞动,将整条龙分成五个部分,分别由一个div来控制:

<div class="dragon-wrap">
    <div class="dragon-content">
        <div class="dragon dragon1"></div>
        <div class="dragon dragon2"></div>
        <div class="dragon dragon3"></div>
        <div class="dragon dragon4"></div>
        <div class="dragon dragon5"></div>
    </div>
</div>

在龙的周边还有三朵云彩在飘,同样将每朵云放置在一个独立的<section>里:

<div class="dragon-wrap">
    <div class="dragon-content">
        <div class="dragon dragon1"></div>
        <div class="dragon dragon2"></div>
        <div class="dragon dragon3"></div>
        <div class="dragon dragon4"></div>
        <div class="dragon dragon5"></div>
        <section class="cloud"></section>
        <section class="cloud"></section>
        <section class="cloud"></section>
    </div>
</div>

还有两串鞭炮,不用多说,用两个div来放置:

<div class="firecrackers firecrackers-left"></div>
<div class="firecrackers firecrackers-right"></div>

最终的HTML就长成这样:

<div class="dragon-poplayer" id="dragon-poplayer">
    <div class="dragon-section dragon-ready-play" id="dragon-ready-play">
        <div class="dragon-play">
            <div class="music">
                <input type="checkbox" name="music" id="music-control">
                <label for="music-control">声音</label>
            </div>
            <div id="music">
                <audio src="//gw.alicdn.com/tfscom/TB1Ydd2LpXXXXaUXFXXsKFbFXXX.mp3" loop="loop" preload="load"></audio>
            </div>
        </div>
    </div>
    <div class="dragon-section dragon-playing" id="dragon-playing">
        <div class="dragon-wrap">
            <div class="dragon-content">
                <div class="dragon dragon1"></div>
                <div class="dragon dragon2"></div>
                <div class="dragon dragon3"></div>
                <div class="dragon dragon4"></div>
                <div class="dragon dragon5"></div>
                <section class="cloud"></section>
                <section class="cloud"></section>
                <section class="cloud"></section>
            </div>
        </div>
        <div class="firecrackers firecrackers-left"></div>
        <div class="firecrackers firecrackers-right"></div>
    </div>
    <div id="close"></div>
</div>

样式

整个舞台是充满整屏的,首先将htmlbody和舞台dragon-poplayer设置为全屏模式:

html,body {
    height: 100vh;
    min-width: 10rem;
    margin-left: auto;
    margin-right: auto;
    background: transparent;
}
body {
    min-height: 100%;
    background: url(http://gw.alicdn.com/mt/TB1.sknLXXXXXbEXpXXXXXXXXXX-750-1333.png) no-repeat;
    background-size: 10rem 100%;
}
.dragon-poplayer,
.dragon-section {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 10rem;
    height: 100%;
    overflow: hidden;
}

其实第一场景的样式很简单,这里就不做过多阐述,将代码贴出来供大家参考:

.dragon-play{
    width: 10rem;
    height: 10.946667rem; //821px
    background: url('//gw.alicdn.com/mt/TB13eupLpXXXXaGXXXXXXXXXXXX-750-821.png') no-repeat center;
    background-size: 10rem 10.946667rem;
    position: absolute;
    z-index: 10;

    .music {
        position: absolute;
        width: 1.866667rem; //140
        height: 0.533333rem; //40px
        top: 3.6rem; //270px
        left: 4.266667rem; //320px
        z-index: 12;

        input[type="checkbox"]{
            opacity: 0;

            &:checked + label:before {
                background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAoCAMAAABZ/...');
            }
        }

        label {
            white-space: nowrap;
            display: block;
            position: absolute;
            top: -0.026667rem; //2px
            left: 0;
            font-size: 0;
            width: 100%;
            height: 0.533333rem; //40px

            &:before {
                content: "";
                display: inline-block;
                width: 0.626667rem; //47px
                height: 0.533333rem; //40px
                background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8A...') no-repeat;
                background-size: 0.626667rem 0.533333rem; //47px 40px
            }
        }
    }
    @at-root #music {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: transparent;
        cursor: pointer;
    }
}

用户点击播放之后,会从第一场景进入到第二场景,在这个过程中会有一个动画效果,就是第一场景慢慢淡出fadeOut,第二场景慢慢淡入animation:

.dragon-ready-play{
    z-index: 100;

    &.is-animationed {
        animation: fadeOut 1.5s ease-in both;
    }
}
.dragon-playing {
    opacity: 0;

    &.is-animationed{
        animation: fadeIn 1s ease both;
    }
}

动画是通过keyframes制作:

// 淡出
@keyframes fadeOut {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

// 淡入
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

在这个过程仅通过CSS我们还有点难度的,需要通过JavaScript来触发,至于怎么触,后面的JavaScript部分来介绍。

其实难度在第二场景,因为在这个场景中我们涉及到三个部分的动画。我们来先看最难的一部分吧,就是龙。

前面也说过了,龙就要是分为五段,每段我们是通过CSS Sprites配合steps()完成。那么在这个过程需要将龙的每一部分拼合出来,如下图所示:

龙头

至于样式如下:

.dragon {
    position: absolute;
    height: 2.453333rem; //184px
    top: 0;
}
.dragon1{
    width: 2.373333rem; //178px
    height: 2.506667rem; //188px
    left: 0;
    z-index: 5;
    background: url('//gw.alicdn.com/mt/TB16t_sIFXXXXaXapXXXXXXXXXX-1780-188.png') no-repeat;
    background-size: 23.733333rem 2.506667rem; //1780px 188px
}

动画的keyframes:

@keyframes dragon-1 {
    to {
        background-position: -23.733333rem; //1780px
    }
}

触发动画:

.dragon-playing {
    opacity: 0;

    &.is-animationed{
        animation: fadeIn 1s ease both;

        .dragon{
            animation-duration: 1s;
            animation-timing-function: steps(10);
            animation-iteration-count: infinite;
        }
        .dragon1{
            animation-name: dragon-1;
        }
    }

其它几个部分就不做详细阐述。在做龙的时候碰到两个坑。

第一个坑就是设计师希望将龙和小人分开来,这样有利于龙的更换(就是随时更换龙的设计效果)。听起来很有吸引力,但在实际制作过程中,才发现龙和小人的配合是非常难以达到一致。最后只好又更换到让他们合成在一起。

第二个坑就是,CSS Sprites的拼合。刚开始将其按纵向拼合,通过更改background-position-y的值。但动画效果非常生硬,才更换成水平排列。在排列Sprites时还有一个细节,就是每个区域(帧)大小一致,不然在播放时候,龙会乱帧。

第二个效果就是云彩飘动,其实这个效果非常简单,就是通过transformtranslate3d()更换他们的X轴位置:

@keyframes colud {
    0%,40%,100% {
        transform: translate(0,0); //0
    } 
    20%, 50%, 80% {
        transform: translate(0.266667rem,0); //20px
    } 
    60% {
        transform: translate(-0.266667rem,0); //20px
    } 
}

第三个动效果是鞭炮的播放。最开始使用的是鞭炮和礼花合在一起,同样通过Sprites来实现,再配合translate3d将整个鞭炮往Y拉。虽然效果出来了,但PD同学说太假了,这不是在放鞭炮,整个鞭炮是在往上拉。想想也是,对于有追求的同学来说,还是很有必要来修改的。而在修改这个效果其实比舞龙动效还难。

最后的思路是把鞭炮和礼花拆分出来,为了动效更生动,鞭炮同样使用Sprites:

鞭炮

礼花

这两个要配合在一起,而且每个部分都采用了多个动画

在这个过程最难的,也可以说是坑吧有两个:

  • 鞭炮慢慢变短,逐渐消失
  • 鞭炮和礼花位置的配合

鞭炮的逐渐消失,在这个过程尝试了很多种方案,都未见效。使用transform的话就会回到当初的效果,如果修改hieght的话,鞭炮会一闪而过。最后在无意中尝试修改鞭炮的max-height。简单点说就是慢慢变为0

@keyframes bianpao2 {
    from {
        max-height: 4.426667rem; //332px
    }
    to {
        max-height: 0;
    }
}

当然这种方案的效果也并不完全完美,怎么看度部都有一种被截取的效果。

另外就是鞭炮和礼花的配合。初始采用移动,但时间无法达到配合。情急之下,就只对礼花做定位处理:

.firecrackers {
    width: 2.213333rem; //166px;
    height: 4.426667rem; //332px;
    background: url('//gw.alicdn.com/mt/TB1zoB3LpXXXXbCXXXXXXXXXXXX-332-332.png') no-repeat;
    background-size: 4.426667rem 4.426667rem; //332px 332px
    position: absolute;
    top: -0.213333rem; //16px

    &.firecrackers-left{
        //left: 0.133333rem; // 10px
        left: 0;
    }
    &.firecrackers-right {
        //right: 0.133333rem; // 10px
        right: -0.533333rem; //40px
    }

    &:after {
        content: "";
        width: 1.626667rem; //122px;
        height: 1.2rem; //90px;
        position: absolute;
        bottom: -0.706667rem; //-53px;
        left: 0.066667rem; //5px;
        background: url('data:image/png;base64,B...') no-repeat;
        background-size: 2.986667rem 1.2rem; //224px 90px;  
    }
}

居然看上去也还是能勉强接受。

最后还有一个效果需要特别提出来,就是龙的位置。因为手淘首页在龙的下面就已嵌入了一个进入年货节主会场的按钮(这个是Native同学配置的)。而我们要处理的是动画的层必须先遮盖住。

.dragon-wrap {
    width: 10rem;
    height: 2.986667rem; //224px
    background:url('//gw.alicdn.com/mt/TB17q71LXXXXXbWXpXXXXXXXXXX-750-224.png') no-repeat center;
    background-size: 10rem 2.986667rem;
    position: absolute;
    top: 5.2rem;//390px
}

但坑来了,手淘在不同的终端设备中,顶部的距离都不一样。这下就烦了,在实在没办法的情况下,只做了手淘的iOS设备做了处理:

@media only screen 
and (min-device-width : 320px) 
and (max-device-width : 480px) {
    .dragon-wrap {
        top: 5.2rem;//390px
    }
}

// iphone5 & 5s
@media only screen 
and (min-device-width : 320px) 
and (max-device-width : 568px) {
    .dragon-wrap {
        top: 5.2rem;//390px
    }
}
// iphone6
@media only screen 
and (min-device-width : 375px) 
and (max-device-width : 667px) {
    .dragon-wrap {
        top: 4.8rem; //360px
    }
}
// iphone6 +
@media only screen 
and (min-device-width : 414px) 
and (max-device-width : 736px) {
    .dragon-wrap {
        top: 4.666667rem; //350px
    }
}

在手猫中还是会有一点遮住手焦。在安卓设备下就更会错位严重了。到目前为止没找到更好的解决方案。

触发动画

样式效果已处理完成。但整个动画我们还是需要JavaScript来触发。而且还有一些其他需要处理的。比如说时间的设置、音乐的控制等。

JavaScript做了以下几件事情:

音乐的播放

// 控制音乐的播放
function musicPlayer (){
    var dragonStage = document.getElementById('dragon-poplayer'),
        switcher = document.getElementById('music'),
        media = switcher.getElementsByTagName('audio')[0],
        chooseMusic = document.getElementById('music-control'),
        wantedDragonDance = document.getElementById('dragon-ready-play'),
        dragonDanceStar = document.getElementById('dragon-playing'),
        firecrackers = document.querySelector('.firecrackers');

    // 获取舞龙音乐选中开始时间
    var musicStartTime = pageData['startTime'];
    // 获取舞龙音乐选中结束时间
    var musicStopTime = pageData['endTime'];
    // 将设置的时间字符串(按冒号)拆分为两部分
    var timeStart = musicStartTime.split(':');
    var timeEnd = musicStopTime.split(':');
    // 设置限制的开始时间
    var limitStart = new Date();
    limitStart.setHours(timeStart[0]);
    limitStart.setMinutes(timeStart[1]);
    // 设置限制的结束时间
    var limitEnd = new Date();
    limitEnd.setHours(timeEnd[0]);
    limitEnd.setMinutes(timeEnd[1]);

    // 获取系统当前时间
    var nowTime = new Date();

    // 如果系统时间在 限制时间之间,checkbox不选中,否则自动选中
    chooseMusic.checked = nowTime < limitStart || nowTime > limitEnd;

    switcher.addEventListener ('click', function (){
        var currentStatus = media.paused ? 'pause' : 'play';
        var wantedStatus = currentStatus === 'pause' && !chooseMusic.checked ? 'play' : 'pause';

        media[wantedStatus]();

        // 如果wantedDragonDance 没有is-animationed类名,就添加,反之什么也不做
        if(!wantedDragonDance.classList.contains('is-animationed')){
            wantedDragonDance.classList.add('is-animationed');
        }

    }, false);

    // 监听wantedDragonDance的webkitAnimationEnd
    // 如果wantedDragonDance的动画完成,给dragonDanceStar 添加类名is-animationed
    wantedDragonDance.addEventListener('webkitAnimationEnd', function(){
        dragonDanceStar.classList.add('is-animationed');
    });
    //监听鞭炮的动作,如果动画播放完,音乐停止,并且删除整个舞台和关闭Poplayer
    firecrackers.addEventListener('webkitAnimationEnd', function(e){
        media.pause();
        document.body.removeChild(dragonStage);
        window.WindVane.call('WVPopLayer', 'close', {});
    }, false);      
}

禁止用户滑动屏幕

// 禁止滑动
function cancleDocumentScroll () {
    document.addEventListener('touchmove', function (e) {
        e.preventDefault();
        return false;
    }, false);
}

关闭音乐和Poplayer

// 关闭WVPopLayer 和 音乐
function closeAll () {
    var colseBtn = document.getElementById('close'),
        switcher = document.getElementById('music'),
        media = switcher.getElementsByTagName('audio')[0];
    colseBtn.addEventListener('click', function () {
        window.WindVane.call('WVPopLayer', 'close', {});
        media.pause();

        var source = appname === 'TM' ? 2 :1 ;
        goldlog('/nhj.1.4','','from='+ source,'H1703624');
    }, false);
}

执行函数

function init (){
    window.WindVane.call('WVPopLayer', 'display', {});
    window.WindVane.call('WVPopLayer', 'increaseReadTimes', {}, function(s){
      // do something when success;
    }, function(e) {
      // do something when failed;
    });
    musicPlayer ();
    cancleDocumentScroll ();
    closeAll ();
}

// 开始执行函数
document.addEventListener('DOMContentLoaded', init, false);

POPLAYER

虽然我们整个动画是使用CSS和JavaScript完成的,也可以说是一个Web Animation。那么要放到APP中,还是需要特殊处理的。在这里我们使用了一种技术:POPLAYER

有关于POPLAYER相关的介绍可以阅读《POPLAYER起来HIGH~~》一文。如果你无法理解,就简单的把他当作是一个WebView或者是一个iframe吧。至于怎么做POPLAYER,偶也不懂。

总结

阅读到这里是不是有点累了,内容偏长。整篇文章主要介绍了揭幕动画的制作过程。简单点说就是如何时通过Web Animation将一个gif动画转换成Web动画。在整个制作过程主要采用了CSS的animation属性,并且配合CSS Sprites。当然这种效果也存在一定的缺陷,性能在APP中还是有所局限性,特别是在POPLAYER中,我们暂时无法开启设备的3D加速器。而且在一些性能较差的设备会有显得更明显。希望我们在以后的技术沉淀中能把这方面做得更好。

两届双十一间手机淘宝基础业务前端技术的演进

自从11.11这个曾经普通的日子被阿里塑造成一年一度的消费者和商家的节日, 阿里的技术小二们就多了一个每年练兵的好机会. 在经历了峰值流量的考验后, 双十一对于阿里更成为一个打破部门间隔阂,推动业务和技术更新的绝好机会. 且日期恰逢年关将近, 我想这也是总结团队一年来技术演进的好机会.

工程化

我们在13年底完成了开发模式上的前后端分离, 从那时起前端项目就不再仅作为Web项目的一部分进行管理. 独立于服务端的开发和发布过程也催生出了我们的前端项目工程化之路.

14~15年两届双十一之间,在构建技术上,我们在新项目中逐步使用Gulp替代了Grunt, 并且引入了commonJS/NPM机制配合 browserify /webpack 来管理代码依赖.

我们发布到阿里NPM平台的一个业务组件

在代码发布方式上,由于支撑平台和工具的完善, 我们除了能做到在命令行中直接发布构建好的JS/CSS静态资源外,在今天我们能根据业务类型在发布平台上选择引用或内联某个资源,甚至通过一个命令把全部资源内联后发布页面到预发环境.

在前端测试技术上, 我们在这两届双十一期间把之前散落各处并难于部署的casperJS ,mocha,chai汇聚到了云端,现在只要一个命令上传测试用例,就能在终端或浏览器里看到项目的单元测试执行情况.

截图为我们自研的云端测试平台 ## React

选择React有推动团队框架技术升级的内部动力也有和兄弟团队合作共建的外部原因. 到目前,除了在新项目上全面采用React外,我们正逐步把一些类似H5收藏夹,H5购物车,H5地址薄等生命周期过长的项目更换为一致的React代码基础, 以提高基础业务的稳定性和长期可维护性.

这个过程中我们的主要挑战是如何进行组件管理和关联数据源, 在经历了几次选型和尝试后,我们最终基于flux**自己实现了一套组件管理方案.

目前,我们正在尝试基于React Component建设适用于手淘基础业务的组件库, 希望在保持高度易用性的同时能和集团甚至业界其他React组件库实现复用.

截图为正在兴建中的手淘React组件库 Valkyrie ## CSS/Flex

如果说要从这两年前端技术领域百花齐放般出现的新技术中选择最接近银弹的一种, 我的答案是flex布局技术.

在15年内随着市场的自然更新和手淘支持基线的上移, 我们开始大规模的在业务中使用flex进行布局. 这种布局方法对我们实践界面自适应提供了强大的支持,且特别适用于电商业务中大量出现的商品展示类页面.

通过flex我们可以看到Web平台从文档展示往APP界面迈进的坚实一步. 值得一提的是在这个时间段内 Apple 在自家生态圈引入了新一代 AutoLayout 布局技术. 尝试从AutoLayout中进行借鉴或许是未来Web布局再进化的一种方向.

我们团队著名前端布道师 @大漠 关于Flex技术的实践文章
使用Flexible实现手淘H5页面的终端适配

Javascript/ES6

跟随HTML5 和 CSS3 , ES6也在这一年来到了我们的开发实践中.

这一年里,我们通过启用Promise改进了对异步流程和异常状态的处理, 通过新的类和函数语法(Class / Arrow Function)把代码重构得更便于阅读和维护. 通过虚拟属性(property set/get)让项目中的数据抽象变得更加自然. 同时这个过程和React一起让JS代码预编译( Babel based )从一年前少数几个尝试CoffeeScript的项目延伸到接近覆盖我们的整条业务链. 这种趋势发展之下,我想未来的前端程序员或许会觉得直接写JavaScript是一种复古行为艺术.

标准化

自从手机淘宝兴起, 很长一段时间内,开发同学们手头的测试机无论机型怎么变, 支持的系统一直只有IOS和Android两大平台. 从今年的双十一开始, 手机淘宝的业务终于扩张到了新的平台 WindowsPhone之上, 由于WindowsPhone 10 版手机淘宝今年双十一前才新近发布,大量的业务需要通过降级到H5页面来进行支持, 基础业务范围这样的情况尤其多.但这没有对我们的双十一开发带来太多的挑战,原因是我们的大量业务在年中就进行了Web标准改造.

由于webkit内核长期领先于标准实现新特性,很长一段时间内前端同学都习惯了在代码中仅仅使用webkit前缀来进行属性声明. 这种情况是移动端页面标准化改造过程中最多的问题点, 解决方法也很简单: 使用sass 函数进行属性声明 + 使用 Firefox / IE 进行回归测试.

在交付真机测试前我们会在Microsoft Edge , Firefox , Chrome 上调试我们的页面. ## HTTPS

在往年的双十一中, 精心制作的页面内容在传输过程中被劫持篡改是每一个阿里前端同学的痛苦,同时这也给公司带来了商业上巨大的风险. 为了避免这种情况再次出现, 我们在今年花了很大的精力进行了全业务范围的HTTPS升级改造,同时为了进一步提高性能,我们还要在这个过程中对引用的资源进行域名收敛改造.

对于基础业务线庞杂的项目来说,这项变更最为繁复. 这种情况下,我们开发了自动化的脚本工具来进行辅助,同时充分实践了CodeReview机制. 并和后端,测试,CDN等多部门的同学一起艰难的把默认环境更换为HTTPS才最终在双十一前建立起手淘前端页面的安全访问环境.

我们通过Git Merge Request流程进行CodeReview ## 其他

在上面提到的技术点之外, 手机淘宝前端团队在最近两届双十一之间还在性能优化,Hybrid/Native领域做出了尝试并积累了丰富的经验和成果, 由于会有专文介绍,我在这里就不进行赘述了.

笔者就职于阿里手机淘宝团队,负责手机淘宝基础业务的前端开发支持.

支不支持webview呢

我百度查不到webview支不支持viewport,但是我采用网易的移动端布局的方案在webview中是有问题的,后来直接把网易的url放进去也是有问题的,但是在手机浏览器打开是没有问题的

Web动画

动画在Web上的运用到目前为止已不是新课题。大家常常能看到的Web动画有CSS动画、JavaScript动画、SVG动画和HTML5的Canvas动画。最近开始也有不少同学开始在探讨WebGL动画。

随着年关将近,今天决定将Web上使用到的一些动画做一个资源整合(主要是为后期的工作做一些储备,因为未来的一段时间的工作主要会和动画交互关联在一起)。在这个集合中主要涵盖了动画的库、框架、教程和性能等。

Web动画资源图形化

早前看到@awwwards-team整理了一份Web Animation Infographics,今天直接先拿来一用。让大家对Web Animation有一个直观的了解。

Web Animation地图

Web Animation地图

Web Animation工具

Web Animation工具

Web Animation性能与技巧

Web Animation性能与技巧

CSS Animation

至于CSS Animation如何在Web中使用,这里不做阐述,这篇文章只是给大家提供现在使用率较高的几个CSS Animation Libraries。

Animate.CSS

Animate.CSS

官网GitHub

Animate.CSS整理了几十种动画特效,而这些动画特效都是使用CSS的animation。可以直接使用到具体的项目中,也可以在基于其基础上做进一步的改良,从而达到你自己需要的动画效果。

animate.css is a bunch of cool, fun, and cross-browser animations for you to use in your projects. Great for emphasis, home pages, sliders, and general just-add-water-awesomeness.

在其基础上扩展出来了AniCollectionMagic CSS3 AnimationCSS3 Animation Cheat SheetMotion UI等。

Effeckt.css

Effeckt.css

官网GitHub

Effeckt.css提供了Web页面中各种交互的动画效果,比如弹出框、按钮、列表、提示信息等。

A Performant Transitions and Animations Library

Hover.css

Hover.css

官网GitHub

Hover.css收集了很多应用在链接、按钮、Logo和图片等上面的悬浮效果。这些效果都是使用CSS3的transitionanimation完成。可以很简单的应用到你的元素中,而且它还提供了Sass和LESS版本,可以根据自己的需求调整变量值。

JavaScript Animation

很多时候CSS Animation并不能满足项目的实际需求,或者说希望通过JavaScript和CSS Animation配合得更完美。因此也诞生了很多款非常优秀的、轻量级的、简易的JavaScript Animation 库和框架。

AniJS

AniJS

官网GitHub

AniJS是一个处理CSS Animation的声明式库,使开发者使用更快和更有说服力。

Velocity.js

Velocity.js

官网GitHub

Velocity是一个动画引擎,它可能是最好用的动画库了。Velocity 提供了类似于jQuery中$.animated()的API,但其不要基于jQuery。它的最大特性就是速度快,而且还支持SVG、滚动、transform、loops和easings等。更值得称赞的是,其兼容性特别强,兼容到IE8和Android 2.3。

GSAP

GSAP

官网GitHub

GSAP是另一个有用的动画库,其主要关注的是性能和兼容性。其非常灵活而且轻量,另一大好处是不依赖任何第三方库,比如jQuery。

Animo.js

Animo.js

官网GitHub

Animo.js是一个强大的而又小的工具,主要用来管理CSS Animation。可以指定回去调函数完成动画。

Bounce.js

Bounce.js

官网GitHub

Bounce.js是一款在线制作动画的小工具和JS库,允许你通过@keyframe创建CSS3 Animation。在线可以很容易生成静态的@keyframe,而且不需要任何额外的JavaScript。

Move.js

Move.js

官网GitHub

Move.js是一个支持CSS3 Animation的JavaScript库。

Transit

Transit

官网GitHub

这是一个有关于CSS Transform和Transition的jQuery插件。

Morf.js

Morf.js

官网GitHub

上面只是罗列了一些较有意思的CSS Animation和JavaScript Animation的库或框架,其实还有很多类似的:

其实在Web Animation不再仅仅局限于CSS和JavaScript的Animation。现在还有SVG、HTML5 Canvas和WebGL等,而且它们对应也有一些优秀的库。在这里就不再罗列出来,大家感兴趣可以从第一张图上找到对应的名称。比如SVG中的Snap.js、SVG.js;WebGL中的Three.js和Canvas中的Fabric.js等。

Web Animation教程

有关于Web Animation的教程,我们以后将会在这两个地方汇总:

如果大家对Web Animation感兴趣,欢迎参与一起分享与学习。在GitHub我们创建了一个库,这里将会收集一些优秀的外文教程,如果您发现有什么好的教程,欢迎给我们提Issues,当然如果您愿意参与翻译,那就更好了。

今天特别推荐几篇有关于Web Animation的教程:

Web Animation DEMO

一切真理都出自于实战,同样的,在未来有关于Web Animation的DEMO分享,我们都将在Codepen上完成。在这里我们除了会放自己做的DEMO之外,还会收集各式各样的DEMO。如果你感兴趣,欢迎关注它

总结

其实是一篇专门对Web Animation的资源收集的文章,如果你对Web Animation不感兴趣,可能对你来说没有任何意义。如果你也对这些炫酷的Animation所吸引,那么这篇文章将会变成你的百宝箱。别的不多说了,感兴趣的欢迎收藏,如果您有这方面的经验或资料,欢迎与我们一起共享。

POPLAYER起来HIGH~~

前言

红包雨戳红包~~ 戳戳戳有木有戳到high?!

搜索框已然被你们玩坏~~ 你们有木有想念它?!

66元的红包~~ 有木有让你抓狂为什么自己竟然中奖了?!

...

这些让你又爱又恨的玩法,跨页面、跨终端,时间紧、任务急,既要保证玩得high,又要保证稳定性,随便玩玩~~~ 双十一玩坏了怎么行!

duangduangduang~~ ---> POPLAYER <---- ~~来了,这些新玩法的背后,是 淘宝无线事业部-技术互动 组的客户端哥哥兢兢业业、呕心沥血的成果。

没错,这是新的 客户端 技术解决方案,技术改变生活,不但改变了广大人民的日常生活,也改变了前端喵的开发生活,更给我们带了新的业务思考~~~

POPLAYER是神马?

简单来说,对于前端开发的童鞋而言,poplayer可以直观的理解为 —— 在当前页面上再覆盖一层 独立页面

  • POPLAYER不会影响被覆盖页面

大家都知道,淘宝天猫的客户端采用hybrid技术,即在native中嵌套html页面(下文简称h5页面),所以我们看到的页面有可能是native的,也有可能是h5的。 poplayer层作为一个独立完整的页面,可以覆盖在任何native或html页面上,但是却不会与被覆盖页面互相影响。

比如这次大家玩得很high的红包雨,就是充分利用了这个特性。

screenshotscreenshot

  • POPLAYER允许透明区域点击穿透

如果只是覆盖在上面有什么好玩?!当然还要更好玩!!!咱能给poplayer层设置透明度,告诉客户端哥哥。“这玩意儿我要透明度超过0.6的时候点击到下层页面!!“。所以只要咱们想,想点哪里点哪里!

还记得哆啦A梦君的小手么?没错,那是一大层poplayer在上面飘,实际上点穿了poplayer才点到了商品详情页!)

screenshot

  • POPLAYER支持多种交互场景

POPLAYER作为独立的页面,要不要展示以什么姿势展示是个问题:

红包雨的打开方式简单明了,切到首页,链接匹配 ~~ 开!!!

玩坏的搜索君,在大家戳一下“搜索”后,命中 ~~ 开!!!

双十一预热万店同庆,接口返回了正确的值,前端同学开心了 ~~ 开!!!

前端君默默的写一个poplayer相关协议头的url地址,戳 ~~ 开!!!

-----------------><----------------

那么怎么关闭?

前端童鞋不开心了 ~ 关!

服务端哥哥不开心了 ~~ 关!

客户端哥哥不开心了 ~~ 关!

这种任(bei)性(bi)妄(wu)为(chi)的灵活性,让开发的童鞋们怎能不开心不快乐!—— 运营妹纸,快来求我啊求我啊~~~!!!

screenshotscreenshot

POPLAYER改变前端生活

poplayer如此happy,那么poplayer对前端同学到底做了什么?

跨页面开发

如果产品君有一个需求,需要同时在首页+商品详情页+搜索页添加一个欢乐氛围的小动画,肿么办?

通常前端同学会说,嗯~~ 我来给你开发一个能让你new着玩的小动画,但是你要让首页+商品详情页+搜索页的同学在她们的页面上实例化我一下,所以我发布一下没问题,你要先搞定那三个页面的同学为了引用这玩意儿发布一下!!

那么问题来了,双十一期间要保证客户端产品的稳定性,啊喂还有页面是native的呢?!

poplayer改变前(chan)端(pin)狗生活!!!盖一层poplayer、配置一下这不是分分钟的事儿!! 既保证你们各个产品线的稳定性,又能让大家high起来~~ 所以你们玩high了各种抽红包有木有!!!

跨平台支持

作为一种新的客户端技术解决方案,最大的问题在于,要兼容天猫ios、天猫android、天猫ipad、淘宝ios、淘宝android、淘宝ipad、淘宝国际,这么多客户端多亏了客户端哥哥全部支持好了!!

实际上,poplayer这种技术已然在之前的实践中逐步应用,比如大家现在司空见惯的产品“淘口令”,比如之前“715哆啦A梦”活动的大量应用,以及后来部分产品线大家以为是native的入口,其实都是poplayer偷偷的在那里跑着,直到“万店同庆”,poplayer终于兼容了全部客户端,并在双十一期间带大家玩到high!~~

所以论poplayer怎样改变了我的生活~~~

为了兼容特意写了独立页面、独立库啊喂~!

因为踩坑专门在内网写了一篇长文啊喂~~!!

跟这么多客户端找来找去很爽么啊喂~~!!

--------------------------><------------------------------

所以论poplayer如何让我的生活更加快乐~~~

双十一过后终于可以去吼客户端哥哥:你们还敢三国杀、快去填坑啊喂~~!!!

前端小妹终于有机会去跟客户端哥哥搭讪、艳遇了啊喂~~!!!

screenshot

用户体验

伪装浮层: poplayer允许独立的页面伪装成浮层的样子,让用户无感知的、流畅的完成页面的相关操作;作为前端开发,再也不用纠结自己开发的东西会给别人的代码空间造成污染,也不用担心因为业务关系、自己引用了别人奇怪的代码影响自己的代码(处女座你说是不是?)

决定展示权: 在传统的前端开发中,前端同学是没有机会在用户无感知的情况下关闭页面的,只要页面被请求回来,通过代码关掉可以,但是总会在用户面前闪一下,用户体验很不好。 比如“万店同庆”,不在活动范围的页面是需要关闭poplayer的,这个关闭有两种方式:1. 链接地址带特定的值,客户端解析直接关闭。 2. 前端请求接口判定对方不符合条件,关闭。 对于第一种方式,根本不用前端操心了有木有!! 对于第二种,不符合条件我就不调用展示poplayer的代码有木有!!

POPLAYER让业务更美好

以上扯了这么多淡,继续扯一下poplayer对业务做了什么——

  • 产品的稳定性V.S.运营的灵活性

其实从上文就可以看出来,由于poplayer允许前端页面独立的、跨页面的展示,同时前端页面的开发迭代是远快于native的,poplayer既能保证产品线业务的独立、稳定运行,又能保证运营活动的快速、灵活推进,宜室宜家,上得了厅堂入得了厨房,搞的定产品忽悠得住运营。运营妹妹、产品哥哥遇到跨产品线的需求时,再也不用跪求各种产品线开发了。

  • 风险控制

人非圣贤孰能无过,几天几夜不睡觉的小伙伴们一不留神文案配错也是有的,链接地址配错了搞不好就要和廉政MM好好解释了,前端开发的童鞋们惺忪着睡眼代码写错了也是难以避免的,这时候只要重新发配置,可以快速关闭全部业务线上的所有poplayer页面,改好了测试好了再重新上线,只要不是时间强相关的业务,用户一般是不知道我们开了小差的,这风险嘛 嘿嘿~~ 千万不要告诉老板是我说的!!!

  • 脑洞大开

poplayer结合一些客户端的AR等新技术,可以玩出来很多脑洞大开的新效果,市场运营的同学们你们可以玩的玩法更多了,脑洞可以开得更大了,行业运营的同学你们可以跨行业、跨产品多瞅瞅西逛逛了,整合各处资源,咱们一起玩起!!

技术改变生活,poplayer改变业务模式~~!!

那啥有新页面别找我开发,我找客户端哥哥填坑呢~~!!!

小结

好了,写到这里相信你已经看出来这是一篇 吹水+吐槽+恶意卖萌 文,当然这里还是必须感谢客户端哥哥一直以来在我的无情践踏中依旧茁壮成长,耐心的解决调试、兼容、大坑等一系列问题,感谢各位看官看我扯到现在。

更多poplayer技术细节前端部分请继续查看本系列文章~~!!

更多poplayer技术细节客户端部分 —— 加入我们吧!!

screenshot

15年双11手淘前端技术巡演 - 前言

15年双11刚落下帷幕。今年众所周知,是全面“无线化”的一年。数据上我就不说了,可以公开的数据我相信大家多多少少也从各方都了解到了。
在整个阿里体系内,无论技术还是业务,都会把每年的双11当作一个战场,同时也是一个“炼金石”。不管是技术还是业务,不经过双11的检验,似乎就没有资格真正的在阿里站得住脚。

此文作为今年双11手淘前端技术巡演的前言,注定会是一篇技术干货含金量偏少的一个引子。笔者也就着第一次作为双11手淘前端的PM的角色,聊一聊在今年双11,在我们所谓“前端”这个职能上,我看到的一些事情和感想。也为接下来即将“扑面而来”的一篇篇各个技术方向的含金量极高的总结和干货做个“抛砖引玉”的作用。

“前端”造的出P1以上的故障么?

或许很多同学会有些奇怪,为什么笔者第一个话题会说这个。
没错,笔者在码这些字的时候,想起了在双11备战期的某天晚饭时,笔者跟自己的老板-大家熟知的@寒冬winter 无意闲聊,大致的话题是说当今“前端”这个职能在阿里体系内,在如今全面“无线化”,“App化”的时代,到底有多大的价值?

没错,前端圈子的确风风火火,各种热闹,各种走在技术变革的最前沿。但是你看,至少在阿里,在手淘App内,我们一没承载主交易链路,二是双11压力最大的流量,服务,QPS抗压也通常不是前端这个“职能”干的。

没错,成熟的Hybrid体系下Web的开发模式的确是研发最快,成本最小的一种方式,今年双11里面大家从手淘手猫各种App访问到的90%以上的会场,活动,小游戏,也确实都是“前端”同学们做出来的。但是,就凭着这些“上层建筑”,就能完全判定前端的价值么?

然后,Winter笑嘻嘻的说,是呀,你可以去看看,历年来在双11的故障单里面,有没有“前端”造出来的P1以上的故障。

问题继续下去似乎没有什么意义,我想说的是,有时候“你能出多大的漏子,可能也意味这你有多大的价值”。

当然,大家可以有心的说,我一个前端bug可以把淘宝首页搞挂,我一个js错误可以把整个搜索搞挂,这样是不是也是巨大价值的体现?反过来说,如果在双11这样的完整的测试流程和发布流程里面,出这样的前端故障丝毫不能体现你的重要,只能暴露你就是个bug。

所以,在以往意义上的前端,不出故障仅仅只是一个60分,甚至不到的标准。更谈不上能出大故障是价值的体现。然而在今年双11这个节点上,终于可以很冠冕堂皇,很自豪的说,“前端”这个职能完全可以承担起一个差错就可能引起P1以上故障的巨大价值体现。
先卖个关子,详细待后面 @勾三股四 @阿里子之 给大家详述。

双11前端团队的PM角色是什么样的体验?

我们按知乎的常用提问模式来说这个话题。双11前端团队的PM这个角色,笔者今年第一次做,TA更偏向于资源和风险的管理,而不是项目的管理,我们单个的项目有对应的单项的技术PM,单个项目的PM可以来源于任何技术职能团队。而在这前端个团队资源PM的角色里,TA的主要的职责有以下几个:

  • 前端资源池的全局管理和分配,横向对接双11每个单项项目组
  • 应对双11随时可能突发的新的需求和紧急项目
  • 风险的把控,团队内资源的阶段性调配
  • 在资源调配和风控之外,还需要对于部分项目的技术方案进行讨论和决策

就以上几个职责,有几个关键性的事情,

  1. 首先需要全局的了解双11相关的所有和前端团队相关的需求和项目,双11的业务基线在哪?需求层面,来源于产品,营销,互动,技术基础的项目各自有哪些?分别的工作量大概是怎么样?是否依赖客户端的双11新增功能或者版本?想要了解这些问题,意味着在双11需求搜集阶段,可能就会很忙碌,几乎要尽可能参加所有和前端资源相关的需求评审和方案确定。因为你必须要知道有多少事情之后,才有可能知道在这些事情下,在团队这么多资源的情况下,怎么去合理的分配。在资源极其紧张的情况下,前期漏掉一个都可能在后面的研发阶段引起极大的风险。
  2. 在有了全局的项目和需求梳理之后,一定还必须对团队的同学们的能力和擅长的方向有明确的认知和把握。因地制宜,合适的同学放到合适的位置。有同学适合做互动类的小游戏,有同学适合模块化的分工,做会场,也有同学适合做技术基础铺垫,横向的推动。对于同学们的技能点了解不算是难事,但是不是合适的人刚刚好就能完全对应上所有的事情的。互动和会场每年一定是需求量和工作量最大的两件事,如果匹配的资源不够,人的调配将会是一个头疼的问题。
  3. 当你好不容易紧紧巴巴把人挨个排到了对应的事情之后,一定记得回过头来看看,整个资源池子里,还有buffer和backup么... 在双11这样的事情上面,永远没有所谓的“意外”这一说。哪怕到了中后期,老板或者集团层面的一个决策随时可能催发新的重要的项目。如果没有提前把这件事情考虑进去,当问题来临的时候,才不至于束手无策。你自己也可以是一个buffer,但是一定不能仅仅是你自己一个。
  4. 以上还是前端团队内部的资源协调,对于手淘,手猫的版本控制,功能集成,发版规划是什么样的同样需要有明确的了解和认知。必须得知道在什么时间会发布什么版本,集成什么功能,有没有依赖这个功能上线的项目。强依赖Native新集成功能的项目和H5独立能承载的项目需要分为两个类别的来考虑,这跟项目时间上的优先级有直接关系,这种分阶段的调配对于一个同学需要支持多个项目的时候尤为重要。
  5. 当前期一切安排部署妥当之后,按部就班走到研发阶段的时候,PM的职责会略微转变,更多要从一个“班长”的角色变为一个“生活委员”和“劳动委员”。尤其是当你自己没有进入具体项目研发的时候,这个阶段你的精力会稍微多一些,服务好辛苦的,各种加班的同学,会是研发期间非常非常重要的事情。你必须要让大家知道大家的辛苦都是被你看在眼里的,同时在服务大家的时候,也能更好的监察各个项目的进度,发掘项目里优秀和大压力的同学。“适时和合理的激励”非常重要。
  6. 当整个研发阶段进入到中后期,你要着重关注的方向又会发生一些变化。需求变更和新增的需求一定是不可能杜绝的,尤其是在双11这样事情上。但是中后期的新增需求和变更往往又是风险最大的。尤其是是随着双11时间的临近,大家的关注度越来越升高,经常会冒出新的项目,而且都是默认自带“老板”属性的。这时候必须要亮起火眼金睛,“老板”属性的的需求也是有优先级的,而且是真是假是要通过一些原则来走的。就算是马总,逍遥子下来的需求,一线执行层也有权利一定到看到亲自的邮件批复或者签字才动工的 。这是对于项目和事情的谨慎和尊重。

所以总结下来,针对双11的前端团队的PM角色,是这样的体验:

  • 前期为了概览全局,需要参加各种评审会议和方案讨论,感觉被全世界需要。
  • 核心研发阶段是一个优秀的生活委员的体验,PM并不是指挥官。
  • 中后期是一个不断和人打交道,辩驳和确认紧急需求的体验,懂得有原则,有纪律的拒绝与接受。

以上,是笔者今年双11手淘前端团队PM的体验,大多数时候,这并不是一个技术活,却必须要担起让众多技术活顺利往前走的责任。

今年双11,手淘前端在技术层面有哪些关键词

本文作为今年双11无线前端技术巡演的引子,在最后一个部分最终会落到技术关键词这个部分,后面紧接而来的各种干货将围绕这些关键词全面展开。

关键词一:性能

从App端侧全面解决“顿”,“卡”,“慢”的问题。在今年双11前夕提出了“521法则” 。

  • App内存节省50%
  • 绘制帧率,滑动体验提升20%
  • App全链路实现 1S 法则
    • 强网(4G/Wifi)实现1S首屏(包括图片)加载
    • 3G 1S首包返回
    • 2G 1S建连,并且实现高复用
      从底层到前端,我们做了哪些事情,最终拿到了什么样的结果,将为大家揭开神秘的面纱。

关键词二: Native化

用Web的开发模式,打造完全Native的体验,在ReactNative之前全面实现三端同构打通,在今年双11会场上大放异彩。我们有什么样的技术大杀器,静待娓娓道来。

关键词三:ES6, 面向未来

在babel的支持下,ES6的盛宴提前展开,手淘无线前端在ES6,甚至ES7的优秀特性上深度实践,有哪些值得一谈,将和大家一一分享。

关键词四:面向社区,Vue & React

将为大家详细阐述手淘无线前端团队在不同业务上针对Vue和React的深度实践,怎么完整的打通开发链路,创造面向团队内的Vue或者React的最佳开发实践。

关键词五:全栈

前端从纯UI层面走到了强交互,富逻辑,再发展到前后端分层,而更进一步,手淘前端团队到今天为止已经有了完整的全栈的能力,以前跟前端不沾边的“集群管理”,“QPS”,“CDN回源策略”等词汇,也都一一的成为了今年双11前端团队服务和系统的事实指标。在全栈这条路上,有太多话可以说。

此外,还有更多的“前端安全”,“UI测试和集成” 等关键词五,关键词六...
在笔者这篇“抛砖引玉”的引言之后,将会一一展开和大家分享和讨论。

请大家拭目以待!

移动端全屏滑动h5活动解决方案

请教下大神,如何解决移动端全屏下子元素的,宽高和布局问题呢?

H5全屏动画展示类的活动页面,比如设计稿750*1334,我怎么设置子元素的大小

我现在用sass计算样式宽高成rem,js来设置html对应的dip和字体大小。
但是全屏活动在ip4小屏幕手机还是不能完美展现,请教大神有什么方法望告知,有尝试百分比来解决或者根据设备整个页面缩放处理,感觉都不是完美···哎 头疼。

webp 图片格式的疑问?

看完这篇文章,激起了我对图片的兴趣.又找到了一片文章,其中对各种图片格式做了介绍.http://blog.ibireme.com/2015/11/02/mobile_image_benchmark/然后 又去搜了下webp 这个格式. 目前已知的情况是 只有谷歌的chorme 支持? 主要是看了一下,对于native 和H5的话,这个webp貌似是非常好的一种优化了.我们公司也是采用的H5,但是加载比起京东和淘宝还是有很大的差距,一直就想找个解决方案. hybird 我们也采用了. 现在就是webp的话,你们是怎么实现的?不是只有谷歌的才支持吗?thks

15年双11手淘前端技术巡演 - H5性能最佳实践

前言

2015年是全面『无线化』的一年,在BAT(财报)几家公司都已经超过50%的流量来自移动端,这次 双11 更是占到了68.67%无线交易 (天猫微博)。

手淘中大量的业务采用H5的方式开发,H5体验好坏全面影响着手淘的使用体验。

今年手机淘宝在技术上重点解决“顿”,“卡”,“慢”的问题,并提出了“521法则” ,具体指:

  • App内存节省50%
  • 绘制帧率,滑动体验提升20%
  • App全链路实现 1S 法则
    • 强网(4G/Wifi)实现1S首屏(包括图片)加载
    • 3G 1S首包返回
    • 2G 1S建连,并且实现高复用 从底层到前端

其中1S加载完成是H5页面需要解决的重点问题,也是难点。

下面给大家介绍我们1年多来解决了些什么样的问题,以及带来多少性能的提升。以下每一条都是手淘在无线领域获得的宝贵经验。

系统&网络环境(手淘2015)

首先给大家介绍一下目前手淘的环境情况(供大家在设备兼容方面参考)。

操作系统

操作系统方面iOS占比42.23%,Android 占比52.38%,阿里云OS 占比5.29%,Windows Phone 占比 0.1%。

_679173aceca648b6a1bfe37c038f00b9

iOS版本 & Android版本

iOS 系统版本占比情况:

  • iOS 9 49%
  • iOS 8 38%
  • iOS 7 11%

Android 系统版本对比iOS 碎片化较为严重,所幸4.0以下版本占比小于10%。

占比如下:

  • 4.4.4版本30%
  • 4.4.2版本16%
  • 4.3版本11%
  • 4.2.2版本10%
  • 5.0版本以上10%

Banners_and_Alerts_____numbers_5f551394f00e41759c4e026c3eb0763a

网络情况

网络情况方面,得力于近年的3G/4G推广已经占35%,2G网络占比15%左右。

PastedGraphic_2_2a17a5229dc441a59c1391a5dbaabbbb

运营商

运营商方面 **移动占据70%的用户,**联通18.12%,**电信11.69%。其中需要注意的是移动3G技术(TD-SCDMA)性能差设备支持少,移动4G容易在信号不理想的地段降级成2G。

PastedGraphic_3_0a5b0e4fa03a49eebe9f2c863af34f01

收集性能数据&建立衡量标准

淘系H5页面主要在手淘客户端中展现,为了了解H5页面在客户端中的性能表现,我们在WebView容器中做了大量性能数据的采集,以页面,数据接口,单个静态资源为维度采集。

H5页面:我们以WebView的DidFinishLoad事件触发作为完成加载(Fully Loaded)的时间。

同时对支持performance.timing的设备收集Timing数据,用于详细分析网络请求各阶段的性能消耗情况。

WebViewDidFinishLoad 官方解释:Sent after a web view finishes loading a frame. [Android](http://developer.android.com/reference/android/webkit/WebViewClient.html#onPageFinished%28android.webkit.WebView, java.lang.String%29) iOS

1年前H5性能状况

针对几个主要的业务,我们将收集到的用户性能数据整理后得到以下的结果(部分业务按传统的网页性能优化方法优化过)。

DraggedImage_25982e5b7f4046efb0f580fc4bd1b005_d2e2135e89d743a8bb3804c208e11a04

性能情况非常不理想,不达标严重。2G下大于10s的占比在50%, 3G:6s内的低于70%近一半。

传统优化

看到上面的性能情况,我们最先想到的优化方法就是PC时代YAHOO 23条web性能优化军规

首先看看我们日常业务的请求瀑布图是怎么样的,根据这些情况看那些可以用规则去优化。

DraggedImage_5e36ed7656a7425d8afabd305bba01dc

请求数优化

  • 在请求数控制方面,将js,css各用combo的方式合并成单个资源。
  • 页面图片等等,只加载首屏资源,提升首屏展示速度。
  • 使用CSS ,SVG,ICONFONT 替换UI图片

合理使用IconFont

iconfont对于前端来说有很多优点:自由变化大小 矢量不失真、自由修改颜色、可以添加一些视觉效果如 阴影、旋转、透明度、兼容IE6。

使用现状

目前大家都基本上从平台上生成,生成后的文件如下:

@font-face {
font-family: "iconfont";
src: url('iconfont.eot'); /* IE9*/
src: url('iconfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('iconfont.woff') format('woff'), /* chrome、firefox */
url('iconfont.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */ }`

这样直接引用在项目中,真实在手机上的网络请求如下(在桌面版Chrome浏览器网络中无法看到这些请求):

DraggedImage_4cf266d1fccb4833928f5f6d263c7f86

这样造成了极大的网络带宽的消耗,这些icon资源往往在界面渲染中占据重要位置。
目前我们移动端只加载ttf一个字体文件,对于较小的文件也可以考虑base64在css文件中。

首屏多个动态接口合并请求

日常的业务中,一个页面往往拆分出多个异步数据接口(后端开发说:解耦),甚至首屏也需要3-5个接口(如动态banner区块,推荐内容,商品列表等),有些还有嵌套关系。
但是这些对页面性能造成不小的影响。

DraggedImage_70cd539eaa23481cb41eac9efd680c9a

手淘中某个接口在各网络下的请求性能,2G,3G下平均的消耗时间在2-6s完成下载。其中几个请求性能稍差点对整个页面影响也深远。

_15_11_17_12_04_02dfa888dec3495891b9e5c6fc4eaac5

禁止重定向

在我们CaseByCase的分析中发现,页面&静态资源的重定向会造成巨大的性能损耗。
特别使用前端JS脚本来实现页面跳转的。

_3_f60e75b7fefc46ed8d9123fdfa94c2a4

图片优化

手淘的特点就是图片多,图片的性能好坏可不仅仅影响用户体验哦,还直接影响着交易,钱 钱 钱…
我们在图片方面做了大量的优化。

使用WebP格式。

手淘2年前已经开始使用WebP格式了(主要native使用),1年前H5全面使用,其中iOS 的webview中由手淘以插件的方式支持。我们以手淘线上真实案例来看使用webp格式的前后效果。
案例为:线上的一个活动页面,打开一看其中的banner 就320多KB,整个页面加载457KB,如果就单单banner图片换成webp格式,整个页面的大小就只有181KB。

使用WebP前

DraggedImage_c2d16e1273bf429aac2118123c477d02

使用WebP后

DraggedImage_914eb83c506e4e25bed4bcacbe1c546c

Banner部分对比

DraggedImage_e2a8298a99a44233ab64738a69541eba

这种业务类型的案例,我们改进一下就可以为用户,为公司省70%的流量费用。

商品图片优化

商品图片在手淘的各个产品中都是必不可少的,为了适应不同业务需求的需要,我们在CDN服务上生成各个尺寸和质量的图片(近100个规格)。

  • q值:根据手淘网络情况,加载不同质量的商品图片(q30,q50,q75,q90)
  • 锐化:根据需要调整图片锐化值
  • DPI:根据设备DPI取适当尺寸商品图片
  • 合理的使用CDN图片尺寸可以带来下载图片的性能提升,还可以减少不必要的内存消耗。 我们日常中会用到的尺寸,每浪费10像素的宽高都可以造成很大的内存资源浪费。

计算方式如下:

DraggedImage_37ea251e09c44f1dbe24f3b689068cb8

图片DPI 优化

根据设备DPI值和图片质量Q值做优化,达到最优视觉体验和加载性能(DPI高,宽高增加后可适当降低质量)。

DraggedImage_6b19fe95e2294949bc5c9c2a85cdd12a

Sprite图片

Sprite图片(又称:雪碧图)被运用在众多使用了很多小图标的网站上。相对于把每张小图标以单个文件的形式引用到页面上,Sprite图片会只要请求一张图片就够了,减少了请求数提升了加载性能,还有就是方便图标管理。但在移动互联网时代在使用Sprite图片需要合理利用,不然反对性能造成影响。

解码内存消耗

Decoded in memory的计算公式: w x h x 4(宽 x 高 x 每个像素4个字节)
如果设备DPI大于1,还需要 X DPI系数。如Retina设备X 4,RetinaHD设备X 9.

DraggedImage_d15b37a3f44a453188bbcedcdd9d9f0b

禁止生成大图且利用率少

由于图片在浏览器中的解码方式,合理的生成紧凑的Sprite图片,即可以带来更少的请求数,又高性能低消耗。

DraggedImage_2fd69d286e5f4ef18caaa3bc908604f1

建议合并成如下:

DraggedImage_dc8095e9f7514aa8a92252c0445b5434

不建议合并成如下:

DraggedImage_1a20c971cf054438bd64dd0043896383

结合Native优化

经过几轮的常规前端性能优化后,页面性能有进步,但是离目标还远远不够。我们也不断的问自己到到底那里慢,那里瓶颈最大。我们开始试着CaseByCase的分析页以及梳理H5在手淘中的全链路性能消耗。

最终做了一些和native结合的优化思路。

HTTPDNS

DNS解析想必大家都知道,在传统PC时代DNS Lookup基本在几十ms内。而我们通过大量的数据采集和真实网络抓包分析(存在DNS解析的请求),DNS的消耗相当可观,2G网络大量5-10s,3G网络平均也要3-5s。

案例:iPhone5s 联通3G

78783DE2_043F_47C4_947D_2F368A44CD77_4a2da76568a345308af786a15338f704

DraggedImage_116eabb710e9451bac870de5c9140126

针对这种情况,手淘开发了一套httpdns,httpdns是面向无线端的域名解析服务,与传统走UDP协议的DNS不同,httpdns基于HTTP协议。 基于HTTP的域名解析,减少域名解析部分的时间并解决DNS劫持的问题。

手淘httpdns服务在启动的时候就会对白名单的域名进行域名解析,返回对应服务的最近ip(各运营商),端口号,协议类型,心跳 等信息。

优点

  • 防止域名劫持

传统DNS由Local DNS解析域名,不同运营商的Local DNS有不同的策略,某些Local DNS可能会劫持特定的域名。采用httpdns能够绕过Local DNS,避免被劫持;另外,httpdns的解析结果包含HMAC校验,也能够防止解析结果被中间网络设备窜改。

  • 更精准的调度

对域名解析而言,尤其是CDN域名,解析得到的IP应该更靠近客户端的地区和运营商,这样才能有更快的网络访问速度。然而,由于运营商策略的多样性,其推送的Local DNS可能和客户端不在同一个地区,这时得到的解析结果可能不是最优的。httpdns能够得到客户端的出口网关IP,从而能够更准确地判断客户端的地区和运营商,得到更精准的解析结果。

  • 更小的解析延迟和波动

在2G/3G这种移动网络下,DNS解析的延迟和波动都比较大。就单次解析请求而言,httpdns不会比传统的DNS更快,但通过httpdns客户端SDK的配合,总体而言,能够显著降低解析延迟和波动。httpdns客户端SDK有几个特性:预解析、多域名解析、TTL缓存和异步请求。

  • 额外的域名相关信息

传统DNS的解析结果只有ip,httpdns的解析结果采用JSON格式,除了ip外,还支持其它域名相关的信息,比如端口、spdy协议等。利用这些额外的信息,APP可以启用或停止某个功能,甚至利用httpdns来做灰度发布,通过httpdns控制灰度的比例。

SSL+SPDY协议

在经过以上的优化方案后,H5页面的性能始终离目标还远。在移动端建连的消耗非常大,在业界也只有SPDY 这个玩意儿比较新颖。
理论上SPDY协议可以完成多路复用的加密全双工通道,显著提升非wifi环境下的网络体验。

DraggedImage_2e08b076724a427a9cf91d6f8ab1146b

  • 多路复用请求优化
  • 服务器推送技术
  • SPDY压缩http头

看着很牛逼的技术,但是等我们第一期上线后发现,从数据上几乎察觉不了变化。

域名收敛

域名收敛是指尽量控制一个页面中使用的域名数量。为什么要这么做呢?我们前面提到DNS解析,减少域名数量可以降低DNS解析的成本。上文还提到SPDY协议,其中一个特性就是复用请求,使用同一个域名可以更多的复用请求。这个PC时代正好相反,我们原先用多个域名提升并发请求量已提升性能。
PC时代的域名数量类似这样的

DraggedImage_fa4838181dc843978eada2f101ed3cd8

域名最终形态(建议)

  • 页面请求 域名一个
  • 静态资源(css,js) 一个
  • 图片资源 一个
  • 动态数据一个
  • 数据统计一个

最终结合SSL+SPDY+域名收敛 才发挥出真正的作用。下图是各网络下SSL 和 非SSL 的性能情况。

Uploading 魔兔_-_无线统一监控平台 2.jpg…

动态数据复用状态

动态数据部分,相信各个公司都差不多,动态数据会有专门的API提供出来。手淘(mtop平台)也一样,接口总会有各种状态,登录失效,令牌过期,签名失效等。手淘使用了重发请求的方式来获取新的签名令牌等。导致如下的情况(始料未及):
在手淘真实环境中发现一次令牌过期,可以造成10多秒的延迟。

DraggedImage_360111fd7f19443990115e75c5b4c071

手淘在启动的时候Native已经做了很多的数据请求,这些也是mtop请求,只是由native程序发出。 我们判断到页面是在手淘客户端中就会使用native发出mtop数据请求,这样就规避了令牌失效,签名失效的问题。

直本次优化后,我们第一次看到了非常激动人心的数据:第一次里程碑式的提升

优化前

_1_593a69d6043a4ad3a5143051e27ead2e

优化后

_2_313ab932ffc0413f9889c21925f12693

预加载和离线化方案

手淘中加载H5页面的首屏白屏时间较长,基于此,集团各BU都内部产生了离线包的方案(手淘,航旅,钱包),将静态资源通过Hybrid的方式提前加载到本地。大量了资源预加载给H5页面带来了极大的提升,资源加载时间降到30ms内。
今年双11前,针对预加载方案,在技术上解决了3个痛点

  • 开发成本(开发方式侵入性强)
  • 静态资源解Combo
  • 更新到达率

关于 预加载&离线化方案详细技术细节很多,这里先简单阐述这个功能带来的性能提升价值。

下图案例:

  • 第一个HTML没有预加载,阻塞页面时间较长
  • 静态资源部分几ms 级别加载,加载性能非常好

lALOAmbUQ80D7M0HgA_1920_1004_40fe7ff23e10467e9f1cf3c02c0498bf

这是第二次里程碑式的提升,3G,4G,WIFI网络性能全面提升。

截至到这个时间点为止,我们个别业务的性能有了显著的提升。以下是其中一个业务的3个性能优化节点的性能优化对比。其中10s以上加载完成的占比大大的缩小,1s以内的占比上升较高。

DraggedImage_4a5daf7dd5cc485d9ce6c44ef83226db

统计方案优化

业务数据统计在日常工作中是必不可少的部分,因统计部分量级不大,往往会忽视掉。其中我们对一个业务CaseByCase的分析后发现。一个**移动 2G/3G.Edge的用户在请求H5页面的时候 log.mmstat.com 的日志埋点花了近6s才加载完成。

514E1116_9ECE_4768_8BE8_C2F502D661A0_af89dc8bee8f47708c3881c573124ed2

DraggedImage_68e0466f8b59416f8a7a29f3fb0f6524

后面我们对数据统计部分做了Native的异步上传。效果是这样滴:

iOS:

_15_11_17_03_09_f470e9f4998a4a5c86918a92d5525ff7

Android:

_15_11_17_03_10_94376eb235b64ed2974650f4738d8cc4

第三次里程碑式的提升,2G网络性能全面提升

渲染优化

  • 禁止使用iframe (阻塞父文档onload事件)
  • 禁止使用GIF图片实现Loading 效果 (降低CPU消耗,提升渲染性能)视频

业务效果

前面说了这么多性能优化,肯定会有问,花了这么多力气,前端到客户端到后端搞了这么多,价值在那里呢。业界google,amazon,microsoft都给过速度性能对业务对GMV的影响。
下面是两个手淘中的业务在持续几个月的性能优化后UV,跳失率,停留时间的变化。

DraggedImage_fdb5687616a24cf092473c43f6401678

DraggedImage_9acd1efea6424102bb78fb1d1e9b2fec

2015 双11 战绩

好了,说了这么多这次双11搞的怎么样呢?

2015_11_H5__numbers_414306f6efe84b3bbde412781cede368

2015_11_H5__numbers

在无线时代,H5页面中的任意一个微小的点都会应网络问题被放大N倍。持续分析用户真实数据和CaseByCase的分析不同业务场景,才能真正找到性能瓶颈。

H5要达到极致的体验,需要大家跳出前端的圈子,从客户端,后端 一起的角度来共同协助发挥各自的优势才能真正做到极致的体验。后面有更多精彩等这大家。

写给前端面试者

不管是刚毕业踏入社会的还是在职场久经奋战的,都经历过被面这一环节。当然也有很多同学开始在面人,为自己的团队选择优秀的血液。而我也是属于这一类,这些年都有在帮公司或朋友的团队物色人才和面试同学。今年在手淘也一样,在给同学面试过程中,让我开始在思考,而且思考了很久,所以这几天静下来写了这篇文章,希望这篇文章对于刚毕业的大学生或者还在继续参加工作面试的同学有所帮助。

在这篇文章中,将不涉及任何的面试题,我只想和大家聊聊面试者与被面者之间的感受。

什么是前端

什么是前端?在这里不做阐述,如果您对这个问题感兴趣的话,建议您可以阅读早前写过的一篇博文《前端路上的旅行》,文章中对什么是前端做过一些介绍,虽然不是非常的准确,但大概意思是表达到了。

为什么选择前端

对于这样的问题,是找不到准确而又正确的答案。既然您参加的面试是有关于前端的工作,那么试问?您有思考过这个问题吗?

如果你是一位从事过前端工作的同学,你或许会这样回答:

  • 我喜欢前端,前端的工作给我即写即得,比起编程更为简单,更知性
  • 前端入门的门槛低,一不小心就入这个行业,后悔已莫及
  • 无奈的选择,当初团队没前端,从别的职位转过来的,然后就这样干下来了
  • 前端前景大,大家都知道全世界都在招前端,而且都很难招(知乎上的热贴:为什么前端工程师很难找?
  • 或许还有更多的回答...

但对于刚毕业的同学,还从未踏入过职场的大学生而言,或许就从未没有思考过,为什么自己选择前端?我臆测下,可能有这样的几个场景:

  • 学长拉下水,不知不觉去面试了前端
  • 非计算机专业而对自己专业又不太感兴趣,但又为了毕业之后有份工作,找了门好学的,容易混饭的先整上
  • 盲目的跟风,听说前端好,那就前端吧
  • 无任何职业的未来思考,首先解决毕业后的工作问题
  • 或许还能很多不同的场景...

不管你是什么样的出发点,既然你选择去面试前端这样的一份工作,你是否有静下心来思考过,自己为什么选择这个行业(甚至可以说,你一点都不了解的行业)。说实在的,还是很多前端面试官会问你这样的一个问题?虽然这样的问题,没有准确或者规范的答案,毕竟不是技术问题,有源可查。

面试前的一些准备工作

说实在的,面试是一件非常头疼的事情。每个人都痛恨面试。虽然你面试的工作机会不一定能拿到,但这也并不意味着你在这方面的能力不行。主要是因为面试的不确定因素太多太多,而且面试还需要一些技巧,不管是面试官或被面者。

不同的人,会因为不同的因素获得不同的面试资格。或许你参加面试的时候,直到面试官坐在你面前之前,有可能他都没有看过或者记得你的简历内容(这样的情景是存在的)。当然,也有另外一个场景,在面试官坐到你面前之前,面试官司通过不同的途径对你已做了一些了解。

就此而言,参加面试者应该做这样的假设:面试官将会深入调查你。因此,当你参加面试之前,你应该做一些准备工作:

用心写好简历

简历是你向面试官展示自己的第一张名片。简历的好也坏直接会影响你在面试官心中的第一印象(这个一点也不夸张)。对于有工作经验的同学,一份好的简历应该做到:

要低调的告诉招聘方,爷很NB。

如何做到这一点,可以看看@Easy给程序员写简历的建议《如何写好技术简历》。

而对于应届毕业生,要注意的就更多了。虽然你没有过多的经验,但你要做到你的简历简单明了,具有较强的针对性,切勿简历上介绍自己精通xxx语言获得xxx奖学金得了xxx奖之类,这一切都是浮云,面试官不会因为你在学校当了什么会的干部,拿了多少奖学金太感兴趣,面试官对你感兴趣的是,你在学校做了什么事情,你有什么作品可拿出手。

那么什么是好简历?简单描述几点:

  • 匹配:简历上每一个元素为应聘岗位而“生”;
  • 措辞:行文流畅,容易理解,描述具体而明确;
  • 模板:商务简洁,突出胜任岗位的核心竞争力。

有两点特别强调一下:尽量让你的信息在一页上展示完;不要使用表格来做你的简历。 而现在的学生,简历大多都是使用表格来制作(至少我很讨厌使用表格制作的简历)。

保持通信顺畅

保持通信的顺畅是很重要的一点,直接会影响你会不会失去一次面试的机会。因为联系你大多都会直接通过简历上的电话。如果面试官想通知你参加面试,而你的电话总是处理关机或者无法接通的情景之下,可想而知,你将会失去这样的一次机会,而这样的一次机会或许就是改变你命运的一次机会。

这次校招我就碰到这样的现象,打电话给大学生通知其面试,但总是处于无法接通。可想而知,就算我再求闲若渴,我也没有这么的时间给你不停的打电话。

需不需要准备面试题

很多同学喜欢去搜集一些前端面试题,而且网上这样的面试题也非常的多,比如:

那么去面试前准备这些题的答案是否有用处(很多面霸把网上出现的题都撸了个遍)。下面发表一下个人看法。

个人认为这些题只能帮助你对相关知识有一定的了解,但对于拿去应付面试还是有所欠缺。你花时间只是背下了这些题的答案,有可能你并没有吃透其中的为什么?就算面试官问到了其中的一些题,你一开始会觉得很幸运,你知道答案是什么?但你没有考虑到的是,就算你碰到了,你知道了答案,你根本无法知道面试官就此题会接下来问你什么。可想而知,如果你只是背题,接下来的为什么?你可能就不知道回答了。

就我个人而言,我一般不备题,随时根据面试者的自我介绍和相关了解之后才出题。对于这样的随机性,你的备题方案是否还有用处呢?

当然,古人云“有备无患”,这是好事,经过这些题,爱钻研的会去再问为什么?不爱钻研的也对相关知识有一个面的了解。最主要的是,面试碰到了相关的题,你不会再紧张,你会有些许的自信。

面试要注意的细节

细节很多时候会决定你的成败。那么在第一次参加面试时还是有些细节需要注意。

时间观念

可能跟你首页基本上是会通过电话或者视频面试,一是节约彼此时间。那么这个时候不管是面试官或被面者都需要有一定的时间观念,不能迟到。如果你有事情会耽搁,应该事先通知彼此。而在现实中没有时间观念的还是很多的。我讲两个自己亲身经历过的事情。

记得有一次在上海去一家公司面试前端。约好的时间是下午两点钟。我请了个假,屁颠屁颠的到达目的地,填好表格。面试方行政通知我等几分钟,面试官就来。可我足足等了半小时,还未见到面试官影子。追问之下,行政告诉我,已通知面试官了,稍后就到。可我等了近一个小时之后,还是没有见到面试官,如此之下,我闪人了。结果在地铁中接到人事电话,问我为什么走了?那么答案还需要我说吗?

上面是自己去被面的一次经历,那么今年面试一大学生,经历的故事让我直接无语。提前一天约好时间面试,结果小朋友告诉我在外面办理事情,不能面试。为了招到人才,我也就忍了,再次约好下午某个时间点,当我再次打电话去的时候,小朋友告诉我马上要和同学去聚餐。此时的我,真想大声吼两声,昵玛,没时间别答应我呀,可我还是忍了。

不管是哪种情形,没有时间观念和失约对于被面者和面试官都不是一个好的体验。何况我们的工作,其中有一个较为重要的就是让你的用户体验好。而且这已不是一个简单的细节,而是失去自己为人的原则,再高一点就是诚信都失了,何来的机会。

真诚面对

当你的技术不能达标时,只要你的人品达标,也能为自己增加不少分数。可能是由于自己的情节,我个人较为喜欢诚实的孩子。喜欢说一不二的孩子。因为你的诚实有可能会打动你的面试官,他会觉得你是可塑之才,说不定给你一个机会。或许很多人都会讨厌那种浮夸之徒吧。自己不会又想想尽一切办法,在面试官面前展示你这方面很优秀,其实有时候这样做会得到相反的效果。

尽量展示自己的软实力

每个人都有自己好的一面也有自己不足的一面,那么在面试的时候,应该尽量展示自己优秀的一面。对于校招生或实习生而言,面试官真的不太在乎你所掌握的技术如何?而更再意的是你是不是有发展的潜力。你处事的能力,学习的能力,解决问题的能力等等。而自己问题都是在和你聊天的过程中去掌握的,所以你应该借此机会,向面试官展示你这方面的软实力。

面试中的总结

多做几次总结会让自己更认识自己。虽然你有面试的机会,但这并不代表你能通过面试,那么每一次的面试失败就是你下一次面试成功的铺垫。我建议每次面试完之后去做一些总结,特别对于刚刚毕业的学生。通过总结你会清楚的知道:

  • **技术:**技术方面缺少什么?面试官看重的是什么?
  • **软实力:**自己拿分项是哪些?自己丢分项是哪些?自己面试过程的沟通能力如何?
  • **失败原因:**面试失败原因是什么?是由于技术不足?还是其他原因造成自己面试失败?
  • 还可以去思考更多的为什么?...

校招面试的现象

这次面试主要针对的是校招生和实习生,总觉得他们都有一个普遍的现象。这些现像和@kejun在2011年写的博文《近期面试感受》非常的类似。

以下部分内容引用@kejun的博文《近期面试感受

用的不是技术,更多是技巧

由于学校里没有系统的前端开发课程,导致对HTML/CSS/JavaScript基本概念的理解非常薄弱。大部分人的学习方式是:先看书,然后觉得书和实践离得很远就直接实践,遇到问题就去网上搜、QQ群问,而这些方式得来的都仅是“技巧”性的东西。或者是跟着学校里的“牛人”学,掺着各种好的、坏的经验全盘接受。

“搜索”式学习害人不浅

面试中有同学觉得书上写的东西不实用,更喜欢边实践边学。但往往实践中主要解决具体问题,从网上搜到一个不好的例子,自己又不足以区分好坏。然后,就把它当成一个解决问题的模式,如果没人纠正,可能几年下来都这么用。网上的资源非常丰富,要区分“技术”和“技巧”。还是那句话,对技术人员来说技巧性的东西不应该太重。

热衷新技术

古人讲究温故而知新还是很有道理的。学习新技术可以给自我镀金,欠缺基础的东西就会内力不足,这样在应用的过程中就会有问题。

学到“二手货”

国内前端技术社区的分享质量总体还是不高。那些照搬国外,加上自己片面认识的资源就是“二手货”。这些资源在看的时候要慧眼识珠啊。最好还是直接看第一手资料。

光看不用,坐等机会

很多同学表示正在看什么什么,或正准备学什么什么。但就是没动手写过,总是希望在实习公司有实践机会。这样的机会可能永远也没有呢,完善自我的技能,是需要自己给自己创造机会,写一些demo,搞一些个人项目,参加一些技术交流,持续关注该技术的发展……坐等只能浪费时间。

不注重基础

正如前面所说,学校没有系统的前端课程,同学获取的知识点都是看书和网上获取,而且这些知识点又是零散型的。造成对很多问题只知其一不知其二。更为可怕的是,很多同学太过自信,觉得HTML/CSS都太简单了,只是JavaScript稍微难一点,这也造成自己对知识的认知度不足。另外还有一些同学太过急于求成,认为看了几个HTML标签和CSS属性自己就懂了。事实是你离其还甚远。

目标过大

毕业后选择BAT这样的大公司实习或就业,对于很多同学来说都是梦寐以求的。然而能进入这些公司的人却少之又少。很多人都在问"国内大型互联网公司(如BAT)对于web前端开发方向校招都考些什么?",但这也仅停留在技术上的面试,却从未思考,BAT这些大公司需要的是技术型人才,而不是技巧型人才。那么你是属于技术型还是技巧型呢?

话又说回来,有目标是好事,能让自己有一个清晰的方向,但过于盲目却会让自己失去自信,失去未来,这样是得不尝失。与其如此,还不如思考一下,自己更适合什么样的环境去实习或工作。这跟追妹子是类似的“可遇不可求”。

总结

这事一篇与技术没有任何关系的文章,这也不是一篇心灵鸡汤,更像是泼冷水。而这些观点仅是我自己的建解,如果说得不对,您可以忽略,更希望你能指正。同时更希望的是,这篇文章对于正在面试或即将面试前端工作的同学有所帮助或有所感悟。

关于font-size和line-height的问题

有一个问题,我在iphone4和5的html中data-dpr为2时,font-size: 64px;
在iphone6的html中data-dpr为2,font-size: 75px;
在iphone6p的html中data-dpr为3,font-size: 124.5px;

我发现font-size用px的话,会有问题,在各手机下会出现字体大小不一样的情况,而字体用rem就没问题。

看到说推荐字体用px,为什么要用px呢?我表示不解。
另外如果字体用px,line-height怎么用呢?我现在字体用px的话line-height用的是rem。

Web中的图标

随着时代的变迁与技术的不断的更新,在当今这个时代,Web中的图标(Icons)不再仅仅是局限于<img>。除了<img>直接调用Icons文件之外,还有Sprites(俗称雪碧图)Icon Font(字体图标)SVG Icon等等。今天我们就来一起探讨一下这些方法在Web中实现Icon的利弊。

思考变革

设计师不管分辨率(Resolution independent)和设备平台,其追求像素完美(Pixel Perfection)、体验一致性;而前端工程师们更为关心的是页面的可访问性(Accessability)、性能以及重构的灵活性,可复用性,可维护性等等。

而当下这个互联网时代,设备多样化,显示分辨率层出不穷,对于Web前端工程师来说可是灾难性,而且碰到的难题也是越来越多:

  • 需要为高PPI(Retina显屏)显示设备准备1.5x2x3x的图标素材
  • 需要针对不同分辨率来调整优化排版
  • 需要考虑不同平台下图片加载的性能问题
  • 需要考虑可访问性,可维护性问题
  • 要考虑的还有很多很多.........

前途是光明的,道路是曲折的。前端工程师一直以来就是见招拆招,从未停止过自己向前的步伐。不信我们一起来看。

原始的<img>

<img>标签,大家都知道是用来给Web页面添加图片的。而图标(Icons)其实也是属于图片,因而在页面中可以直接使用<img>标签来加载图标。并且可以加载任何适用于Web页面的图标格式,比如:.jpg(或.jpeg)、.png.gif。对于今天的Web,除了这几种图片格式之外,还可以直接引用.webp.svg图像(图标)。

优势

  • 更换简单方便,只需要修改图标路径或覆盖图标文件名
  • 图标大小易于掌握

看上去这都不是什么优势,我也只能为其想到这两条了。

劣势

  • 增加HTTP请求数,如果页面使用的图标过多,直接拉高了HTTP的请求数,也就直接影响页面的加载性能
  • 不易适配各种终端和分辨率,特别是高PPI的显示设备,有可能会造成图标模糊(除非是通过img加载矢量图标.svg,或者一开始就加载适合高PPI的图标)
  • 不易修改图标的样式,比如颜色,阴影等
  • 不易维护

Sprites 图标(雪碧图)

虽然img可以帮助前端工程师往Web页面中添加需要的图标,但其不足之处也是众所周知的。由于img的局限性与不足,2004年3月@Dave Shea提出了一种全新的技术CSS Sprites(在国内常常将这种技术称为CSS雪碧,又称CSS精灵)。

CSS Sprites出现之后,很多互联网团队都在使用这种技术,比如:

Amazon

Amazon

YouTube

YouTube

Google

Google

Facebook

Facebook

Yahoo

Yahoo

当然,国内使用CSS Sprites也不少:

淘宝

淘宝

新浪微博

新浪微博

诸如此类的还很有很多。

在此对于CSS Sprites的技术就不做过多的阐述,如果您想了解这方面的相关知识,可以阅读下面的文章科普:

Sprites分类

早期CSS Sprites使用的都是位图,而且为了适合Web页面使用环境,采用的都是.png文件格式,但在现在只使用位图,会受到很多的限制,比如在Retina屏下,位图会模糊。也就是说,为了适配各种终端设备分辨,CSS Sprites不在局限于位图,也可以将SVG这样的矢量图集合在一起。其和位图最大的不同之处可以根据设备分辨率,调整Sprites的尺寸,从而不影响图标在设备的呈现质量

相对而言,SVG更适合当前的Web页面,当然,这种技术也受到一定的局限性,比如说修改ICON图标颜色之类,就必须去修改.svg文件,这对于前端人员来说是无法接受。有关于SVG Sprites相关的介绍,可以阅读下面相关文章:

优势

  • 减少HTTP请求数
  • 可以是任意图形,也可以是任意色彩
  • 兼容性极好(对于位图的Sprites兼容性都非常的好,但对于SVG的Sprites,还是受到浏览器的限制,最起码要支持SVG的浏览器才能得到支持)

劣势

  • 增加开发时间,需要人肉或通过相关工具,将图形零散的图形合并到一起,而不同的合并方式,图形的色彩对Web的性能有直接的影响;
  • 增加维护成本,要增加新的图标合成进来,是件较难的事情,甚至直接会影响到前面又定位好的图片。目前为止,使用自动编译工具,相对比人肉处理要理想一些;
  • 图片尺寸固定,在位图的Sprites中无法通过CSS来修改图标的大小,但在SVG的Sprites中可以配合CSS的background-size调整图标的大小;

字体图标(Icon Font)

虽然CSS Sprites有其足够的优势,而且众多开发者都在使用这种技术,但是到了今天,不得不对CSS Sprites说再见(其实是跟位图的图标说再见)。

随着Retina屏幕的出现,大家都发现自己在Web中使用的图标变得模糊不清,直接拉低了自己产品的品质。对于Web前端人员也必须面对考虑各种高清屏幕的显示效果。由此也造成同样的前端在代码实现的时候需要根据屏幕的不同来输出不同分辨率的图片。不管你采用的是何种手段:

不管使用哪种方法,都不是一件易事,比如使用image-set和媒体查询,只适合背景图像;而对于srcsetpicture方法仅适合Web引入图像的情景。而且这些方法直到目前为止在浏览器上都受到了很多的限制。

为了解决屏幕分辨率对图标影响的问题,字体图标(Icon Font)就顺势而生了。字体图标是一种全新的设计方式,更为重要的是相比位图而言,使用字体图标可以不受限于屏幕分辨率,冲着这一点就具有非常强的优势,而且字体图标还具有一个优势是,只要适合字体相关的CSS属性都适合字体图标,比如说:

  • 使用font-size修改图标大小
  • 使用color修改图标颜色
  • 使用text-shadow给图标增加阴影
  • ....

基于这些原因,现在Web开发中,使用字体来制作图标的应用也越来越多。

优势

  • 减少了HTTP的请求
  • 很容易任意地缩放
  • 很容易地改变颜色
  • 很容易地产生阴影
  • 可以拥有透明效果
  • 浏览器兼容性较好
  • 可以到很CSS很好支持
  • 可以快速转化形态
  • 可以做出跟位图一样可以做的事情
  • 本身体积更小

劣势

  • 它们只能被渲染成单色或CSS3的渐变色
  • 使用限制性很大,除非你想花时间去创作你自己的字体图标
  • 创作字体图标很耗时间
  • 可访问性差
  • 字体文件体积过大,直接影响页面加载性能,特别是加载一个包含数百图标的Fonts,却只使用其中几个图标
  • 在不同的设备浏览器字体的渲染会略有差别,在不同的浏览器或系统中对文字的渲染不同,其显示的位置和大小可能会受到font-sizeline-heightword-spacing等CSS属性的影响,而且这种影响调整起来较为困难
  • 为了实现最大程度的浏览器支持,可能要提供至少四种不同类型的字体文件。包括.ttf.woff.eot.svg格式字体
  • 不兼容旧的手机浏览器:Opera mini,Android 2.1,Windows Phone 7.5-7.8
  • 在手机上可能与系统字体冲突

SVG图标

为了适配各种分辨率,让图标显示更完美,除了字体图标之外,还可以使用SVG图标。SVG图标是一种矢量图标。其实回过头来看,字体图标其实也是使用SVG封装过的。

为什么要使用SVG图标

SVG图标实际上是一个服务于浏览器的XML文件,而不是一个字体或像素的位图。它是由浏览器直接渲染XML,在任何大小之下都会保持图像清晰。而且文件中的XML还提供了很多机会,可以直接在代码中使用动画或者修改颜色,描边等。不需要借助任何图形编辑软件都可以轻松的自定义图像。除此之外,SVG图像也有过字体图标的一个主要优势:拥有多个彩色图像的能力。

优势

  • SVG图标是矢量图形文件,可以随意修改大小,而且不会影响图标质量
  • 可以使用CSS样式来自定义图标颜色,比如颜色、尺寸等效果
  • 所有SVG图标可以全部放在一个SVG的文件中(SVG Sprites),节省HTTP的请求
  • 使用SMIL、CSS或者JavaScript可以制作动画效果
  • 可以使用gzip的方式把文件压缩到很小
  • 可以很精细的控制SVG图标的每一部分

劣势

  • 浏览器兼容性较差
  • 需要学习SVG相关知识
  • 需要了解使用制作软件绘制SVG图形或专业的SVG图形编辑软件

为什么要使用SVG图标替代字体图标

为了让图标能更好的适配各种屏幕分辨率,大家首先的就是字体图标和SVG图标,那么这里为何又要谈:为什么要使用SVG图标替代字体图标@Chris Coyier的《Inline SVG vs Icon Fonts》(中文译文可以点击这里阅读)和@Ian Feather的《Ten reasons we switched from an icon font to SVG》(中文译文可以点击这里阅读)。

字体图标 vs SVG图标

字体图标 SVG图标
图标是矢量 浏览器会以字体解析它,所以浏览器会以文字的方式来对图标做抗锯齿处理,这可以导致字体图标没有期待中的那么锐利 SVG是XML文件,浏览器直接解析XML文件,直接就是矢量图形,图标锐利,体积也小
可控制性 可以通过font-size、color、text-shadow等CSS来控制图标 除了字体图标一样的CSS控制方法之外,还可以单独控制一个复合SVG图标中的某一部分,也可以给图标描边
控制图标位置 图标位置会受到line-height、vertical-align、letter-spacing等属性影响 SVG图标的大小就是很精确的SVG图形的大小
图标加载 跨域时没有合理的CORS头部、字体文件未加载、@font-face在Chrome中的bug和不支持@font-face的浏览器等,这些原因都会造成字体图标渲染失败 SVG图标就是文档本身,只要支持SVG的浏览器,都能正常的渲染
语义化,易访问性 为了更好的显示图标,通常使用伪元素或伪类来做,这样做语义化较差 SVG图标就是一个小图片。SVG的语义就是”我是一张图片“,感觉可能更好
易用性 使用一个已造好的字体图标集从来都不有效,因为有太多的图标未使用。而创建一个你自己的字体图标集也不是轻松的事情,需要懂得相关的编辑工具或应用软件 SVG图标会简单一些,因为你可以自己手动地操作,如果需要的话,你可以使用相关的编辑工具
浏览器支持度 得到非常好的支持性,可以一直支持到IE6,在Opera mini,Android 2.1,Windows Phone 7.5-7.8没到支持 浏览器支持性一般,IE8和Android 2.1以及其以下浏览器不支持。不支持可以采用降级处理,但不并完美

DataURI

DataURI是利用Base64编码规范将图片转换成文本字符,不仅是图片,还可以编码JS、CSS、HTML等文件。通过将图标文件编码成文本字符,从而可以直接写在HTML/CSS文件里面,不会增加任何多余的请求。

但是DataURI的劣势也是很明显的,每次都需要解码从而阻塞了CSS渲染,可以通过分离出一个专用的CSS文件,不过那就需要增加一个请求,那样与CSS Sprites、Icon Font和SVG相比没有了任何优势,也因此,在实践中不推荐这种方法。需要注意的是通过缓存CSS可以来达到缓存的目的。

优势:

  • 不增加请求数

劣势:

  • 通常比图片要大不到10%
  • 每次加载页面都需要解码
  • 不支持IE6/7,IE8最大支持32KB
  • 难于维护

性能对比

不管使用哪种方案来制作Web页面的图标,大家都会比较关心其对页面的性能有多大的影响。在这里提供一个测试用例,在这些用例中,页面加载了28232 x 32的图标。

Web Icons

这些图标直接通过IcoMoon APP获取。并且分别采用了img加载.png、CSS Sprites(png和svg的Sprites)、字体图标和SVG图标的方式写的用例:

具体代码就不做演示,接下来通过在线性能测试工具**WebPageTest**(除了这个在线测试工具之外,还可以点击这里获取其他的在线测试工具)来做一个简单的测试。当然这样的测试可能不会非常的准确,但或多或少能从相关的数据上向大家形象的展示不同的方案对页面性能的影响会有多大。

特别声明:以下提供的测试数据受到网络直接影响,仅提供做为示例参考。

PNG/SVG文件

PNG/SVG文件

页面使用<img>加载了282.png.svg图标。每个图标的大小是32px x 32px

PNG Sprites

PNG Sprites

282.png图标集成在一个Sprites文件中,Sprites图片文件大小是108kb

SVG Sprites

SVG Sprites

282.svg图标集成在一个Sprites文件中,Sprites图片文件大小是180kb

SVG

SVG

在页面中直接使用SVG源码制作的图标。

Icon Font

Icon Font

使用Font制作的图标。

以上图表中的数据仅做参考,因为在线测试,网速之类直接影响到测试结果。熟悉性能测试的同学,可以直接在本地测试用例,拿到更具有价值的参考数据。也希望同学能将这方面的结果在评论中与我们一起分享。

如何选择

前面介绍了Web中制作图标的几种常见方案,每种方案都有其自己的利弊。那在实际中要如何选择呢?这需要根据自身所在的环境来做选择:

  • 如果你需要信息更丰富的图片,不仅仅是图标时,可以考虑使用<img>
  • 使用的不是展示类图形,而是装饰性的图形(包括图标),而且这部分图形一般不轻意改变,可以考虑使用PNG Sprites
  • 如果你的图标之类需要更好的适配于高分辨率设备环境之下,可以考虑使用SVG Sprites
  • 如果仅仅是要使用Icon这些小图标,并且对Icon做一些个性化样式,可以考虑使用Icon Font
  • 如果你需要图标更具扩展性,又不希望加载额外的图标,可以考虑在页面中直接使用SVG代码绘制的矢量图

当然,在实际开发中,可能一种方案无法达到你所需的需求,你也可以考虑多种方案结合在一起使用。

总结

全文主要介绍了Web中图标的几种方案之间的利与弊。相对而言,如果不需要考虑一些低版本用户,就当前这个互联网时代,面对众多终端,较为适合的方案还是使用SVG。不管是通过img直接调用.svg的文件还是使用SVG的Sprites,或者直接在页面中使用SVG(直接代码),都具有较大的优势。不用担心,使用的图标在不同的终端(特别是在Retina屏)会模糊不清。而且SVG还有一个较大的优势,你可以直接在源码中对SVG做修改,特别是可以分别控制图标的不同部分,加入动画等。

当然,你或许会有众多的顾虑,不懂SVG的怎么破,就算不懂SVG,你也可以借助SVG的图形编辑软件或者工具,来协助你。除此之外,除了这些方式在Web中嵌入图标之外,对于一些简单的小图标,可以考虑直接使用CSS代码来编写,这种方式可能较为费时费力,但其具有的优势,我想大家都懂的。

最后非常感谢您花时间阅读这篇文章,如果您有更好的思路或建议,非常欢迎在下面的评论中与我一起分享。

Flint探秘

Flint探秘

前言

前端逐年发展,从实现刀耕火种的纯静态页面,到开发复杂交互的前端应用,各种框架、自动化工具层出不穷。在国内的前端界忙着造 MVVM 的轮子、为选择哪家框架而斗嘴的时候,国外的团队已经开始思考开发体验的优化了。

手淘前端内部目前比较流行的工作流,是 MVVM 框架 + 各种第三方库 + NPM 包管理 + Gulp 流 + Webpack 打包 + 本地服务器 + Hot Loader 插件或者是 LiveReload 插件,再配合上喜欢的编辑器、浏览器。从上古时期的写完 HTML 、 CSS 、 JavaScript ,直接浏览器刷新,慢慢发展到今天需要编译的自动化工作流,前端开发的能力上了一个台阶的同时,复杂性也逐年升高,越来越“折腾”。

如果你跟我一样不想再“折腾”下去,可以随着我一起了解一下 Flint 。它是国外团队最近的一个作品,还在Beta当中,没有公开发布。在这篇文章当中,我会逐步从浅到深,聊一聊这款框架的特性和脑洞。

PS: 在本文中,由于Flint 本身的语法以及编译不是重点,侧重于表达特性和原理,所以对于这两部分不作介绍。


Flint 介绍

什么是 Flint

首页介绍图

据官网所说, Flint 是一个前端的编译器,它连接了编辑器和浏览器,多种特性让开发 Web 应用更加高效、快捷。第一呢, Flint 目前还在马不停蹄地开发当中,官网、代码说变就变,说不定笔者发稿的时候,官网的描述又变动了;第二呢,据源码分析,编译只是 Flint 项目的一个部分,它其实包含了服务器、命令行、工作流等等多种东西,所以综合来说, Flint 是一个智能的前端开发环境。

Flint 使用

安装 Flint 很简单,可以通过npm安装:

npm install -g flint

安装完成之后,Flint 有三个命令很重要:

flint new
flint run / flint
flint build

分别是新建项目,运行项目和构建项目。

我们首先来尝试新建一个项目,并且进入项目运行它:

flint new hello
cd hello & flint

Flint运行图

可以见到项目已经跑起来了, Flint 为我们运行了一个本地的服务器,注意命令行的最下方, Flint 为我们设置了4个快捷键,现在键盘按 O 键,打开浏览器就可以看到一个 "Hello World" 呈现在眼前了。

浏览器helloworld

$EDITOR 配置

上文的4个快捷键中的 Editor 快捷键,是跟系统的 $EDITOR 环境变量挂钩的。所以如果你系统默认没有配置这个变量,上文那里会显示错误警告。这里不再赘述 $EDITOR 的配置,请参考相关资料。

插件配置

官方希望在 Atom 上完成整个 Flint 生态的闭环,建立编辑器与浏览器之间的连接,这个就需要编辑器的插件来配合。根据官方的 issue 来看,正在研发 Atom-Flint 插件,随后开始 Sublime&Vim 上插件的开发。所以,如果你希望更好的体验 Flint 带来的乐趣,下载一个 Atom 编辑器,安装上 Atom-Flint 这个插件。(不过此插件目前不太稳定,容易报 Bug )可以体验的一点是,如果你的代码出错,浏览器热更新的同时,你的编辑器也会定位到哪行出错,进行提示。

经过笔者的尝试,目前大部分特性都还没有实现,此文不深入介绍。

Atom-Flint

项目目录解析

Flint 出了新建项目一眼可以看到的 main.js 文件之外,还有一个核心的隐藏目录 .flint 。这个目录是整个Flint运行时的内容,结构如下图所示:

编译前

package.json 文件对应 node 的标准配置文件

flint.json 文件对应 Flint 自身的配置文件

index.html 文件对应项目的主页面

static 目录对应项目的静态资源文件目录

接着,让我们来了解一下 Flint 编译之后的情况:

flint build

编译后

编译之后的目录,与之前的相比,多了 build 目录,其中存放了编译之后产生的主页面(添加了相关资源文件的引用), _ 目录里面存放了运行时所需要的 Flint&React&App 源码,以及样式文件。


特性总览

路由

Flint 自带简单的路由功能,从而能够支持前端完成简单的 SPA 应用,这是非常赞的,不需要添加任何插件

NPM 包自动安装 & 本地 import

重新使用 run 命令运行项目,并打开 main.js 文件,在最上方加入:

import jquery from 'jQuery'

然后保存,观察命令行,提示你 jquery 安装成功。进入刚刚提到的 .flint 目录,发现多了 node_modules 目录,这个是 npm 包的安装目录,其中就有刚刚导入的 jQuery 包。这就是 Flint 一个非常令人兴奋的特性—— NPM 包自动安装。由于 Flint 是 ES6 语法,只要你按照规范,在 Flint run 时使用 import 引入你想要的远程包,它就会自动替你把想要引入的包下载到目录里面,这样你无感知的就可以使用了。

经过源码分析和测试, Flint 也同样支持 require 语法自动引入;也支持 import 本地的 npm 包。

超级热更新

Flint 另外一个令人兴奋的特性,就是它集成了热更新技术。相信如果经常使用 Webpack 的开发者,应该已经尝到了热更新技术的甜头。它能够使得在不刷新浏览器的情况下,更改本地的前端代码(组件),浏览器自动更新预览。 Flint 直接集成了这项技术,而且建立了专门的通道进行错误的实时反馈。当然,其实 Flint 做编辑器插件,不仅是建立报错的连接,更重要的是将社区最新的 "hot update on save" 推进到更新的 "hot update on type" ,能够真正做到敲代码的同时,项目及时编译反馈到浏览器预览,此特性仍待观察。

给力的样式

给力样式

Flint 使用 $ 符号作为选择器,不仅支持左图所示的静态样式,也支持右图的动态样式,支持简单的样式表达式。根据文档描述,静态的样式会在编译过程中,被抽取出来,直接转化为静态的 CSS ,加速性能。


深入分析

命令行

Flint 提供了三个主要的命令, new&run&build。分别是新建项目、运行项目和构建项目,正好是整个开发的一套流程。

对于 new 命令来说,支持 -u 参数,描述是 "start with a scaffold" ,使用这个参数,可以新建一些脚手架,快速形成项目结构。通过源码分析,是直接从 Github 上拉取的。

对于 run 命令来说,背后支持的是 Flint 的专门的一个 runner 项目,这是一个开发的运行时,主要进行了本地服务器的搭建、 Gulp 工作流的编译、包的安装与卸载、一个传送消息的 bridge 。

对于 build 命令来说,也支持一个 -w 的参数,也就是日常的 watch ,会对项目进行持续的构建。背后也是 runner 项目在支持,只不过这次会把 Flint.js 拷贝到项目目录中,从而可以脱离 runner ,在生产服务器上运行 compiler 之后的项目。

build命令

Gulp 编译流程

上文提到的 runner 的核心其实是一个 Gulp 编译流程:

源码1

通过 Babel 去转换源码,把自己的 transform 写成一个 Babel 的插件,去按自己的喜好编译项目:

源码2

可以看到 flintTransform 作为一个插件配置到了 babel 对象上。

PS:可以看到 Gulp 的那段源码的最后一句,一连串写了很多的 .pipe(pipefn()) ,可能是为以后更多的命令留空吧,亦或者是怕 Gulp 处理有问题?求不吝赐教。

NPM 包自动下载

关于包管理,内部是通过正则表达式匹配项目中的源码 .js 文件, 看看有没有满足的 import 或者 require 语句,然后将其添加到项目目录中的 package.json ,并且调用 npm install 进行安装, Webpack 对项目�打包;相反,检测到包变更,通过 npm uninstall 进行卸载。亲测如果擅自修改 package.json 或者增改 node_modules 的包,很容易就会出错。

require

Flint 内部对包的管理是分为外部引用和内部导出的。由于支持了 import 本地的包,所以专门有个 internal 机制针对本地 exports 出来的包进行管理。

exports

服务器

关于服务器,其实 Flint 是内部起了一个基于 Express 的服务器,外加 WebSocket 进行消息的通讯。


总结

Flint 带来的启示

本文大概探索了 Flint 整体的特性和一些原理分析,最令人兴奋的是作者团队无比大的脑洞,为我们带来了很多新的启示,比如连接浏览器与编辑器,比如超级热更新,比如 NPM 包的自动下载等等。目前手淘前端团队的工具链、组件库都已经能够胜任生产工作,基本能够覆盖整体链路。当然在关注更大更全的覆盖率的同时,我们也要静下来思考,开发体验流程上是否简单、智能化。 Flint 带来了一些新的思路,希望可以在未来将这些应用在我们日常的开发当中。

前端的变革

在这个双十一结束的点上,打算分享点东西,其实我一直乐意写些实在的技术点,因为不同环境里工程手段和团队发展很不一样。不过到这个双十一是我在阿里的第三个整年,我想把这些年里我们做的真正重要的事总结总结。

发布

在我来阿里之前,其实没太想过发布这个事情,在阿里的时候,也没想过发布方式是如此重要,不过最开始来的时候,听说前端跟服务端的配合方式吓了一跳:前端产出demo页面,服务端负责把参考html代码来写vm(就是velocity模板,阿里是用Java的),这个过程就是传说中的“套页面”了,这个过程往往不是那么愉快的,有时候服务端的同学会把标签嵌套搞错,前端出了bug,则需要服务端重新改模板。这个模式我一开始确实觉得不妥,但是并没有把它当做非常严重的事情而,直到后来回忆起来,我才知道当时的想法错的有多么离谱。

然而我运气特别好,在我自己没有做出正确判断的情况下,这个问题被服务端同学解决了——虽然我们当时脑子里想的都是“要搞webapp”。当时我们对WebApp的理解是单页应用,那么以静态页面+AJAX就是首选方案了,于是服务端同学帮我们搭建了一个叫做AWP的发布平台,和一个CDN源站,又驱动把h5.m.taobao.com这个域名(我知道你们要吐槽这个域名……历史啦历史原因)指向了CDN,这个事情对CDN来说也有划时代的意义,我们稍后再说。

从今天的视角来看,这个发布平台第一次让前端工程师有了向线上的发布能力,之前前端工程师基本都是发布资源文件和提供素材的,这个事情之后,前端工程师开始有真正意义上的“发布”,我之前跟团队同学开玩笑说,在这之前,前端连搞出线上故障的能力都没有——不要以为这是好事,没风险意味着没价值。

分离与同构

接下来一个很重要的事情,是手机淘宝目前的一个核心能力有关——API网关,通俗点说,就是所有的服务端提供API,而且通过统一的出口提供出来。这个便利条件,促成了一个前所未有的前端开发模式:前端开发纯静态页面,跟客户端调用同一套服务端API。

这个模式深度地解决了两个关键问题:

  • 前后端分离
  • 前端客户端同构(当然这里还不是彻底的同构)

当然这个方案有得的同时,也有舍弃:它舍弃了服务端渲染的能力。而对手机淘宝的业务和架构来说,这个做法是合时宜的。这时候的一些放弃,也为接下来也为后续的一些发展做了铺垫。

另一个对同构非常重要的,是手机淘宝的导航系统,这个事情我原以为也是比较无意中达成的,但是去年手淘Android架构师离职的时候跟我坦诚他暗中推动了很多,看来世上没有那么多偶然啊:)。总之从某个时期开始,我发现手淘的业务形态开始以页面为单位组织,所以顺道跟团队的人一起,推动了几件事:

  • 业务以页面为单位组织
  • 每个"页面",具有切换Web和Native的能力
  • 页面对应url(基础业务不论web还是native都对应taobao:协议,手淘打开web页面对应taobaovebview:协议)

这个形态让手淘变成了一个类似浏览器的东西,从业务的角度,让子业务在Native和Web之间切换更低成本,也让后来Weex等方案的诞生有了土壤。

向服务端延伸

其实说到全栈,大家一般的印象是高手、全能,感觉很遥远。但是其实并不一定是如此。我们试图用一种更亲和前端的方式去推动全栈化。考虑到阿里的服务端基本上涉及的量会比较大,需要一定的性能和容量意识,只能要求少数人去获得真正意义上的服务端开发能力。

所以我盯上了一样东西——CDN。CDN的回源服务,它的前面有CDN缓存挡着,相对来说需要承载的压力是比较低的,非常适合前端来维护。业务上,我则选择了运营业务,因为运营业务特点鲜明,页面生存周期短,短期内爆发性量大,服务端申请机器大张旗鼓搞集群也是一种浪费。

一旦脑洞开了,就会发现其实这个方向上能做的事情就变得多了起来,因为CDN+回源服务的模式在面对大流量时有极大优势,所以我们发现,非个性化的、非隐私的数据,其实都可以这么搞,最典型的场景是全局配置、运营推荐的导购内容等。到后来时,一些客户端的同学也开始用这个静态集群上面的服务。

我们的使用方式也让CDN从一个静态资源服务逐渐变成了一部分业务的载体,这种CDN的使用方式,也是不多见的,幸好阿里CDN团队是很喜欢面对新挑战的,甚至帮我们做了ABTest。

向客户端延伸

其实我特别想写这一段请直接参考勾三股四的文章……

Weex是原来WeApp的基础上发展起来的,中间经历两次大规模的重构,第一次重构,代码几乎全部重写,第二次,整个设计方案全变,从原来的服务端设计变成前端设计。

其实Weex主要的功臣和贡献者不止前端,负责Android和iOS实现的、以及服务端的同学都付出了巨大努力,而双十一会场的前端@mingelz 似乎也不明不白地被拽进坑里……

勾三股四已经写了四篇,技术上我就不重复了,其实我想说的是,我们从2013年就开始干这件事,这项目一开始坑到首批参与者几乎全部离职只剩下一个搞客户端的还在……光重写就两遍,一个玩具和一个真正能用的技术方案之间,差距就是如此。(所以拜托某些围观群众不要说我们造轮子了好么,不是所有外国东西都比**东西早的)

性能

我挺早就提出来过一个观点——一切不做profiling的性能优化都是耍流氓。

现在我觉得应该改改:一切没有线上监控的性能优化都是耍流氓。

性能这块网上的文章很多,看上去写的认真的也有很多,但仔细看其实有些错的挺离谱的。你不亲身经历一遍,没法体会到,真机测试跟你想象之间的区别,以及测试环境跟线上之间的区别。

我一直觉得性能最适合驱动的是前端,前端是距离用户最近的代码,所以前端应该做的,不仅仅是以前端的角度去解决问题,从一个url到一个页面的过程,涉及到WebView、网络层、DNS、运营商、服务端等各个环节,找出问题,分析原因,设计方案,推动解决,这是一个完整的性能优化过程。

我们比较幸运的是,我们有一群靠谱的合作伙伴,HTTPDNS、SPDY、ZCache等等技术方案,给我们带来了充足的弹药,但是再好的东西也需要人去用,前端在这件事上,必须明确自己owner的角色,对最终结果负责。

工具和库

这个事情我又要反省,我早先对node的判断是有问题的,我一开始把node看做“只是另一个服务端解决方案”,然而忽视了node在工具方面的价值和npm的价值。所以现在我们正在做亡羊补牢的是:工具链和基础库的npm化。

任何一个搞自己的库的团队总会被质疑在造轮子。我们的基础库从最开始的base.js,逐步发展到lib系列,到现在的npm化,自己搞的原因只有一个——我们更好。我们的基础库最好的地方就是坚持了单一职责的原则,没胡乱塞东西,这也让性能优化成为了可能。

工具链方面,我们则采取了全然不同的策略,几乎是全面贯彻了“拿来主义”,我们要做的,是把babel、sass、browserify、uglify、webpack、gulp等工具做整合,形成真正意义的工程体系,最近大家也在畅想,我们是否能能把构建、调试、发布等功能集成起来,做成一个ide?这应该是下一个双十一之后该讲的故事了。

结语

前端一直是一个变化很快的职能,它太年轻,年轻意味着可能性和机会,也意味着不成熟和痛苦。自加入手淘前端来,我经常担心的事情就是,我们团队每一天都在做一样的事,或者在不断跟随和尝试,最终一无所获。所幸至今回顾,每年还是总有点不同,也算给行业贡献了些经验值吧。

Font Boosting

最近在做一个手机端页面时,遇到了一个奇怪的问题:字体的显示大小,与在CSS中指定的大小不一致。大家可以查看这个Demo(记得打开Chrome DevTools)。

Font Boosting Test

就如上图所示,你可以发现,原本指定的字体大小是24px,但是最终计算出来的却是53px,看到这诡异的结果,我心中暗骂一句:这什么鬼!

随后开始对问题各种排查:某个标签引起的?某个CSS引起的?又或者是某句JS代码引起的。通过一坨坨的删代码,发现貌似都不是。我不禁又骂,到底什么鬼!不过中间还是发现了一些端倪:当页面中的标签数量或者文本数量大于某一个值,或者当CSS定义的字体大小落在某个区间时,这个问题才会被触发。而且字体变大后的值也随着原始定义的字体大小而改变。

然后自然就是各种搜索,终于有了新的发现。原来这个特性被称做「Text Autosizer」,又称「Font Boosting」、「Font Inflation」,是 Webkit 给移动端浏览器提供的一个特性:当我们在手机上浏览网页时,很可能因为原始页面宽度较大,在手机屏幕上缩小后就看不清其中的文字了。而 Font Boosting 特性在这时会自动将其中的文字字体变大,保证在即不需要左右滑动屏幕,也不需要双击放大屏幕内容的前提下,也可以让人们方便的阅读页面中的文本。

不过这个特性并不总是有必要的,还好在查到问题原因的同时,大家也讨论了对这个问题的一些处理方案:

  1. 手动指定 viewport width=320,这时 Font Boosting 不会被触发。(后边可以知道,这个说法不严谨,在其他设置均为默认值时,这一条才有效)
  2. Font Boosting 仅在未限定尺寸的文本流中有效,给元素指定宽高,就可以避免 Font Boosting 被触发。
  3. 显然第 2 条方案是有缺陷的,文本内容不可能都指定宽高。不过还好,我们通过指定 max-height , min-height, min-width, max-width(经 @ovaldi 指正,只有 max-height 有效) 也是可以的。比如 body * { max-height: 999999px; } 就可以无副作用的禁掉 Font Boosting 特性。当然,我觉得没必要使用通用选择器,用类似 p { max-height: 999999px; } 可能更好一些。

到这里,我们已经明白问题所在,并且也有解决方案了。但是有一个问题仍然困扰着我:当字体大于某一个值时(比如当不指定viewport width,手机屏幕width=320,字体大于等于82px时),这个 Font Boosting 就始终不会被触发。Chrome 是如何计算的,这其中的逻辑又是什么?

这一次问题解决起来就没有那么容易了,我先是各种搜索无果,然后自己人肉去试,慢慢找规律,但是发现变化不是线性的,看来这个公式还比较复杂。终于在今天被我发现了这篇文章:Chromium's Text Autosizer,彻底解释了我的疑问。

Font Boosting 具体的实现代码在 TextAutosizer.cpp 这个文件中可以看到,有兴趣的可以翻一下。

简单说来,Font Boosting 的计算规则伪代码如下:

multiplier = Math.max(1, deviceScaleAdjustment * textScalingSlider * systemFontScale * clusterWidth / screenWidth);
if (originFontSize < 16) {
    computedFontSize = originFontSize * multiplier;
}
else if (16 <= originFontSize <= (32 * multiplier - 16)) {
    computedFontSize = (originFontSize / 2) + (16 * multiplier - 8);
}
else if (originFontSize > (32 * multiplier - 16)) {
    computedFontSize = originFontSize;
}

其中变量名解释如下,更具体的说明可以参考上边的两个链接。

  • originFontSize: 原始字体大小
  • computedFontSize: 经过计算后的字体大小
  • multiplier: 换算系数,值由以下几个值计算得到
    • deviceScaleAdjustment: 当指定 viewport width=device-width 时此值为 1,否则值在 1.05 - 1.3 之间,有专门的计算规则
    • textScalingSlider: 浏览器中手动指定的缩放比例,默认为 1
    • systemFontScale: 系统字体大小,Android设备可以在「设备 - 显示 - 字体大小」处设置,默认为 1
    • clusterWidth: 应用 Font Boosting 特性字体所在元素的宽度(如何确定这个元素请参考上边两个链接)
    • screenWidth: 设备屏幕分辨率(DIPs, Density-Independent Pixels),如 iPhone 5 为 320

说了这么多,貌似只需要记住 p { max-height: 999999px; } 就OK了。。。-_-!!!

Update 2015-7-24:
@yisibl 姐姐说,用 max-height: 100% 可能会更好一些。

Ref.

  1. Webkit Bug 84186 Webkit Bugs 上记录的这个问题,最早从 2012 年 4 月份就开始讨论这个问题了,但好像都没有引起我们的任何关注。
  2. Chromium's Text Autosizer 关于 Font Boosting 最重要的一篇文章,更确切的说是论文。
  3. Font boosting in mobile browsers
  4. Font Boosting 一个俄国人用英文写的文章。
  5. 一堆 StackOverflow 上的问答,用 Font Boosting 搜可以出来一大坨,这里就不列了。

如何为用户省电

如何为用户省电

前言

21世纪的基本生理需求应该是电源和wifi了。有电有网,“基情四射”;没电没网,只能“左手右手”了。

随着HTML5以及CSS3技术支持与发展,以及手机越来越高性能带来的可行性。移动端的页面显然会越来越烧电。

用户一旦感觉到浏览这破页面手机电量流失很快,很大程度生会影响用户的浏览质量,继而影响转化率,甚至用户丢失。

试想一下,当你正在酣畅淋漓的浏览网页,才刚冲过点不久的手机,马上发现电量又见红了,并且可能伴随着手机过热,哦~雅蠛蝶! 只能忍痛关页面了。

我们要做的还有很多,怎样在小细节上让用户的手机在电量上更坚挺,提升用户浏览网页的快感,让性福与高潮更持久一点,就是今天的主题了

(盗图一张) ## 用什么省电

我们借助 Page Visibility API等,来提升用户体验
Page Visibility ,我们可以至少达到以下一种或几种的好处:

  • 节省服务器资源,Ajax 轮询这类服务器资源占用常常会被忽略,关闭这种请求可以节省资源。
  • 节省内存消耗。
  • 节省带宽消耗。

什么是Page Visibility

Page Visibility 是 HTMl5 推出的一个API, 会在浏览器的 document 对象上添加两个属性 hidden 和 visibilityState 。如果当前的标签被激活了,那么 document.hidden 的值为 false ,否则为 true 。visibilityState 则有4个可能值:

  • hidden:当浏览器最小化、切换标签、电脑锁屏时 visibilityState 值是 hidden
  • visible:当浏览器最顶级上下文(context)的 document 至少显示在一个屏幕当中时,返回 visible;当浏览器窗口没有最小化,但是浏览器被其他应用遮挡时,这时也为 visible
  • prerender:当文档被加载到屏幕画面以外或者不可见时返回 prerender,这个是非必要属性,浏览器可选择性的支持。
  • unloaded:当文档将要被离开 ( unload ) 时返回 unloaded,浏览器也可选择性的支持这个属性

参考 :Don't lose user and app state, use Page Visibility

我们做了什么

轮播图模式是用的非常多的一种交互模式,当顶部有一个轮播图,当用户在浏览商品列表的时候,轮播图消失在屏幕中;当用户点击某个商品进入详情页面,此时轮播图都应该停止轮播,只有当用户返回或者再次回到轮播图位置,轮播图才继续轮播效果。
我们针对 banner 轮播图组件,添加了这个特性的支持,做到了类似native的轮播位置记忆功能。做到 H5 和 Native 的体验更加一致,减少耗电量和资源占用。

  • 当浏览器最小化、切换标签、电脑锁屏时,将轮播图停止掉;打开时,开启;
  • 当轮播图在页面可视区域内,开启轮播,否则,将轮播图停止掉

怎么做

  • 监听 Page Visibility 事件,获取 visibilityState 的状态值,确定开启停止
  • 如果获取不到状态值,配合使用监听 pagehow 和 pagehide 到达同样目的
  • 手淘 webview 环境,通过 WindVane 提供的 Background 和 Active 事件达到同样的效果
  • 监听浏览器滚动,获取轮播图位置,查看是否在可视屏幕中

为用户带来了什么

手淘提供了特有的方法来获得监听事件,并且兼容性非常好

  • 减少了资源占用,节省了用户的电量消耗
  • 保持了轮播位置的记忆功能。
  • 提升了用户体验

Page Visibility的支持情况

Page Visibility 在手机平台的支持度在 50% 左右,这也就意味着,我们可以帮 50% 的用户提升用户体验
对于目前浏览器版本更新的速度,支持度只会越来越好,受益的用户也会越来越多。
当然,手淘有自己独立的支持方法,也就是说,手淘能更好更便捷的让用户的体验得到提升。

平台和浏览器支持情况

### 用户手机环境情况

以下数据为个人单独demo小范围测试 拎了几款代表性手机 浏览器版本为最新

附录:

魅族 note1 Android5.1

#### 华为T50 GEM-703L EMUI3.1

#### 三星Galaxy GT-I9100 Android4.0

#### 小米 MI-4c MIUI7.0.8.0 Android5.1.1

#### iPhone5S IOS9.1

#### iPhone6S IOS9.1

好东西

手机淘宝前端的图片相关工作流程梳理

注:本文摘自阿里内网的无线前端博客《无线前端的图片相关工作流程梳理》。其实是一个月前写的,鉴于团队在**第二届 CSS Conf 上做了《手机淘宝 CSS 实践启示录》的分享,而图片工作流程梳理是其中的一个子话题,故在此一并分享出来,希望仍可以给大家一些经验和启发。另外,考虑到这是一篇公开分享,原版内容有部分删节和调整,里面有一些经验和产出是和我们的工作环境相关的,不完全具有普遍性,还请见谅。

今天很荣幸的跟大家分享一件事情,就是经过差不多半年多的努力,尤其是最近 2 周的“突击扫尾”,无线前端团队又在工具流程方面有了一个不小的突破:我们暂且称其为“图片工作流”梳理。

图片!图片!图片!

要说最近 1 年里,无线前端开发的一线同学最“难搞”的几件事,图片处理绝对可以排在前三。

  • 首先,我们首先要从视觉稿 (绝大部分出自 photoshop) 里把图片合理的分解、测量、切割、导出——俗称“切图”
  • 然后,我们要把切好的图放入页面代码中,完成相关的本地调试
  • 第三步,把本地图片通过一个内部网站 (名叫 TPS) 上传到我们的图片 CDN 上,并复制图片的 CDN 地址,把本地调试用的相对路径替换掉
  • 第四步,不同的图片、不同的外部环境下 (比如 3g 还是 wifi),我们需要给图片不一样的尺寸、画质展现,并有一系列的配置需要遵循
  • 如果视觉稿有更改 (不要小看这件事,微观上还是很频繁的哦),不好意思,从第一步开始再重新走一遍……

这里面“难搞”在哪些地方呢?我们逐一分析一下:

  1. “切图”的效率并不高,而且每一步都很容易出现返工或再沟通
  2. 打开 TPS 网站上传图片放到前端开发流程中并不是一个连贯流畅的步骤,而且 GUI 相比于命令行工具的缺陷在于无法和其它工具更好的集成
  3. 替换 CDN 图片路径的工作机械而繁琐,并且代码中替换后的图片地址失去了原本的可读性,非常容易造成后期的维护困惑甚至混乱
  4. 适配工作异常繁杂和辛苦,也很容易漏掉其中的某个环节
  5. 视觉变更的成本高,web 的快速响应的特点在丧失

所以可能把这些东西画成一张图表的话:

团队的单点突破

在最近半年的一段时间里,无线前端团队先后发起了下面几项工作,从某个点上尝试解决这些问题:

lib.flexible

首先,我们和 UED 团队共同协商约定了一套 REM 方案 (后更名为 flexible 方案,进而演进为 lib.flexible 库),通过对视觉稿的产出格式的约定,从工作流程的源头把控质量,同时在技术上产出了配套的 lib.flexible 库,可以“抹平”不同设备屏幕的尺寸差异,同时对清晰度进行了智能判断。这部分工作前端的部分是 @wintercn 寒老师和 @terrykingcha 共同创建的。

视觉稿辅助工具普及

其次,我们于去年 12 月开始启动了一个“视觉稿工具效率提升”的开放课题,由团队的 @songsiqi 负责牵头,我们从课题的一开始就确立了 KPI 和 roadmap,经过一段时间的调研和落实,收罗了很多实用的辅助工具帮助我们提升效率,同时布道给了整个团队。比如 cuttermanparkerSize Marks

img-uploader

@hongru 去年主持完成的一系列 One-Request 前端工具集当中,有一个很有意义的名叫 or-uploadimg 的图片上传工具。它把 TPS 的图片上传服务命令化了。这给我们对图片上传工作批量化、集成化提供了一个非常重要的基础!这个工具同时也和淘宝网前端团队的另一个 TPS 图片上传工具有异曲同工之妙。大概用法是这样的,大家可以感受一下:

var uploader = require('@ali/or-uploadimg');

// 上传 glob 多张图
uploader('./**/*.jpg', function (list) {
    console.log(list)
});
// 上传多张
uploader(['./1.jpg', './3d-base.jpg'], function (list1, list2) {
    console.log(list1, list2);
})

// 上传单张
uploader('./3d-base.jpg', function (list1) {
    console.log(list1)
})

随后团队又出现了这一工具的 gulp 插件,可以对图片上传的工作流程做一个简单的集成,具体集成方式是分析网页的 html/css 代码,找到其中的相对图片地址并上传+替换 CDN URL。

var gulp = require('gulp');
var imgex = require('@ali/gulp-imgex');

gulp.task('imgex', function() {
    gulp.src(['./*.html'])
        .pipe(imgex())
        .pipe(gulp.dest('./'));

    gulp.src('./css/*.css')
        .pipe(imgex({
            base64Limit: 8000, // base64化的图片size上限,小于这个size会直接base64化,否则上传cdn
            uploadDest: 'tps' // 或者 `mt`
        }))
        .pipe(gulp.dest('./css'));
});

lib.img

lib.img 是团队 @chenerlang666 主持开发的一个基础库,它是一套图片自动处理优化方案。可以同时解决屏幕尺寸判断、清晰度判断、网络环境判断、域名收敛、尺寸后缀计算、画质后缀计算、锐化度后缀计算、懒加载等一系列图片和性能相关的问题。这个库的意义和实用性都非常之高,并且始终保持着快速的业务响应和迭代周期,也算是无线前端团队的一个明星作品,也报送了当年度的无线技术金码奖。

px2rem

px2rem 是 @颂奇 主持开发的另一个小工具,它因 lib.flexible 方案而生,因为我们统一采用 rem 单位来最终记录界面的尺寸,且对于个别1像素边框、文本字号来说,还有特殊的规则作为补充 (详见 lib.flexible 的文档)。

同样的,它也有 gulp / browser 的各种版本。

img4dpr

img4dpr 则是一个可以把 CSS 中的 CDN URL 自动转成 3 种 dpr 下不同的尺寸后缀。算是对 lib.img 的一个补充。如果你的图片不是产生在 <img> 标签或 JavaScript 中,而是写在了 CSS 文件里,那么即使是 lib.img 恐怕也无能为力,img4dpr 恰恰是在解决这个问题。

完事儿了吗?

看上去,团队为团队做了很多事情,每件事情都在单点上有所突破,解决了一定的问题。

但我们并没有为此停止思考

有一个很明显的改进空间在这里:今天我们的前端开发流程是一整套工程链路,每个环节之间都紧密相扣, 解决了单点的问题并不是终点,基于场景而不是功能点的思考方式,才能够把每个环节都流畅的串联起来,才能给前端开发者在业务支持的过程当中提供完美高效畅通无阻的体验——这是我们为之努力的更大的价值!也是我认为真正“临门一脚”的最终价值体现!

基于场景的思维方式

这种思维方式听上去很玄幻,其实想做到很简单,我们不要单个儿看某个工具好不好用,牛不牛掰,模拟真实工程场景,创建个新项目,从“切图”的第一步连续走到发布的最后一步,看看中间哪里断掉了?哪里衔接的不自然?哪里不完备?哪里重复设计了?哪里可以整合?通常这些问题都会变得一目了然。

首先,在 Photoshop 中“切图”本身的过程对于后续的开发流程来说是相对独立的,所以这里并没有做更多的融合 (从另外一个角度看,这里其实有潜在的改造空间,如何让“切图”的工作也能集成到前端工具链路中,这值得我们长期思考)

然后,从图片导出产生的那一刻起,它所经历的场景大概会是这么几种:

  • 放入 images 文件夹
    • to HTML
      • src: (upload time) -> set [src] -> webpack require -> hash filename (upload time) -> file-loader
      • data-src
        • (upload time) -> set [data-src] -> lib.img (auto resize)
    • to JavaScript: element.src, element.style.backgroundImage
      • (upload time) -> set [data-src] data
      • (upload time) -> set [src] (manually resize)
      • (upload time) -> set element.style.background -> lib.img (manually resize)
    • to CSS: background-image
      • (upload time) -> set background -> postcss (upload time) -> px2rem, img4dpr

其中 (upload time) 指的是我有机会在这个时机把图片上传到 CDN 并把代码里的图片地址替换掉;(* resize) 指的是我有机会在这个时机把图片的域名收敛/尺寸/画质/锐化度等需求处理掉。

经过这样一整理,我们很容易发现问题:

  1. 图片上传存在很多种可选的时机,并没有形成最佳实践
  2. 有些链路完全没有机会做必要的处理 (如 to HTML -> src 的链路无法优化图片地址)
  3. 有些链路处理图片的逻辑并不够智能 (比如需要手动确定优化图片选项的链路)
  4. 图片上传 CDN 之后必须手动替换掉源代码里的图片路径,这个问题在任何一个链路里都没有得到解决
  5. CSS 相关的小工具很多,比较零散,学习和使用的成本在逐步变高变复杂
  6. 没有统一完善的项目脚手架,大家创建新项目都需要初始化好多小工具的 gulp 配置 (当然有个土办法就是从就项目里 copy 一份 package.json 和一份 gulpfile.js)

基于场景的“查漏补缺”

在完善场景的“最后一公里”,我们做了如下的工作:

  1. 把所有的 CSS 工具集成到了 postcss,再通过 postcss 的 gulp 插件、webpack 插件、browserify 插件令其未来有机会灵活运用到多种场景而不需要做多种工具链的适配,即:postcss-px2rem、postcss-img4dpr,同时额外的,借此机会引入 postcss-autoprefixer,让团队拜托旧的 webkit 前缀,拥抱标准的写法
  2. 把图片上传的时机由最早的 or-imgex-gulp 在最后阶段分析网页的html/css代码上传替换其中的图片,变为在 images 目录下约定一个名为 _cdnurl.json 的文件,记录图片的 hash 值和线上 CDN 地址,并写了一个 @ali/gulp-img-uploader 的 gulp 插件,每次运行的时候会便利 images 文件夹中的图片,如果出现新的 hash 值,就自动上传到 CDN,并把相应生成的 CDN URL 写入 _cdnurl.json
  3. 同时,这个文件可以引入到页面的 JavaScript 环境中,引入到 img4dpr 工具中,引入到 lib.img 的逻辑中,让 HTML/CSS/JavaScript 的各种使用图片的场景都可以访问到 _cdnurl.json 中记录的本地图片路径和线上地址的对应关系
  4. 这也意味着 lib.img, img4dpr 需要做相应的改动,同时
  5. 页面本身要默认把 _cdnurl.json 的信息引入以做准备
  6. 创建一个 lib.cdnurl 的库,在图片未上传的情况下,返回本地路径,在已经上传的情况下,返回 CDN URL,这样通过这个库作支持,外加 lib.img、img4dpr,开发者可以做到在源代码中完全使用本地路径,源代码的可读性得到了最大程度的保证
  7. 基于 adam 创建一个包含全套工具链路的项目模板 (脚手架)

上述几件事我们于上周一做了统一讨论和分工,这里要感谢 @mingelz @songsiqi @chenerlang666 的共同努力!!

夹带私货 (偷笑)

我在这个过程中,融入了之前一段时间集中实践的 vue 和 webpack 的工程体系,在 vue 的基础上进行组件化开发,在 webpack 的基础上管理资源打包、集成和发布,最终合并在了最新的 just-vue 的 adam template 里面。

之前不是在文章的最后卖了个“最后一公里”的关子吗,这里介绍的图片工作流改进就是其中的一部分:)

同时,我基于 lib.img 的思路,结合 vue.js 自身的特点,写了一个 v-src 的 directive,在做到 lib.img 里 [data-src] 相同目的的同时,更好的融入了 vue.js 的体系,同时加入了更高集成度的功能,稍后会再介绍。

夹带了私货之后是不是我就没法用了?

最后我想强调的是,除了自己的这些“私货”之外,上面提到的几个改进点和这些个人的内容是完全解耦的,如果你不选择 vue.js 或 webpack 而是别的同类型工具或自己研发的一套工具,它依然可以灵活的融入你的工作流程中。

最终效果

我们在团队内部把这些工作流程以脚手架的方式进行了沉淀,并放在了团队内部叫做 adam 的 generator 平台上 (后续会有介绍) 取名叫做 just-vue (时间仓促,adam 和相关的 generator 未来会在适当的时机开放出来)。大致用法:

安装 adam 和 just-vue 模板:

tnpm install -g @ali/adam
adam tmpl add <just-vue git repo>

交互式初始化新项目:

$ adam

? Choose a template: just-vue
? Project Name: y
? Git User or Project Author: ...
? Your email address: ...
Awesome! Your project is created!
 |--.gitignore
 |--components
 |--|--foo.vue
 |--gulpfile.js
 |--images
 |--|--_cdnurl.json
 |--|--logo.png
 |--|--one.png
 |--|--taobao.jpg
 |--lib
 |--|--lib-cdnurl.js
 |--|--lib-img.js
 |--|--vue-src.js
 |--package.json
 |--README.md
 |--src
 |--|--main.html
 |--|--main.js
 |--|--main.vue

目录结构剖析

然后大家会看到项目目录里默认就有:

  • gulpfile.js,里面默认写好了图片批量上传并更新 _cdnurl.json、webpack 打包、htmlone 合并 等常见任务
  • images 目录,里面放好了关键的 _cdnurl.json,还有几张图片作为示例,它们的 hash 和 CDN URL 已经写好了
  • src/main.*,主页面入口,包括一个 htmlone 文件 (main.html),一个 webpack 文件 (main.js) 和一个 vue 主文件 (main.vue),默认引入了需要的所有样式和脚本,比如 lib.img, lib.flexible, lib.cdnurl, _cdnurl.json, v-src.js 等,我们将来主要的代码都会从 main.vue 写起——额外的,我们为 MT 模板开发者贴心的引入了默认的 mock 数据的 <script data-mt-variable="data"> 标签,不需要 MT 模板开发环境的将其删掉即可
  • components 目录,这里会把我们拆分下来的子组件都放在这里,我们示范性的放了一个 foo.vue 的组件在里面,并默认引入了 lib.cdnurl 库
  • lib 这里默认放入了 lib.img, lib.cdnurl, v-src.js 几个库,这几个库在未来逐步稳定之后都会通过 tnpm + CommonJS 的方式进行管理,目前团队 tnpm + CommonJS 的组件整合还需要一定时间,这里是个方便调整迭代的临时状态。

然后,我们来看一看 main.vue 里的细节,这才是真正让你真切感受到未来开发体验的地方。

图片工作场景

首先,新产生任何图片,尽管丢到 images 目录,别忘了起个好理解的文件名

CSS 中的图片

然后,在 main.vue 的第 11 行看到了一个 CSS 的 background-image 的场景,我们只是把 url(../images/taobao.jpg) 设为其背景图片:

background-image: url(../images/taobao.jpg);

完成了!就这样!你在发布之前不需要再关注额外的事情了。没有手动上传图片、没有另外的GUI、没有重命名、没有 CDN 地址替换、没有图片地址优化、没有不可读的代码

HTML 中的图片

我们再来看看 HTML 里的图片,来到 39 行:

<img id="test-img" v-src="../images/one.png" size="cover">

一个 [v-src] 特性搞定!就这样!你在发布之前不需要再关注额外的事情了 (这里 [size] 特性提供了更多的图片地址优化策略,篇幅有限,大家感兴趣可以移步到 lib/vue-src.js 看其中的实现原理)。

JavaScript 中的图片

最后再看看在 JavaScript 里使用图片,来到 68 行:

this.$el.style.backgroundImage = 'url(' + cdn('../images/logo.png') + ')'

只加入了一步 cdn(...) 的图片生成,也搞定了!就这样!你在发布之前不需要再关注额外的事情了。

发布

那有人可能会怀疑: “那你都说发布之前很方便,发布的时候会不会太麻烦啊?”

好问题,发布就两行命令:

# 图片增量上传、webpack 打包、htmlone 合并,最终生成在 dist 目录
gulp
# 交互式上传到 awp
awp

正常的命令行反应是类似这样的:

$ gulp
[04:46:48] Using gulpfile ~/Sites/alibaba/samples/y/gulpfile.js
[04:46:48] Starting 'images'...
uploaded ../images/logo.png e1ea82cb1c39656b925012efe60f22ea http://gw.alicdn.com/tfscom/TB1SDNqIFXXXXaTaXXX7WcCNVXX-400-400.png
uploaded ../images/one.png 64eb2181ebb96809c7202a162b9289fb http://gw.alicdn.com/tfscom/TB1G7JHIFXXXXbTXpXX_g.pNVXX-400-300.png
uploaded ../images/taobao.jpg 4771bae84dfc0e57f841147b86844363 http://gw.alicdn.com/tfscom/TB1f2xSIFXXXXa1XXXXuLfz_XXX-1125-422.jpg
[04:46:48] Finished 'images' after 46 ms
[04:46:48] Starting 'bundle'...
[04:46:49] Version: webpack 1.10.1
      Asset     Size  Chunks             Chunk Names
    main.js  17.1 kB       0  [emitted]  main
main.js.map  23.5 kB       0  [emitted]  main
[04:46:49] Finished 'bundle' after 1.28 s
[04:46:49] Starting 'build'...
"htmlone_temp/cdn_combo_1.css" downloaded!
"htmlone_temp/cdn_combo_0.js" downloaded!
[04:46:57] >> All html done!
[04:46:57] Finished 'build' after 8.07 s
[04:46:57] Starting 'default'...
done
[04:46:57] Finished 'default' after 130 μs
$ awp (交互式过程略)

你甚至可以写成一行:

gulp && awp

最终这个初始化工程的示例页面的效果如下

设计变更了?

这条链路是我们之前最不愿意面对的,今天,我们来看看这条链路变成了什么,假设有一张设计图要换:

  1. 在 Photoshop 里把图重新切下来
  2. 同名图片文件放入 images 文件夹
  3. 运行 gulp && awp

就这样!

额外的,如果尺寸有变化,就加一步:更改相应的 CSS 尺寸代码

总结

在整个团队架构的过程中,大家都在不断尝试,如何以更贴近开发者真实场景的方式,还原真实的问题,找出切实有效的解决方案,而不仅仅是单个功能或特性。这样我们往往会找到问题的关键,用最精细有效的方式把工作的价值最大化。其实“基于场景的思维方式”不只是流程设计的专利,我们业务上的产品设计、交互设计更需要这样的思维。我个人也正是受到了一些产品经理朋友们的思维方式的影响,把这种方式运用在了我自己的工作内容当中。希望我们产出的这套方案能够给大家创造一些价值,更是向大家传递我们的心得体会,希望这样的思维方式和做事方式可以有更多更广的用武之地。

双十一敲钟项目总结

项目背景介绍

4号得到消息要做一个紧急项目,双十一当晚10点30分,北京水立方,美国纽约证券交易所为“2015天猫双十一全球狂欢节”举行远程开市敲钟仪式,见证这场全球商业的狂欢。到时马大大会和8位曾获得阿里公益“天天正能量”奖的人物代表举行远程开市敲钟仪式,这是纽交所首次为一家**的互联网企业举行远程敲钟仪式。我负责在手淘上开发一个敲钟功能,让手淘的一亿多用户能够在手机上和马总一块敲响开市钟。
screenshot

项目特点

快,时间紧迫,需要在短时间内完成需求以及应对各种变更
变,这种大老板拍的需求充满变数,因为pd觉得好还不行,还得大老板拍板。但是大老板们又很忙,没办法及时拍板。所以只能先按照PD的意愿做出来。心理和代码上都得做好快速应对可大可小变数的准备。
质,代码质量要求够高,减少bug量,缩短测试时间

项目开发过程

页面是放在poplayer里面展示的,什么是poplayer呢,它就是手淘在native界面上覆盖了一个新的webview容器,然后这个webview是透明的,透过这个webview还能看到底下的页面。这样需要在native页面上增加一些内容时就不用发版,更灵活。开发过程和普通手机浏览器里的页面是一样的,只需要在适当的时间调用手淘开放的方法打开和关闭poplayer。更多关于poplayer的介绍详见:#18

由于时间不够充裕,没办法等视觉稿出来再开发,所以前端和视觉同时开工。这个项目使用的是reactjs开发,react通过数据来控制DOM的特性,js里没有了对DOM的操作,让我们可以在脱离视觉稿的情况下进行页面逻辑部分的编码。
大致的开发过程如下。首先,根据需求内容将页面拆分成6个状态

/*
    pageStatus:
        SmallPreTip: 等待开始的悬浮icon
        SmallStartTip: 开始抽奖的悬浮icon
        Error: 错误页
        CountdownWait: 显示倒计时等待页面
        RefreshWait: 显示刷新等待页面
        WaitAward: 显示抽奖页面
        AwardSuccess: 中奖了
        AwardFail: 没中奖
*/

每个状态对应于一个component,每个component里面通过props特性来和父级元素通信。
screenshot

最终通过修改数据来控制页面的内容

         switch(pageStatus) {
                case 'SmallPreTip':
                    content = (
                        <SmallTip start="false"></SmallTip>
                    )
                    break;
                case 'SmallStartTip':
                    content = (
                        <SmallTip start="true"></SmallTip>
                    )
                    break;
                case 'Error':
                    content = (
                        <ErrorCom></ErrorCom>
                    )
                    break;
                case 'CountdownWait':
                    content = (
                        <PopPreTip countdown="true" leftTime={this.state.leftTime}></PopPreTip>
                    )
                    break;
                case 'RefreshWait':
                    content = (
                        <PopPreTip refresh="true"></PopPreTip>
                    )
                    break;
                case 'WaitAward':
                    content = (
                        <WaitAward uid={this.state.uid}></WaitAward>
                    )
                    break;
                case 'AwardSuccess':
                    content = (
                        <AwardSuccess></AwardSuccess>
                    )
                    break;
                case 'AwardFail':
                    content = (
                        <AwardFail></AwardFail>
                    )
                    break;
                default:
                    content = null;
                    break;
            }

搭建好大的展示模型后,就是接口调用和结果处理了。在这个项目里,我将所有api调用放在一个js文件里集中处理。

param.doSee = {
    api: 'mtop.taobao.aplatform.get',
    v: '1.0',
    ecode: 1,
    data: {
        bizType: 'zhong.go',
     }
};

function mtopError(ret,errorCallback) {
    errorCallback && errorCallback(ret);
}

var api = {};
for (let i in param) {
    api[i] = function(){
        var p = new Promise(function(resolve,reject) {
            mtop.request(param[i],function(ret){
                resolve && resolve(ret);
            },function(ret){
                mtopError(ret,reject)
            })
        })
        return p;
    }
}

也就是说,如果我需要调用新的api,只需在param里新增一项内容,无需再去重复编码mtop的调用部分,而且对于mtop的异常返回可以做一些共用的处理。
在需要调用api的地方,也不必太关注api的入参部分,只需处理返回结果就行。
mtop 是手淘这边封装好的调用接口的组件库,可以理解成一个普通的jsonp请求。

api.getPageStatus().then((ret) => _getPageStatusSuccess(ret.data,params),(ret) => _getPageStatusFail(ret,params));

最后根据接口的返回,根据需要控制pageStatus的值就可以。
将页面逻辑写完,待视觉稿出来,只需根据视觉稿逐一完成各子组件里的样式部分、动画部分和交互部分。

这种开发模式的优点除了可以和设计同行外,最主要的还是页面层级结果清晰,出问题是能够快速定位。这个项目,最终的开发时间是2.5d,6号开始开发,8号下午就提测了。视觉定稿是在开发的最后一天出来的,当然不可避免之后又修改了一版。
测试适配阶段几乎0bug,除了一些图片加载不出来我只能wontfix的bug,后期图片都是会使用手淘的zcache功能提前缓存到本地,可以避免这个问题。刚开始PM还担心适配会出现很多问题,害怕时间不够。但是自从使用了lib-flexible,基本适配无压力。lib-flexible的适配分成两种方案,dpr(根据设备的devicePixelRatio来设置不同的size,单位是px)和rem(根据屏幕宽度定义1rem=x px,然后单位用rem),基本弄清楚什么时候该用哪个就没问题了。坚持三个原则:

  • 字号dpr,其他用rem
  • 容器里如果有文字并且不能折行,容器相关的设置也应当用dpr
  • 如果是那种文字必须在一行展示完,但是字又比较多的情况,则字也用rem。当然这种情况必须提前跟视觉沟通好,不建议自己单独使用,否则后面容易返工。

关于flexible的更多介绍,可以看一下大漠爷爷的这篇文章#17

总的来说,这个距离双十一不到一周的时间里提出来的项目还是挺成功的,10号就完成上线,距离11号开放入口还有一天的buffer,上线后也没有出现任何问题,直至下线。当然项目本身比较简单,然后PD的想法和大老板想法比较吻合,后期没什么变更。

高效的开发离不开工具,推荐使用以下几个工具:

  • gulp-px3rem自动换算css里px到rem
  • autoprefixer 自动给高级css3属性带上各种前缀,减少适配问题
  • webpack&babel 转换jsx和es6语法,麻麻再也不用担心我用react和es6了

vue+webpack在“双十一”导购产品的技术实践

双十一中,无线前端的产品可以说非常的丰富。在双十一中,互动始终是重头的一部分,但是与以往不一样的地方是,导购产品在本次双十一中有着不俗的表现。而今年的双11导购业务占据了5大模块里的后三个,除了必抢,其它业务均是由手淘的同学来完成的,笔者作为导购产品的一员,选择导购产品来给大家解读其中的技术实践。

本次双十一的导购产品都有哪些?

看到这些截图,相信很多人都很熟悉,不管是双十一晚会摇一摇摇出的“清单”,还是大家抢完红包迫不及待点开的“我的双十一”,又或者是点开“我的双十一”标签进入的人群会场寻找与自身匹配的商品,这些都是本次双十一的导购产品。

这么多的导购产品,本次双十一导购产品的技术体系又是什么样的,在双十一中,无线导购产品使用了什么技术?这一定是大伙关心的问题,下面将展开介绍双十一无线导购产品的技术实践。

技术选型的思考

对于双十一的场景,我们希望能够选择一个足够适应大型应用的的技术方案,能够支撑起双十一这样的大场景,整体思考上有:

  • 适合大型应用的MVVM的框架,去掉复杂的DOM操作,让代码变得易于review和维护
  • 模块化产品,方便随时增减一个模块
  • 方便与团队现有的库,工具进行整合

基本技术方案——vue+webpack

  • vue.js——轻量级的MVVM框架
  • webpack——前端模块化解决方案

有同学会问,前端社区可选用的工具和框架这么多,为什么挑选了vue+webpack在双十一的项目中进行实践,那么我们来盘点下选用上的思考:

  • vuejs——轻量、学习成本低、双向绑定、无dom的操作、组件的形式编写

    vuejs是个轻量级的mvvm框架,集合了angular的基本功能,却又比angular更为精简,功能上涵盖了双向绑定、指令、逻辑控制、过滤器、事件监听、函数等。框架的特点使得项目在状态变更、分页的场景下可以拥有很大的便利——所有的操作只需要变更数组,没有任何的dom操作。

  • webpack——CommonJS的引用和编写方式、loader非常的丰富,包括vue-loader、css-loader、less-loader

    webpack是前端组件化的解决方案,webpack提供了核心的CommonJS引用方案去引用资源,如下:

    //app.js
    module.exports = {
        title: 'xxxx'
    }
    
    //entry.js
    
    var app = require('./app.js');
    console.log(app.title);
    
    

    但是,webpack与以往的CommonJS引用思路不太一样,webpack允许任何的静态资源作为模块进行引用,包括css、less、html...等等,那么我们需要做的仅仅是加载必要的loader(加载器),如css-loader、less-loader、style-loader等等,那么webpack与vue结合起来,我们可以通过vue-loader,这样我们编写的方式就变成如下:

    <style>
        /*样式编写*/
        #compot{
            width: 100px;
        }
    </style>
    <template>
        <!--模板-->
        <div id="compt"></div>
    </template>
    <script>
        //模块编写
        module.exports = {
    
        }
    </script>
    

    最后保存为*.vue文件,入口文件的初始化只需要:

    var opts = require('*.vue');//引用*.vue文件
    var main = new Vue(opts);//实例化
    main.$mount('#app');//渲染
    

    这么一来,使用vue+webpack,我们实现了:

    更多的实践demo笔者在这里就不展开了,有兴趣的小伙伴可以阅读下无线前端@勾股 的一篇博文——just-vue

工具链路在实际开发上的整合

当然,仅仅使用vue+webpack,已经可以以一种高效的方式进行开发了,但是为了和无线团队的“私货”结合起来,我们的开发链路还更完善些,不过是不是有“私货”就不能使用了,不是的,事实上,“私货”相对独立,开发时也可以完全剥离出来,开发过程可以自行选择。

本次导购产品中,使用vue+webpack的过程中,将其结合到了gulp中,同时引用gulp-htmlone、autoprefixer-core,主要解决以下几个问题:

  • 1、读取webpack配置,运行webpack的loader
  • 2、自动补全css3的前缀
  • 3、打包、下载、压缩js代码,最后将所有文件打包成一个*.html文件

图片处理?

上述的介绍中,似乎涵盖了html、css、js,但是始终没有提到图片上面的处理,作为前端页面重要的一环,图片怎么可能在无线前端中被忽略,笔者下面展开介绍图片处理上,我们团队的做法

首先,先说说痛点,下图是不是大多数情况下图片的工作流程

那么,在面对双十一那么大的页面量时,如果以这样的方式去工作,那一定会崩溃的...因此在双十一前,团队已经整理出了一套完整的图片工作方案,因此在双十一才能以一个全新的面貌展现给大家,下面笔者给大家进行介绍:

  • 1、切图、测量(开发阶段)

    切图和测量的过程,选用的工具依旧是ps或者sketch,借助ps cc2014版的切图插件Cutterman和标注工具Parker进行工作;而sketch就不需要提了,本身自带的强大标注和导出功能已经让工作非常便利了

  • 2、上传和替换地址(打包阶段)

    以往的图片上传和替换地址的工作都为人工所为,如今无线提供一套图片工具解决方案,通过监听图片目录里图片文件的更换来执行上传命令,同时还支持将小图片转成base64,最后在替换地址上,根据不同的屏幕分辨率选择不同的图片尺寸,总结下来这套工具做了下面几件事:

    • 监听图片文件,执行上传图片
    • 小图转换base64
    • 替换掉代码中的相对路径,包括img[src]、background、element.style.background
    • css代码中添加根据屏幕分辨率选择图片的逻辑

  • 3、图片自动优化(代码运行阶段)

    在这一层面做的图片优化,是在代码执行过程,也就是页面渲染过程。这个过程基于无线前端的组件lib.img,可以实现根据尺寸、弱网判断、屏幕分辨率进行图片的处理,同时提供懒加载的功能,归纳起来有:

    • 根据尺寸选择图片
    • 弱网判断进行选择
    • 设备分辨率进行选择
    • 根据图片质量要求进行选择

  • 汇总起来

前期脚手架搭好后,最后我们的图片工作流程可能就只需要做下面几个工作,也就是上述"开发阶段"需要做的事情:

  • 1、切图、测量,将图片放入项目的images文件夹
  • 2、代码中使用相对路径

OMG!就这么简单?是的!就是这么简单!

“双十一”做的更多的事

不得不说,面对双十一这么大量级的一个场景,无线前端对产品质量层面做了很大的把关,当然少不了的是前端界经久不衰的话题——性能优化,那么题主给大家盘点下导购小组做的性能优化上的事。

性能优化,性能优化主要集中在两个层面上的优化

  • 1、网络加载,可以从资源和图片上展开,主要有
    • 资源打包压缩成一个html文件
    • html文件作为离线包加载到手淘包中,因此整个html文件是不存在网络请求loading的
    • 图片使用工具进行尺寸控制,在css代码中添加不同屏幕下采用不同尺寸的图片的逻辑
    • 图片组件lib.img对图片进行精细化控制,根据网络环境、高清屏幕、屏幕尺寸选择不同的图片,将大图控制在30kb左右
    • 小图采用base64加载
    • 首屏渲染接口控制为一个
  • 2、内存优化,主要在代码执行和图片大小两个方面考虑
    • 图片大小控制在30kb左右,避免长列表加载过多图片时产生内存过高的问题
    • 采用高性能统计方案,性能更优
    • css、js代码书写尽量避免性能高耗写法

前端打底数据

  • 由于双十一场景特殊,尽管咱们的后端和算法同学做了大量的优化、数据打底,但是作为页面的负责,还是需要考虑到接口无法访问的情况,那么双十一期间根据接口对数据进行了打底,具体逻辑如下图:

    不得不提的是,ods是无线基于静态服务器的系统,本身不带业务逻辑,仅仅提供了三个功能:1、定时获取接口数据;2、将数据静态存储起来;3、提供jsonp的方式供前端调用

最后总结

以上便是无线前端导购小组在本次双十一的工作流程,当初考虑使用vue+webpack,大家也在想新的技术是不是能够经得住考验,事实证明,经历了双十一这一个大的熔炉下锻造,这样的技术实践已经完全成熟了。最后,以一张大图结束,总结一下无线导购组vue+webpack在开发链路上的实践

回头看看我们的技术目标,我们实现了:

  • vuejs——mvvm构建应用,完全无dom操作,状态管理变得方便
  • webpack——模块化编写,单文件组件编写
  • 图片工具——与现有的工具和组件进行整合,完美打通图片工作链路

对无线电商动态化方案的思考(一)

如题,接下里的一段时间里,我会从无线电商动态化的角度谈一谈自己的理解和思考

无线电商动态化

无线

首先要谈的是无线,或者说移动。手机和传统桌面端计算机有着非常大的不同,首先是随时随地,这样它就需要同时也可以拥有更多的传感器,比如镜头、体感、定位、蓝牙、NFC 等等,当然也包括多点触摸屏,人们的操作和交互方式也发生了很大的变化;其次它的屏幕尺寸和电脑完全不同,也由此产生了完全不同的阅读体验和阅读方式;再有就是能耗和稳定性变得非常非常关键,如何流畅的展示复杂界面和大数据,如何有效控制由于资源问题而导致的 crash,如何帮用户省电,如何长时间使用还不会发烫,都变成了很重要的课题。总而言之,一个输入,一个输出,一个性能。

还有一个潜在的问题,就是无线端很多形态大家都还在一起摸索之中,这其中有很高的“试错”成本,我们的产品经理甚至会在项目初期就主动坦承我们需要很多试错,需要快速迭代。所以在无线端探索性质的东西非常多,远远多过桌面端。

电商

电商这个话题其实挺大的,我只说一些从个人技术角度看到的电商:就是 1 什么地方都得能随时改,2 随时改

从电商平台的角度,我们无时无刻需要为用户挑选和推荐更好更适合的选购信息,比如有新品上架了,有商品降价了,有促销活动了……它作为平台一定是自由的,信息是充分流通的,所以也必须是非常灵活实时的。

再有就是电商是和真金白银打交道的,这要求我们对技术方案的稳定性和准确性做非常严格的把控,对项目工程化有极高的要求,项目过程中的半点松懈都有可能对用户产生极大的影响。

动态化

今天的移动端,大家基本都是通过各式各样的 app 来完成自己的事情,而这些 app 都是 native 的,有必要的版本控制和发布节奏。浏览器的位置相对没有那么明显,更多的充当了一个“快速降级”的角色,即如果我们没有安装 X (类) 应用,那么当我需要使用 X 的时候,就用浏览器打开一个 X 的网站。

既然大家的主战场都是 native app 了,那基于上面我们对于无线和电商这两个关键词的分析和理解,如何在充分合理运用设备的输入输出、并且能够快速迭代、实时调整、还能把控性能和各方面风险,就变成了重中之重。

比如每年双十一当天,整个天猫和淘宝的各条产品线都处在高度“敏感”的状态,活动当天出现的任何一个状况都可能产生非常重大的影响,需要我们可以灵活调整;在平时卖家和买家产生的任何信息和数据,都希望可以最通畅的发布出来。尤其是卖家,如何“装修”好自己的店铺,“打扮”好自己的宝贝信息,“张罗”好自家的活动,都需要非常强的动态性做支撑。

移动端的 HTML5

先反观今天的桌面端,浏览器已经是绝对的主角了,我个人身为一个前端工程师算是亲身经历了浏览器从默默无闻到变成桌面端第一使用时长的全过程。而今天的手机则是 native app 的天下。我觉得原因有很多,比如体验、能耗、生态、入口等等,同时也是因为手机上的浏览器需要顺应很多颠覆式的变化并且在多个厂商之间寻求统一。

HTML5 其实在移动端一如既往的保有着它的跨平台和快速实现的优良特点,随着手机操作系统的不断升级,越来越多的 HTML5 新特性有机会在实际产品中运用起来。但也一直有很多局限性。

比如提供的功能有限——这个问题目前很多地方都在通过 Hybrid 方案来解决,把底层的能力或业务上的特殊需求封装成 JavaScript API 暴露给 webview,从而让 Web 页面可以通过 JavaScript 调到这些特性。相信任何团队在这条路上经过一段时间,都已经把问题解决得差不多了。

第二个问题是性能问题,今天在移动端,设备的性能,不论是从 CPU 还是内存还是电池都不能和桌面端同日而语,简单的界面用起来是没感觉的,一旦交互复杂或数据量达到一定程度,webview 的弊端还是比较明显的。

上述两点都是比较绝对的,而由于性能和能力缺失而引发的上层技术方案的倾斜则会产生更多深远的影响。比如二维码扫描与识别、省市区地理位置选择、包括一些复杂的动画设计,这些问题都不是“前端能不能做”的问题,而是做出来效果好不好的问题,或“卡不卡”的问题。

在桌面端,浏览器之所以能一统江湖,其实不只要归功于 W3C 和 HTML5,我们的浏览器还有一位“好朋友”:那就是 Adobe Flash Player Plugin,它虽然不是标准和开放的,但一直有着广泛的用户,也对浏览器的能力做了大范围的扩展,而性能也是 native 级别的,这恰恰解决了我们上面提到的几个问题——后期 Adobe 还提供过 AIR 平台,可以完全甩开 Web 开发桌面应用。而现在的移动端就没有这么好的“福利”了,迟迟没有人站出来充当“移动端的 flash”的角色。

看上去还不错的解决方案

我所在的团队一直在不断思考和探索无线电商动态化这个问题。在这个过程中,我们看到了很多不错的方案,也做了很多自己的尝试

  • 传统 Hybrid:基于嵌入 native app 的 webview,通过对 API 的扩展达到解决问题的目的,这个已经流行好一阵子了,无需多提
  • 为 Hybrid 引入 native 界面:我个人最早是在去年 QCon 北京听 集鹄 (大叔) 的分享,当时只有安卓版本,大概做法是以类似 plugin 的方式把一块 native 界面引入到 webview 当中,这样在整体还是 webview 的情况下,可以局部达到特殊的 native 效果。随后在今年的 Velocity 北京,也看到了一个类似的方案,这次 native 界面是“盖在 webview 上”的,并随着 webview 滚动条做相应滚动,以保持界面的一致效果。
  • WeApp:这是我们无线事业部2年前开启的一个项目,设计一套 JSON 格式,可以描述界面的结构和样式,然后各端实现对这套 JSON 格式的解析和渲染。这样我们只要每次请求不一样的 JSON 数据,就可以展示出不一样的界面效果,和之前的 Hybrid 方案不同的是,在 native app 里我们的展示效果是 100% native 的,并且我们还做了 HTML5 版本的“降级”渲染方案,用户在浏览器里同样有机会借助我们的渲染方案把整个界面展示出来。
  • React Native:这位老兄毫无疑问是今年全球最火热的移动技术方案之一,界面是 100% 的 native,彻底颠覆了移动应用的技术栈和开发体验。但最重要的是,它在运行时是用 JavaScript 进行控制的。这件事为我们打开了一个巨大的脑洞:一旦我可以在线发布和加载 JavaScript 文件,那也就意味着我具备了动态性的能力!于是乎出现了通过动态发布 React Native 代码达到动态性目的的技术方案。

最终

我们经过一系列的调研、思考和讨论之后,提出了一套完全针对无线电商动态化的技术方案。它同样:

  • 致力于移动端
  • 能够充分调度 native 的能力
  • 能够充分解决或回避性能瓶颈
  • 能够灵活扩展
  • 能够多端统一
  • 能够优雅“降级”到 HTML5
  • 能够保持较低的开发成本
  • 能够快速迭代
  • 能够轻量实时发布
  • 能够融入现有的 native 技术体系
  • 能够工程化管理和监控

我们觉得它并不是那种第一眼看上去很伟大的东西,但是它能够帮我们解决几乎所有实际业务中遇到的问题,是个非常务实的家伙。

我们甚至觉得这个方案里面没有什么高深的东西,没有颠覆式的新科技,但我们非常针对性的关注细节,并且在实际业务当中产生的效果是绝对颠覆式的。

更令人振奋的是,这套技术方案在今年的双十一全球狂欢盛典中经受住了考验,我们成功的支持了主会场的前期研发和当天的发布即实时调整。这也让我们更有勇气和信心面对接下来的更多挑战!

我给它起了一个酷酷的新名字:Weex

weex-512

关于 Weex 我们有太多收获想分享,今天先开个头,谈一谈自己对无线电商动态化的浅见。

继续阅读:

stylus 字体 Vue项目px转rem

1、新手基础薄弱 文档字体大小根据像素比sess写法能转下stylus语法写下吗
2、vue项目有快捷方法换算px=>rem

对无线电商动态化方案的思考(三)

之前两篇我们分别谈到了对无线电商动态化的理解,以及我们自己的技术方案:Weex,今天我们分享一些其中的技术细节

data-binding

首先要介绍一下我们是怎么做到数据绑定的,首先,我们对上层撰写的代码遵循了传统的 mustache 书写习惯。即:

<template>
  <text style="color: {{mainColor}}">{{title}}</text>
</template>
<script>
  module.exports = {
    mainColor: '#FF9900',
    title: 'Hello Weex!'
  }
</script>

我们的 transformer 对这样的语法的解析思路非常简单直接,即:

  • 不含 mustache 语法的值,直接做为最终返回值
  • 含 mustache 语法的值 (如 {{xxx}}),转换为:function () {return xxx}

所以上面的模板最终会被转换成:

{
  type: 'text',
  style: {
    color: function () {
      return this.mainColor
    }
  },
  content: function () {
    return this.title
  }
}

在客户端的 JavaScript 引擎中,我们会进行这样的判断:

// parent, key, value, updater, data
if (typeof value === 'function') {
  updater = value
  parent._bindKey(data, null, key, updater)
}
else {
  data[key] = value
}

只要是函数类型,就进行数据绑定,否则一次性赋值,简单易懂易用。

这样在相应的数据发生改写的时候,界面结构和样式会通过 _bindKeyupdater 自动触发更新。

关于如何在 JavaScript 上实现数据监听,也算是一个很重要的细节,不过市面上已经有大把优秀的实现案例了,我们也没有在这个环节上做特别的事情,所以不做赘述。(p.s. 在 Weex 的 JS Framework 中,我们复用了 Vue.js 的数据监听 (observer/*.js) 和依赖收集 (watcher.js) 的代码实现,在此特别要感谢一下!)

transformer

在 transformer 中,我们主要的工作就是对 HTML、CSS、JavaScript 代码进行解析和重组。这里我们用到了三个非常重要的库:

用 htmlparser 可以把 HTML 转换成 JSON

var fragment
var handler = new htmlparser.DefaultHandler(function (err, data){
  fragment = data
})
var parser = new htmlparser.Parser(handler)
parser.parseComplete(content)
...

用 cssom 可以把 CSS 转换成对象供二次处理

var css = cssom.parse(content)
var rules = css.cssRules

rules.forEach(function (rule) {
  rule.selectorText
  rule.style
  ...
})

用 uglify-js 可以把 JavaScript 代码进行细节解析处理

// 遍历语法树
new uglifyjs.TreeWalker(function(node, descend) {...})
...
// 格式化代码
uglifyjs.parse(code)
...

而且因为有了 transformer,我们可以把传统 mvvm 需要在客户端甚至 DOM 上完成的模板解析、数据绑定语法解析等工作提前处理完毕。所以免去了客户端运行时现解析模板源文件的负担。更酷的是,因为模板解析是不依赖真实 DOM 的,所以我们可以大大方方的把语法设计成 <img src="{{xxx}}"> 而不必担心任何副作用。而高级的表达式、过滤器等数据绑定语法也都可以在 transformer 这一层提前处理好,这样在撰写体验持续增强的情况下,运行时并不会产生额外的负担——这都要归功于我们引入了这一层 transformer

debugger tools

我们为 native 界面调试设计了贴心的远程调试工具,主要解决三个痛点问题:

  1. 调试 JavaScript 代码,任意设置断点 debug
  2. 运行命令行代码 (Console) 对程序做实时的判断
  3. 渲染结构的树形审查

解决问题的办法是:

devtools

图:远程调试工具工作原理

  1. 客户端设置一个开关,可以把 JS Bridge 对接到一个远程的 websocket 服务器,而不是对接到本地的 JavaScript 引擎
  2. 本地准备一个网页,其中运行了完整的 JavaScript 引擎的代码,并且也可以链接到一个远程的 websocket 服务器,这样客户端的 native 层和本地网页里的 JavaScript 引擎就串联起来了
  3. 原本通过 JS Bridge 的双向通信内容可以被 websocket 连接记录下来
  4. JavaScript 引擎里的所有代码都可以通过本地浏览器的开发者工具进行 debug 和 console 控制
  5. 开发一个简易的 Chrome Devtools Extension,可以得到 Weex 实例的界面结构,并以目录树的方式呈现出来。

这样,一个客户端开关,一个 websocket 服务器,一个本地的 JavaScript 引擎页面,一个开发者工具扩展,我们就实现了 Weex 的远程调试。

devtools-screenshot

图:开发者工具的审查元素功能扩展

unit test 和 ci 实践

Weex 的 JavaScript 引擎作为一个相对底层的项目,品控需要做到非常严格和极致,否则一个小小的失误在客户端长期运行之后都有可能带来灾难性的后果。

本次 Weex 的开发当中,我们认真实践了基于 mocha、chai 和 sinon 的单元测试,每个源代码的文件夹都放了一个名叫 __test__ 的文件夹,里面放了这个目录下所有同名的 JavaScript 文件,每个文件的内容都是当前文件夹内对应文件的测试用例。

_2015-11-18_19_08_07

_2015-11-18_19_08_17

图:所有的源文件都在同级目录下有一个 __test__ 文件夹放相应的测试代码

在项目中后期,我们还引入了集团内部的一个 ci 系统,每次开发新功能,先开一个分支,然后写测试用例,最后进行代码实现知道跑通所有的 test case,搞定之后发起 merge request,ci 系统会自动在线上运行所有的回归测试,再次验证其正确性和各项指标。

network_graph_-_weapp-plus___js-framework___gitlab

图:项目分支管理

cise

图:CI 实践

其实有关项目品控的话题还有很多,我们也在逐渐实践当中。

isomorphic (同构)

我们已经在内部版本实现了简单的服务端渲染,有了这个东西会怎样呢?

一旦我们可以在服务端直接渲染出界面的 JSON 结构,客户端就可以绕过 JavaScript 解析过程,直接根据 JSON 结构把界面渲染出来。与其对应的逻辑控制稍后也会初始化好,并和界面效果最终保持同步,但在整个过程中,首屏加载的时间会进一步缩短。而且,更令人兴奋的是,如果当前界面刚好没有交互逻辑,甚至后期的 JavaScript 也不需要参与了,这是一条更短的链路!

反哺 HTML5

通过对 Weex 技术方案的探索,我们把组件化开发、transformer 机制、同构等理念反哺到 HTML5 开发,会有什么样的惊喜呢?

那就是一个极简的针对无线前端的 HTML5 MVVM 库:我暂时取名叫做 v.js

v.js 的名字来自著名的 MVVM 库 Vue.js,我希望这个库更适合移动端,目前它可以具备所有 MVVM 的核心功能,通过 transformer 提前解析模板,运行时更快速,体积是 Vue.js 的 1/3,而且支持服务端的同构。这些都是基于移动端现状的二次改进,目前还在细节构思和研发当中。希望不久的将来可以跟大家见面。

<template>
  <div>
    <img src="http://www.taobao.com/favicon.ico" width="32" height="32" onclick="foo"></img>
    <span style="color: red;">Hello{{name}}!</span>
  </div>
</template>

<style>
  .title {color: red;}
</style>

<script>
  module.exports = {
    data: {
      title: 'World'
    },
    methods: {
      foo: function (e) {
        this.title = 'V'
      }
    }
  }
</script>

基于 v.js 的源文件

vjs-demo

v.js 的展示效果

{
  "ref":"0","type":"root","attr":{},"style":{},
  "children":[
    {"ref":"1","type":"div","attr":{},"style":{},"component":"...",
      "children":[
        {"ref":"2","type":"image","attr":{"height":"32","width":"32","src":"http://www.baidu.com/favicon.ico"},"style":{},"event":["click"]},
        {"ref":"3","type":"span","attr":{"value":"HelloWorld!"},"style":{"color":"red"}}
      ]
    }
  ]}

同构的 JSON 数据,方便 native 快速渲染

<root ref="0">
  <div ref="1">
    <image ref="2" height="32" width="32" src="http://www.baidu.com/favicon.ico" onclick="$fire"></image>
    <span ref="3" value="HelloWorld!" style="color: red"></span>
  </div>
</root>

同构的 HTML 代码,方便浏览器快速渲染

篇幅有限,写了三篇还是觉得不够,期待和大家更多的交流我晚上再针对大家频繁问及或感兴趣的问题做一个整理欢迎继续阅读大家近期针对 Weex 的常见问题汇总整理

阿里无线前端团队更多精彩的内容还在后面排队,我这边对无线电商动态化方案的思考就先写到这里了:)

双11密令红包的前端技术方案

玩法简介

今年的双11不仅买买买令人兴奋,密令红包也让人欲罢不能、抢到手软。每天,都有新鲜出炉的密令从各种渠道放出,只要打开手机淘宝,在搜索框输入密令就可以抢现金红包啦~

一年一度的红包盛事没有彩蛋怎么行呢?输入秋裤、冰箱、手机膜试试看?哎呀我的手机怎么这样了?

玩法图

咳咳,双11已经结束,我们还是回归具体需求和技术实现本身吧:P

需求整理

如上所述,密令红包主要分为以下两种:

  • 密令红包:商家、明星、平台或者个人预先设置一段密令,用户通过搜索框输入密令参与抽奖,得到红包。
  • 关键词红包:对于用户输入的一些特定搜索关键词,播放一段动效(2种打底动效 + 6种特殊动效),然后参与抽奖获得红包。

今年的双11是无线主导,我们不能满足于只做手淘中的页面,还要在更多的地方把这个玩法落地:

  • 应用:淘宝、天猫
  • 操作系统:iOS、Android
  • 终端:Phone、Pad、PC

设计一套技术方案来满足这样的需求,并在短时间内快速实现,还是蛮有挑战的。待解决的问题主要有两个: 跨终端适配玩法接入

跨终端适配

这个玩法几乎所有的页面都需要适配Phone、Pad和PC。使用响应式开发,一套页面适配所有端曾经是我们的梦想,但是也得回归现实。不管是从设计方面,还是技术实现方面,密令红包的PC版本都与Phone版本存在着较大的差异,针对PC和Phone做两套页面的实现是无法避免的。

对于Phone页面,我们使用寒冬 @wintercn 大大亲自设计的手机淘宝可伸缩布局方案lib.flexible。为了在工程上解决rem值转换的问题, @songsiqi 开发了px2rem工具集gulp插件webpack loaderpostcss插件一应俱全。

PC版本是需要以模块方式提供给首页、搜索结果等页面,页面兼容旧版本IE,使用KISSY。

而对于Pad页面,之前的实践和积累都是比较缺乏的。

  • 比起Phone和PC,Pad的使用场景受限,页面pv/uv都比较小。
  • Pad的展示效果类似PC,但交互体验更像是Phone。
  • 手机淘宝的自适应布局方案lib.flexible并没有考虑Pad。

结合淘宝Pad、天猫Pad页面的具体情形,和密令红包本身的需求,考虑到实现的成本,最终的解决方案是: Pad页面在PC页面模块的基础上适配而来,做一些额外的样式和功能定制 。Pad页面和PC页面的不同之处在于,接入方是自己的页面,而非外部页面。

同一模块适配PC和Pad页面

玩法接入

整个密令玩法都是基于 搜索框 来玩的。对于不同的终端,接入的方式也不同。

Phone、Pad:使用poplayer接入

Poplayer是淘宝/天猫APP的一个native组件,可以在当前native页面上覆盖一层h5页面,当poplayer页面背景为透明或半透明时,可配置用户是否能够点透至被覆盖页面。Poplayer的出现,融合了native与h5,我们得以利用h5页面快速开发、快速迭代的优势,对下方已有的native页面的交互功能进行增强。主要应用场景如下:

  • 添加入口:持续放在native页面的某个位置,例如手淘的图文详情上浮动的“问大家”入口。
  • 弹层覆盖:例如今年双11的密令红包,就是使用了poplayer,在正常的搜索逻辑基础上接入了密令红包玩法。具体过程如下图所示:

Phone、Pad接入

PC:提供KISSY模块接入外部页面

PC上的密令玩法会接入淘宝/天猫/海淘的首页、搜索结果等页面。与Phone和Pad不同的是,这次我们暴露给外部页面的是KISSY模块,而非页面。KISSY模块的依赖关系如下图所示:

PC接入模块依赖图

接入密令红包玩法相关模块之后的搜索过程如下图所示:

PC接入中转逻辑

最终效果展示

以C2C红包为例:

  • Phone效果:

    Phone效果

  • Pad效果:

    Pad效果

  • PC效果:

    PC效果

小结

就这样, 搜索被我们玩坏了!!!

这些玩法和一个个对应的技术点,拆开来看似乎不是很特别,但是作为整套解决方案,把一路的探索过程和踩过的坑梳理清楚并沉淀下来,还是很有意义的。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.