Cloudflare D1 - 免費資料儲存

LaughingZhu發表於2024-09-09

前言

自從上次將部落格專案的圖片從 七牛雲 遷到了 Cloudflare R2 之後就發現,Cloudflare 這個賽博菩薩的產品是真的不錯,非常的適合白嫖,DevNow 專案作為一個開源部落格,整體來說是希望越少依賴一些服務越好,使整個構建、部署流程更加的 輕便 和 快捷 ,讓對於前端不是很熟的同學也能快速的搭建一個自己的部落格。

這篇文章其實完全是個人愛好,只要是實現如何整合 Cloudflare D1 來給文章詳情頁增加一個瀏覽量。目前還沒想好是否要往 DevNow 專案上同步這個功能,就現在自己的 blog 上試試水,如果大家有需求的話,後邊在看是不是可以寫一個指令碼整合到 DevNow 專案中。

方案

方案整體來說是很多的,這裡簡單說下我考量的一些因素:

  • 自己有伺服器,可以自己搭建一個資料庫實現。
  • 透過一些比較成熟的資料儲存服務,可能需要訂閱功能。
  • 藉助類似 CloudflareVercel 這樣的服務商上的一些服務來實現,資料量少的話就是白嫖。severless 的實現方案對前端來說也比較友好。

這裡還有一個考量是後續可能會考慮透過 Cloudflare Page 部署前端的方案,完全從 Vercel 切到 Cloudflare,減少多個服務商帶來的管理和整合的複雜度。

先簡單介紹一些 Cloudflare D1 ;

官方的介紹:
使用 D1 短短數秒內即可建立一個無伺服器的關聯式資料庫。透過熟悉的查詢語言、時間點恢復功能和經濟實惠的定價,賦能您構建下一個重大專案。

方案

其實這裡主要看 Free 版的服務,這裡其實有兩個服務可以選擇, KVD1 都可以用來儲存資料,這裡選擇 D1 主要考量因素就是 KV 的每日寫入操作只有 1000 次,整理來對於一些好的部落格網站可能會超過,所以直接上 D1

整合到 DevNow 專案中

:::tip[注意]

整體來說所有的操作都有兩種實現方式,一個是在 Cloudflare D1 的官網裡邊操作,一個是透過命令列來實現。

我這裡主要記錄一下命令列的實現,透過一些命令和程式碼來建立和部署 D1 的服務,可以更好的理解整個的流程。

前置條件 :已經完成 Cloudflare 賬號的註冊和信用卡💳的繫結,否則無法後續的流程

:::

參考文件:Cloudflare D1

1. 建立一個Worker

pnpm create cloudflare@latest d1-tutorial
// d1-tutorial 是 <WORDER_NAME>

對於設定,請選擇以下選項:

  • For What would you like to start with? choose Hello World example.
  • For Which template would you like to use? choose Hello World Worker.
  • For Which language do you want to use? choose TypeScript.
  • For Do you want to use git for version control? choose Yes.
  • For Do you want to deploy your application? choose No (we will be making some changes before deploying).

然後專案中將會得到以下的一個目錄。

D1 floder

2. 建立資料庫

D1 資料庫在概念上與許多其他資料庫類似:資料庫可能包含一個或多個表、查詢這些表的能力以及可選索引。 D1 使用熟悉的SQL 查詢語言↗ (與 SQLite 使用的一樣)。

切換到您剛剛為 Workers 專案建立的目錄:

cd d1-tutorial

執行以下命令併為您的資料庫命名。在本教程中,資料庫名為 devnow

npx wrangler d1 create devnow


# 然後會輸出以下內容:
# [[d1 databases]]
# binding ="DB" # available in your Worker on env.DB
# database name ="prod-d1-tutorial"
# database id ="<unigue-ID-for-your-database>"

這將建立一個新的 D1 資料庫並輸出下一步所需的繫結配置。

wrangler命令列介面是 Cloudflare 的工具,用於在終端中管理和部署 Workers 應用程式和 D1 資料庫。它是在您使用npm create cloudflare@latest初始化新專案時安裝的。

3. 將 Worker 繫結到您的 D1 資料庫

您必須為 Worker 建立繫結才能連線到 D1 資料庫。繫結允許您的 Workers 訪問 Cloudflare 開發者平臺上的資源,例如 D1。您可以透過更新wrangler.toml檔案來建立繫結。

複製第二步輸出的結果到 wrangler.toml 檔案中。

[[d1_databases]]
binding = "DB" # available in your Worker on env.DB
database_name = "prod-d1-tutorial"
database_id = "<unique-ID-for-your-database>"

到這裡一個本地的 D1 資料庫就建好了

4. 對 D1 資料庫進行查詢

4.1 更新 schema.sql 檔案:

// d1-turorial/schema.sql

DROP TABLE IF EXISTS PageView;
CREATE TABLE IF NOT EXISTS PageView (
	PageId INTEGER PRIMARY KEY AUTOINCREMENT,
	Path TEXT UNIQUE NOT NULL,
	Count INTEGER,
	LastVisited INTEGER
);
INSERT INTO PageView (PageId, Path, Count, LastVisited)
VALUES
(1, '/posts/1', 0, 1725673703),
(2, '/posts/2', 0, 1725673703),
(3, '/posts/3', 0, 1725673703);

4.2 初始化數本地據庫

npx wrangler d1 execute devnow --local --file=./schema.sql

4.3 驗證資料是否在資料庫中

npx wrangler d1 execute devnow --local --command="SELECT * FROM PageView"

看到如下圖結果,即表示本地資料庫建立成功:

D1 sql result

5 在 Worker 中編寫查詢方法

這裡實現了根據 /d1/pageview/ 來請求,然後 獲取更新 頁面資料的介面。

// page: d1-tutorial/src/index.ts

export interface Env {
  DB: D1Database;
}

export default {
  async fetch(request, env): Promise<Response> {
    const url = new URL(request.url);
    const pathname = url.pathname; // 獲取頁面路徑

    // 檢查是否以 https://d1.laughingzhu.cn/d1/pageview 開頭
    if (!pathname.startsWith('/api/pageview')) {
      return new Response('Not Found', { status: 404 });
    }

    // 繼續處理 https://d1.laughingzhu.cn/d1/pageview 路徑下的請求
    const trimmedPath = pathname.replace('/api/pageview/', '/'); // 去除開頭部分,獲取具體頁面路徑
    if (request.method === 'GET') {
      try {
        // 查詢指定 path 的瀏覽量
        const result = await env.DB.prepare(`SELECT Count FROM PageView WHERE Path = ?`)
          .bind(trimmedPath)
          .first();

        // 如果沒有記錄,返回 0
        const count = result ? result.Count : 0;
        return new Response(JSON.stringify({ path: trimmedPath, count }), {
          headers: { 'Content-Type': 'application/json' }
        });
      } catch (error: any) {
        console.error('Database operation failed:', error);
        return new Response(`Failed to fetch page views for ${trimmedPath}: ${error.message}`, {
          status: 500
        });
      }
    }
    if (request.method === 'POST') {
      // 使用 UPSERT 來嘗試更新 count,如果沒有該頁面則插入新的記錄
      try {
        // UTC 轉成北京時間
        const utcTimestamp = Math.floor(Date.now() / 1000);
        const { results } = await env.DB.prepare(
          `
            INSERT INTO PageView (Path, Count, LastVisited)
            VALUES (?, 1, ?)
            ON CONFLICT(Path)
            DO UPDATE SET Count = Count + 1, LastVisited = ?
          `
        )
          .bind(trimmedPath, utcTimestamp, utcTimestamp)
          .run();
        // return Response.json(results);
        if (results) {
          return Response.json({
            message: `Page views for ${trimmedPath} updated.`,
            data: results
          });
        } else {
          return new Response(`Page views for ${trimmedPath} updated, but no results returned.`, {
            status: 200
          });
        }
      } catch (error: any) {
        console.log(error);
        return new Response(`Failed to update page views for ${trimmedPath}: ${error.message}`, {
          status: 500
        });
      }
    }

    // 如果是其他方法,不允許操作
    return new Response('Method Not Allowed', { status: 405 });
  }
} satisfies ExportedHandler<Env>;

配置 Worker 後,您可以在全域性部署之前在本地測試您的專案。

6. 本地開發執行 Worker

npx wrangler dev

當您執行 wrangler dev 時,Wrangler 會提供一個 URL(很可能是 localhost:8787)來檢視您的 Worker。

示例:

我們透過瀏覽器訪問 http://localhost:8787/d1/pageview/posts/hello-word

get result

查詢返回以下資料,這是因為資料庫中沒有,預設是0。

到這裡本地除錯就完事了。

7.部署線上 D1 資料庫

建立遠端線上資料庫:

npx wrangler d1 execute devnow --remote --file=./schema.sql

查詢遠端線上資料庫:

npx wrangler d1 execute prod-d1-tutorial --remote --command="SELECT * FROM Customers"

這裡和步驟 4.2 、 4.3 的區別就是 --local 換成了 --remote .

7.1 部署 worker 方法

npx wrangler deploy

這裡主要是部署我們在 worker 中實現的方法,不如上述步驟 4.4 在 d1-turorial/src/index 中實現的查詢、更新頁面資料的方法。

到這裡就可以整個 wokrer 和 資料庫就部署完成了,我們可以到線上去看一下。

online table

我們可以看到 Cloudflare 給我們提供了預設的 Worker 的訪問路由,為了方便,我們也可以新增一個自定義路由,透過自定義路由訪問,如下。

worker way

到這裡整個 Cloudflare D1 資料庫這裡就接入完成了。

接下來我們在前端專案中來增加相關的請求方法。

DevNow 前端專案中增加查詢、更新瀏覽量方法

1. 實現方案:

整體來說這裡由於期望 瀏覽量 這個資料比較實時,所以大概方案我能想到的是兩種方案:

  • CSR 去做,可以透過整合 React 來實現在執行時去請求 api 來完成(最簡單);
  • 渲染方案從 SSG 切換到 SSR ,這樣頁面會在每次訪問的時候在服務端動態的生成,資料可以保持實時。(這裡遇到一些坑,下邊會提)

最終我選擇了 SSR 方案,主要考量的因素如下:

使用 CSR 執行時請求會導致介面暴露,由於沒有使用者身份體系,很容易讓一些無聊的人刷介面,主要我們白嫖每天是限額,超出是要收錢的。

所以這裡採用了 SSR 的方案,這樣就可以不暴露介面來實現,不過這裡可能會犧牲掉 SSG 帶來的更好快取效果。

這裡主要是刪除、替換適用於 SSG 方案中的一些 API :

// page: src/posts/[...slug].astro

// 刪除  export const prerender = true;

// 刪除 getStaticPaths() 相關方法
//可以透過 getEntry 來獲取對應的文章內容

import { getEntry } from 'astro:content';
const { slug } = Astro.params;
const post = await getEntry('doc', slug);

2. Astro 中如何接入

開始使用了 Astro 提供的 API 端點 ,後來仔細瞭解完這個是 SSG 的方案,之後在構建的時候請求,後邊就不會走了,所以基於 API 端點 的實現方式 PASS

其實 Astro 中 資料請求 的文件中有提到,可以使用 Fetch 來實現。

官閘道器於 fetch 的

fetch 呼叫將會在構建時執行,並且資料都可用於元件模板中來生成動態 HTML。如果啟用 SSR 模式,任何 fetch 呼叫都將在執行時執行。

// page: src/posts/[...slug].astro
//增加如下來實現動態獲取訪問量

const { slug } = Astro.params;
const data = await fetch(`https://xxx.d1.hosts/d1/pageview/posts/${slug}`);
const result = await data.json();

這裡可以提出到一個指定的檔案來維護,示例我就不改了。

最佳化空間:只有線上上環境才請求,線上不請求,來增加資料的準確度。

更新的實現同上。

3. 頁面快取的問題

部署到線上環境時發現個問題,只有部署完第一次訪問會增加請求對應的方法,後邊就不請求了,猜測是 Vercel 這邊有快取,然後在 Response 體裡看到了 x-vercel-cache: HIT 。第一次是 MISS 以後所有的都是 HIT ,這裡其實可以理解成部署完第一次訪問的時候 Vercel 幫我們生成了一份快取,後續所有的人訪問都走的是快取,這樣在速度上就會有更好的體驗,本來是個好東西,還給我卡主了。

然後就各種開始找方法來避免快取生效。給請求加 禁止快取欄位,給 vercel.json header 增加對應路由不快取的配置發現都沒有用。

這裡可以透過開啟 D1 的實時日誌來除錯:

實時日誌

然後突然想到會不會是 ISR 導致的問題,之前預設開啟了,搜尋了一會發現,ISR 模式會自動快取我們的檔案。解決方案就是 將 astro.config.js 中的 ISR 配置刪除即可。

完事,到這裡專案裡就可以展示 瀏覽量了。效果圖如下:

效果圖

原文連結:Cloudflare D1 - 免費資料儲存

相關文章