日志更新

如何用 JavaScript 开发迷宫游戏

3月6日在杭州 Google GTUG 活动上的分享《Maze Game》,本想讲 《Android 手机开发及HTML5在手机开发中的应用》,但由于 Google 邀约得比较晚,来不及准备,就在原先准备在团队内分享的未完成 PPT ——《Maze Game》的基础上,做了补充和完善。

游戏部分思路来源于同事 季札 开发的迷宫游戏:www.sansi.org/static/maze/maze.html

Script 元素 type 属性的妙用

我们经常使用动态创建 JavaScript 的方式来实现 JavaScript 文件的无阻塞(Non-blocking)、并行下载(Parallel )。其实还可以添加自定义的 type 属性(比如 text/cache),达到预加载但不执行的效果:

var doc = document,
    el = doc.createElement("script"),
    head = doc.getElementsByTagName('head')[0];

    el.src = "http://yui.yahooapis.com/2.8.2r1/build/yahoo-dom-event/yahoo-dom-event.js";
    el.type = "text/cache";
    head.insertBefore(el, head.firstChild);

注:Firefox 3.6.x 不请求 JavaScript 文件。

扩展阅读:

前端代码 Review 的一些思考

前端代码 Review 的一些思考

查看大图:http://www.planabc.net/wp-content/uploads/2011/codereview.png

欢迎大家针对其中的问题,一起思考与交流!

扩展阅读:

模拟实现 Range 的 insertNode() 方法

最近对 RangeSelection 比较感兴趣。

基本非 IE 的浏览器都支持 DOM Level2 中的 Range,而 IE 中仅有自己的简单处理方法(Text Rang)。

扩展阅读:

而 IE 下的 Text Rang 主要用来处理文本,并非 DOM 节点,那如何在 IE 下模拟 DOM Level2 中的 Range 呢?

根据规范的 API,我们需要模拟下述属性和方法:

function zRange() {
    // Inital states
    this.startContainer = document;
    this.startOffset = 0;
    this.endContainer = document;
    this.endOffset = 0;
    this.commonAncestorContainer = document; 
    this.collapsed = true; 

    // Range constants
    this.START_TO_START = 0;
    this.START_TO_END = 1;
    this.END_TO_END = 2;
    this.END_TO_START = 3;
}

zRange.prototype = {
    // Public methods
    setStart : function(node, offset){},
    setEnd : function(node, offset){},    
    setStartBefore : function(node){},
    setStartAfter : function(node){},
    setEndBefore : function(node){},
    setEndAfter : function(node){},
    collapse : function(toStart) {},
    selectNode : function(node) {},
    selectNodeContents : function(node){},
    deleteContents : function() {},
    extractContents : function(){},
    cloneContents : function() {},
    surroundContents : function () {},
    insertNode : function(node) {},
    cloneRange : function() {},
    detach : function() {},    
    compareBoundaryPoints : function (how, sourceRange) {},
    constructor : zRange
}

我们还可以看一组使用 Range 方法和属性的统计数据,对于 2/8 原则的实践或许有帮助:

/**
 * Resource reference : http://kb.operachina.com/node/147
 *
 * collapse             51,435 
 * setStartBefore       43,138 
 * setStartAfter        40,270 
 * selectNodeContents   37,027 
 * collapsed            12,862 
 * selectNode            4,636 
 * deleteContents        3,935 
 * setStart              3,171
 * startOffset           3,150
 * setEnd                3,086
 * detach                2,732 
 * startContainer        2,659
 * endOffset             2,647 
 * insertNode            2,321 
 * cloneContents         2,261 
 * endContainer          2,236 
 * cloneRange            1,993 
 * setEndAfter           1,911 
 *
 */

下面我们简单讨论一下 Range 的 insertNode() 方法的模拟实现:

The insertNode() method inserts the specified node into the Range’s context tree. The node is inserted at the start boundary-point of the Range, without modifying it.

If the start boundary point of the Range is in a Text node, the insertNode operation splits the Text node at the boundary point. If the node to be inserted is also a Text node, the resulting adjacent Text nodes are not normalized automatically; this operation is left to the application.

The Node passed into this method can be a DocumentFragment. In that case, the contents of the DocumentFragment are inserted at the start boundary-point of the Range, but the DocumentFragment itself is not. Note that if the Node represents the root of a sub-tree, the entire sub-tree is inserted.

从上面的引用得知 insertNode() 方法用来在选区的开头插入节点,固我们先获取 Range 对象的 startContainer(Range 是从那个节点中开始的,即选区中第一个节点的父节点) 和 startOffset(在 startContainer 中 Range 开始的偏移位置) 属性。

var sc = this.startContainer, 
    so = this.startOffset;

注: 如果 startContainer 是文本节点、注释节点或者是 CData 节点,startOffset 是指 Range 开始前的字符数,否则,偏移是 Range 中的第一个节点在其父节点中的索引。

接下来,我们需将情况分为下面两种:

第一种情形:startContainer 节点为文本节点、注释节点或者是 CData 节点。“startOffset 是指 Range 开始前的字符数”。

  • 如果 startOffset 等于 0,则表示 Range 是从 startContainer 起始位置开始,应将 node 插入到 startContainer 节点之前 sc.parentNode.insertBefore(node, sc);
  • 如果 startOffset 大于等于 startContainer 本身的节点长度,则表示 Range 是从 startContainer 末尾位置开始,应将 node 插入到 startContainer 节点之后,即如果存在下一节点,则插入到下一之前 sc.parentNode.insertBefore(node, sc.nextSibling);,如果不存在下一节点,则加入到 startContainer父节点最后 sc.parentNode.appendChild(node);
  • 如果 startOffset 在 startContainer 本身的节点长度之内,我们通过oSplitNode = object.splitText( [iIndex])将节点在startOffset 一分为二 nn = sc.splitText(so);,分为两个节点,则应将 node 插入到新生成的第二个节点之前 sc.parentNode.insertBefore(node, nn);

    The text node that invokes the splitText method has a nodeValue equal to the substring of the value, from zero to iIndex. The new text node has a nodeValue of the substring of the original value, from the specified index to the value length. Text node integrity is not preserved when the document is saved or persisted.

if (so===0) {
    // At the start of text
    sc.parentNode.insertBefore(node, sc);
} else if (so >= sc.nodeValue.length) {
    // At the end of text
    if (ns) {
        sc.parentNode.insertBefore(node, sc.nextSibling);
    } else {
        sc.parentNode.appendChild(node);
    }
} else {
    // Middle, need to split
    // http://msdn.microsoft.com/zh-cn/library/ms536764.aspx
    nn = sc.splitText(so);
    sc.parentNode.insertBefore(node, nn);
}

第二种情形:startContainer 节点为非 第一种情形中的节点。“偏移是 Range 中的第一个节点在其父节点中的索引”。获取 startContainer 中子节点偏移量为 startOffset 的节点 cn = sc.childNodes[so];,如果存在,则按照 insertNode 方法的定义,应将 node 插入到该节点之前 sc.insertBefore(node, cn);,如果不存在,即 startOffset 大于等于 sc.childNodes.length,则应将 node 插入到 startContainer 的子节点最后sc.appendChild(node);

if (sc.childNodes.length > 0) {
    cn = sc.childNodes[so];
}

if (cn) {
    sc.insertBefore(node, cn);
} else {
    sc.appendChild(node);
}

详细代码实现如下:

zRange.prototype.insertNode = function(node) {
    var sc = this.startContainer, 
        so = this.startOffset, 
        p = sc.parentNode,
        ns = sc.nextSibling,
        nn, cn;

    // 如果节点是 TEXT_NODE 或者 CDATA_SECTION_NODE
    if ((sc.nodeType === 3 || sc.nodeType === 4 || sc.nodeType === 8 ) && sc.nodeValue) {
        if (so === 0) {
            // At the start of text
            p.insertBefore(node, sc);
        } else if (so >= sc.nodeValue.length) {
            // At the end of text
            if (ns) {
                p.insertBefore(node, ns);
            } else {
                p.appendChild(node);
            }
        } else {
            // Middle, need to split
            // http://msdn.microsoft.com/zh-cn/library/ms536764.aspx
            nn = sc.splitText(so);
            p.insertBefore(node, nn);
        }
    } else {
        if (sc.childNodes.length > 0) {
            cn = sc.childNodes[so];
        }

        if (cn) {
            sc.insertBefore(node, cn);
        } else {
            sc.appendChild(node);
        }
    }
}

剩下的方法,大家可以尝试着去模拟一把,其实并不复杂,也许会其乐无穷,呵呵