iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Modern Web

Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延系列 第 23

【Day 23】— 入門 JavaScript 網頁架設:ngrok + backup.json 簡易雲端備份

  • 分享至 

  • xImage
  •  

摘要
承接 Day 22 的「資料主權」,今天把資料送到「你自己的小後端」:

  1. 用 Express 在本機開一個 POST /backup API;
  2. 用 ngrok 把本機 3000 連到一個公開 HTTPS;
  3. 前端把 localStorage 的紀錄 POST 到後端,寫入 backup.json,模擬雲端備份。

這是你第一次走完整流程:前端 → 後端 → 硬碟存檔。就算重啟伺服器,資料也仍在 backup.json。

為什麼要做 ngrok + 簡易備份?

  • 真實感:體驗「前後端協作」與 API 呼叫,接近實務專案。
  • 離線補位:就算沒有正式雲端或資料庫,也能先保存在你的電腦硬碟。
  • 跨裝置:用 ngrok 暴露本機,手機/別的電腦也能打到你的 API(開發與 Demo 超方便)。

學習重點

  1. fetch() 送 POST JSON 到自訂 API(含逾時與錯誤處理)。
  2. Express 後端解析 JSON、用 fs.writeFileSync / fs.readFileSync 寫入 backup.json。
  3. ngrok 建立公開隧道,取得 HTTPS 網址讓前端呼叫。
  4. 成功/失敗 UI 回饋與例外處理(網路失敗、API 拒絕、JSON 格式錯等)。

核心流程

  1. 在專案根目錄新增 Node 後端(server.js),提供 POST /backup。
  2. npm run server 在本機開 3000;再用 ngrok 把 http://localhost:3000 映射成一個公開 HTTPS。
  3. 前端新增「備份到雲端(ngrok)」按鈕,將 localStorage 的紀錄 POST 到 https://你的-ngrok-網址/backup
  4. 伺服器把資料寫進 backup.json;前端顯示「已備份 N 筆」或錯誤訊息。

實作

假設已具備(前端既有):

  • readRecords()/writeRecords()(Day 4–5)
  • showPage('history') 與歷史頁區塊(Day 12–16)
  • 匯出工具(Day 22)
  • 安裝 Node.js / npm(Day 1)
  • 你已在專案根目錄(與 index.html 同層)

A. 後端

  1. 初始化與安裝套件
npm init -y
npm i express cors
  1. 建立 server.js(新檔)
// server.js  (Day23-NEW)
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = process.env.PORT || 3000;
const BACKUP_PATH = path.join(__dirname, 'backup.json');

app.use(cors());                 // 允許跨域(方便前端直接呼叫)
app.use(express.json({ limit: '2mb' })); // 解析 JSON 請求

// 健康檢查
app.get('/health', (_req, res) => {
  res.json({ ok: true, time: new Date().toISOString() });
});

// 寫檔小工具:把陣列資料保存在 backup.json 中
function writeBackupFile(payloadArray) {
  fs.writeFileSync(BACKUP_PATH, JSON.stringify(payloadArray, null, 2), 'utf-8');
}

function readBackupFile() {
  if (!fs.existsSync(BACKUP_PATH)) return [];
  try {
    const raw = fs.readFileSync(BACKUP_PATH, 'utf-8');
    const data = JSON.parse(raw);
    return Array.isArray(data) ? data : [];
  } catch {
    // 檔案壞掉或非 JSON → 先備份原檔,再重建
    const corrupted = BACKUP_PATH.replace(/\.json$/, `.corrupted-${Date.now()}.json`);
    fs.copyFileSync(BACKUP_PATH, corrupted);
    return [];
  }
}

// 備份 API:接受前端 POST 的 JSON
app.post('/backup', (req, res) => {
  const body = req.body;

  // 最少要有 records 陣列
  if (!body || !Array.isArray(body.records)) {
    return res.status(400).json({ ok: false, error: 'INVALID_PAYLOAD', message: 'payload 必須包含 records 陣列' });
  }

  // 讀舊 -> push 新 -> 寫回
  const current = readBackupFile();
  const entry = {
    id: Date.now().toString(),
    receivedAt: new Date().toISOString(),
    clientId: String(body.clientId || 'unknown'),
    count: body.records.length,
    records: body.records
  };
  current.push(entry);

  try {
    writeBackupFile(current);
    return res.json({ ok: true, id: entry.id, saved: entry.count });
  } catch (err) {
    console.error('寫入 backup.json 失敗:', err);
    return res.status(500).json({ ok: false, error: 'WRITE_FAILED' });
  }
});

app.listen(PORT, () => {
  console.log(`✔ server listening on http://localhost:${PORT}`);
});

註:Day 23 的 server.js 不包含 GET,因此無法透過 ngrok 開啟你的網頁。

  1. 可選:package.json scripts(新增)
{
  "scripts": {
    "server": "node server.js"
  }
}
  1. 建立 .gitignore(避免把備份檔推上 Git)
backup.json
node_modules/

啟動後端:

npm run server

看到 "server listening on http://localhost:3000" 表示 OK

B. ngrok(把本機 3000 暴露為公開 HTTPS)

  1. 註冊 ngrok:https://ngrok.com/

  2. 登入後選擇 Set up & Installation,可選 HomeBrew 安裝(若有 HomeBrew)或 Download

  3. 在終端機運行

ngrok http 3000

成功會出現以下畫面:

複製這個紅色底線的 HTTPS 網址,下一步要貼進前端。

C. 前端

  1. 在「歷史回顧」區塊下面,新增「雲端備份」UI(放在 Day 22 的匯出區塊附近即可):
<!-- [Day23-NEW] 簡易雲端備份(ngrok) -->
<div id="cloudBackupSection" style="margin-top:.75rem;">
  <button id="btnBackupNow" type="button">備份到雲端(ngrok)</button>
  <p id="backupFeedback" class="muted" aria-live="polite" style="margin:.25rem 0 0;"></p>
</div>
  1. 在 script.js 加入變數與邏輯(貼在「常數 / 偏好」區塊之後、Day 22 區塊附近即可):
// [Day23-NEW] 固定備份 API URL
const BACKUP_ENDPOINT = "https://你的ngrok網址.ngrok-free.app/backup";
const btnBackupNow = document.getElementById('btnBackupNow');
const backupFeedback = document.getElementById('backupFeedback');

// [Day23-NEW] fetch POST(含逾時控制)
async function postJSONWithTimeout(url, payload, timeoutMs = 10000) {
  const ctrl = new AbortController();
  const id = setTimeout(() => ctrl.abort('TIMEOUT'), timeoutMs);

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type':'application/json' },
      body: JSON.stringify(payload),
      signal: ctrl.signal
    });
    clearTimeout(id);

    const data = await res.json().catch(()=> ({}));
    if (!res.ok) {
      const msg = data?.message || `HTTP ${res.status}`;
      throw new Error(msg);
    }
    return data;
  } catch (err) {
    clearTimeout(id);
    throw err;
  }
}

// [Day23-NEW] 備份動作:把 localStorage 的紀錄送到後端
btnBackupNow?.addEventListener('click', async () => {
  const records = readRecords();
  if (!Array.isArray(records) || records.length === 0) {
    backupFeedback.textContent = '目前沒有可備份的紀錄。';
    return;
  }

  const payload = {
    clientId: 'fixed-demo', // 或用 randomUUID() 產生
    exportedAt: new Date().toISOString(),
    records
  };

  backupFeedback.textContent = '上傳中…';
  btnBackupNow.disabled = true;

  try {
    const json = await postJSONWithTimeout(BACKUP_ENDPOINT, payload, 10000);
    backupFeedback.textContent = json?.ok
      ? `備份成功:已保存 ${json.saved ?? records.length} 筆(編號 #${json.id || 'N/A'})。`
      : '備份完成,但回應格式非預期。';
  } catch (err) {
    backupFeedback.textContent = `備份失敗:${String(err.message || err)}`;
  } finally {
    btnBackupNow.disabled = false;
  }
});

驗證

  1. 後端啟動:npm run server,能在 http://localhost:3000/health 拿到 { ok:true }。
  2. ngrok 隧道:ngrok http 3000,複製 HTTPS 網址,加上 /backup,例如:
    https://abcd-1234-...ngrok-free.app/backup
  3. 前端呼叫:在歷史頁貼上網址,按「備份到雲端(ngrok)」。
  4. 成功:畫面顯示「備份成功:已保存 N 筆(編號 #xxxx)」(若用 Live Server 頁面會先自動刷新)。
  5. 伺服器根目錄生成/更新 backup.json,可看到你送出的紀錄被追加進陣列。
  6. 中斷測試:關掉 ngrok 或 server(ctrl+C),再按備份 → 能看到超時或錯誤訊息(UI 不卡住)。

常見錯誤 & 排查

  1. CORS 錯誤
  • 確認 server.js 有 app.use(cors())。
  • 前端一定要打 ngrok 的 HTTPS 網址,不要打 localhost:3000(不同來源)。
  1. 逾時 / 連不上
  • ngrok 視窗是否還在?ngrok http 3000 是否成功?
  • server.js 有沒有在跑?(終端應該看到 listening log)
  1. 400 INVALID_PAYLOAD
  • 傳的 payload 必須是 { records: Array }。
  • 確保 readRecords() 回來的是陣列;若是空值先新增一筆測試資料。
  1. backup.json 壞掉了
  • 伺服器會自動把壞檔備份為 backup.corrupted-*.json,並重建新檔;
  • 重新嘗試備份一次。
  1. URL 無效
    用 UI 的「記住這個網址」來檢查格式,務必是 https://.../backup。

安全與實務提醒

  • 這是開發教學用的最小後端,沒有認證,不要把敏感資料放進去;
  • ngrok URL 是公開的,知道網址的人都能打到你的 API;
  • 正式環境請加:API Key / JWT / HTTPS-only / 速率限制 / Schema 驗證 / DB;
  • backup.json 請加到 .gitignore,避免上傳到公開 Repo。

上一篇
【Day 22】— 入門 JavaScript 網頁架設:匯出 JSON / CSV 記錄
系列文
Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言