iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Modern Web

Line Bot × NestJS:30 天開發日記系列 第 16

Day 16:Cloudflare Workers 圖片代理實作

  • 分享至 

  • xImage
  •  

2025 鐵人賽背景圖

前言

Day 15 文章中,我們處理了 ImageMap Message 的背景圖,接著我們遇到了圖片不能出現副檔名動態路徑支援不同解析度的問題。今天主要會透過 Cloudflare Workers 來解決這兩個問題,讓 Cloudflare Workers 達到圖片代理的作用。

ImageMap 處理流程涉及多個工具協作,整體步驟如下:

  1. Canva 創建 ImageMap 背景圖片:使用 Canva 設計並匯出 ImageMap 所需的背景圖片。
  2. Bot Designer 產生 JSON 資料:透過 Bot Designer 根據 Canva 創建的背景圖片產生 ImageMap 的 JSON 資料。由於 ImageMap 的參數設定較為複雜,建議使用 Bot Designer 來自動產生基礎結構。
  3. 處理 baseUrl 的限制問題:Bot Designer 產生的 JSON 中包含 baseUrl 參數,但直接使用 Cloudinary 的網址並不符合 LINE ImageMap 的規範。
    • 不能包含副檔名
    • 必須支援動態路徑存取五種不同解析度(240、300、460、700、1040)
  4. CloudFlare Worker 作為代理層:為了解決上述限制,我們需要建立 CloudFlare Worker 作為代理層,處理副檔名移除和動態解析度路由的問題。
  5. 手動調整 JSON 的 baseUrl:將 Bot Designer 產生的 JSON 中的 baseUrl 替換成 CloudFlare Worker 的端點網址。
  6. 完成部署:修改後的 JSON 即可直接應用於後端伺服器,成功發送 LINE ImageMap 訊息。

今天主要會說明 4 ~ 5 項。

本日程式碼的範例連結

CloudFlare Worker 圖片代理

CloudFlare Worker 是一個 Serverless 服務平台,以 Function 為部署單位,適合處理這類輕量級的 URL 轉換需求。

我們可以透過 Cloudflare Workers 來處理請求轉向。當系統接收到包含 public_idwidthimageType 路由參數組合的請求時,Worker 會自動將請求重新導向至對應解析度的 Cloudinary 圖片 URL。

Step 1:登入 CloudFlare 官網

登入後點選左側選單的「Workers」選項,接著點擊右側的「建立」按鈕開始建立新的 Worker。

CloudFlare Worker 建立

Step 2:選擇建立方式

這邊選擇使用 Hello World 建立方式

CloudFlare Worker 選擇 Hello World 建立方式

Step 3:設定 Worker 名稱

為 Worker 取一個有意義的名稱,這裡命名為linebot-imagemap,需要注意此命名會直接影響最終 Worker 服務的網址

設定 CloudFlare Worker 名稱

Step 4:完成部署並編輯代碼

系統顯示已成功部署 Worker,點選右上角的「編輯代碼」選項即可開始撰寫程式碼。

CloudFlare Worker 部署成功

Step 5:驗證 Worker 服務運行狀態

當頁面顯示「Hello World!」時,表示 Worker 服務已正常運行並能夠響應請求。

這邊注意 Worker 服務的網址格式為 https://{Worker名稱}.{帳戶名稱}.workers.dev/

CloudFlare Worker 顯示響應成功

Step 6:修改 Function 內容以符合圖片導向規則

Worker 需要支援 baseUrl/{image width} 的請求格式

目標是透過路由參數取得 Cloudinary 圖片讀取所需的參數:

  1. publicId:Cloudinary 識別特定圖片的唯一 ID,可從 Cloudinary 分享連結中取得
  2. imageType:圖片的副檔名格式
  3. width:解析度(LINE 平台會自動提供)

baseUrl 定義為 {Worker 服務網址}/{publicId}/{imageType} 的格式,讓 LINE 平台能夠將寬度參數附加在路由路徑上。

應用範例:
假設圖片 publicIdImageMap_02_escz9a,副檔名為 png

  • baseUrl:https://{Worker名稱}.{你的帳戶}.workers.dev/ImageMap_02_escz9a/png
  • LINE 呼叫時會自動加上寬度:https://{Worker名稱}.{你的帳戶}.workers.dev/ImageMap_02_escz9a/png/240

Worker Function 程式碼修改

export default {
  async fetch(request) {
    const url = new URL(request.url); // 解析請求 URL
    const pathSegments = url.pathname.split('/').filter(segment => segment !== '');
    // 解析請求網址取得路由參數 publicId、imageType 跟 width
    const [publicId, imageType, width] = pathSegments 
    const cloudinaryUrl = `https://res.cloudinary.com/dseg0uwc9/image/upload/w_${width},f_auto,q_auto/v1754466769/2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD/${publicId}.${imageType}`;
    return fetch(cloudinaryUrl);
  }
};

Step 7:重新佈署 worker

修改後要記得重新部署,看到部署成功才代表已成功更新 Worker Function 程式碼

CloudFlare Worker 儲存成功

Step 8:試著把路由參數補上,看能不能成功顯示 Cloudinary 圖片

在右側導覽列的當前 worker 網址後方補上對應的路由參數 publicIdimageTypewidth,即可看到對應解析度的圖片!

這樣一來,未來如果要替換其他 ImageMap 背景圖時,只需要修改 publicIdimageType 參數,就能快速製作出 LINE ImageMap Message 所需的 baseUrl

CloudFlare Worker 圖片代理測試

進階:加入參數驗證機制

為了避免非法請求造成問題,建議為路由參數加上驗證機制。由於 ImageMap 僅支援 jpegpng 格式,因此需要限制可接受的圖片類型。

Worker Function 完整程式碼

// 驗證函式
function validateParameters(publicId, imageType, width) {
  // 驗證參數完整性
  if (!publicId || !imageType || !width) {
    return 'Missing required parameters. Expected format: /{publicId}/{imageType}/{width}';
  }
  
  // 驗證 width 是否為有效數字
  const widthNum = parseInt(width);
  if (isNaN(widthNum) || widthNum <= 0) {
    return 'Width must be a positive number';
  }
  
  // 驗證圖片類型
  const validImageTypes = ['jpeg', 'png'];
  if (!validImageTypes.includes(imageType.toLowerCase())) {
    return `Invalid image type. Supported types: ${validImageTypes.join(', ')}`;
  }
  
  // 驗證 width 是否為 Line ImageMap 支援的尺寸
  const validWidths = [240, 300, 460, 700, 1040];
  if (!validWidths.includes(widthNum)) {
    return `Invalid width. Supported widths: ${validWidths.join(', ')}`;
  }
  
  return null; // 驗證通過
}

export default {
  async fetch(request) {
    const url = new URL(request.url); // 解析請求 URL
    const pathSegments = url.pathname.split('/').filter(segment => segment !== '');
    const [publicId, imageType, width] = pathSegments // 取得路由參數 publicId、imageType 跟 width
    
    const validationError = validateParameters(publicId, imageType, width);
    if (validationError) {
      return new Response(validationError, {
        status: 400,
        headers: { 'Content-Type': 'text/plain' }
      });
    }

    const cloudinaryUrl = `https://res.cloudinary.com/dseg0uwc9/image/upload/w_${width},f_auto,q_auto/v1754466769/2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD/${publicId}.${imageType}`;
    return fetch(cloudinaryUrl);
  }
};

至此,ImageMap 圖片的 baseUrl 部分已經準備就緒,接下來將功能整合到 line-message 模組中。

本日結語

ImageMap Message 在處理上相對其他訊息類型更為複雜,特別是在圖片 URL 的規格限制上。透過 CloudFlare Worker 搭配 Cloudinary 的組合,就可以輕鬆處理好 ImageMap 的圖片問題。

由於 ImageMap 和 FlexMessage 都屬於參數較多、設定較為複雜的訊息類型,建議在開發時搭配 GUI 工具來簡化設置流程,降低手動配置的負擔。


上一篇
Day 15:共通屬性處理優化及 Imagemap 圖片製作
下一篇
Day 17:ImageMap Message 從互動海報到影片嵌入
系列文
Line Bot × NestJS:30 天開發日記18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言