iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
自我挑戰組

從零開始全端實作 Express.js + TypeScript + DevOps 系列 第 12

【Day 12】JavaScript 介紹與基本語法、HTML Canvas

  • 分享至 

  • xImage
  •  

點我查看目錄

前言

我們前面花了一些時間討論關於 HTML 與 CSS 的相互操作,但目前網頁還沒有任何功能,因此我們現在就來開始做這件事情。而我希望透過一個簡單、但同時又可以讓各位快速看到網頁反映的內容來進行。幸運的是我偶然看到在 MDN 教學網站上有一個 2D Breakout Game,這難度剛好可以讓我們來寫遊戲的功能。但因為網站有提到需要具備一定 JavaSctipt 基礎,因此並不會從頭講述 JavaScript 語法,所以這次的內容我就會設計一下,讓內容更加容易上手一些。

什麼是 JavaScript


這就要回到 1995 年的矽谷,那時的 Netscape 公司跟 IE 開打瀏覽器戰爭。當年的 Netscape 就有一個很狂的目標,就是要稱霸整個網路界,那也伴隨一個在當時也很狂的想法,就是把網頁增加互動成分,變得不再只是靜態的樣子。而那公司內有一位剛被僱用的年輕工程師 Brendan Eich,就被公司高層賦予了一個幾乎不可能完成的任務:「用 10 天的時間做一個程式語言讓網頁可以有互動功能」

Brendan Eich 當時面臨的最大難題是:如何在極短的時間內設計出一個功能足夠強大、又足夠簡單的語言?他從三個不同的程式語言中汲取靈感,然後再把這些結合在一起,做出了 Mocha 這語言。

C 語言的結構

當時 C 語言非常普及,很多程式設計師都熟悉它的語法。所以,Brendan 決定從 C 語言中拿來一些基本結構,比如說用 {} 大括號來表示程式區塊、forwhile 迴圈來控制程式流程,還有 ; 來表示語句的結束。這樣,開發者一看就知道該怎麼寫,降低了學習門檻。

Scheme 的函數邏輯

當年還有一個程式語言叫做 Scheme,經常被用來處理函數邏輯。Brendan 想要 JavaScript 也能處理複雜的函數邏輯,所以他加入了 Scheme 的一些特性,特別是「閉包(Closure)」這個概念。這是什麼意思呢?簡單來說,閉包讓程式可以「記住」一個函數內部的變數,即使這個函數已經結束執行了,你還可以保留這些變數。想像一下,假設你去餐廳點餐,通常在結束點餐並取餐後,沒有人會記得你點了什麼。但是,如果有 Closure 的機制,就像餐廳記得你上次點了什麼菜一樣,你下次來的時候,餐廳服務生還能告訴你上次點的菜是什麼,這樣你就可以直接選擇「同樣的餐點」。這種機制就非常適合處理網頁上的事件(例如按鈕點擊)之類的行為,讓 JavaScript 能夠記住使用者的操作,然後即時回應給使用者。

Java 的 OOP 特性

當時,Java 具有很明確的 物件導向(Object-Oriented) 的特性(如類別 Class 的概念),在當時相當流行。Brendan 從中設計了一個物件(Object)系統,讓開發者可以像 Java 那樣把功能和資料包裝成一個的「物件」,但他並沒有把 Java 的類別系統原封不動地導入進來,因為他認為 Java 的類別結構實在過於繁瑣,處理簡單網頁事件根本就是殺機用牛刀啊。而 Brendan 同時引入了一個更簡單的系統,叫做「原型(Prototype)繼承」。這有點像是「模仿」的概念。想像你創造了一個「模範生」,其他學生都可以把這個模範生當作「範本」來學習(繼承)他的特性,但你不需要定義一整套繁瑣的學習規則。也因此就保留了物件導向的好處,又避免程式碼便得過度複雜。

Mocha → LiveScript → JavaScript

不久之後,Netscape 將 Mocha 這款「咖啡」改名為 LiveScript。然而,為了搭上 Java 這款「咖啡」語言的順風車,就與當時擁有 Java 版權的 Sun Microsystems 合作,將其定名為 JavaScript 了, 你一定要記得 Java 跟 JavaScript 還是很不一樣的。 而有時候你真的不知道、或難以想像到這一個商業行銷手段,就這樣摧生了一個你到哪都能看到的通用語言了。

以下我就不再講述 JavaScript 演進的歷史了,有興趣的各位可以從 EMCA 網站中找到相關的故事。

準備 JavaScript 實做小遊戲


在進入網頁遊戲開發的世界時,我們最好的起點就是純 JavaScript。這就就像是你要蓋一棟大樓,你得先了解如何用鋼筋跟水泥才能打造地基與建築物骨架。而在你掌握了 JavaScript 之後,你就能輕鬆運用各式各樣的「框架」,來為你的遊戲專案、或是網頁專案有更不一樣的呈現方式,這也是我們後續要做的事情。

但是,你同時也要知道這些框架就像是為工匠進行量身打造的工具,幫助你完成一些重複而無聊的工作(例如處理 UI 元件、遊戲邏輯的設置),讓開發過程可以更為輕鬆。但如果一開始就完全依賴這些工具,直到當某個功能出現問題時,你可能會直接不知道發生了什麼事情,因為你不知道框架的「機械運作」究竟出了什麼毛病。所以囉,馬步要紮得好,後面練功才會輕鬆。我也還在蹲馬步的過程中,大家一起加油吧!

HTML Canvas

我們在進入 JS 之前,要先做一些事情。我們到先到 ./pages/game.html 中進行一些修改如下:

  1. 先找到我們放遊戲的區塊,也就是 <h2>Game Area</h2> 所在的 <header> 之下,加入以下這段。這個 Canvas 顧名思義,就是處理 JS 繪圖的「畫布」區塊。那我們就會用 JS 控制讓這區塊呈現出你畫出來的內容。但具體要怎麼畫呢?待會會說到。

    <canvas id="game-canvas" width="800" height="600"></canvas>
    

    看起來會像這樣。

    ...
      <section id="game-area">
          <header>
              <h2>Game Area</h2>
              <p>Difficulty: <span id="difficulty-level"></span></p>
          </header>
          **<canvas id="game-canvas" width="800" height="600"></canvas>**
      </section>
    ...
    
  2. 而我們先在 HTML 底部附近加上 <script> </script> 的標記,像這樣:

            <section id="game-controls">
                <h3>Game Controls</h3>
    						...
            </section>
        </main>
    
        **<script> 
    		    // We will write JS here.
        </script>**
    </body>
    </html>
    

    OK,這樣大致準備就緒了,就可以開始了

一些 JS 基礎操作


以下就針對我們會用到的 JS 基礎操作進行說明,各位有興趣的話,可以在這些網站多學習:

1. https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures-v8/
2. https://www.w3schools.com/js/default.asp
3. 其他的可以參考這裡:https://www.upwork.com/resources/top-sites-for-online-education-and-learning#codecademy
  1. 了解變數的定義方式

    在 JavaScript 中,我們可以使用 letconst 關鍵字來定義變數。變數可以用來儲存數值、字串或物件等資料類型。例如

    let x = 50; // Define a variable x with **initial** value 50.
    let y = 100; // Define a variable y with **initial** value 100.
    
    const brickX = 1; // Define a variable brickX with a **fixed** value 1.
    

    這裡的 xy 可以理解為我們 Canvas 上的座標,之後我們會使用這些來定位圖形的位置。

    1. 你可能會發現還有 var,這主要差異在於「是否可以重複宣告」,主要是給函數或判斷式在 { } 外面也可以被存取。現在我們已經不推薦這個方式,都以 letconst 為主。

      if (true){
          var player1 = "Alice";
          let player2 = "Bob";
          const player3 = "Charlie";
      }
      
      console.log(player1); // show the player1 message: "Alice"
      console.log(player2); // ReferenceError:player2 is not defined
      console.log(player3); // ReferenceError:player3 is not defined
      
    2. let 與 const 的差異主要在於 能不能重新賦值。簡單來說:

      let x = 50;
      x = x + 10;
      console.log("x: ", x) // show the message of x: 60
      

      如果寫成這樣就錯了:

      const x = 50;
      x = x + 10; // Error: Attempt to assign to const or read-only variable.
      
  2. 進行數值操作

    我們在數值操作中,會使用「加等於」例如:x += dx 相當於 x = x + dx,用來更新 x 的值。讓我們來看看這樣做的效果:

    let dx = 2;
    let dy = -2;
    
    x += dx; // increase x by 2
    y += dy; // decrease y by 2
    

    更新後,x 會從 50 變成 52,y 則會從 100 變成 98。這樣的變化在動畫中會讓物體看起來像是往右上方移動。

  3. 函數的基本結構

    在 JavaScript 中,我們可以使用 function 關鍵字來定義一個函數。函數是一組可重複執行的程式碼,主要用來處理一些執行程式的邏輯結構。例如:

    function draw() {
      // Logic of drawing on canvas
    }
    
    draw();
    

    這個 draw() 函數目前還沒有執行任何動作,目前只是被定義出來,但如果我們之後調用這函數,例如寫上 draw();,就會執行 function draw() { … } 之中的 {} 區塊內的程式碼。

  4. 條件判斷式(ifelse-ifelse

    假設我們希望當 x 座標數值超過 600 或(||)低於 0 時,就要觸發「反彈」大概就是球撞牆要反彈的意思。我們就可以這樣呈現:

    if (x + dx > 600 || x + dx < 0) {
      dx = -dx; // Reverse the movement by adding minus sign.
    }
    

    若希望 Fancy 一些,就是超出右邊邊界或左邊邊界時,會分別跳出訊息,這樣就可以用 else if 依序檢查多個條件進行判斷:

    if (x + dx > 600) {
      dx = -dx;
      console.log("Hit the right wall");
    } else if (x + dx < 0) {
      dx = -dx;
    	console.log("Hit the left wall");
    }
    

    當所有 ifelse if 條件都不符合時,我們可以使用 else 來處理所有其他情況:

    if (x + dx > 600) {
      dx = -dx;
      console.log("Hit the right wall.");
    } else if (x + dx < 0) {
      dx = -dx;
    	console.log("Hit the left wall.");
    } else {
    	console.log("Exception Occurs.")
    }
    
  5. 執行重複邏輯 for

    當你要重複執行一個程式碼達到一定次數,可以使用 for 迴圈進行變數控制,就是透過設定 index 初始值、條件判斷與 index 更新,for 迴圈就能夠控制執行順序,並根據設定好的規則來遍歷(Iterate)你的資料。

    for (initial index value; condition; index counter) {
      // Program to execute.
    }
    

    舉例來說,你被女友懲罰要打字說 Sorry 重複 100 次,我們可以這樣寫:

    for (let i = 1; i <= 100; i++) {
      console.log(`${i}. I'm sorry.`);
    }
    

    這裡我們使用「模板字串(Template Literal)」,簡單來說就是一種做成一個字串格式的方式,只需要用 反引號(`)來包住你的字串,而不是用一般的雙引號 " 或單引號 '。這樣做可以讓 JS 將 ${} 中的變數替換為對應的值。

    或是你現在要計算遊戲總分,你定義了一個陣列,可以順便學一下陣列操作:

    let score = [30, 40, 15, 25, 36];
    let total = 0;
    
    for (let i = 0; i < scores.length; i++) {
      total += scores[i];
    }
    
    console.log(`Total Score is ${total}.`);
    

    其中 .length 是取出該物件的「長度」,像是以上 score.length 的值就會是 5。而在 for 迴圈內的陣列中的 i 是 index,你也可以換成 jk 等你想要的變數。而 score[0] 會是 30、score[3] 是 25,你要記得這些程式語言是從 0 開始數的。

  6. 相對簡潔的 forEach

    如果你要檢查每一個元素,且你不會在迴圈中途中斷,就可以使用 forEach 把整體變得較為簡潔。像是:

    let score = [30, 40, 15, 25, 36];
    let total = 0;
    
    score.forEach(function(score) {
      total += score;
    });
    
    console.log(`Total Score is ${total}.`); // Total Score is 146.
    

    你會發現這結構不太一樣,這就跟前面提到的「物件」有很大的關係。forEach 之所以能用「方法(Method)」的形式來呼叫,是因為這本身就是 JavaScript 陣列物件(Array Object)的一部分,也就是說,你只有在處理陣列的時候才會用到 forEach。結構如下:

    array.forEach(function(element, index, array) {
      // Program to execute.
    });
    
    let score = [30, 40, 15, 25, 36];
    
    score.forEach(function(number) {
      if (number > 35) {
        // Escape from the function without interript the forEach loop.
        return;
      }
      console.log(number); // Output: 30, 15, and 25
    });
    
  7. 使用 getElementById 選取 HTML 元素

    我們知道 JavaScript 就是要來控制網頁互動的,所以 JS 就會有一些能夠選取和操作 HTML 頁面上元素的方法。在這段程式碼中,我們選取了一個 <canvas> 元素:

    const canvas = document.getElementById("game-canvas");
    

    這行程式碼是根據 HTML 元素的 id 屬性(這裡是 "myCanvas")來選取畫布元素,並將它賦值給變數 canvas。如果我們在 ./pages/game/html 中把 canvas 印出來,就會看到這樣的內容:

    		          ... 
                <header>
    						  ...
                </header>
                <canvas id="game-canvas" width="800" height="600"></canvas>
            </section>
            ...
        <script>
            const canvas = document.getElementById("game-canvas");
            console.log("canvas: \n", canvas)
        </script>
    </body>
    </html>
    

    你可以在瀏覽器的 Console 裡面看到 canvas 變數就是裝這些內容:

    <canvas id="game-canvas" width="800" height="600"></canvas>
    

    一個很重要的事情是,Canvas 的起始座標在左上角,意思是最左上角的座標是 (0, 0),而在最右下角則是 ({canvas.width}, {canvas.height})。這跟你平常學的 Cartesian 座標有點差異,但還算好理解。

  8. 取得 2D 繪圖的上下文

    canvas 元素本身只是網頁中的一個空白畫布,就像一張什麼都還沒畫的紙一樣。要在這張紙上畫圖,我們需要有一個「畫家」來進行操作;而「繪圖上下文(Context)」會提供這個「畫家」的工具與能力。

    const ctx = canvas.getContext("2d");
    

    在這行程式碼中,我們傳入了 "2d" 作為參數,這表示我們要獲取一個「2D 繪圖上下文」。而我們使用 const 的原因是因為這變數的 Reference 不能被改變,不然突然改掉上下文就會造成整組就大亂了(想像你在讀物理課本,結果突然被換成歷史課本)。而後這行程式碼會返回一個繪圖上下文物件 ctx,這物件就有許多繪圖的方法,像是 arc()fill() 等,我們接下來舊會開始介紹。

  9. 使用方法繪製圖形

    這段程式碼中使用了 arc() 方法來繪製圓形:

    ctx.arc(x, y, 10, 0, Math.PI * 2);
    

    這個方法有五個參數:

    • x, y:圓心的座標。
    • 10:圓的半徑。
    • 0, Math.PI * 2:繪製的起始角度和結束角度(這裡是完整的 360 度圓形)。
  10. 操作屬性來更改繪圖顏色

    接著,我們可以透過修改 fillStyle 屬性來改變填充顏色:

    ctx.fillStyle = "#0095DD";
    

    這行程式碼是把 ctxfillStyle 屬性設為藍色(#0095DD),接著我們可以用 fill() 來填充這個顏色。

  11. 基本圖形繪製

    draw() 函數中,我們首先會開始一個新的路徑,然後用 arc() 繪製圓形:

    ctx.beginPath(); // Start the drawing path
    ctx.arc(x, y, 10, 0, Math.PI * 2); // Draw an arc (circle).
    ctx.fill(); // Fill the colour
    ctx.closePath(); // Close the drawing path
    

    這段程式碼會讓圓形出現在指定的 xy 座標地方。同時要注意的是,調用 ctx 必須要先告訴電腦說要從哪裡「開始一個新的繪圖路徑」與「「結束當前繪圖路徑」,而中間就是畫出圖形所需要執行的步驟。因為這些 ctx 這個 Method 之中的所有 Function 都是被呼叫的,因此呼叫的順序就很重要了!

  12. 使用 setInterval 來控制動畫

    動畫的核心原理就是不斷地重繪圖形,讓圖形在畫布上移動。這段程式碼中的 setInterval 會以 10 毫秒的間隔調用 draw() 函數,並根據 xy 的變化來重新繪製圓形,達到動畫效果。

    setInterval(draw(), 10);
    
  13. 使用變數更新座標位置

    在每次調用 draw() 時,程式碼會更新 xy 的值,這樣圓形就會在畫布上「移動」:

    const canvas = document.getElementById("myCanvas");
    const ctx = canvas.getContext("2d");
    
    let x = canvas.width / 2; // The ball starts at the middle of the canva
    let y = canvas.height - 30;
    let dx = 2;
    let dy = -2;
    
    function drawCircle() {
      ctx.beginPath();
      ctx.arc(x, y, 10, 0, Math.PI * 2);
      ctx.fill();
      ctx.closePath();
    }
    
    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
      drawCircle();
    
      // Determine the status
      if (x + dx > canvas.width || x + dx < 0) {
        dx = -dx;
      }
      if (y + dy > canvas.height || y + dy < 0) {
        dy = -dy;
      }
    
      // Update the coordinate
      x += dx;
      y += dy;
    }
    
    setInterval(draw, 10);
    

    這樣做的結果是,每次 xy 的值變化後,圓形的位置也會跟著變化,這樣就達到動畫移動的效果囉!

總結


總之,以上就是我們會用到的所有 JavaScript 的語法,接下來就是針對這些進行活用。下一章就會開始逐項進入遊戲元件、如磚頭、球…等的設計環節。我想整個專案會變得越來越有趣,畢竟我們可以看到一些會動的東西。然而,你就會發現這些語法就可以做很多事情了,我們常用的技術不外乎就是這些的應用。但一個設計網頁的高手是一直在提昇自己的 JavaScript 技術的,所以我們還得持續迭代自己的技術。當然你還可能會聽到比較「優雅」的 CoffeeScript、純函數的 PureScript、以及強型別的 TypeScript。這些都是 JavaScript 的親家,但理念都不太一樣。先預告一下,後續我會把這些東西融合 TypeScript,因為有時候隨著專案擴大時,就需要額外的機制來處理,這就留到後面再說了~


上一篇
【Day 11】CSS 歷史與語法介紹(3)CSS 3 與設計打磚小蜜蜂遊戲的外觀與界面
系列文
從零開始全端實作 Express.js + TypeScript + DevOps 12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言