第六章:整合到網站

在前面的章節中,我們已經學習了 Blockly 的基本概念、安裝設置、使用方法、自定義積木以及程式碼生成。本章將探討如何將 Blockly 整合到現有的網站中,包括如何處理事件、儲存和讀取積木配置,以及響應式設計考量等方面。

將 Blockly 嵌入網頁

將 Blockly 嵌入到網頁中是一個相對簡單的過程,但需要注意一些細節以確保良好的使用者體驗。

基本嵌入步驟

以下是將 Blockly 嵌入網頁的基本步驟:

1. 引入 Blockly 庫:在 HTML 頁面中引入必要的 Blockly 腳本檔案。
2. 創建容器元素:為 Blockly 工作區創建一個容器元素。
3. 初始化 Blockly 工作區:使用 `Blockly.inject()` 函數初始化工作區。
4. 配置工具箱:定義工具箱的內容和結構。
5. 處理事件:設置事件監聽器以響應使用者操作。

完整的嵌入示例

以下是一個完整的示例,展示如何將 Blockly 嵌入到網頁中:




  
  
  Blockly 整合示例
  


  

Blockly 整合示例

JavaScript 程式碼

Python 程式碼

事件處理

Blockly 提供了豐富的事件系統,允許您監聽和響應使用者的操作。

常見事件類型

以下是一些常見的 Blockly 事件類型:

- Blockly.Events.BLOCK_CHANGE:積木屬性變更(如字段值)。
- Blockly.Events.BLOCK_CREATE:創建新積木。
- Blockly.Events.BLOCK_DELETE:刪除積木。
- Blockly.Events.BLOCK_MOVE:移動積木。
- Blockly.Events.VAR_CREATE:創建新變數。
- Blockly.Events.VAR_DELETE:刪除變數。
- Blockly.Events.VAR_RENAME:重命名變數。
- Blockly.Events.UI:使用者界面事件(如選擇積木)。

監聽事件

您可以使用 `workspace.addChangeListener()` 方法來監聽工作區事件:

workspace.addChangeListener(function(event) {
  // 檢查事件類型
  if (event.type === Blockly.Events.BLOCK_CHANGE) {
    console.log('積木變更:', event);
  } else if (event.type === Blockly.Events.BLOCK_CREATE) {
    console.log('積木創建:', event);
  } else if (event.type === Blockly.Events.BLOCK_DELETE) {
    console.log('積木刪除:', event);
  } else if (event.type === Blockly.Events.BLOCK_MOVE) {
    console.log('積木移動:', event);
  }
  
  // 更新程式碼顯示
  updateCodeDisplay();
});

function updateCodeDisplay() {
  const jsCode = Blockly.JavaScript.workspaceToCode(workspace);
  document.getElementById('jsCode').textContent = jsCode || '// 沒有積木';
  
  const pythonCode = Blockly.Python.workspaceToCode(workspace);
  document.getElementById('pythonCode').textContent = pythonCode || '# 沒有積木';
}

自定義事件

您也可以創建和觸發自定義事件:

// 創建自定義事件類
class CustomEvent extends Blockly.Events.Abstract {
  constructor(block) {
    super(block);
    this.type = 'custom_event';
  }
  
  // 實現必要的方法
  toJson() {
    const json = super.toJson();
    return json;
  }
  
  fromJson(json) {
    super.fromJson(json);
  }
}

// 註冊自定義事件類
Blockly.Events.registerClass(CustomEvent.prototype.type, CustomEvent);

// 觸發自定義事件
function triggerCustomEvent(block) {
  const event = new CustomEvent(block);
  Blockly.Events.fire(event);
}

儲存與讀取功能

在實際應用中,我們通常需要儲存使用者創建的積木配置,以便稍後載入。Blockly 提供了多種方式來序列化和反序列化工作區狀態。

使用 XML

XML 是 Blockly 傳統的序列化格式:

// 儲存工作區到 XML
function saveWorkspaceToXml() {
  const xmlDom = Blockly.Xml.workspaceToDom(workspace);
  const xmlText = Blockly.Xml.domToText(xmlDom);
  
  // 儲存到本地存儲
  localStorage.setItem('blocklyWorkspace', xmlText);
  
  // 或者儲存到伺服器
  fetch('/save-workspace', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ xml: xmlText }),
  })
  .then(response => response.json())
  .then(data => {
    console.log('儲存成功:', data);
  })
  .catch(error => {
    console.error('儲存失敗:', error);
  });
}

// 從 XML 載入工作區
function loadWorkspaceFromXml() {
  // 從本地存儲載入
  const xmlText = localStorage.getItem('blocklyWorkspace');
  if (xmlText) {
    workspace.clear();
    const xmlDom = Blockly.Xml.textToDom(xmlText);
    Blockly.Xml.domToWorkspace(xmlDom, workspace);
  }
  
  // 或者從伺服器載入
  fetch('/load-workspace')
  .then(response => response.json())
  .then(data => {
    if (data.xml) {
      workspace.clear();
      const xmlDom = Blockly.Xml.textToDom(data.xml);
      Blockly.Xml.domToWorkspace(xmlDom, workspace);
    }
  })
  .catch(error => {
    console.error('載入失敗:', error);
  });
}

使用 JSON

JSON 是 Blockly 較新的序列化格式,提供了更多的靈活性:

// 儲存工作區到 JSON
function saveWorkspaceToJson() {
  const json = Blockly.serialization.workspaces.save(workspace);
  
  // 儲存到本地存儲
  localStorage.setItem('blocklyWorkspaceJson', JSON.stringify(json));
  
  // 或者儲存到伺服器
  fetch('/save-workspace-json', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ workspace: json }),
  })
  .then(response => response.json())
  .then(data => {
    console.log('儲存成功:', data);
  })
  .catch(error => {
    console.error('儲存失敗:', error);
  });
}

// 從 JSON 載入工作區
function loadWorkspaceFromJson() {
  // 從本地存儲載入
  const jsonText = localStorage.getItem('blocklyWorkspaceJson');
  if (jsonText) {
    const json = JSON.parse(jsonText);
    workspace.clear();
    Blockly.serialization.workspaces.load(json, workspace);
  }
  
  // 或者從伺服器載入
  fetch('/load-workspace-json')
  .then(response => response.json())
  .then(data => {
    if (data.workspace) {
      workspace.clear();
      Blockly.serialization.workspaces.load(data.workspace, workspace);
    }
  })
  .catch(error => {
    console.error('載入失敗:', error);
  });
}

響應式設計考量

在現代網頁設計中,響應式設計是非常重要的,以確保您的應用程式在各種設備上都能良好運行。

自適應工作區大小

Blockly 工作區的大小應該能夠適應不同的螢幕尺寸:

// 監聽視窗大小變化
window.addEventListener('resize', function() {
  // 調整 Blockly 工作區大小
  Blockly.svgResize(workspace);
});

// 初始調整大小
Blockly.svgResize(workspace);

使用 CSS 媒體查詢

使用 CSS 媒體查詢來調整佈局:

/* 桌面佈局 */
.content {
  display: flex;
  flex-direction: row;
}

#blocklyDiv {
  flex: 2;
  height: 100%;
}

.output {
  flex: 1;
  padding: 20px;
}

/* 平板和手機佈局 */
@media (max-width: 768px) {
  .content {
    flex-direction: column;
  }
  
  #blocklyDiv {
    height: 60vh;
  }
  
  .output {
    height: 40vh;
    overflow: auto;
  }
}

觸控支援

確保您的 Blockly 應用程式在觸控設備上也能良好運行:

// 在初始化 Blockly 工作區時啟用觸控支援
const workspace = Blockly.inject('blocklyDiv', {
  toolbox: toolbox,
  // 其他配置...
  touch: {
    longPressDelay: 750,  // 長按延遲(毫秒)
    scrollDelay: 500,     // 滾動延遲(毫秒)
    tapDelay: 250         // 點擊延遲(毫秒)
  }
});

實際案例:創建一個互動式教學平台

讓我們通過一個實際案例來綜合應用上述知識,創建一個互動式的 Blockly 教學平台,它包含多個教學關卡,每個關卡都有特定的任務和目標。

HTML 結構




  
  
  Blockly 教學平台
  


  

Blockly 教學平台

關卡 1:基本操作

在這個關卡中,您需要使用基本積木來完成一個簡單的任務:計算兩個數字的和,並顯示結果。

程式碼

輸出

CSS 樣式

/* styles.css */
html, body {
  height: 100%;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

.container {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.header {
  background-color: #f5f5f7;
  padding: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.level-selector {
  display: flex;
  align-items: center;
}

.level-selector select {
  margin-left: 10px;
  padding: 5px;
  border-radius: 5px;
  border: 1px solid #ccc;
}

.content {
  display: flex;
  flex: 1;
}

.task-panel {
  width: 250px;
  padding: 20px;
  background-color: #f9f9f9;
  overflow: auto;
}

#blocklyDiv {
  flex: 1;
  height: 100%;
}

.output-panel {
  width: 300px;
  padding: 20px;
  background-color: #f9f9f9;
  overflow: auto;
}

.code {
  white-space: pre-wrap;
  font-family: monospace;
  background-color: #f0f0f0;
  padding: 10px;
  border-radius: 5px;
  margin-bottom: 20px;
}

.output {
  background-color: #fff;
  padding: 10px;
  border-radius: 5px;
  border: 1px solid #ddd;
  min-height: 100px;
  margin-bottom: 20px;
}

button {
  padding: 8px 16px;
  background-color: #0066cc;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  margin-right: 10px;
  margin-bottom: 10px;
}

button:hover {
  background-color: #0055aa;
}

.task-buttons {
  margin-top: 20px;
}

.footer {
  background-color: #f5f5f7;
  padding: 10px;
  text-align: center;
}

/* 響應式設計 */
@media (max-width: 1024px) {
  .content {
    flex-direction: column;
  }
  
  .task-panel, .output-panel {
    width: auto;
    max-height: 200px;
  }
  
  #blocklyDiv {
    height: 50vh;
  }
}

JavaScript 代碼

// script.js
document.addEventListener('DOMContentLoaded', function() {
  // 定義關卡配置
  const levels = {
    1: {
      title: '關卡 1:基本操作',
      description: '在這個關卡中,您需要使用基本積木來完成一個簡單的任務:計算兩個數字的和,並顯示結果。',
      toolbox: {
        kind: 'categoryToolbox',
        contents: [
          {
            kind: 'category',
            name: '數學',
            colour: '%{BKY_MATH_HUE}',
            contents: [
              { kind: 'block', type: 'math_number' },
              { kind: 'block', type: 'math_arithmetic' }
            ]
          },
          {
            kind: 'category',
            name: '輸出',
            colour: '#5C81A6',
            contents: [
              {
                kind: 'block',
                type: 'text_print'
              }
            ]
          }
        ]
      },
      checkFn: function(code) {
        // 檢查是否使用了數學運算和輸出
        return code.includes('console.log') && code.includes('+');
      },
      hint: '嘗試使用數學積木來計算兩個數字的和,然後使用輸出積木來顯示結果。'
    },
    2: {
      title: '關卡 2:條件判斷',
      description: '在這個關卡中,您需要使用條件判斷積木來檢查一個數字是否大於 10,如果是,則輸出「大於 10」,否則輸出「不大於 10」。',
      toolbox: {
        kind: 'categoryToolbox',
        contents: [
          {
            kind: 'category',
            name: '邏輯',
            colour: '%{BKY_LOGIC_HUE}',
            contents: [
              { kind: 'block', type: 'controls_if' },
              { kind: 'block', type: 'logic_compare' },
              { kind: 'block', type: 'logic_operation' },
              { kind: 'block', type: 'logic_negate' },
              { kind: 'block', type: 'logic_boolean' }
            ]
          },
          {
            kind: 'category',
            name: '數學',
            colour: '%{BKY_MATH_HUE}',
            contents: [
              { kind: 'block', type: 'math_number' }
            ]
          },
          {
            kind: 'category',
            name: '文字',
            colour: '%{BKY_TEXTS_HUE}',
            contents: [
              { kind: 'block', type: 'text' }
            ]
          },
          {
            kind: 'category',
            name: '輸出',
            colour: '#5C81A6',
            contents: [
              {
                kind: 'block',
                type: 'text_print'
              }
            ]
          }
        ]
      },
      checkFn: function(code) {
        // 檢查是否使用了條件判斷和輸出
        return code.includes('if') && code.includes('console.log') && code.includes('>');
      },
      hint: '使用「如果」積木來檢查一個數字是否大於 10,然後在不同的條件下使用輸出積木。'
    },
    // 更多關卡...
  };

  let currentLevel = 1;
  let workspace;

  // 初始化 Blockly 工作區
  function initBlockly() {
    if (workspace) {
      workspace.dispose();
    }

    const level = levels[currentLevel];
    
    workspace = Blockly.inject('blocklyDiv', {
      toolbox: level.toolbox,
      grid: {
        spacing: 20,
        length: 3,
        colour: '#ccc',
        snap: true
      },
      zoom: {
        controls: true,
        wheel: true,
        startScale: 1.0,
        maxScale: 3,
        minScale: 0.3,
        scaleSpeed: 1.2
      },
      trashcan: true,
      scrollbars: true,
      sounds: true
    });

    // 監聽工作區變化事件
    workspace.addChangeListener(function(event) {
      if (event.type === Blockly.Events.BLOCK_CHANGE || 
          event.type === Blockly.Events.BLOCK_CREATE || 
          event.type === Blockly.Events.BLOCK_DELETE || 
          event.type === Blockly.Events.BLOCK_MOVE) {
        
        // 生成 JavaScript 程式碼
        const code = Blockly.JavaScript.workspaceToCode(workspace);
        document.getElementById('codeDiv').textContent = code || '// 沒有積木';
      }
    });

    // 調整工作區大小
    Blockly.svgResize(workspace);
  }

  // 載入關卡
  function loadLevel(levelNum) {
    currentLevel = levelNum;
    const level = levels[currentLevel];
    
    document.getElementById('levelTitle').textContent = level.title;
    document.getElementById('levelDescription').textContent = level.description;
    document.getElementById('levelSelect').value = currentLevel;
    
    initBlockly();
    document.getElementById('codeDiv').textContent = '// 沒有積木';
    document.getElementById('outputDiv').textContent = '';
  }

  // 初始載入第一關
  loadLevel(1);

  // 關卡選擇事件
  document.getElementById('levelSelect').addEventListener('change', function() {
    loadLevel(parseInt(this.value));
  });

  // 重置按鈕事件
  document.getElementById('resetButton').addEventListener('click', function() {
    workspace.clear();
    document.getElementById('codeDiv').textContent = '// 沒有積木';
    document.getElementById('outputDiv').textContent = '';
  });

  // 檢查按鈕事件
  document.getElementById('checkButton').addEventListener('click', function() {
    const code = Blockly.JavaScript.workspaceToCode(workspace);
    const level = levels[currentLevel];
    
    if (level.checkFn(code)) {
      alert('恭喜!您已完成這個關卡的任務。');
      
      // 如果有下一關,詢問是否前進
      if (levels[currentLevel + 1]) {
        if (confirm('是否前進到下一關?')) {
          loadLevel(currentLevel + 1);
        }
      }
    } else {
      alert('任務尚未完成,請再試一次。');
    }
  });

  // 提示按鈕事件
  document.getElementById('hintButton').addEventListener('click', function() {
    const level = levels[currentLevel];
    alert(level.hint);
  });

  // 運行按鈕事件
  document.getElementById('runButton').addEventListener('click', function() {
    const code = Blockly.JavaScript.workspaceToCode(workspace);
    const outputDiv = document.getElementById('outputDiv');
    
    if (!code) {
      outputDiv.textContent = '請先添加一些積木';
      return;
    }
    
    // 清空輸出區域
    outputDiv.textContent = '';
    
    // 重定向 console.log 到輸出區域
    const originalConsoleLog = console.log;
    console.log = function() {
      const output = Array.from(arguments).join(' ');
      outputDiv.textContent += output + '\n';
    };
    
    try {
      // 運行程式碼
      eval(code);
    } catch (e) {
      outputDiv.textContent += '運行錯誤: ' + e.message;
    } finally {
      // 恢復原始的 console.log
      console.log = originalConsoleLog;
    }
  });

  // 監聽視窗大小變化
  window.addEventListener('resize', function() {
    Blockly.svgResize(workspace);
  });
});

最佳實踐與注意事項

在將 Blockly 整合到網站時,以下是一些最佳實踐和注意事項:

性能優化:對於複雜的工作區,考慮使用虛擬化技術或分頁加載來優化性能。

安全性考慮:在運行生成的程式碼時,使用沙箱環境或其他安全機制,特別是在處理用戶生成的程式碼時。

可訪問性:確保您的 Blockly 實現考慮到可訪問性,包括鍵盤導航、屏幕閱讀器支持和高對比度模式等。

國際化:如果您的網站需要支援多種語言,使用 Blockly 的國際化功能來提供翻譯。

備份和恢復:實現自動保存和恢復功能,避免使用者丟失工作。

漸進式增強:設計您的應用程式,使其在不支援 JavaScript 或有限制的環境中仍能提供基本功能。

測試:在不同的瀏覽器和設備上徹底測試您的 Blockly 實現,確保它在各種環境中都能正常工作。

在本章中,我們探討了如何將 Blockly 整合到網站中,包括基本嵌入步驟、事件處理、儲存與讀取功能,以及響應式設計考量。我們還通過一個實際案例展示了如何創建一個互動式的 Blockly 教學平台。

Blockly 是一個強大而靈活的工具,可以為您的網站添加視覺化程式設計功能。通過本教學,您應該已經掌握了使用 Blockly 的基本知識和技能,能夠開始在自己的專案中實現視覺化程式設計。

無論您是創建教育工具、遊戲開發平台,還是其他需要視覺化程式設計的應用程式,Blockly 都能為您提供一個強大的基礎。希望本教學能夠幫助您充分利用 Blockly 的潛力,創建出令人驚嘆的視覺化程式設計體驗。