Google Firebase Studio - daniel-qa/Vue GitHub Wiki

先把需求壓到最精簡、但實用:

📝 發布文章(新增)

📚 文章列表(讀取)

✏️ 編輯文章(更新)

❌ 刪除文章(刪除)

👍 一個 Like 按鈕(互動)

🔐 只有登入者可以發文(Auth)

這就是典型 CRUD + 一點互動,Firebase 超擅長。


  • 核心學習重點

Firebase 的優勢展示:

無需後端伺服器 - 前端直接連接 Firebase
即時同步 - serverTimestamp() 自動處理時間
原子操作 - increment() 避免競爭條件
安全規則 - 透過 Firebase Console 設定權限
擴展性 - 自動處理大量請求

實際應用場景:

✅ 使用者系統 (Auth)
✅ 內容管理 (Firestore CRUD)
✅ 社交互動 (Like 功能)
✅ 權限控制 (僅作者可編輯)

  • Like 按鈕

Like 按鈕新增到 PostsPanel.vue:

❤️ 愛心圖示 + 讚數顯示

顯示在每篇文章的作者資訊旁邊 所有人都可以按讚(不需登入)

  • 按讚邏輯

使用 increment(1) 自動增加 likeCount

點擊後立即更新 Firestore

自動重新載入文章列表顯示新讚數

  • UI 調整

按鈕樣式: 灰色背景,圓角設計

作者資訊和 Like 按鈕並排顯示

編輯/刪除按鈕保持在右側


  • 刪除
async function deletePost(postId, postTitle) {
  msg.value = "";
  if (!user.value) {
    msg.value = "❌ 請先登入";
    return;
  }
 
  const confirmed = confirm(`確定要刪除文章「${postTitle}」嗎?此操作無法復原。`);
  if (!confirmed) {
    return;
  }
 
  try {
    const postRef = doc(db, "posts", postId);
    await deleteDoc(postRef);
    msg.value = "✅ 刪除成功";
    if (editingId.value === postId) {
      cancelEdit();
    }
    await loadPosts();
  } catch (e) {
    msg.value = `❌ 刪除失敗:${e.code || e.message}`;
  }
}
  • 刪除按鈕

紅色按鈕顯示在每篇文章的右下角(作者本人可見) 與「編輯」按鈕並排顯示

  • 確認對話框

點擊刪除時會彈出確認視窗 顯示文章標題,提醒「此操作無法復原」 使用者可以取消操作

  • 刪除邏輯

使用 deleteDoc 從 Firestore 刪除文件 如果正在編輯該文章,會自動取消編輯模式 刪除後自動重新載入文章列表

  • 權限控制

只有文章作者本人可以看到刪除按鈕 需要登入才能執行刪除操作


c:\Firebase\firebase-blog-web\
├── src/
│   ├── firebase.js              # Firebase 配置
│   ├── main.js                  # 應用入口 + auth 監聽
│   ├── App.vue                  # 主組件 (Auth + Posts)
│   └── components/
│       ├── AuthPanel.vue        # 登入/註冊介面
│       └── PostsPanel.vue       # 文章發布/列表
├── package.json
└── vite.config.js

  • Firestore Security Rules(Firebase 安全規則語言)

用途:寫在 Firebase Console → Firestore Database → 規則,用來決定「誰可以對哪些文件做哪些操作」。

這段在講什麼(逐行拆解)

1) allow create / update / delete

 allow create: if request.auth != null
        && request.resource.data.authorId == request.auth.uid;

create:新增文件(例如 addDoc()、setDoc() 新建)

update:更新既有文件(例如 updateDoc()、setDoc(..., {merge:true}))

delete:刪除文件(例如 deleteDoc())

allow create: if <條件>; 的意思就是:符合條件才允許 create

2) 這些關鍵字是什麼

request.auth => 代表「發出這個請求的人」的登入資訊

request.auth != null 就是 有登入(匿名也算登入)

resource.data 代表 資料庫裡原本就存在的那筆文件

update/delete 才有「原本的文件」可比對,所以常用它來驗證作者

request.resource.data 代表「這次請求想要寫進去的資料(寫入後會長這樣)」

create/update 常用它來檢查寫入內容是否合法(例如 authorId 不准亂填)


✅ Firestore Rules(CRUD 安全版:Create 需登入、Update/Delete 只限作者)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /posts/{postId} {
      // 文章列表公開可讀(想改成登入才可讀也行)
      allow read: if true;

      // ✅ 只有登入者能新增,且 authorId 必須等於自己的 uid
      allow create: if request.auth != null
        && request.resource.data.authorId == request.auth.uid;

      // ✅ 只有作者能更新
      allow update: if request.auth != null
        && resource.data.authorId == request.auth.uid
        // 防止把 authorId 偷改成別人
        && request.resource.data.authorId == resource.data.authorId;

      // ✅ 只有作者能刪除
      allow delete: if request.auth != null
        && resource.data.authorId == request.auth.uid;
    }

    // 其他集合一律拒絕(避免誤開)
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

按 發布 / Publish。

立刻驗證(建議你一定要做)
A) 用同一個帳號(作者本人)

編輯自己的文章 → ✅ 成功

刪除自己的文章 → ✅ 成功

B) 用另一個帳號(非作者)

先登出

用另一個 Email 註冊/登入(例如 [email protected])

嘗試去編輯/刪除 test1 的文章(前端按鈕可能會 disabled,但你可以先把前端 disabled 拿掉或直接在 Console 用 REST 不方便;最簡單:先確認 Firestore Console 右側「刪除文件」在非作者身分是做不到的)

預期:permission-denied

你前端按鈕現在會 disabled(正常)

你目前 UI 版已經有:

:disabled="!user || p.authorId !== user.uid"

所以「非作者」看不到操作是合理的;真正的安全靠我們這份 rules。

我們先把 Firestore Rules 收到「只有登入者能新增(Create)」,其他(讀/改/刪)先暫時保持原本(測試模式的開放)讓你先跑功能不中斷。

你現在去這裡

Firebase Console → Firestore Database → 規則(Rules)

你會看到一段 rules。

把 rules 改成下面這份(只限制 Create 需登入)

直接整段覆蓋貼上,然後按「發布 / Publish」

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /posts/{postId} {
      // 文章:所有人可讀(你要公開列表的話)
      allow read: if true;

      // ✅ 只有登入者能新增
      allow create: if request.auth != null;

      // 先暫時開放更新/刪除(下一步我們再收緊到只有作者)
      allow update, delete: if true;
    }

    // 其他集合先全部拒絕(避免誤開)
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
立刻驗證(超重要)

在前端 先登出 → 點「發布文章」

預期:顯示 ❌ 發文失敗:permission-denied(或類似)

再 登入 → 點「發布文章」

預期:✅ 發文成功

  • Step 1:先在 Firebase Console 建立 Firestore

到 Firebase Console:

Build → Firestore Database → 建立資料庫

模式:先選 測試模式(之後再改規則)

地區:選你方便的

完成後你會看到 Firestore Database 的資料頁面。


1) 前端:新增 PostsPanel
A) 建檔 src/components/PostsPanel.vue

把下面整份貼進去:

<script setup>
import { ref, onMounted } from "vue";
import { auth, db } from "../firebase";
import { onAuthStateChanged } from "firebase/auth";
import {
  addDoc,
  collection,
  query,
  orderBy,
  limit,
  getDocs,
  serverTimestamp,
} from "firebase/firestore";

const title = ref("");
const content = ref("");
const msg = ref("");
const posts = ref([]);
const user = ref(null);

onMounted(() => {
  onAuthStateChanged(auth, (u) => {
    user.value = u;
  });
});

async function loadPosts() {
  msg.value = "";
  try {
    const q = query(collection(db, "posts"), orderBy("createdAt", "desc"), limit(20));
    const snap = await getDocs(q);
    posts.value = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
  } catch (e) {
    msg.value = `❌ 讀取失敗:${e.code || e.message}`;
  }
}

async function createPost() {
  msg.value = "";
  if (!user.value) {
    msg.value = "❌ 請先登入才可以發文";
    return;
  }
  if (!title.value.trim() || !content.value.trim()) {
    msg.value = "❌ Title / Content 不能空白";
    return;
  }

  try {
    await addDoc(collection(db, "posts"), {
      title: title.value.trim(),
      content: content.value.trim(),
      authorId: user.value.uid,
      authorEmail: user.value.email,
      createdAt: serverTimestamp(),
      updatedAt: serverTimestamp(),
      likeCount: 0,
    });
    title.value = "";
    content.value = "";
    msg.value = "✅ 發文成功";
    await loadPosts();
  } catch (e) {
    msg.value = `❌ 發文失敗:${e.code || e.message}`;
  }
}

onMounted(loadPosts);
</script>

<template>
  <div style="max-width: 720px; margin: 24px auto; text-align: left;">
    <h2>Posts</h2>

    <div style="padding: 12px; border: 1px solid #ddd; border-radius: 8px;">
      <div style="margin-bottom: 8px;">
        <label>Title</label>
        <input v-model="title" style="width: 100%; padding: 8px; margin-top: 6px;" />
      </div>

      <div style="margin-bottom: 8px;">
        <label>Content</label>
        <textarea v-model="content" rows="4" style="width: 100%; padding: 8px; margin-top: 6px;"></textarea>
      </div>

      <div style="display:flex; gap: 8px;">
        <button @click="createPost">發布文章</button>
        <button @click="loadPosts">重新載入</button>
      </div>

      <p style="margin-top: 10px; white-space: pre-wrap;">{{ msg }}</p>
    </div>

    <hr style="margin: 18px 0;" />

    <div v-if="posts.length === 0">(目前沒有文章)</div>

    <div v-for="p in posts" :key="p.id" style="padding: 12px 0; border-bottom: 1px solid #eee;">
      <div style="font-weight: 700;">{{ p.title }}</div>
      <div style="white-space: pre-wrap; margin: 6px 0;">{{ p.content }}</div>
      <div style="font-size: 12px; color: #666;">
        by {{ p.authorEmail || p.authorId }} · likeCount: {{ p.likeCount ?? 0 }}
      </div>
    </div>
  </div>
</template>
B) 改 src/App.vue:同時顯示 Auth + Posts

把 App.vue 改成:

<script setup>
import AuthPanel from "./components/AuthPanel.vue";
import PostsPanel from "./components/PostsPanel.vue";
</script>

<template>
  <AuthPanel />
  <PostsPanel />
</template>
2) 驗證:發第一篇文章

你的 AuthPanel 先保持登入狀態(畫面顯示登入成功)

在 Posts 區塊輸入 Title/Content

點 發布文章

回 Firebase Console(你現在這頁)
左邊會自動出現集合 posts,點進去會看到一筆 doc ✅

1) 覆蓋 src/components/AuthPanel.vue(完整版)

把你現在那個極簡版整份換成下面這份:

<script setup>
import { ref } from "vue";
import { auth } from "../firebase";
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
} from "firebase/auth";

const email = ref("");
const password = ref("");
const msg = ref("");

async function signup() {
  msg.value = "";
  try {
    const cred = await createUserWithEmailAndPassword(auth, email.value, password.value);
    msg.value = `✅ 註冊成功:${cred.user.email}`;
  } catch (e) {
    msg.value = `❌ 註冊失敗:${e.code || e.message}`;
  }
}

async function login() {
  msg.value = "";
  try {
    const cred = await signInWithEmailAndPassword(auth, email.value, password.value);
    msg.value = `✅ 登入成功:${cred.user.email}`;
  } catch (e) {
    msg.value = `❌ 登入失敗:${e.code || e.message}`;
  }
}

async function logout() {
  msg.value = "";
  try {
    await signOut(auth);
    msg.value = "✅ 已登出";
  } catch (e) {
    msg.value = `❌ 登出失敗:${e.code || e.message}`;
  }
}
</script>

<template>
  <div style="max-width: 420px; margin: 40px auto; text-align: left;">
    <h2>Auth Demo</h2>

    <label>Email</label>
    <input
      v-model="email"
      type="email"
      placeholder="[email protected]"
      style="width: 100%; padding: 8px; margin: 6px 0 12px;"
    />

    <label>Password</label>
    <input
      v-model="password"
      type="password"
      placeholder="至少 6 碼"
      style="width: 100%; padding: 8px; margin: 6px 0 12px;"
    />

    <div style="display: flex; gap: 8px;">
      <button @click="signup">註冊</button>
      <button @click="login">登入</button>
      <button @click="logout">登出</button>
    </div>

    <p style="margin-top: 12px; white-space: pre-wrap;">{{ msg }}</p>
  </div>
</template>
2) 測試(照這順序)

Email:[email protected]

Password:test1234(>= 6 碼)

點 註冊

點 登出

點 登入

同時看 Console(你之前加的 onAuthStateChanged):

登入成功會印出 uid

登出會回到 signed out
我們用最穩的方式一步步確認,照做一定會出現登入畫面:

1) 先確認檔案結構(很關鍵)

你的專案要長這樣:

src/App.vue

src/main.js

src/components/AuthPanel.vue

(components 資料夾沒有就自己建)

2) 直接把 src/App.vue 整份覆蓋成這樣

先不要管原本 Vite 預設內容,直接換掉

<script setup>
import AuthPanel from "./components/AuthPanel.vue";
</script>

<template>
  <AuthPanel />
</template>
3) 建 src/components/AuthPanel.vue(最小可跑版)

先用這個極簡版,確保元件能顯示:

<template>
  <div style="max-width:420px;margin:40px auto;text-align:left">
    <h2>Auth Demo</h2>
    <p>如果你看到這行,代表 AuthPanel 已掛上首頁 ✅</p>
  </div>
</template>
4) 回到瀏覽器,按一下重新整理

你應該立刻看到 Auth Demo 這行。

如果還是 Vite 預設畫面:
99% 是你改到「不是同一個專案資料夾」,或 Vite dev server 不是跑這個資料夾。
這時請你回到 cmd,確認你 npm run dev 是在 C:\Firebase\firebase-blog-web 執行的。

5) 確認極簡版 OK 後,再把 AuthPanel 換成完整版(註冊/登入)

我再把完整版貼一次你直接覆蓋即可(含註冊/登入/登出)。

  • Step 1:新增元件 src/components/AuthPanel.vue

把我前一則提供的 AuthPanel.vue 建起來(含註冊/登入/登出那段)。

  • Step 2:把首頁換成 AuthPanel(改 src/App.vue)

把 src/App.vue 改成:

<script setup>
import AuthPanel from "./components/AuthPanel.vue";
</script>

<template>
  <AuthPanel />
</template>

然後回到瀏覽器刷新

你就會在首頁看到 Email/Password 輸入框 + 三個按鈕(註冊/登入/登出)。

接著用:

Email:[email protected]

Password:test1234

按 註冊 就能完成第一個功能。


你 Console 已經出現 auth state: signed out,代表:

Vite/Vue 專案 OK

Firebase SDK 初始化 OK

Auth 也已經可用(至少能讀到狀態)

接下來我們進入第一個「可用功能」:註冊 / 登入 / 登出(Email/Password)
(先把 Auth 打通,後面才鎖「只有登入者可發文」。)
  • 1 ) 先確認 Console 已開 Email/Password
Firebase Console → Build → Authentication → Sign-in method
✅ Email/Password 要是 Enabled
(如果你不確定也沒關係,你照做下面程式,錯誤訊息會直接告訴我們。)

  • 1. 前端先能連上 Firebase,然後 開 Auth + Firestore。

把 Firebase 接進來,然後用 Console 看到 signed out 代表連線成功。

下一步:把 Firebase 接進來,然後用 Console 看到 signed out 代表連線成功。

1) 建 src/firebase.js

在專案建立檔案 src/firebase.js,貼這段:

import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "AIzaSyBr4SSowFxfOxbPK5tvY0XwtU6xYODA4BM",
  authDomain: "fir-blog-lab.firebaseapp.com",
  projectId: "fir-blog-lab",
  storageBucket: "fir-blog-lab.firebasestorage.app",
  messagingSenderId: "994110275058",
  appId: "1:994110275058:web:1b7c165f56d810c3b0ff6b",
};

export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
2) 在 src/main.js 加監聽(驗證用)

打開 src/main.js,在 createApp(App).mount('#app') 前後都可以,加上:

import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase";

onAuthStateChanged(auth, (user) => {
  console.log("auth state:", user ? user.uid : "signed out");
});
3) Firebase Console 也要先開服務(否則下一步做登入會卡)

到 Firebase Console:

Build → Authentication → Sign-in method → 開啟 Email/Password

Build → Firestore Database → 建立資料庫(先測試模式)

4) 驗證結果

回到你這個頁面(127.0.0.1:58589),按 F12 → Console
你應該會看到:

auth state: signed out

看到這行就完成「前端成功接上 Firebase」。
  • 初始化
npm i firebase
npm run dev

http://127.0.0.1:58589/


  • Firebase Console 入口

https://console.firebase.google.com/

https://magecomp.com/support/knowledgebase/firebase-console/?utm_source=chatgpt.com

很多人以為有個叫「Firebase Studio」的獨立工具,
但實際上你真正使用的是 Firebase Console + SDK + Hosting 這一整套雲端開發環境。

npm install firebase

// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
  apiKey: "AIzaSyBS6cEGosMNMHD8ai9XK7nFljAhSc5Oo2U",
  authDomain: "myfire-8ab17.firebaseapp.com",
  projectId: "myfire-8ab17",
  storageBucket: "myfire-8ab17.firebasestorage.app",
  messagingSenderId: "164224360045",
  appId: "1:164224360045:web:6e6384cc2ae018bfd7e964",
  measurementId: "G-PRYK9MGN8P"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
⚠️ **GitHub.com Fallback** ⚠️