连轴转的刷新,不断变向的页面转换,以及tap事件的周期性的延迟仅仅是如今移动web环境使人头疼事情的一小部分。开发者正试图尽量的靠近原生应用,但却常常被各类兼容问题,系统复位,和僵化的框架打乱步调。css
在这篇文章中,咱们将讨论建立一个移动HTML 5 web app须要的最低限度的东西。主要观点是去除如今移动框架试图隐藏的隐含复杂性。你会看到一个简约方法(使用核心的HTML 5APIs)和使你可以写出本身的框架或给你如今在用的框架贡献代码的基本原则。html
一般状况下,GPUs处理精细的3D建模或者CAD图表,但这种状况下,咱们想要原始的制图(divs, 背景,下落式阴影的文字,图像等等...) 能经过GPU平滑地展示出来而且有流畅的动画。不幸的是,大多数前端开发者没有考虑动画处理的机制并将其装载在第三方框架,可是这些核心的CSS3特性应 该被掩盖吗?让我来给大家一些关于为何关心这件事是十分重要的理由:前端
1. 内存分配和计算压力- 若是你将全部元素都合成在DOM里,仅仅是为了硬件加速,在你的代码基础上继续工做的另外一我的可能会想狠狠揍你一顿。html5
2. 电源消耗- 显然地,当硬件开始工做,电源也随之开始消耗。当进行移动端开发时,开发者开发移动应用必需要考虑设备多样化的约束。广泛流行的状况是浏览器开发商开始使其产品能适应多样的设备硬件。node
3. 冲突- 我曾经历太小故障:将硬件加速应用到一部分可以加速的页面。值得确信的是若是你有重复的加速区域是很是重要的。android
为了尽量地使户交互平滑而且接近真实,咱们必须使浏览器为咱们工做。理想的状况是,咱们想要移动设备的CPU创建初始化动画,而后使GPU仅仅负责动画处理过程当中合成不一样的层。这就是translate3d, scale3d, translateZ作的事- 他们给了动画元素到他们各自的层,所以容许设备能平滑渲染。若是想要了解更多加速合成,WebKit工做原理,Ariya Hidayat 在他的博客里提供了许多信息。 css3
让咱们看看开发移动WEB应用时最经常使用的三种用户交互方法:滑动、翻转、旋转效果。web
你能够在这个连接查看代码的实际效果: http://slidfast.appspot.com/slide-flip-rotate.html (注意: 这个演示是为移动设备创建的,因此请启动模拟器,或者使用手机、平板电脑,或把你的浏览器窗口减少到约1024px或更小).ajax
首先,咱们将剖析滑动、翻转、旋转过渡,及如何使其加速。请注意每一个动画是如何只需3、四行CSS和JavaScript便可实现的。正则表达式
在这三种经常使用效果中最经常使用的是滑动,滑动页面变换模拟了移动应用的天然感受。滑动转换用来向视图区域带来一个新的内容。
要实现滑动效果,首先咱们要声明元素标签:
1
2
3
4
5
6
7
8
9
10
11
|
<
div
id
=
"home-page"
class
=
"page"
>
<
h1
>Home Page</
h1
>
</
div
>
<
div
id
=
"products-page"
class
=
"page stage-right"
>
<
h1
>Products Page</
h1
>
</
div
>
<
div
id
=
"about-page"
class
=
"page stage-left"
>
<
h1
>About Page</
h1
>
</
div
>
|
注意咱们是如何让页面向左或向右演出的。本质上,它能够是任何方向,但水平是最多见的。
咱们如今只需几行CSS就能够产生有硬件加速的动画。咱们交换页面上的div元素的class时,动画就会实际发生。
1
2
3
4
5
6
7
|
.page {
position
:
absolute
;
width
:
100%
;
height
:
100%
;
/*activate the GPU for compositing each page */
-webkit-transform: translate
3
d(
0
,
0
,
0
);
}
|
translate3d(0,0,0)做为“银弹”方法而闻名。
当用户点击一个导航元素,咱们执行下面的JavaScript来交换class。没有第三方框架被使用,这是纯JavaScript!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
function
getElement(id) {
return
document.getElementById(id);
}
function
slideTo(id) {
//1.) the page we are bringing into focus dictates how
// the current page will exit. So let's see what classes
// our incoming page is using. We know it will have stage[right|left|etc...]
var
classes = getElement(id).className.split('
');
//2.) decide if the incoming page is assigned to right or left
// (-1 if no match)
var stageType = classes.indexOf('
stage-left
');
//3.) on initial page load focusPage is null, so we need
// to set the default page which we'
re currently seeing.
if
(FOCUS_PAGE ==
null
) {
// use home page
FOCUS_PAGE = getElement(
'home-page'
);
}
//4.) decide how this focused page should exit.
if
(stageType > 0) {
FOCUS_PAGE.className =
'page transition stage-right'
;
}
else
{
FOCUS_PAGE.className =
'page transition stage-left'
;
}
//5. refresh/set the global variable
FOCUS_PAGE = getElement(id);
//6. Bring in the new page.
FOCUS_PAGE.className =
'page transition stage-center'
;
}
|
stage-left或stage-right成为stage-center,会推进页面滑入视图中心。咱们彻底依靠CSS3完成繁重的工做。
1
2
3
4
5
6
7
8
9
10
11
12
|
.stage-
left
{
left
:
-480px
;
}
.stage-
right
{
left
:
480px
;
}
.stage-
center
{
top
:
0
;
left
:
0
;
}
|
接下来,让咱们看看处理移动设备检测与适应的CSS。咱们能够定位每种设备和每种分辨率(参考 媒体查询解析)。 我在演示中使用的只是几个简单的例子来覆盖移动设备上大多数的竖立和横放视图。这对应用每种设备自己的硬件加速功能也颇有用。好比,由于Webkit的桌 面版本加速了全部转换元素(无论是二维仍是三维),因此在这个水平上创建媒体查询和排除加速颇有意义。注意,在Android Froyo 2.2+如下,硬件加速技巧不会提供任何速度的改进。全部合成都是在软件内部实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* iOS/android phone landscape screen width*/
@media
screen
and (max-device-
width
:
480px
) and (orientation:
landscape
) {
.stage-
left
{
left
:
-480px
;
}
.stage-
right
{
left
:
480px
;
}
.page {
width
:
480px
;
}
}
|
在移动设备上,翻转实际上以把页面击飞(译者注:若是你熟悉棒球,很容易想像)而闻名。在这里咱们用一些简单的 JavaScript 在iOS 和 Android (基于WebKit)设备上来处理这个事件。
在这个地址可查看实际执行效果http://slidfast.appspot.com/slide-flip-rotate.html.
当处理触摸事件和转换效果时,你要作的第一件事就是得到元素当前位置的句柄。在WebKitCSSMatrix上能够看到更多信息。
1
2
3
4
5
|
function
pageMove(event) {
// get position after transform
var
curTransform =
new
WebKitCSSMatrix(window.getComputedStyle(page).webkitTransform);
var
pagePosition = curTransform.m41;
}
|
因为咱们为页面翻转使用的是CSS3的ease-out转换,usualelement.offsetleft不会工做。
下一步咱们要找出用户翻转的是哪一个方向,并对事件(页面导航)设定一个发生的阈值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
if
(pagePosition >= 0) {
//moving current page to the right
//so means we're flipping backwards
if
((pagePosition > pageFlipThreshold) || (swipeTime < swipeThreshold)) {
//user wants to go backward
slideDirection = 'right
';
} else {
slideDirection = null;
}
} else {
//current page is sliding to the left
if ((swipeTime < swipeThreshold) || (pagePosition < pageFlipThreshold)) {
//user wants to go forward
slideDirection = '
left';
}
else
{
slideDirection =
null
;
}
}
|
你会注意到咱们测量击打时间是毫秒级的。这容许导航事件在用户快速点击屏幕来翻页时也会发生。
为了定位页面和当手指正触摸屏幕时使动画看起来天然,咱们在每次事件触发后都使用CSS3转换。
1
2
3
4
5
6
7
8
9
10
|
function
positionPage(end) {
page.style.webkitTransform =
'translate3d('
+ currentPos +
'px, 0, 0)'
;
if
(end) {
page.style.WebkitTransition =
'all .4s ease-out'
;
//page.style.WebkitTransition = 'all .4s cubic-bezier(0,.58,.58,1)'
}
else
{
page.style.WebkitTransition =
'all .2s ease-out'
;
}
page.style.WebkitUserSelect =
'none'
;
}
|
我想玩弄一下三次曲线来让转换带有最好的天然感受,但ease-out已经玩了这个花样。
最后,为让导航发生,咱们必须调用咱们以前在上一个演示里定义的slideTo()方法。
1
2
3
4
5
6
7
8
|
track.ontouchend =
function
(event) {
pageMove(event);
if
(slideDirection ==
'left'
) {
slideTo(
'products-page'
);
}
else
if
(slideDirection ==
'right'
) {
slideTo(
'home-page'
);
}
}
|
接下来,让咱们来看看在本演示使用的旋转动画。在任什么时候候,你能够旋转页面将看到180度旋转后反面的“联系人”菜单选项。 一样的,只须要几行CSS和一些JavaScript指定一个点击时的transition class。注:旋转过渡则没法正确的在大多数版本的 Android上呈现,由于它缺少3D CSS transform 的支持。不幸的是,Android提供了“侧手翻”页面旋转特性,来替代翻转。咱们建议在android获得支持以前使用transition来进行翻转。
正面与背面的基本结构:
1
2
3
4
5
6
7
8
|
<
div
id
=
"front"
class
=
"normal"
>
...
</
div
>
<
div
id
=
"back"
class
=
"flipped"
>
<
div
id
=
"contact-page"
class
=
"page"
>
<
h1
>Contact Page</
h1
>
</
div
>
</
div
>
|
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function
flip(id) {
// get a handle on the flippable region
var
front = getElement(
'front'
);
var
back = getElement(
'back'
);
// again, just a simple way to see what the state is
var
classes = front.className.split(
' '
);
var
flipped = classes.indexOf(
'flipped'
);
if
(flipped >= 0) {
// already flipped, so return to original
front.className =
'normal'
;
back.className =
'flipped'
;
FLIPPED =
false
;
}
else
{
// do the flip
front.className =
'flipped'
;
back.className =
'normal'
;
FLIPPED =
true
;
}
}
|
CSS:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/*----------------------------flip transition */
#bac
k,
#front {
position
:
absolute
;
width
:
100%
;
height
:
100%
;
-webkit-backface-
visibility
:
hidden
;
-webkit-transition-duration: .
5
s;
-webkit-transform-style: preserve
-3
d;
}
.
normal
{
-webkit-transform: rotateY(
0
deg);
}
.flipped {
-webkit-user-select: element;
-webkit-transform: rotateY(
180
deg);
}
|
如今咱们讲完基本变换的方法了,让咱们看看它们是如何工做和合成的。
为了使这个奇妙的调试会话得以发生,让咱们启动你喜欢的一个IDE和浏览器。我使用Mac,所以操做可能和你的操做系统的命令与方式都不一样。首先我在命令行设置一些调试中使用的环境变量,而后启动Safari浏览器。打开Terminal,键入如下内容:
这样就能开启Safari的两个调试助手功能。CA_COLOR_OPAQUE 会向咱们展示哪一个元素被实际合成和加速了。 CA_LOG_MEMORY_USAGE 会向咱们展示当向backing store发送咱们的绘制操做时使用了多少内存。这能够确切告诉你你给移动设备施加了多少压力,以及可能提示你你对GPU的使用会消耗目标设备多少电量。
如今让咱们启动Chrome,这样咱们能够很好地看到每秒多少帧(FPS)的信息:
注意:不要在全部页选项中激活 GPU 合成。当浏览器检测到你标签中的合成项目,会只在左边角落显示FPS计数器,而这不是咱们在本案例中想要的。
若是你在威力加强版的Chrome中查看本讲座效果页面,你会在左上方看到红色的 FPS 计数器。
这就是咱们怎样知道硬件加速功能被开启的方法。这也给了咱们一个关于动画如何运行的和你是否有任何疏漏的想法(继续运行本应中止的动画)。
另外一种让硬件加速变得实际可视化的方法是,若是你经过先设置我上面提到的环境变量来用Safari打开相同的页面。每一个被加速的DOM元素都会有一个红色色调。这告诉了咱们到底层合成了哪些元素。注意,白色的导航由于不能加速而没有变红。
Chrome在 about:flags中也有一个相似的设置 “Composited render layer borders”。
另外一个看到合成层的好方式,是开启这个选项以后查看WebKit的落叶演示。
最 后,要真正了解咱们的应用程序的图形硬件性能,让咱们来看看内存是如何被消耗的。这里咱们能够看到,咱们正在把绘图指令产生的1.38MB数据推动到 Mac OS上的CoreAnimation缓冲区。核心动画缓冲区是被OpenGL ES和GPU共享的,来建立你最终在屏幕上看到的像素。
当咱们简单地调整一下浏览器窗口尺寸或把窗口最大化,咱们会看到当即膨胀了。
这 给你一个想法,内存是如何被消耗在移动设备上,只有当你调整浏览器的正确尺寸。若是你在调试或测试iPhone环境,请从320像素调整到480像素。我 们如今明白了硬件加速究竟如何工做的,以及怎样来调试。这是一种用阅读数字来了解的方式,但也是真正看到GPU内存缓冲区可视化工做的方式,确实让事情变 得透明了。
如今是时候把咱们的页面和资源缓存提高到一个新水平了。就像jQuery Mobile及其相似框架所使用的方法,咱们要用并发AJAX调用来预取和缓存咱们的网页。
让咱们来指出一些移动网络的核心问题和咱们为何须要这么作的缘由:
从滑动,翻转,和旋转演示 构建代码,咱们开始先加上一些二级页面并连接到它们。而后咱们将解析连接并飞速建立转换。
如你所见,这里咱们利用了语义标记。仅仅是到另外一个页面的连接。子页面像它的父页面同样遵循相同的节点/类结构。咱们能够更进一步的给"page" 节点使用data-*属性,等等……这里是位于一个单独的html文件中(/demo2/home-detail.html)的详细页(子页面),它将被 加载,缓存并在app加载时为页面转换预先创建。
<div id="home-page" class="page"> <h1>Home Page</h1> <a href="demo2/home-detail.html" class="fetch">Find out more about the home page!</a> </div>
如今让咱们来看看JS。为简单起见,我没对代码添加助手或进行优化。咱们在这里作的是遍历一个指定的DOM节点的数 组,挖出要提取和缓存的连接。注意,对于本演示,fetchAndCache()方法在页面加载时被调用。咱们在下一节中检测网络链接时会再次使用它,并 决定它什么时候该被调用。
var fetchAndCache = function() { // iterate through all nodes in this DOM to find all mobile pages we care about var pages = document.getElementsByClassName('page'); for (var i = 0; i < pages.length; i++) { // find all links var pageLinks = pages[i].getElementsByTagName('a'); for (var j = 0; j < pageLinks.length; j++) { var link = pageLinks[j]; if (link.hasAttribute('href') && //'#' in the href tells us that this page is already loaded in the DOM - and // that it links to a mobile transition/page !(/[\#]/g).test(link.href) && //check for an explicit class name setting to fetch this link (link.className.indexOf('fetch') >= 0)) { //fetch each url concurrently var ai = new ajax(link,function(text,url){ //insert the new mobile page into the DOM insertPages(text,url); }); ai.doGet(); } } } };
咱们确保经过使用“ AJAX ”对象进行了适当的异步发送处理。在 Working Off the Grid with HTML5 Offline中调用的一个AJAX里有对使用localStorage的一个更高级的解释。在这个例子中,你会看到一个基本用法,用来缓存每一个请求,并当服务器未返回成功的(200)响应时提供以前所缓存的对象。
function processRequest () { if (req.readyState == 4) { if (req.status == 200) { if (supports_local_storage()) { localStorage[url] = req.responseText; } if (callback) callback(req.responseText,url); } else { // There is an error of some kind, use our cached copy (if available). if (!!localStorage[url]) { // We have some data cached, return that to the callback. callback(localStorage[url],url); return; } } } }
不幸的是,因为本地存储使用UTF-16字符编码,每一个字节被看成2个字节存储,将咱们的存储限制从5MB降到 总共只有2.6MB。 在应用程序缓存范围以外提取和缓存这些页面/标记的整个缘由在下一节中透露。
经过最近在HTML5中iframe元素的 进展,咱们如今有了一个简单而有效的方式来解析AJAX调用返回给咱们的响应文本。有不少3000行脚本解析器和去除脚本标签的正则表达式之类的东西。但 为什么不让浏览器代为作它最擅长的?在这个例子中,咱们要把响应文本写到一个暂时隐藏的iframe中。咱们使用HTML5的“沙箱”属性,它禁用脚本并提 供了许多安全特征…
从规范上来说: 当设置了 sandbox 属性后, 在Iframe的内容上开启了一组额外的限制。 它的值应该是一组无序的、空格分隔的 token, 而且是大小写敏感的。 能够设置的值分别是 allow-forms, allow-same-origin, allow-scripts, 和 allow-top-navigation. 当属性设置了之后, 内容处理后,将被看成同源,forms 和 scripts 将被禁止,指向其余浏览上下文的 link 将被禁止,插件也被禁用。 为了防止危险的 HTML 内容形成破坏, 它应使用一个 text/html-sandboxed MIME 类型.
var insertPages = function(text, originalLink) { var frame = getFrame(); //write the ajax response text to the frame and let //the browser do the work frame.write(text); //now we have a DOM to work with var incomingPages = frame.getElementsByClassName('page'); var pageCount = incomingPages.length; for (var i = 0; i < pageCount; i++) { //the new page will always be at index 0 because //the last one just got popped off the stack with appendChild (below) var newPage = incomingPages[0]; //stage the new pages to the left by default newPage.className = 'page stage-left'; //find out where to insert var location = newPage.parentNode.id == 'back' ? 'back' : 'front'; try { // mobile safari will not allow nodes to be transferred from one DOM to another so // we must use adoptNode() document.getElementById(location).appendChild(document.adoptNode(newPage)); } catch(e) { // todo graceful degradation? } } };
Safari 正确的阻止了 Node 从一个 doc 到另外一个的隐式移动。若是一个新的子节点在不一样的 doc 上建立,将抛出一个错误。 那么这里咱们使用adopt Node,一切都很好。
那么为何还要用Iframe,而不只仅用innerHTML?即使innerHtml现在已经是html5规范的一部分,将服务器的响应直接插入未 检查过的区域的作法也是有危害的。写做本文期间,我发现几乎全部人都是使用的innerHTML。如Jquery在其核心中使用,仅在发生异常时有一个回 调函数来处理。而JQuery Mobile 也是这样使用的。固然我没有针对innerHTML的"随机中止工做"的情况作过任何严格的测试,但查看比较各个平台的对iframe和 innerHTML的不一样做用效果将十分有趣,更想知道那种方式的性能会好些...在这两种方式下其实我都已经听到了很多抱怨了。
既然咱们有能力来缓存(预测缓存)咱们的web应用,咱们必须提供更好的网络链接类型检测功能使得咱们的应用更加智能。
这就是为何移动应用的开发在 在线/离线模式和链接速度下变得十分敏感的缘由。进入The Network Information API网络信息API. 每次我在演讲这个功能点的时,台下总有人会举起收提问"那咱们使用它作什么呢?".那么确定有种方式来开发一个超级智能的移动应用的。
第一烦人场景是...在高速列车上从移动设备访问一个Web站点,网络的链接在各个不一样的时刻和不一样的地理环境下极可能失去,所以致使各类不一样的传 输速度。 (如, HSPA 或 3G在一些城镇地区能够用, 但偏远地区可能只支持速度很慢的2G技术). 下面的代码解决了网络链接问题中的大部分场景。
接下来的代码演示的是:
window.addEventListener('load', function(e) { if (navigator.onLine) { // new page load processOnline(); } else { // the app is probably already cached and (maybe) bookmarked... processOffline(); } }, false); window.addEventListener("offline", function(e) { // we just lost our connection and entered offline mode, disable eternal link processOffline(e.type); }, false); window.addEventListener("online", function(e) { // just came back online, enable links processOnline(e.type); }, false);
在 上述事件 的监听中 , 咱们 必须 告诉 咱们 的 代码是否被 事件 或 实际 页面 请求 刷新所 调用 。 主要 的 缘由 是 由于 在 联机 和 脱机 模式 之间 切换 时 , 不会触发(fired) 关于页面正在 加载中的 事件 。
下一步 , 咱们 作 一个 简单 的 检查是否存在 匿名 在线 或 加载 事件 。此代码须要禁用连接重置, 当 从 脱机 模式切换 为 联机状态 。这个应用须要 更 复杂 的功能, 你 可能 须要作一些逻辑插入来执行 恢复抓取内容和为间歇性链接而处理UX。
function processOnline(eventType) { setupApp(); checkAppCache(); // reset our once disabled offline links if (eventType) { for (var i = 0; i < disabledLinks.length; i++) { disabledLinks[i].onclick = null; } } }
processOffine()函数也是一样的过程。假设你想让你的app到离线模式而且试图恢复以前场景全部的事务。下面的代码找出全部的外部连接而且让它们失效,永远在咱们离线的应用中捕获用户,hoho
function processOffline() { setupApp(); // disable external links until we come back - setting the bounds of app disabledLinks = getUnconvertedLinks(document); // helper for onlcick below var onclickHelper = function(e) { return function(f) { alert('This app is currently offline and cannot access the hotness');return false; } }; for (var i = 0; i < disabledLinks.length; i++) { if (disabledLinks[i].onclick == null) { //alert user we're not online disabledLinks[i].onclick = onclickHelper(disabledLinks[i].href); } } }
好,这是多好的东东。如今咱们的app知道处于何种链接状态,当它在线时,咱们也能够检查链接类型,而且相应的调整它。我曾经监听典型的北美网络供应商下载而且潜在地给每种链接的添加了注释。
function setupApp(){ // create a custom object if navigator.connection isn't available var connection = navigator.connection || {'type':'0'}; if (connection.type == 2 || connection.type == 1) { //wifi/ethernet //Coffee Wifi latency: ~75ms-200ms //Home Wifi latency: ~25-35ms //Coffee Wifi DL speed: ~550kbps-650kbps //Home Wifi DL speed: ~1000kbps-2000kbps fetchAndCache(true); } else if (connection.type == 3) { //edge //ATT Edge latency: ~400-600ms //ATT Edge DL speed: ~2-10kbps fetchAndCache(false); } else if (connection.type == 2) { //3g //ATT 3G latency: ~400ms //Verizon 3G latency: ~150-250ms //ATT 3G DL speed: ~60-100kbps //Verizon 3G DL speed: ~20-70kbps fetchAndCache(false); } else { //unknown fetchAndCache(true); } }
fetchAndCache进程,有不少的设置参数,可是在此我仅仅让他执行同步(给参数false)的或者异步(给参数true)的去取给定链接的资源。
Edge (同步) 请求时间线
WIFI (异步) 请求时间线
这 容许基于慢速或快速链接对用户体验的调整至少采起一些方法。这毫不是终结一切的解决方案。另外一个要作的是,在慢速链接上,当应用程序仍在后台获取某个连接 的页面时,若是点击这个连接,要抛出一个加载中模态。这个关键思想是减小延迟,同时用最新最棒的HTML5提供的对用户的链接充分利用其所有能量。点此查看检测网络的演示.
移动HTML5应用程序刚刚上路。如今你能够看见很是简单且基础的彻底围绕 HTML5 建立的移动“框架”以及配套技术。我认为对开发者来讲,重要的是在核心中处理解决这些问题,且不要用封装掩盖它。