<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>資料分頁 &#8211; 小豬日常</title>
	<atom:link href="https://piglife.tw/tag/%E8%B3%87%E6%96%99%E5%88%86%E9%A0%81/feed/" rel="self" type="application/rss+xml" />
	<link>https://piglife.tw</link>
	<description>Hello World，一個紀錄生活與學習的地方</description>
	<lastBuildDate>Fri, 02 Jan 2026 22:20:44 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://piglife.tw/wp-content/uploads/2017/10/cropped-logo-1-32x32.png</url>
	<title>資料分頁 &#8211; 小豬日常</title>
	<link>https://piglife.tw</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>使用 Firebase Firestore 匯出抽獎資料為 CSV 檔案的實作說明</title>
		<link>https://piglife.tw/technical-notes/firebase-firestore-csv-export/</link>
					<comments>https://piglife.tw/technical-notes/firebase-firestore-csv-export/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Fri, 02 Jan 2026 22:20:44 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[CSV匯出]]></category>
		<category><![CDATA[Firebase]]></category>
		<category><![CDATA[Firestore]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[前端匯出]]></category>
		<category><![CDATA[資料分頁]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/firebase-firestore-csv-export/</guid>

					<description><![CDATA[本文說明如何結合 Firebase Firestore 與前端 JavaScript 實作抽獎資料的...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>本篇文章介紹如何在 PHP 環境中結合 Firebase Firestore，實作一個前端按鈕匯出抽獎資料為 CSV 檔案的功能。此功能適合需要將 Firestore 資料批次匯出，並提供給非技術人員下載的場景。讀者需具備基本 PHP 與 JavaScript 知識，並了解 Firebase Firestore 的基本操作。</p>
<h2 class="wp-block-heading">Firebase 初始化與匿名登入</h2>
<p>在前端使用 Firebase SDK，首先透過 <code>initializeApp</code> 初始化 Firebase，並使用匿名登入 (<code>signInAnonymously</code>) 取得存取 Firestore 的權限。這樣做可以避免在伺服器端直接操作 Firebase，減少安全風險。</p>
<pre><code class="lang-js language-js js">const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
await signInAnonymously(auth);
const db = getFirestore(app);</code></pre>
<h2 class="wp-block-heading">分頁抓取 Firestore 資料</h2>
<p>Firestore 資料量可能很大，因此使用分頁查詢避免一次讀取過多資料造成效能問題。透過 <code>orderBy(&quot;__name__&quot;)</code> 與 <code>startAfter</code> 搭配 <code>limit</code>，逐頁取得資料直到抓完為止。</p>
<pre><code class="lang-js language-js js">async function fetchAllEntriesPaged(db, pageSize = 500) {
  const colRef = collection(db, ENTRY_COLLECTION_PATH);
  let all = [];
  let lastDoc = null;
  while (true) {
    let q = query(colRef, orderBy(&quot;__name__&quot;), limit(pageSize));
    if (lastDoc) q = query(colRef, orderBy(&quot;__name__&quot;), startAfter(lastDoc), limit(pageSize));
    const snap = await getDocs(q);
    if (snap.empty) break;
    snap.forEach(d =&gt; all.push({ id: d.id, ...d.data() }));
    lastDoc = snap.docs[snap.docs.length - 1];
    if (snap.size &lt; pageSize) break;
  }
  return all;
}</code></pre>
<h2 class="wp-block-heading">CSV 格式處理與下載</h2>
<p>匯出 CSV 需要將資料轉成符合格式的字串，並處理特殊字元（如逗號、雙引號、換行）。<code>csvEscape</code> 函式負責這部分。下載時利用 Blob 物件與 <code>URL.createObjectURL</code> 產生可下載的連結。</p>
<pre><code class="lang-js language-js js">function csvEscape(val) {
  if (val === null || val === undefined) return &quot;&quot;;
  const s = String(val);
  if (/[&quot;,\n\r]/.test(s)) return `&quot;${s.replace(/&quot;/g, &#039;&quot;&quot;&#039;)}&quot;`;
  return s;
}

function downloadCSV(lines, filename) {
  const csv = &quot;\uFEFF&quot; + lines.join(&quot;\r\n&quot;);
  const blob = new Blob([csv], { type: &quot;text/csv;charset=utf-8;&quot; });
  const url = URL.createObjectURL(blob);
  const a = document.createElement(&quot;a&quot;);
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}</code></pre>
<h2 class="wp-block-heading">時間戳記格式轉換</h2>
<p>Firestore 的時間欄位格式特殊，可能是 Timestamp 物件或其他形式，透過 <code>tsToISO</code> 函式統一轉成 ISO 8601 字串，方便 CSV 讀取與後續處理。</p>
<pre><code class="lang-js language-js js">function tsToISO(v) {
  try {
    if (!v) return &quot;&quot;;
    if (typeof v.toDate === &quot;function&quot;) return v.toDate().toISOString();
    if (typeof v.seconds === &quot;number&quot;) return new Date(v.seconds * 1000).toISOString();
    return String(v);
  } catch {
    return &quot;&quot;;
  }
}</code></pre>
<h2 class="wp-block-heading">實作匯出流程</h2>
<p>按下「下載 CSV」按鈕後，觸發 <code>runExport</code> 函式，依序完成初始化、登入、資料分頁抓取、CSV 組合、下載檔案與狀態更新。過程中會禁用按鈕避免重複點擊，並顯示目前狀態。</p>
<pre><code class="lang-js language-js js">async function runExport() {
  $status.textContent = &quot;&quot;;
  $btn.disabled = true;
  $btn.textContent = &quot;匯出中...&quot;;
  try {
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);
    await signInAnonymously(auth);
    const db = getFirestore(app);
    $status.textContent = &quot;正在抓取資料...&quot;;
    const rows = await fetchAllEntriesPaged(db, 500);
    if (!rows.length) {
      $status.textContent = &quot;沒有任何資料可匯出。&quot;;
      return;
    }
    const headers = [&quot;docId&quot;, &quot;name&quot;, &quot;classroom&quot;, &quot;teacher&quot;, &quot;entryDate&quot;, &quot;entryNumber&quot;, &quot;isWinner&quot;, &quot;drawTime&quot;, &quot;drawnBy&quot;, &quot;createdAt&quot;, &quot;updatedAt&quot;];
    const lines = [];
    lines.push(headers.map(csvEscape).join(&quot;,&quot;));
    for (const r of rows) {
      const line = [
        r.id ?? &quot;&quot;,
        r.name ?? &quot;&quot;,
        r.classroom ?? &quot;&quot;,
        r.teacher ?? &quot;&quot;,
        r.entryDate ?? &quot;&quot;,
        r.entryNumber ?? &quot;&quot;,
        (r.isWinner === true) ? &quot;true&quot; : &quot;false&quot;,
        r.drawTime ?? &quot;&quot;,
        r.drawnBy ?? &quot;&quot;,
        tsToISO(r.createdAt),
        tsToISO(r.updatedAt),
      ].map(csvEscape).join(&quot;,&quot;);
      lines.push(line);
    }
    const filename = `christmas_raffle_entries_${getTodayDate()}.csv`;
    downloadCSV(lines, filename);
    $status.textContent = `匯出完成：共 ${rows.length} 筆`;
  } catch (e) {
    console.error(e);
    $status.textContent = `匯出失敗：${e.message}`;
  } finally {
    $btn.disabled = false;
    $btn.textContent = &quot;⬇️ 下載 CSV&quot;;
  }
}</code></pre>
<h2 class="wp-block-heading">實際應用與延伸</h2>
<p>此實作適合用於管理後台快速匯出 Firestore 資料，方便非技術人員下載報表。未來可擴充支援更多資料欄位，或改用後端匯出以保護 Firebase 金鑰安全。也可以加入進度條或分頁匯出功能，提升使用體驗。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>Firebase 配置需正確填寫並允許匿名登入。</li>
<li>Firestore 權限規則需設定允許讀取資料。</li>
<li>大量資料匯出時，注意瀏覽器記憶體限制與執行時間。</li>
<li>CSV 格式需妥善處理特殊字元避免格式錯亂。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">
&lt;?php
function at_christmas_export_menu_page()
{
    ?&gt;
    &lt;div class=&quot;wrap&quot;&gt;

&lt;h1&gt;匯出抽獎資料 CSV&lt;/h1&gt;

&lt;p&gt;
            &lt;button id=&quot;at-export-btn&quot; class=&quot;button button-primary&quot;&gt;⬇️ 下載 CSV&lt;/button&gt;
            &lt;span id=&quot;at-export-status&quot; style=&quot;margin-left:10px;&quot;&gt;&lt;/span&gt;
        &lt;/p&gt;
    &lt;/div&gt;

    &lt;script type=&quot;module&quot;&gt;
        import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js&quot;;
        import { getAuth, signInAnonymously } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js&quot;;
        import {
            getFirestore,
            collection,
            query,
            getDocs,
            orderBy,
            limit,
            startAfter
        } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js&quot;;

        const firebaseConfig = {
            apiKey: &quot;YOUR_API_KEY&quot;,
            authDomain: &quot;YOUR_AUTH_DOMAIN&quot;,
            projectId: &quot;YOUR_PROJECT_ID&quot;,
            storageBucket: &quot;YOUR_STORAGE_BUCKET&quot;,
            messagingSenderId: &quot;YOUR_MESSAGING_SENDER_ID&quot;,
            appId: &quot;YOUR_APP_ID&quot;,
            measurementId: &quot;YOUR_MEASUREMENT_ID&quot;
        };

        const projectId = firebaseConfig.projectId || &quot;YOUR_PROJECT_ID&quot;;
        const ENTRY_COLLECTION_PATH = `artifacts/${projectId}/public/data/christmas_raffle_entries`;

        const $btn = document.getElementById(&quot;at-export-btn&quot;);
        const $status = document.getElementById(&quot;at-export-status&quot;);

        function getTodayDate() {
            const now = new Date();
            const y = now.getFullYear();
            const m = String(now.getMonth() + 1).padStart(2, &quot;0&quot;);
            const d = String(now.getDate()).padStart(2, &quot;0&quot;);
            return `${y}-${m}-${d}`;
        }

        function csvEscape(val) {
            if (val === null || val === undefined) return &quot;&quot;;
            const s = String(val);
            if (/[&quot;,\n\r]/.test(s)) return `&quot;${s.replace(/&quot;/g, &#039;&quot;&quot;&#039;)}&quot;`;
            return s;
        }

        function tsToISO(v) {
            try {
                if (!v) return &quot;&quot;;
                if (typeof v.toDate === &quot;function&quot;) return v.toDate().toISOString();
                if (typeof v.seconds === &quot;number&quot;) return new Date(v.seconds * 1000).toISOString();
                return String(v);
            } catch {
                return &quot;&quot;;
            }
        }

        async function fetchAllEntriesPaged(db, pageSize = 500) {
            const colRef = collection(db, ENTRY_COLLECTION_PATH);

            let all = [];
            let lastDoc = null;

            while (true) {
                let q = query(colRef, orderBy(&quot;__name__&quot;), limit(pageSize));
                if (lastDoc) q = query(colRef, orderBy(&quot;__name__&quot;), startAfter(lastDoc), limit(pageSize));

                const snap = await getDocs(q);
                if (snap.empty) break;

                snap.forEach(d =&gt; all.push({ id: d.id, ...d.data() }));
                lastDoc = snap.docs[snap.docs.length - 1];

                if (snap.size &lt; pageSize) break;
            }
            return all;
        }

        function downloadCSV(lines, filename) {
            const csv = &quot;\uFEFF&quot; + lines.join(&quot;\r\n&quot;); // BOM for Excel
            const blob = new Blob([csv], { type: &quot;text/csv;charset=utf-8;&quot; });
            const url = URL.createObjectURL(blob);
            const a = document.createElement(&quot;a&quot;);
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            a.remove();
            URL.revokeObjectURL(url);
        }

        async function runExport() {
            $status.textContent = &quot;&quot;;
            $btn.disabled = true;
            $btn.textContent = &quot;匯出中...&quot;;

            try {
                const app = initializeApp(firebaseConfig);
                const auth = getAuth(app);
                await signInAnonymously(auth);

                const db = getFirestore(app);

                $status.textContent = &quot;正在抓取資料...&quot;;
                const rows = await fetchAllEntriesPaged(db, 500);

                if (!rows.length) {
                    $status.textContent = &quot;沒有任何資料可匯出。&quot;;
                    return;
                }

                const headers = [
                    &quot;docId&quot;,
                    &quot;name&quot;,
                    &quot;classroom&quot;,
                    &quot;teacher&quot;,
                    &quot;entryDate&quot;,
                    &quot;entryNumber&quot;,
                    &quot;isWinner&quot;,
                    &quot;drawTime&quot;,
                    &quot;drawnBy&quot;,
                    &quot;createdAt&quot;,
                    &quot;updatedAt&quot;
                ];

                const lines = [];
                lines.push(headers.map(csvEscape).join(&quot;,&quot;));

                for (const r of rows) {
                    const line = [
                        r.id ?? &quot;&quot;,
                        r.name ?? &quot;&quot;,
                        r.classroom ?? &quot;&quot;,
                        r.teacher ?? &quot;&quot;,
                        r.entryDate ?? &quot;&quot;,
                        r.entryNumber ?? &quot;&quot;,
                        (r.isWinner === true) ? &quot;true&quot; : &quot;false&quot;,
                        r.drawTime ?? &quot;&quot;,
                        r.drawnBy ?? &quot;&quot;,
                        tsToISO(r.createdAt),
                        tsToISO(r.updatedAt),
                    ].map(csvEscape).join(&quot;,&quot;);
                    lines.push(line);
                }

                const filename = `christmas_raffle_entries_${getTodayDate()}.csv`;
                downloadCSV(lines, filename);

                $status.textContent = `匯出完成：共 ${rows.length} 筆`;
            } catch (e) {
                console.error(e);
                $status.textContent = `匯出失敗：${e.message}`;
            } finally {
                $btn.disabled = false;
                $btn.textContent = &quot;⬇️ 下載 CSV&quot;;
            }
        }

        $btn.addEventListener(&quot;click&quot;, runExport);
    &lt;/script&gt;
&lt;?php
}
?&gt;</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/firebase-firestore-csv-export/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
