1. 程式人生 > >DOM練習小記--簡單的拼單詞遊戲

DOM練習小記--簡單的拼單詞遊戲

表現 erl one cli 每次 什麽 兩個 image sco

(資料源自《Head First Ajax》第七章)

書中另一個單頁遊戲案例,綜合了DOM和事件處理,先總結一下頁面布局和js編程的思路。涉及到服務端交互的暫時按下,畢竟還沒有自己把服務端環境搭起來。

1) 內容簡介

書中給的樣本頁面和樣式是布置好的,初始靜態頁面如下:

技術分享圖片

  1. 4row*4col布局,每次初始化都會隨機出現16個字母圖片;
  2. 點擊任意字母會把對應字母輸入到旁邊字母框中,每次提交前每個字母只能點擊一次;
  3. 需要拼出一個合法的單詞,OK後點擊submit提交(Ajax);
  4. 如果合法則在分數框計分數(元音+2,輔音+1),並把該單詞輸出在下面的單詞框中;如果不合法則提示單詞錯誤;
  5. 輸入框和字母盤刷新;

HTML/CSS

與拼圖遊戲的表格布局不同,這一次的布局使用了“<div>+CSS”,相比表格布局,CSS布局的優點是更靈活,更保持在所有瀏覽器中的一致性,並有利於優化和維護;

  <div id="letterbox">
   <a href="#" class="tile t11"></a>        
   <a href="#" class="tile t12"></a>        
   <a href="#" class="tile t13"></a>        
   <a href="#" class="tile t14"></a>
   ……

字母盤大概的布局像這樣重復的4列,通過class的通用類名和特殊類名能方便地設置樣式和定位;

對於每個方塊中的字母圖片也是在CSS中設置的背景:

#letterbox a.tile { background: url(‘../images/tiles.png‘) ……}
#letterbox a.la { background-position: 0px 0px; }
#letterbox a.lb { background-position: -80px 0px; }
#letterbox a.lc { background-position: -160px 0px; }
#letterbox a.ld { background-position: -240px 0px; }
#letterbox a.le { background-position: -320px 0px; }
#letterbox a.lf { background-position: -400px 0px; }
……

實際上為所有方塊中的背景加載的背景是同一張圖,只是通過為不同字母標誌的類名設置不同的顯示位置,就能顯示對應的字母圖像,而不必每次都為每個方塊去加載單個的圖像。

旁邊的輸入框和單詞收集框都是div,有各自id,在操作DOM增刪節點時非常方便。

2) 總體思路

  1. 通用方法:除需要添加window.onload事件的addLoadEvent和創建請求的createReqest之外,因為涉及到一個元素同時擁有多個類名,還需要一個在原有類名基礎上增加類名的addClassName方法。
  2. initPage, 初始化頁面時,字母盤中隨機生成16個字母,需要隨機生成數字並輸出對應字母的方法randomizeTiles; 並為每個字母(鏈接)和submit框綁定點擊事件處理函數;
  3. randomizeTiles, 關於隨機字母,書中的客戶給出了26個字母在100個字母中出現的頻率表,可以按照這個表初始化一個數組,然後用0~99之間的隨機數(Math.floor(Math.random()*100))作為索引值從中取出對應字母;
  4. addLetter, 點擊字母鏈接處理函數:首先找出所點擊圖片對應的字母;把該字母輸入到輸入框中,並禁用已點擊過的圖片鏈接;
  5. submitWord, 點擊提交框處理函數:確認輸入框不為空,將其內容發送請求到服務器確認是否合法,並設置回調函數updateScore處理服務器返回的響應;
  6. updateScore, 處理接收到的響應:確認是否合法,計算分數,把合法結果附加到單詞收集框並清空輸入框,最後刷新字母盤;

3) 代碼要點

  1. addClassName通用函數

最初是只接收元素和類名兩個參數,直接在原來的基礎上添加,但後來發現這樣會無休止地疊加下去,所以有必要把上次添加的相同功能的類名刪除掉再重新添加。每次刷新字母盤,上次刷新添加的字母類名,以及點擊之後添加的disabled類都可以刪掉。所以每次添加類名前只需要保留前兩個類名就可以了。我給addClassName增加了第三個可選參數number, 即添加前保留原類名的數量。

function addClassName(element,name,number) {
  var oldclass;
  if (typeof element.className !== "string") {
    oldclass = "";
  } else if (number !== undefined) { //未指定number;
    var classArr = element.className.split(/\s+/);
    classArr.length = number;
    oldclass = classArr.join(" ");
  } else {
    oldclass = element.className;
  }
  element.className = oldclass+" "+name;
}
  1. initPage初始化頁面

    addLoadEvent(initPage); //頁面加載完成後執行
    function initPage() {
      var letterbox = document.getElementById("letterbox");
      var letterlinks = letterbox.getElementsByTagName("a");
      var i, len = letterlinks.length;
      for (i=0; i < len; i++) {
    var letterlink = letterlinks[i];
    randomizeTiles(letterlink);
    letterlink.onclick = addLetter;
      }
      var submitbtn = document.getElementById("submit");
      submitbtn.onclick = submitWord;
    }

    在這裏因為生成隨機字母和綁定點擊事件都需要遍歷所有<a>元素,所以我讓randomizeTiles接收元素作為參數而把遍歷放在這個函數中,一次完成兩件工作,然後再為提交按鈕綁定事件。但是後來發現,當點擊提交按鈕之後,字母表盤需要刷新(並解除禁用重新綁定)而提交按鈕不需要再綁定,所以需要把字母盤和提交按鈕的點擊事件分開綁定,好單獨調用刷新字母盤的方法。最後我把刷新字母盤的工作都交給了randomizeTiles.

更改後

addLoadEvent(initPage);
function initPage() {
  randomizeTiles();
  var submitbtn = document.getElementById("submit");
  submitbtn.onclick = submitWord;
}
  1. randomizeTiles生成隨機字母盤

    function randomizeTiles() {
      var frequencyTable= new Array("a", "a", "a", "a", "a", "a", "a", "a", "b", "c", "c", "c", "d", "d", "d",
      "e", "e", "e", "e", "e", "e", "e", "e", "e", "e", "e", "e", "f", "f", "g",
      "g", "h", "h", "h", "h", "h", "h", "i", "i", "i", "i", "i", "i", "i", "j",
      "k", "l", "l", "l", "l", "m", "m", "n", "n", "n", "n", "n", "n", "o", "o",
      "o", "o", "o", "o", "o", "o", "p", "p", "q", "q", "q", "q", "q", "q", "r",
      "r", "r", "r", "r", "r", "s", "s", "s", "s", "s", "s", "s", "s", "t", "t",
      "t", "u", "u", "v", "v", "w", "x", "y", "y", "z");
      var letterbox = document.getElementById("letterbox"),
      letterlinks = letterbox.getElementsByTagName("a"),
      i, 
      len = letterlinks.length;
      for (i=0; i < len; i++) {
    var letterlink = letterlinks[i];
    var x = Math.floor(Math.random()*100);
    var classLetter = "l"+frequencyTable[x];
    addClassName(letterlink, classLetter, 2); 
    letterlink.onclick = addLetter;
      } 
    }
    其中數組frequencyTable是按照每個字母在100個字母中出現的頻數(“客戶”已提供)直接以字面量的形式創建。這樣對運行來說可能是最高效的。反而最開始我思考要用什麽方法通過循環復制生成數組更像是舍近求遠的做法。如果輸入費勁可以借助excel快速復制再整體復制文本過來,也會很方便。
  2. addLetter 點擊字母的事件處理

    function addLetter() {
      var currentWord = document.getElementById("currentWord"),
      p;
      if (currentWord.childNodes.length === 0) {
    p = document.createElement("p");
    currentWord.appendChild(p);
      } else {
    p = currentWord.firstChild;
      }
      var letter = this.className.slice(-1),
      letterText = document.createTextNode(letter);
      p.appendChild(letterText);
      //禁用該圖片
      this.onclick = null;
      addClassName(this, "disabled"); //用於改變圖片樣式
    }

    和書中不同的是,我覺得只在第一次輸入字母的時候創建<p>子元素節點就可以了,後面都可以只在<p>中增加文本節點。 相對來說代碼更簡單一點。

  3. submitWord 點擊提交

    function submitWord() {
      var currentWord = document.getElementById("currentWord");
      //如果為空則不提交
      if (currentWord.innerHTML == "" || currentWord.firstChild.innerHTML == "") {
    return false;
      } else {
    wordRequest = createRequest();
    if (wordRequest === null) {
      alert("Unable to create a request, sorry.")
      return false;
    } else {
      var url = "lookup-word.php?word="+currentWord.firstChild.innerHTML;
      wordRequest.onreadystatechange = updateScore;
      wordRequest.open("GET",url,true);
      wordRequest.send();
    }
      }
    }
  4. updateScore 處理響應

    function updateScore() {
      if (wordRequest.readyState == 4) {
    if (wordRequest.status == 0) {
      var currentWord = document.getElementById("currentWord"),
          resText = wordRequest.responseText;
      if (resText === "-1") {
        currentWord.innerHTML = "";
        alert("It\‘s not a proper word, please try again");
        randomizeTiles();
      } else if (!isNaN(resText)) {
        var wordList = document.getElementById("wordList"),
            p = currentWord.firstChild.cloneNode(true);
        wordList.appendChild(p);
        currentWord.firstChild.innerHTML = "";
        var score = document.getElementById("score"),
            oldscore = score.firstChild.nodeValue.slice(7);
            newscore = +oldscore + (+resText),
            scoreText = document.createTextNode("Score: "+newscore);
        score.replaceChild(scoreText, score.firstChild)
        randomizeTiles();
      } else {
        alert("Sorry, there is an error in the response.");
      }
    }
      }
    }

    這裏先判斷結果是否是-1(單詞不合法),然後再判斷是否是一個數值字符串(正常情況下只有這兩種情況);isNaN()這個方法是會嘗試對參數進行數值轉換,如果失敗則會返回true,表明參數不是可轉化為數值的類型。另外這裏我原先用innerHTML改變元素內容,後也采用操作節點的方式進行了修改。

總結

  1. 用到的DOM屬性和方法:
  • childNodes
  • appendChild()
  • replaceChild()
  • firstChild()
  • getElementById()
  • getElementsByTagName()
  • createElement()
  • createTextNode()
  1. 數組和字符串的方法:
  • string.slice(index), 字符串從index位置向後截取子字符串, 如果有第二個參數則是到哪個位置停止截取;

相似的還有substring(), substr()(第二個參數指定返回字符串個數),它們接收負值參數的表現不同,slice()可接受兩個負值參數;

  • string.split(), 把字符串基於指定的分隔符分割成若幹子字符串返回它們組成的數組, 並可以指定返回數組的長度(第二個參數);這個在addClassName中用於把原來的className分成子類名並利用指定array.length保留一定數量的類名;

這樣我可以把

    var classArr = element.className.split(/\s+/);
    classArr.length = number;

改成

    var classArr = element.className.split(/\s+/, number);

順便復習下長得比較像的splice()方法,這是數組方法,接收的參數為(要刪除的第一項位置, 要刪除的項數,從該位置要插入的項……);借此可以完成對數組任意項的刪除、替換以及插入任意項;返回它刪除的數組(如無則空數組);值得註意的是,它操作的是原始數組。

這樣上面的代碼還可以改成一步到位的:
var classArr = element.className.split(/\s+/); classArr.splice(number, 2, name); //可能還有"disabled"類名所以刪除2項;
不過這裏因為涉及到沒有指定number的情況,所以沒有改用這種方式。

  1. 最後,是編程的過程中,盡量減少重復的DOM操作,一次查詢或遍歷最好能把一串不沖突的動作都完成。涉及到累加( +=, push等相關) 要記得看是替換還是不停累加下去;

DOM練習小記--簡單的拼單詞遊戲