iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Modern Web

從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器系列 第 25 篇

DAY 25|資料備份:資料主權歸於使用者,實作 CSV 報表匯出功能

  • 分享至 

  • xImage
  •  

在前面的文章中,我們實現了以 IndexedDB 做為本地儲存的 Local-First 架構。

但對於一個「不收集個人資料、資料存在使用者本地裝置」的工具來說,光是把資料存放在瀏覽器裡還不夠。萬一使用者更換電腦、清除瀏覽器快取,或是想把紀錄拿去做其他用途呢?

因此,我希望可以有資料匯出功能,讓使用者可以備份相關紀錄,所以接下來就要來加上 CSV 報表匯出 功能。


資料結構轉化的挑戰:陣列扁平化

TaskTimer 的原始資料結構是有層級關係的:

主任務(Task)→ 子任務(Subtask) → 計時紀錄(Records)

但是,試算表軟體(如 Microsoft Excel、Google Sheets)讀取的 CSV 檔案是單層的橫列表格。

因此,我們需要進行 資料扁平化(Data Flattening) :將每一筆「計時紀錄」連同它所屬的主任務名稱與子任務名稱,拆解組合為獨立的一列資料。


匯出 CSV 的三個關鍵細節

在用原生 JavaScript 寫 CSV 匯出時,有三個地方需要特別注意:

1. UTF-8 BOM 標頭(\uFEFF):

如果直接輸出 UTF-8 編碼的中文文字,Microsoft Excel 開啟時常會出現整頁 中文亂碼。在字串開頭加上 \uFEFF(Byte Order Mark)可以強制引導 Excel 以 UTF-8 正確解析中文字元。

2. 特殊字元跳脫(CSV Escaping):

使用者的「備註」或「任務名稱」中可能包含逗號 ,、雙引號 " 或換行符號 \n。若沒有處理,會導致 CSV 欄位錯位拆分。
規則是:只要欄位內含有特殊字元,就必須將整段內容用雙引號包起來,並把內部的 " 替換為 ""。

3. 動態下載觸發(Blob & Object URL):

利用原生 Blob 物件將文字轉換為二進位檔案,並建立臨時的 <a download> 標籤觸發瀏覽器下載,下載完成後及時釋放記憶體(URL.revokeObjectURL)。


實作 CSV 匯出邏輯

實作 CSV 匯出的核心邏輯可拆解為三個步驟:格式防錯、資料扁平化 與 觸發下載。

1. 處理 CSV 欄位跳脫(Escape)

當欄位內容含有逗號 ,、雙引號 " 或換行符號 \n 時,必須用雙引號將內容包起來,並把內部的 " 替換為 "",避免試算表解析欄位錯亂:

function escapeCSVField(str) {
  if (typeof str !== "string") return str;
  if (str.includes(",") || str.includes('"') || str.includes("\n")) {
    return `"${str.replace(/"/g, '""')}"`;
  }
  return str;
}

2. 將嵌套資料「扁平化」展開

走訪 tasks 陣列,把主任務、子任務與計時紀錄展開組合成單一維度的橫列字串陣列:

// 1. 定義 CSV 標頭
const headers = ["主任務名稱", "子任務名稱", "子任務狀態", "計時時間區間", "專注時間(分鐘)", "備註"];
const csvRows = [headers.join(",")];

// 2. 展開三層嵌套結構
tasks.forEach((task) => {
  task.subtasks?.forEach((sub) => {
    // 遍歷每一筆計時紀錄並轉為 CSV 行
    sub.records?.forEach((rec) => {
      csvRows.push([
        escapeCSVField(task.title),
        escapeCSVField(sub.title),
        sub.status === "completed" ? "已完成" : "進行中",
        escapeCSVField(rec.timeRange || "-"),
        rec.durationMinutes || 0,
        escapeCSVField(rec.note || "")
      ].join(","));
    });
  });
});

3. 加入 UTF-8 BOM 並觸發瀏覽器下載

在字串最前方加上 \uFEFF 標頭防範 Excel 中文亂碼,最後利用原生 Blob 與動態 <a> 標籤觸發下載:

// 3. 組合內容字串,加入 \uFEFF 防範 Excel 中文亂碼
const csvString = "\uFEFF" + csvRows.join("\n");

// 4. 建立 Blob 物件與動態下載連結
const blob = new Blob([csvString], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);

const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.setAttribute("download", `TaskTimer_Backup_${new Date().toISOString().split("T")[0]}.csv`);
document.body.appendChild(downloadLink);

// 5. 觸發下載並釋放記憶體
downloadLink.click();
document.body.removeChild(downloadLink);
URL.revokeObjectURL(url);

成果驗證

點擊頁面上的 「下載 CSV 檔案」 按鈕後,瀏覽器會自動下載檔名如 TaskTimer_Backup_2026-09-27.csv 的檔案。

開啟後,可以獲得結構完整、中文呈現正常的統計報表:

下載 CSV 檔案


上一篇
DAY 24|Local-First 架構(三):整合資料到 IndexedDB
系列文
從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言