开发应用案例分析:Hang On Man

开发应用案例分析:Hang On Man

概述

Hang On Man是一个用HTML5的实现的经典猜字游戏。 该游戏采用了许多HTML5,CSS和JavaScript的功能。

目录

功能

从Web应用开发者来看,感兴趣的功能有:

  • 使用 Chromium API(如果有的话)或JavaScript的国际化
  • 相对CSS,确定对象的计算方式的属性值 
    valuedocument.defaultView.getComputedStyle(element).getPropertyValue(property);
  • 该文档片段用于无昂贵的重新渲染的DOM操作 document.createDocumentFragment();
  • 用于保存状态和使用的本地存储器 JSON.stringify()JSON.parse()
  • 从本地JSON文件中读取数据
  • CSS转换,切换和动画 -webkit-transform, -webkit-transition, -webkit-keyframes
  • 串行动画 "transitionEnd" 事件
  • body类用于一次修改整个APP的外观(参见下面的"Night colors"图)
  • 图片技巧: 图片masks, 边界图片, 线性与光线渐变(使用 -webkit-gradient ),和SVGs
  • 用于各种音频需求的HTML5音频标签(背景声音、按键音、游戏失败的声音、游戏成功的声音等)。

Chrome API的使用(i18n)

为了国际化,所有用户可见的字符串都要写入一个名为“messages.json”的文件(在该目录下 “_locales/LocaleCode”,LocalCode是表示这样的代码,“en”表示英语),在你命名的每一个用户可见的字符串和把它输入到一个信息文件中的地方。 扩展的 manifest, CSS文件和JavaScript代码使用每个字符串的名字去获取它的本地化版本。

如何获取信息?在 js/getMessages.js 文件,下面的函数 “initGetMessage” 提供了一个函数 window.getMessage()用相同的API window.chrome.i18n.getMessage()功能,如果存在Chromium功能(如:以扩展方式在 Chromium或Google Chrome运行),实施简单地使用那个功能。


function initGetMessage ()
{
    if (window.getMessages) {
        return;
    }

    if (window.chrome && window.chrome.i18n && window.chrome.i18n.getMessage) {
        window.getMessage = chrome.i18n.getMessage;
    }
    else {
        var request = new XMLHttpRequest();
        request.open("GET", "_locales/en/messages.json", false);  // TODO: synchronous for now
        request.send();
        var requestStr = request.responseText;
        try {
	    messages = JSON.parse(requestStr);
            window.getMessage = fallbackGetMessage;
        }
        catch(err) {
	    console.log("Unable to read fallback messages from _locales/en_US/messages.json");
            window.getMessage = errorGetMessage;
        }
    }
}			

如果不是(在“else”下描述),我们回退到提供大多数Chromium功能的简单实施(但关于消息参数处理的简化)。

计算风格的使用

在“.css”文档中(hangonman.css, start.css 或figure.css ),对每个风格属性都有一个准确的CSS值。 如果你检查了HTMLElement's style object的话,这就是你将得到的值。 如果在CSS文件中没有明确定义属性,在许多情况下,该值将是一个空字符串(“”)。 在一些情况下,你需要知道"computed value",它是一个由渲染引擎基于当前窗口的大小等计算得到的值。 在 js/hangonman.js我们用下面的代码计算computed value(在完全相同的方式下,这不能在所有浏览器中都起作用):


function getStyle (elem, prop)
{
    return document.defaultView.getComputedStyle(elem).getPropertyValue(prop);
}
                                

使用DOM-文档片段

当正在执行的添加或修改的文档树的多个操作时,这种方法是非常有效的。 不是每次直接修改文档树(非常低效的),较好的办法是使用由XX创建临时“白板” createDocumentFragment()在最后才插入文档树结果之前进行所有的操作。

函数 “createDocumentFragment()” ,创建一个空文档片段。 其结果是一个临时contaner,用于在介绍最终的结果到文档树之前创建和修改新的元素或属性。

当你改变DOM(添加元素,改变类等),视图的部分或全部将被重新渲染。 在Hang On Man,我们为每个字母动态创建为了divs,以支持各种字母的语言。 一种方法是等到DOM的内容准备好,然后将一系列字母divs加到文件中,它们每个都有自己的集合类和样式值。 这项工作需要很多的渲染通道,并且延迟应用的最终显示。 在Hang On Man中,我们使用文档片段的概念去创建文档之外的DOM子树,一次性完成所有的事情,只需要一个渲染通道。 该片段可以在DOM准备好之前构造,一旦脚本被加载,将允许开始运算。


 var fragment = document.createDocumentFragment();
for (i = 0; i < num; ++i) {
    var child = document.createElement("div");
    addStylesAndClasses(child);
    fragment.appendChild(child);
}      

一旦DOM被加载:


var parent = document.getElementById("parent");
parent.appendChild(fragment);      

随之,片段的所有孩子会成为它父亲的孩子(片段从来不是DOM的一部分)。

本地存储的使用

游戏状态以localStorage对象保存。 In js/hangonman.js,有函数, initGameState(), restoreGameState(), and saveGameState(),那将写到下面的对象或从该对象读取。 下面是 “restoreGameState”理解函数。


function restoreGameState ()
{
    if (localStorage && localStorage["com.intel.hom.word"] &&
	localStorage["com.intel.hom.gameInProgress"] && (localStorage["com.intel.hom.gameInProgress"] === "true"))
    {
	wrongGuesses = localStorage["com.intel.hom.wrongGuesses"] || "";
	rightGuesses = localStorage["com.intel.hom.rightGuesses"] || "";
	word = localStorage["com.intel.hom.word"];
	gameType = localStorage["com.intel.hom.gameType"] || 0;
	gameInProgress = true;
    }
    else {
	initGameState();
    }
}

由于每个应用的localStorage信息没有被装载,我们以每个数据开始 "com.intel.hom." (hom == Hang On Man) 避免冲突。
需要注意的是存储在本地存储器中的阵列和其它复杂的数据结构是通过编组成字符串或从字符串编组,使用 JSON.stringify() and JSON.parse()。 下面的函数“restoreSettings()”使用它们。


function restoreSettings ()
{
    try {
        useSounds = JSON.parse(localStorage["com.intel.hom.useSounds"]);
    }
    catch (e) {
        useSounds = true;
        localStorage["com.intel.hom.useSounds"] = JSON.stringify(useSounds);
    }
} 

JSON文件的使用

游戏,Hang On Man使用几个单词列表。 播放器将不同的单词列表中选择(animals, nations, wine, bodyparts, common phrases, etc)。
每组都存储在独立的JSON文件中。 我们使用此函数读取 XmlHttpRequest()与JSON的解析方法结合使用。 下面的代码显示了有多少单词列表已读取。

本例是使用JSON文件去保存本地的字符串和定义Web应用程序清单文件

你将会在读取的时候发现很多关于Jason的信息。


function readwordlist(item)
{
    var file = "data/"+item.src;
    var request = new XMLHttpRequest();
    request.open("GET", file, true);
    request.onload = function(e) {
        var requestStr = this.responseText;
        try {
	    item.data = JSON.parse(requestStr);
        }
        catch(err) {
	    console.log("Unable to read wordList: "+file);
        }
    }
    request.send();
}	

在下列函数中 “initWordLists()”,我们将同步和有顺地读取所有的列表(wordLists.length)。


function initWordLists (wordLists)
{
    var newGameList = document.getElementById("newGame_list");
    var gameTypeLabel = document.getElementById("game_type");
    for (var i = 0; i < wordLists.length; ++i) {
        var item = wordLists[i];
        if (item.hasOwnProperty("src")) {
            readwordlist(item);
        }
        var titleString = getMessage(item.title);
        var gameElem = document.createElement("div");
        var cl = gameElem.classList;
        gameElem.index = i;
        cl.add("newGame_type");
        addButtonEffects(gameElem);
        if (i == gameType) {
            cl.add("selected");
            gameTypeLabel.innerText = titleString;
        }
        gameElem.innerText = titleString;
        gameElem.addEventListener("click", selectGameType, false);
        newGameList.appendChild(gameElem);
    }
}	

在加速初始应用程序加载时间中的下一步是使用异步请求或webworker在后台去即时加载这些列表。 当用XHRs开发应用时,需要考虑该页是由该函数加载 file://uri 策略是不同允许从不同的域去请求文件,即使是从localhost(本地文件)。 变通的安全检查的方法有:
启动本地web服务,并用http:// uri而不是a file:// uri访问你的应用,将以扩展的方式加载你的应用 [仅Chromium]
使用 --disable-web-security 运行chromium-browser的开关。 [仅Chromium]。

CSS转换,过渡和动画的使用

两个属性超时值间的转换步骤。 在这种情况下,我们将从不可见(不透明性是零)变为可见(不透明度为1)。 由于div的消失,它将从大的(3x)变成正常大小。 通过添加“show”类到元素来触发该转换。 注意,另外,该 z-index 从0变化到20,这样,当它是不可见时,它也被放在其他的div后面,所以它不捕获鼠标事件。


#letters {
    z-index: 0;
    opacity: 0;
    -webkit-transform: scale(3, 3);
    -webkit-transition: opacity 2s ease-in, -webkit-transform 2s ease-in;
}

letters.shown {
    z-index: 20;
    opacity: 1;
    -webkit-transform: scale(1, 1);
}		

这里是两个在同一时间运行的动画的例子,但有不同的运行时间。这里的效果是在游戏中的鸟盘旋变小,随机地移动。


@-webkit-keyframes move-horiz {
  0% {left: 0px;
      -webkit-transform: skewX(0deg);}
  50% {-webkit-transform: skewX(1deg);
       left: 5px; }
  75% {-webkit-transform: skewX(.5deg);
       left: 2px;}
  100% {-webkit-transform: skewX(-.75deg);
        left: -3px;}
}

@-webkit-keyframes move-vert {
  0% {top: 0px;}
  50% {top: 5px;}
  100% {top: -5px;}
}

.dialog.shown .inner {
  -webkit-animation: move-horiz 2.5s infinite alternate ease-in-out,
                     move-vert 1.6s infinite alternate ease-in-out;
}				

在某些情况下,当一个转换完成的时候我们需要知道。 对于云彩掠过天空,一旦某个云已到达边上,它的移动就算结束,调用下面的函数它们将被销毁 destroyCloud()。 另请注意,用classList属性在标准的HTML5的方式中处理类。


var cloudElem = document.createElement("div");
var classList = cloudElem.classList;
classList.add("cloud");
cloudElem.addEventListener('webkitTransitionEnd', destroyCloud, false);		

Body类的使用

期限 "body classes" 指的是关于body元素的类的使用或一些外部元素来控制多个子元素的外观。 在Hang on Man中, 我们将通过添加"night"类到<body>来应用一个夜主题。

你将会在css/hangonman.css文件中发现下面的代码。


.night #container {
    background-image: -webkit-gradient(radial,
       center bottom, 1,
       center bottom, 600,
	from(rgb(254, 220, 112)),
       color-stop(0.60, rgb(228, 188, 222)),
	to(rgb(22,41,118)));
}
.night #skyline {
    background-image: none;
    background-color: #333;
    -webkit-mask-size: contain;
    -webkit-mask-position: left bottom;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-image: url("../images/skyline.png");
}
.night .cloud {
    background-image: -webkit-gradient(linear, left top, left bottom,
                                       from(rgb(254, 220, 112)), to(rgb(217,86,78)));
}         

上面的"night"类使用下面这种情况下(在initDialogs函数,在 hangonman.js 文件),


    var now = (new Date()).getHours();
    var morning =  6; // 6am
    var evening = 18; // 6pm
    bodyElem = bodyElem || document.querySelector("body");
    if ((now >= evening) || (now < morning)){
        bodyElem.classList.add("night");
    }    

简化实例:


<body>
  <div id="sky">
    <div class="cloud"></div>
    <div class="cloud"></div>
    ...
  </div>
</body>

#sky {
  background-image: url("daysky.png");
}

.cloud {
  background-color: white;
}

.night #sky {
  background-image: url("nightsky.png");
}

.night .cloud {
  background-color: gray;
}		

图片的背景,Mask和边界的使用(图像技巧)

渐变为背景:

大多数主流浏览器现在支持CSS3渐变,wide浏览器的支持使他们更吸引人。优势有:用CSS3渐变你也可以指定回退(即图像),使不支持他们浏览器用图片代替。

还有,这是一个不错的选择,因为下面的原因:

  • 更少的HTTP请求
  • 可扩展的CSS渐变

下列代码表明在 Hang on man应用中定义的CSS光线和线性渐变怎么样。


background-image: -webkit-gradient(radial,
                  center bottom, 1,
                  center bottom, 600,
                  from(#bce9fd),
                  color-stop(0.60, #43c0fa),
                  to(#3384c0));

background-image: -webkit-gradient(linear, left top, left bottom,
from(rgb(254, 220, 112)), to(rgb(217,86,78)));       

注:支持CSS3渐变的浏览器,不加载备用图像。

用图片作mask:

将图片应用到MASK(如:.png或.svg图片),将图片的URL加到 -webkit-mask-image属性。
-webkit-mask-image属性使用MASK图片的本地属性。


#skyline {
    background-color: #333;
    -webkit-mask-image: url("../images/skyline.png");
}           

图片用于边框:

border-image属性允许你把一个小图像跨越一个更大的元素。 你可以把图像拉伸穿过一个按钮或整页。

为了应用border-image,我们可以使用CSS -webkit-border-image: url(borderImage.png) <slice_height> <slice_width> <type>;
type =定义边框是否重复、圆的或拉伸(重复/圆的/拉伸)。

该应用程序在一些地方使用此属性去使图形拉伸,以适应不同长度的字符串翻译。 例如,“新游戏”对话框,看起来像由两只鸟在床单上,就被拉伸到最长类别的宽度。 该 "New Game" > 和 "Quit" 建筑,在屏幕的两侧,也 stretched 但是使用 "round" 方法跨越建筑宽度去显示一些建筑窗口。


#newGame.control {
    border-width: 94px 20px 5px 20px;
    -webkit-border-image: url("../images/building1.svg") 94 20 5 20 round round;
}

.dialog .inner {
    border-width: 145px 95px 35px 75px;
    -webkit-border-image: url("../images/sheet2.png") 145 95 35 75 stretch stretch;
}

Using Scalable Vector Graphics (SVG)
#myElement {
  background-image: url("../images/cloud.svg");
}   

HTML5的音频支持

HTML5 audio 标签用于不使用脚本或add-on控件去添加音频文件到你的应用中播放。 在当前HTML5规范草案中没有提及浏览器所支持的音频格式。 但最常用的音频格式 ogg, mp3 and wav

你可在下index.html文件中看到下列代码,它是用于背景音乐的音频标签。 以预载和循环方式使用音频标签。


 <audio class="background" loop="true" preload="auto"><source src="audio/Background.ogg" type="audio/ogg" /></audio>

hangonman.js 文件,在函数 “window.addEventListener"当收到页面加载事件时,背景音频开始播放。 当焦点是不在于该应用时,音频暂停播放。


  backgroundSound = document.querySelector("audio.background");		

window.addEventListener("load", function (event)
{
    var infocus = true;

    if (useSounds) {
        backgroundSound.play();
    }

    window.onblur = function() {
        if(infocus)
        {
            infocus = false;
            if (useSounds)
            {
                backgroundSound.pause();
                if (dialogSound&&isDialogUp)
                {
                    dialogSound.pause();
                }
            }
        }
    };

    window.onfocus = function() {
        if(!infocus)
        {
            infocus = true;
            if (useSounds)
            {
                backgroundSound.play();
                if (dialogSound&&isDialogUp)
                {
                    dialogSound.play();
                }
            }
        }
    };

}, false); 

截图

下面是Hang On Man应用中的截图。


图1:这是应用程序被加载最初的样子。


图2:选择游戏模式的菜单。


图2:在选择了游戏模式时,我们看到这个页面。