--- tags: - backend - pocketbase - tutorial - sqlite - api aliases: - PocketBase 教學 - PocketBase 實作 created: 2024-12-22 updated: 2024-12-22 --- # PocketBase 入門到實作:從零到上線的完整教學 ![pocketbase-tutorial-cover](https://hackmd.io/_uploads/SJEPpqw7Zl.jpg) ## 前言 上一篇介紹了各種 Supabase 替代品,這篇要深入 PocketBase 的實際操作。從安裝、設定、前端整合到部署上線,完整走一遍流程。 PocketBase 的特點是簡單直接。不需要 Docker,不需要資料庫安裝,下載一個檔案就能跑。這讓它特別適合: - 想快速驗證想法的個人開發者 - 學習後端開發的初學者 - 需要輕量後端的小型專案 - 不想維護複雜基礎設施的團隊 這篇教學會涵蓋: 1. 安裝與啟動 2. Admin 後台操作 3. 資料庫設計 4. 前端整合(含完整程式碼) 5. 認證系統 6. 自訂 API 7. 部署到線上 8. 實戰案例:待辦清單應用 --- ## 環境準備 PocketBase 是單一執行檔,不需要預先安裝任何東西。支援的作業系統: - Linux(amd64、arm64) - macOS(Intel、Apple Silicon) - Windows --- ## 第一步:安裝與啟動 ### 方法一:直接下載 ```bash # macOS (Apple Silicon) wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_darwin_arm64.zip # macOS (Intel) wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_darwin_amd64.zip # Linux wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_linux_amd64.zip # 解壓縮 unzip pocketbase_*.zip # 啟動 ./pocketbase serve ``` 啟動後會看到: ``` 2024/01/01 12:00:00 Server started at http://127.0.0.1:8090 - REST API: http://127.0.0.1:8090/api/ - Admin UI: http://127.0.0.1:8090/_/ ``` ### 方法二:Docker 如果偏好容器化部署: ```dockerfile FROM alpine:latest ARG PB_VERSION=0.28.4 RUN apk add --no-cache unzip ca-certificates ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip RUN unzip /tmp/pb.zip -d /pb/ EXPOSE 8080 CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080"] ``` ```bash docker build -t pocketbase . docker run -d -p 8080:8080 -v $(pwd)/pb_data:/pb/pb_data pocketbase ``` ### 首次設定 開啟 `http://127.0.0.1:8090/_/` 會看到管理員帳號設定頁面。填入 email 和密碼後,就能進入 Admin 後台。 這個帳號是超級管理員(Superuser),擁有所有權限。實際專案中請使用強密碼。 --- ## 第二步:認識 Admin 後台 Admin 後台分為幾個主要區塊: ### Collections(資料集合) 類似資料庫的 Table。每個 Collection 包含: - **欄位定義**:支援多種類型(Text、Number、Bool、Date、File、Relation 等) - **API 規則**:控制 CRUD 權限 - **索引設定**:優化查詢效能 PocketBase 預設有兩個系統 Collection: - `_superusers`:管理員帳號 - `users`:一般使用者(Auth Collection) ### Logs 查看 API 請求紀錄、錯誤訊息。 ### Settings 全域設定,包含: - 應用程式名稱 - SMTP 設定(用於發送驗證信) - 檔案儲存設定(本地或 S3) - 認證選項 - 備份設定 --- ## 第三步:建立第一個 Collection 以部落格文章為例,建立 `posts` Collection: ### 在 Admin 後台操作 1. 點擊「New collection」 2. 輸入名稱:`posts` 3. 類型選擇:「Base」(一般資料集合) 4. 新增欄位: | 欄位名稱 | 類型 | 設定 | |---------|------|------| | title | Text | Required | | content | Editor | Required | | slug | Text | Required, Unique | | status | Select | Options: draft, published | | author | Relation | 關聯到 users | | cover | File | 允許圖片類型 | | published_at | DateTime | - | ### 設定 API 規則 在 Collection 設定的「API Rules」區塊: ```javascript // List/Search Rule - 只有已發布的文章對外公開 status = "published" // View Rule - 同上,或作者本人可看草稿 status = "published" || author = @request.auth.id // Create Rule - 只有登入使用者可建立 @request.auth.id != "" // Update Rule - 只有作者可編輯 author = @request.auth.id // Delete Rule - 只有作者可刪除 author = @request.auth.id ``` 這些規則用類似 SQL WHERE 的語法,`@request.auth` 代表當前登入的使用者。 --- ## 第四步:前端整合 ### 安裝 SDK ```bash npm install pocketbase ``` ### 初始化 ```javascript import PocketBase from 'pocketbase'; const pb = new PocketBase('http://127.0.0.1:8090'); // 如果在 Node.js 環境,需要處理 localStorage // pb.authStore = new BaseAuthStore(); ``` ### CRUD 操作 #### 建立記錄 ```javascript const record = await pb.collection('posts').create({ title: '我的第一篇文章', content: '<p>這是內容...</p>', slug: 'my-first-post', status: 'draft', author: pb.authStore.record.id }); console.log(record.id); // 自動生成的 ID ``` #### 查詢記錄 ```javascript // 取得列表(分頁) const resultList = await pb.collection('posts').getList(1, 20, { filter: 'status = "published"', sort: '-created', expand: 'author' }); console.log(resultList.items); console.log(resultList.totalItems); console.log(resultList.totalPages); // 取得單筆 const record = await pb.collection('posts').getOne('RECORD_ID', { expand: 'author' }); // 用其他欄位查詢 const post = await pb.collection('posts').getFirstListItem( `slug = "my-first-post"` ); ``` #### 更新記錄 ```javascript const updated = await pb.collection('posts').update('RECORD_ID', { title: '更新後的標題', status: 'published', published_at: new Date().toISOString() }); ``` #### 刪除記錄 ```javascript await pb.collection('posts').delete('RECORD_ID'); ``` ### 檔案上傳 ```javascript // 建立 FormData const formData = new FormData(); formData.append('title', '帶圖片的文章'); formData.append('content', '<p>內容</p>'); formData.append('slug', 'post-with-image'); formData.append('status', 'draft'); formData.append('author', pb.authStore.record.id); formData.append('cover', fileInput.files[0]); // File 物件 const record = await pb.collection('posts').create(formData); // 取得檔案 URL const coverUrl = pb.files.getUrl(record, record.cover); // 加上尺寸參數(自動縮放) const thumbUrl = pb.files.getUrl(record, record.cover, { thumb: '300x200' }); ``` ### 即時訂閱 PocketBase 支援 SSE(Server-Sent Events)實現即時資料同步: ```javascript // 訂閱整個 Collection pb.collection('posts').subscribe('*', function (e) { console.log(e.action); // 'create' | 'update' | 'delete' console.log(e.record); }); // 訂閱特定記錄 pb.collection('posts').subscribe('RECORD_ID', function (e) { console.log('記錄已更新:', e.record); }); // 取消訂閱 pb.collection('posts').unsubscribe('*'); pb.collection('posts').unsubscribe('RECORD_ID'); pb.collection('posts').unsubscribe(); // 取消此 Collection 所有訂閱 ``` --- ## 第五步:認證系統 ### 使用者註冊 ```javascript const user = await pb.collection('users').create({ email: 'user@example.com', password: '12345678', passwordConfirm: '12345678', name: '使用者名稱' }); // 發送驗證信(需要先設定 SMTP) await pb.collection('users').requestVerification('user@example.com'); ``` ### 登入 ```javascript // Email + 密碼 const authData = await pb.collection('users').authWithPassword( 'user@example.com', '12345678' ); console.log(pb.authStore.isValid); // true console.log(pb.authStore.token); // JWT console.log(pb.authStore.record); // 使用者資料 ``` ### OAuth2 登入 需要先在 Admin 後台設定 OAuth2 提供者(Settings → Auth providers)。 ```javascript // 自動處理彈窗流程 const authData = await pb.collection('users').authWithOAuth2({ provider: 'google' }); ``` ### 登出 ```javascript pb.authStore.clear(); ``` ### 檢查登入狀態 ```javascript if (pb.authStore.isValid) { console.log('已登入:', pb.authStore.record.email); } else { console.log('未登入'); } // 監聽狀態變化 pb.authStore.onChange((token, record) => { console.log('認證狀態改變'); }); ``` ### 刷新 Token ```javascript // Token 即將過期時刷新 if (pb.authStore.isValid) { await pb.collection('users').authRefresh(); } ``` --- ## 第六步:自訂 API 路由 有時候內建的 CRUD API 不夠用,需要自訂邏輯。 ### 使用 JavaScript Hooks 在 `pb_hooks` 目錄下建立 `.js` 檔案: ```javascript // pb_hooks/custom_routes.pb.js // 自訂 GET 路由 routerAdd("GET", "/api/custom/stats", (e) => { // 執行 SQL 查詢 const result = $app.db() .newQuery(` SELECT COUNT(*) as total, SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END) as published FROM posts `) .one(); return e.json(200, result); }, $apis.requireAuth()); // 需要認證 // 自訂 POST 路由 routerAdd("POST", "/api/custom/publish/{id}", (e) => { const id = e.request.pathValue("id"); const record = $app.findRecordById("posts", id); // 檢查權限 if (record.get("author") !== e.auth.id) { throw new ForbiddenError("只有作者可以發布"); } record.set("status", "published"); record.set("published_at", new Date().toISOString()); $app.save(record); return e.json(200, record); }, $apis.requireAuth()); ``` ### Hooks(事件鉤子) ```javascript // pb_hooks/hooks.pb.js // 建立記錄前 onRecordCreate((e) => { // 自動設定 slug if (!e.record.get("slug")) { const title = e.record.get("title"); const slug = title.toLowerCase().replace(/\s+/g, '-'); e.record.set("slug", slug); } return e.next(); }, "posts"); // 建立記錄後 onRecordAfterCreateSuccess((e) => { console.log("新文章建立:", e.record.id); // 可以在這裡發送通知、更新快取等 }, "posts"); // 刪除記錄前 onRecordDelete((e) => { // 阻止刪除已發布的文章 if (e.record.get("status") === "published") { throw new BadRequestError("無法刪除已發布的文章"); } return e.next(); }, "posts"); ``` --- ## 第七步:部署到線上 ### 方法一:VPS 直接部署 適合:個人專案、小型應用 ```bash # 1. 上傳執行檔和 pb_data 到伺服器 scp pocketbase user@server:/opt/pocketbase/ scp -r pb_data user@server:/opt/pocketbase/ # 2. SSH 到伺服器 ssh user@server # 3. 設定 systemd 服務 sudo nano /etc/systemd/system/pocketbase.service ``` ```systemd [Unit] Description=PocketBase After=network.target [Service] Type=simple User=www-data WorkingDirectory=/opt/pocketbase ExecStart=/opt/pocketbase/pocketbase serve yourdomain.com Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ```bash # 4. 啟動服務 sudo systemctl daemon-reload sudo systemctl enable pocketbase sudo systemctl start pocketbase # 5. 檢查狀態 sudo systemctl status pocketbase ``` 使用 `yourdomain.com` 作為參數時,PocketBase 會自動處理 Let's Encrypt 憑證。 ### 方法二:Docker + Nginx 適合:需要更多控制、多服務架構 ```yaml # docker-compose.yml version: '3.8' services: pocketbase: build: . container_name: pocketbase volumes: - ./pb_data:/pb/pb_data - ./pb_hooks:/pb/pb_hooks restart: unless-stopped networks: - web nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./certs:/etc/nginx/certs depends_on: - pocketbase networks: - web networks: web: ``` ### 方法三:Railway / Fly.io / Render 這些 PaaS 平台支援 Docker 部署,適合不想管理伺服器的情況。 以 Fly.io 為例: ```toml # fly.toml app = "my-pocketbase" primary_region = "nrt" # 東京 [build] dockerfile = "Dockerfile" [mounts] source = "pb_data" destination = "/pb/pb_data" [http_service] internal_port = 8080 force_https = true ``` ```bash fly launch fly deploy ``` --- ## 實戰案例:待辦清單應用 把前面學的整合起來,建立一個完整的待辦清單應用。 ### 資料結構 建立 `todos` Collection: | 欄位 | 類型 | 設定 | |------|------|------| | title | Text | Required | | completed | Bool | Default: false | | user | Relation | 關聯 users, Required | | due_date | DateTime | - | API 規則: ```javascript // 所有操作都限制為擁有者 List: user = @request.auth.id View: user = @request.auth.id Create: @request.auth.id != "" && @request.body.user = @request.auth.id Update: user = @request.auth.id Delete: user = @request.auth.id ``` ### 前端程式碼(Vue 3 範例) ```vue <script setup> import { ref, onMounted, onUnmounted } from 'vue' import PocketBase from 'pocketbase' const pb = new PocketBase('http://127.0.0.1:8090') const todos = ref([]) const newTodo = ref('') const loading = ref(false) // 載入待辦事項 async function loadTodos() { loading.value = true try { const records = await pb.collection('todos').getFullList({ sort: '-created', filter: `user = "${pb.authStore.record.id}"` }) todos.value = records } finally { loading.value = false } } // 新增待辦 async function addTodo() { if (!newTodo.value.trim()) return const record = await pb.collection('todos').create({ title: newTodo.value, completed: false, user: pb.authStore.record.id }) todos.value.unshift(record) newTodo.value = '' } // 切換完成狀態 async function toggleTodo(todo) { const updated = await pb.collection('todos').update(todo.id, { completed: !todo.completed }) const index = todos.value.findIndex(t => t.id === todo.id) todos.value[index] = updated } // 刪除待辦 async function deleteTodo(todo) { await pb.collection('todos').delete(todo.id) todos.value = todos.value.filter(t => t.id !== todo.id) } // 即時同步 let unsubscribe onMounted(async () => { await loadTodos() // 訂閱變更 unsubscribe = await pb.collection('todos').subscribe('*', (e) => { if (e.action === 'create') { // 避免重複新增自己建立的 if (!todos.value.find(t => t.id === e.record.id)) { todos.value.unshift(e.record) } } else if (e.action === 'update') { const index = todos.value.findIndex(t => t.id === e.record.id) if (index !== -1) { todos.value[index] = e.record } } else if (e.action === 'delete') { todos.value = todos.value.filter(t => t.id !== e.record.id) } }) }) onUnmounted(() => { unsubscribe?.() }) </script> <template> <div class="todo-app"> <h1>待辦清單</h1> <form @submit.prevent="addTodo" class="add-form"> <input v-model="newTodo" placeholder="新增待辦事項..." :disabled="loading" /> <button type="submit" :disabled="loading || !newTodo.trim()"> 新增 </button> </form> <ul class="todo-list"> <li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.completed }" > <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo)" /> <span>{{ todo.title }}</span> <button @click="deleteTodo(todo)" class="delete">×</button> </li> </ul> </div> </template> <style scoped> .todo-app { max-width: 500px; margin: 0 auto; padding: 20px; } .add-form { display: flex; gap: 10px; margin-bottom: 20px; } .add-form input { flex: 1; padding: 10px; } .todo-list { list-style: none; padding: 0; } .todo-list li { display: flex; align-items: center; gap: 10px; padding: 10px; border-bottom: 1px solid #eee; } .todo-list li.completed span { text-decoration: line-through; color: #999; } .delete { margin-left: auto; background: #ff4444; color: white; border: none; padding: 5px 10px; cursor: pointer; } </style> ``` --- ## 常見問題 ### Q: 資料如何備份? ```bash # 停止服務後複製 pb_data sudo systemctl stop pocketbase cp -r /opt/pocketbase/pb_data /backup/pb_data_$(date +%Y%m%d) sudo systemctl start pocketbase # 或使用 SQLite 線上備份(不需停止服務) sqlite3 /opt/pocketbase/pb_data/data.db ".backup /backup/data_$(date +%Y%m%d).db" ``` ### Q: 如何重設管理員密碼? ```bash ./pocketbase superuser upsert admin@example.com newpassword ``` ### Q: 效能瓶頸在哪? SQLite 的寫入鎖是主要限制。如果需要高併發寫入,考慮: - 使用佇列處理寫入請求 - 評估是否真的需要 PocketBase(可能 PostgreSQL 更適合) ### Q: 可以用在生產環境嗎? 可以,但要評估: - 預期流量(SQLite 適合中低流量) - 資料重要性(確保備份策略) - 團隊維護能力 --- ## 進階學習資源 - [[PocketBase快速搭建指南]] - 更多部署選項 - [[PocketBase自訂API路由]] - 深入 API 擴充 - [[PocketBase認證與權限設定]] - 完整的權限控制 - [官方文件](https://pocketbase.io/docs/) - [GitHub 討論區](https://github.com/pocketbase/pocketbase/discussions) --- ## 結語 PocketBase 不是要取代所有後端方案,而是提供一個極簡的選擇。當你的需求是快速驗證想法、建立個人專案、或者只是需要一個簡單的後端時,它能讓你在幾分鐘內就有一個功能完整的後端服務。 這篇教學涵蓋了從入門到實戰的主要知識點。實際專案中可能還會遇到其他問題,但有了這個基礎,查閱官方文件或社群討論應該都能找到解答。 祝開發順利。